Aller au contenu

Guide pour un agent de code

Ce guide s'adresse à un agent de type Claude Code qui doit travailler avec le labo : interroger les données, appeler les fonctions AI_*, charger des fichiers, lancer dbt ou la prévision, écrire du code transposable chez le client. Il vaut dans ce dépôt comme dans un autre projet qui se sert du labo comme d'un Snowflake local. Il ne remplace pas les documents de référence : il dit quoi faire, dans quel ordre, et renvoie au détail.

Pour faire évoluer le labo lui-même (nouveau code, nouveau composant), lire d'abord methode-claude-code.md : le cycle PLAN.md, les hooks, les agents et /gate s'appliquent.

1. Règles à respecter avant toute action

Règle Conséquence pour l'agent
.env et secrets/ ne se lisent pas Ne jamais faire cat .env, grep ... .env, ni afficher une variable secrète. Charger l'environnement sans le lire : uv run --env-file .env <commande>. Les noms des variables sont dans .env.example
Aucun secret dans le code, les logs, les commandes Les identifiants viennent de l'environnement (LabConfig.from_env()). Ne pas les recopier dans une commande, un fichier, un message
LiteLLM est la seule sortie vers un LLM Pas d'appel à Ollama, OpenAI ou autre fournisseur en direct. Passer par cortex_lab (alias cortex-*) ou par LITELLM_BASE_URL. LiteLLM 1.82.7 et 1.82.8 sont interdits
Données synthétiques ou publiques Aucune donnée du client dans le labo ni dans un prompt. Les fichiers de test viennent de data/generate.py
Aucun tiret cadratin (U+2014) Nulle part : code, docs, commits. Un hook refuse l'écriture
Prose en français, code en anglais Commentaires de code, identifiants et messages de commit en anglais
Plan d'abord Dans ce dépôt, aucun code sans PLAN.md approuvé. La ligne d'approbation n'est écrite que par Stéphane
Actions à confirmer git commit, git push, docker compose down : demander d'abord. Ne pas supprimer de table, de namespace ni de fichier MinIO hors des objets créés par l'agent
Briques du NUC Le labo consomme pg-central, minio-central et LiteLLM. Ne jamais monter sa propre base, son propre stockage ni sa propre gateway

2. Vérifier que le labo répond

cd /home/stephanev/Documents/snowflake/snowflake-lab-kit
docker compose ps                                   # polaris, trino, marquez : running (healthy)
curl -s http://127.0.0.1:18080/v1/info              # Trino : "starting": false
curl -s http://127.0.0.1:15000/api/v1/namespaces    # Marquez : liste JSON

Si un service manque : docker compose up -d (autorisé sans confirmation). Un 401 de Trino sur analytics ou un échec de polaris-init se traite avec le tableau de dépannage de exploitation.md, sans lire .env.

Points d'accès depuis l'hôte : Trino http://127.0.0.1:18080, Polaris http://127.0.0.1:18181 (OAuth), Marquez http://127.0.0.1:15000. Depuis le LAN : openflake.nuc, polaris.openflake.nuc, marquez.openflake.nuc.

3. Où sont les données

Un seul catalogue, nommé comme la base Snowflake : analytics. Les noms ont trois niveaux, analytics.<schéma>.<table>, identiques dans DuckDB, Trino et Snowflake. Identifiants en minuscules, jamais entre guillemets.

Schéma Contenu Écrit par
raw Fichiers chargés tels quels, colonnes de preuve (_source_file, _loaded_at...) ingest.raw_loader
standardized Tables typées et nettoyées, snapshots SCD2 dbt
semantic Dimensions et faits (dim_hotel, dim_comp_set, fct_hotel_daily), seeds, modèle sémantique MetricFlow dbt
intake Historique de chargement, registre de schémas, exceptions, file de revue, suggestions et règles de mapping ingest, dbt (seed mapping_rules)
forecast forecast (une ligne par quantile), backtest_metrics conteneur forecast
shared_calendar Snapshots datés du calendrier (lecture seule pour lab_tech) ingest.shared_snapshots

Les tables it_* et les namespaces it_<id>... sont des restes de tests d'intégration : les ignorer.

Lister l'existant (dans Trino ; lab_reader ne voit que semantic et shared_*, prendre lab_tech pour tout voir) :

SHOW SCHEMAS FROM analytics;
SHOW TABLES FROM analytics.semantic;
DESCRIBE analytics.semantic.fct_hotel_daily;

Détail des colonnes et des couches : collecte-retraitement.md. Droits : socle.md.

4. Recettes

Toutes les commandes se lancent depuis la racine du dépôt. --env-file .env fournit les identifiants sans que l'agent les voie.

Interroger en SQL avec Trino

Trino n'a pas de mot de passe : le nom d'utilisateur fixe les droits. lab_reader lit semantic et shared_* ; lab_tech lit et écrit tout analytics sauf shared_*. Prendre lab_reader pour une lecture.

import trino

con = trino.dbapi.connect(host="127.0.0.1", port=18080, user="lab_reader", catalog="analytics", schema="semantic")
cur = con.cursor()
cur.execute("SELECT * FROM analytics.semantic.fct_hotel_daily LIMIT 5")
print(cur.fetchall())

À lancer avec uv run python script.py (le client trino est dans les dépendances). Trino ne connaît pas les fonctions AI_*.

Interroger avec DuckDB, fonctions AI_* comprises

from lab.duckdb_connect import connect

con = connect()  # MinIO, catalogue Polaris attaché sous "analytics", fonctions AI_* et GET enregistrées
print(con.execute("SELECT count(*) FROM analytics.raw.pms_daily").fetchall())
con.close()

À lancer avec uv run --env-file .env python script.py. Sans aucun service : connect(s3=False, polaris=False) donne une connexion hors ligne avec les seules fonctions AI_*.

Appeler une fonction Cortex en SQL

On écrit les appels comme dans Snowflake ; on lit les champs avec GET(...), pas avec la syntaxe :champ.

SELECT GET(AI_CLASSIFY('Chambre propre mais accueil lent', ['room', 'service', 'price']), 'labels') AS labels;
SELECT AI_SENTIMENT('Chambre propre mais accueil lent', ['room', 'service']) AS sentiment;
SELECT AI_COMPLETE('cortex-complete', 'Résume en une phrase : ...') AS answer;
SELECT AI_CLASSIFY('texte', ['a', 'b'], NULL, TRUE) AS detailed;  -- {"value": ..., "error": ...}

Une erreur par ligne renvoie NULL, comme dans Snowflake. Pour comprendre un NULL, rejouer avec return_error_details à TRUE. AI_COMPLETE n'accepte que les alias autorisés (cortex-* et CORTEX_COMPLETE_EXTRA_ALIASES). Fonctions couvertes, signatures et écarts : cortex-lab.md et .claude/skills/cortex-compat/references/contracts.md. Une fonction absente de ces contrats ne s'invente pas : elle passe par /new-cortex-function.

Appeler une fonction Cortex en Python

from cortex_lab.compat import complete, classify_text, sentiment

print(complete("cortex-complete", "Donne trois idées de promotion hôtelière."))
print(classify_text("Je veux réserver une suite", ["booking", "complaint"]))

cortex_lab.compat bascule vers snowflake.cortex quand CORTEX_BACKEND=snowflake : le même code tourne chez le client. Style Snowpark : cortex_lab.compat.snowpark_functions.

Produire et charger des fichiers

uv run python -m data.generate landing --root /tmp/landing --as-of 2026-10-22 --history-days 7
uv run --env-file .env python -m ingest.raw_loader --date 2026-10-22 --source pms

Le dépôt des fichiers dans MinIO passe par LandingStore.put (exemple complet dans exploitation.md). Le chargeur affiche un statut par fichier (loaded, skipped_already_loaded, rejected, unclassified) et émet le lineage vers Marquez si MARQUEZ_URL est défini. Un fichier déjà chargé n'est pas rechargé (mémoire de 14 jours, comme Snowpipe). Pour essayer sans toucher à raw : --raw-schema it_scratch_<nom>.

Retraiter avec dbt

cp -n dbt/profiles.example.yml dbt/profiles.yml
uv run --env-file .env dbt build --project-dir dbt --profiles-dir dbt                   # cible duckdb (défaut)
uv run --env-file .env dbt build --project-dir dbt --profiles-dir dbt --target trino    # même projet, autre adapter

Un seul projet dbt ; seul l'adapter change (dbt-snowflake chez le client). Ne pas écrire de SQL propre à DuckDB ou à Trino dans un modèle : si c'est inévitable, passer par une macro avec une variante par adapter.

Interroger les métriques

DBT_PROJECT_DIR=dbt DBT_PROFILES_DIR=dbt uv run --env-file .env mf query --metrics occupancy_rate --group-by metric_time --limit 5

Métriques : rooms_sold_total, room_revenue_total, rooms_available_total, occupancy_rate, adr, revpar (dbt/models/semantic/semantic_models.yml). La CLI mf n'a pas d'option --target : elle suit la cible par défaut du profil.

Prévoir

docker compose --profile forecast run --rm forecast forecast --as-of 2026-10-22
docker compose --profile forecast run --rm forecast backtest --as-of 2026-10-22 --origins 6 --step-days 14

Résultats dans analytics.forecast.forecast et analytics.forecast.backtest_metrics. Construire l'image une fois avant : docker compose --profile forecast build forecast. Détail : prevision.md.

Lire le lineage

curl -s http://127.0.0.1:15000/api/v1/namespaces/snowlab/jobs | python3 -m json.tool | head -40

5. Écrire du code transposable chez le client

  • Noms à trois niveaux analytics.<schéma>.<table>, en minuscules, sans guillemets.
  • Fonctions AI_* écrites comme dans Snowflake, champs lus par GET(...).
  • En Python, importer depuis cortex_lab.compat, jamais depuis cortex_lab.core dans du code destiné au client.
  • Types : ceux de lab/types.py, qui donnent l'équivalent Snowflake.
  • Configuration par variables d'environnement, jamais en dur.
  • Ce qui change chez le client et ce qui reste à confirmer : portabilite-snowflake.md.

6. Tester

uv run --env-file .env pytest -q -m "not integration"            # sans service, quelques minutes
uv run --group forecast --env-file .env pytest -q -m integration # pile démarrée, une vingtaine de minutes
python3 .claude/hooks/no_em_dash.py --scan .
python3 .claude/hooks/guard_litellm.py --scan .

Les tests d'intégration écrivent dans la pile réelle (tables it_*, dates fictives après 2099) et redémarrent le conteneur polaris : ne pas les lancer pendant qu'une autre session utilise le labo. Dans ce dépôt, aucun travail n'est soumis sans /gate au vert. Détail : tests-et-gate.md.

7. Pièges connus

Piège Comportement attendu de l'agent
Trino accepte n'importe quel nom d'utilisateur sans mot de passe Se déclarer lab_reader pour lire. lab_tech peut supprimer définitivement des tables (purge activée, voir exploitation.md)
Une fonction AI_* renvoie NULL Rejouer avec return_error_details à TRUE avant de conclure
La réponse d'un AI_* diffère de Snowflake Normal : le modèle local (qwen3:8b) n'est pas celui du client. Seuls les contrats (forme, erreurs, limites) sont identiques. Ne pas tester la qualité d'une réponse
con.sql(...).fetchone() lève AttributeError DuckDB 2.0 : une relation ne garde plus de résultat. Utiliser con.execute(...).fetchone() ou con.sql(...).fetchall()
Un DuckDB hors ligne échoue au premier connect() Les extensions httpfs et iceberg se téléchargent au premier usage : relancer une fois avec accès réseau
DROP TABLE depuis DuckDB ou PyIceberg Les fichiers restent dans MinIO (pas de purge)
Un nouveau schéma shared_* lab_reader ne le lit via Polaris qu'après ajout à READER_NAMESPACES (infra/polaris/init_catalog.py)
Les identifiants Polaris d'un principal sont réinitialisés Stéphane recopie secrets/polaris_credentials.env dans .env ; l'agent ne lit ni l'un ni l'autre

8. Bloc à reprendre dans le CLAUDE.md d'un autre projet

Pour qu'un agent d'un autre dépôt se serve du labo comme d'un Snowflake local, ajouter à son CLAUDE.md :

## Labo Snowflake-like

Ce projet s'exécute contre le labo local `/home/stephanev/Documents/snowflake/snowflake-lab-kit`
(guide : `docs/guide-agent.md` de ce dépôt).

- Catalogue `analytics`, noms `analytics.<schéma>.<table>` en minuscules, sans guillemets.
- Lecture SQL : Trino `http://127.0.0.1:18080`, utilisateur `lab_reader`, sans mot de passe.
- Fonctions Cortex : DuckDB du labo (`lab.duckdb_connect.connect()`) ou `cortex_lab.compat` en Python ;
  champs lus par `GET(...)`. Aucun appel LLM hors LiteLLM.
- Identifiants : `.env` du labo, jamais lu ni affiché ; commandes lancées par `uv run --env-file .env`
  depuis la racine du labo.
- Données synthétiques uniquement ; aucune donnée du client.
- Ne pas écrire dans `analytics` hors d'un schéma de travail dédié, ne rien supprimer sans confirmation.