cortex_lab
Skill de référence : .claude/skills/cortex-compat/SKILL.md. Contrats vérifiés : .claude/skills/cortex-compat/references/contracts.md.
But : le même code appelle les fonctions AI_* au labo et chez le client. Au labo, elles passent par cortex_lab puis LiteLLM ; chez Snowflake, elles s'exécutent nativement. Les réponses diffèrent, les contrats (noms, signatures, formes de retour, erreurs, limites) sont identiques. Cible : DuckDB et Python uniquement ; Trino n'est pas ciblé (ses fonctions IA ont d'autres noms).
Fonctions couvertes
| Fonction | Contrat | Alias LiteLLM | Façade SQL DuckDB | Snowpark (cortex_lab.snowpark) |
snowflake.cortex (cortex_lab.cortex) |
|---|---|---|---|---|---|
AI_CLASSIFY |
vérifié le 2026-10-22 | cortex-classify |
macro ai_classify, 2 à 4 arguments |
ai_classify |
classify_text |
AI_SENTIMENT |
vérifié le 2026-10-22 | cortex-sentiment |
macro ai_sentiment, 1 à 3 arguments |
ai_sentiment |
sentiment (score float) |
AI_COMPLETE (forme chaîne) |
vérifié le 2026-10-22, partiellement | nom de modèle passé en argument (cortex-complete au labo) |
macro ai_complete, 2 à 6 arguments |
ai_complete |
complete |
GET |
page GET vérifiée le 2026-10-22 |
aucun | macro get |
get |
aucun |
PROMPT |
vérifié le 2026-10-23 | aucun | macro prompt, 2 à 11 arguments (le gabarit et 1 à 10 expressions) |
prompt |
aucun |
AI_FILTER (texte et PROMPT) |
vérifié le 2026-10-23 | cortex-filter |
macro ai_filter, 1 à 2 arguments |
ai_filter |
aucun |
AI_EXTRACT (texte) |
vérifié le 2026-10-23 | cortex-extract |
macro ai_extract(text, responseFormat, scores := NULL), arguments nommés admis |
ai_extract |
aucun |
AI_TRANSLATE |
vérifié le 2026-10-23 | cortex-translate |
macro ai_translate, 3 à 4 arguments |
ai_translate |
translate |
AI_SUMMARIZE (texte) |
vérifié le 2026-10-25 | cortex-summarize |
macro ai_summarize(text, return_error_details := NULL) |
aucun (absente de la référence Snowpark) | summarize |
AI_AGG |
vérifié le 2026-10-23 | cortex-agg |
macro d'agrégat ai_agg(expr, instruction) |
ai_agg |
aucun |
AI_SUMMARIZE_AGG |
vérifié le 2026-10-23 | cortex-summarize-agg |
macro d'agrégat ai_summarize_agg(expr) |
ai_summarize_agg |
aucun |
AI_EMBED (texte) |
vérifié le 2026-10-23 | cortex-embed (bge-m3, 1 024 dimensions) |
macro ai_embed, 2 à 3 arguments |
ai_embed |
embed_text_1024 (embed_text_768 refusée) |
AI_SIMILARITY (texte) |
vérifié le 2026-10-23 | cortex-embed |
macro ai_similarity, 2 à 4 arguments |
ai_similarity |
aucun |
AI_COUNT_TOKENS |
vérifié le 2026-10-23 | aucun (estimation) | macro ai_count_tokens, 2 à 5 arguments |
ai_count_tokens |
aucun |
AI_REDACT |
vérifié le 2026-10-23 | cortex-redact |
macro ai_redact(input, categories := NULL, return_error_details := NULL, mode := NULL) |
ai_redact |
aucun |
Les alias de chat pointent vers ollama_chat/qwen3:8b et l'alias cortex-embed vers ollama/bge-m3-cpu sur la brique LiteLLM du NUC, sans repli vers un modèle cloud (voir exploitation.md). La clé du labo est limitée à ces alias cortex-* : un appel sur claude-sonnet-4-6 est refusé (401), test d'intégration. Toute autre fonction AI_* passe par /new-cortex-function. Les fonctions sur fichiers (TO_FILE, AI_PARSE_DOCUMENT, AI_EXTRACT fichier, AI_FILTER image, AI_SUMMARIZE fichier) et les formes image sont exclues (décision A1 du 2026-10-23). Le détail de chaque contrat et ses écarts de fidélité sont dans contracts.md.
Organisation du module
cortex_lab/
__init__.py
core/
client.py client LiteLLM (SDK openai pointé sur LITELLM_BASE_URL), chat et embeddings, interface injectable, garde de fenêtre
errors.py erreurs par ligne, enveloppe {"value": ..., "error": ...}
limits.py limites documentées, fenêtre du modèle local
routing.py alias LiteLLM par fonction, alias de modèle pour AI_COMPLETE, noms de modèles d'embedding
functions/
ai_classify.py ai_sentiment.py ai_complete.py prompt, schéma Pydantic ou JSON, validation
ai_filter.py ai_extract.py ai_translate.py ai_summarize.py ai_agg.py ai_summarize_agg.py
ai_embed.py ai_similarity.py ai_redact.py ai_count_tokens.py prompt.py
duckdb/
__init__.py register(con) : UDF internes et macros publiques
macros.sql macros aux noms Cortex et macro get
snowpark.py fonctions ai_* style Snowpark (expressions de colonnes DuckDB)
cortex.py fonctions style snowflake.cortex (appels directs du cœur)
compat.py bascule à l'import selon CORTEX_BACKEND
dbt_plugin.py plugin dbt-duckdb : configure_connection appelle register
Toute la logique vit dans core/. Les façades n'adaptent que les arguments.
Contrats
AI_CLASSIFY
- Signature :
AI_CLASSIFY(input, list_of_categories [, config_object] [, return_error_details]). - Catégories : au moins 2 valeurs uniques, sensibles à la casse ; chaînes, ou objets
{'label': ..., 'description': ...}(description de 25 mots maximum). La page SQL de Snowflake ne fixe aucun maximum pour une entrée texte (limite de la fenêtre de jetons, précision moindre au-delà de vingt catégories) ; le maximum de 100 vient du docstring Snowpark et de l'entrée document. Le code du labo refuse au-delà de 100 : écart nouveau, à traiter par avenant (relecture du 2026-10-24). config_object:task_description(50 mots maximum),output_mode(singlepar défaut oumulti),examples(objetsinput,labels,explanation).- Retour :
{"labels": [...]}, exactement un label en modesingle, un ou plusieurs en modemulti. - Référence : https://docs.snowflake.com/en/sql-reference/functions/ai_classify (lue le 2026-10-22) ; Snowpark : https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/latest/snowpark/api/snowflake.snowpark.functions.ai_classify (lue le 2026-10-22).
AI_SENTIMENT
- Signature :
AI_SENTIMENT(text [, categories] [, return_error_details]). - Catégories : 10 au maximum, 30 caractères chacune. Fenêtre de contexte : 2 048 jetons (estimés au labo à 4 caractères par jeton).
- Retour :
{"categories": [{"name": ..., "sentiment": ...}, ...]}, entréeoveralltoujours présente et en premier ;sentimentdansunknown,positive,negative,neutral,mixed. - Référence : https://docs.snowflake.com/en/sql-reference/functions/ai_sentiment (lue le 2026-10-22).
AI_COMPLETE (forme chaîne)
- Signature :
AI_COMPLETE(model, prompt [, model_parameters, response_format, show_details] [, return_error_details]). model_parameters:temperatureettop_pentre 0 et 1,max_tokens(défaut 4096, plafond 8192 tiré du seul docstring Snowpark : la page SQL ne donne aucun plafond, relue le 2026-10-23),guardrails(TRUE est une erreur au labo : Cortex Guard non reproduit).response_format:{'type': 'json', 'schema': {...}}, schéma validé parjsonschema(Draft 2020-12), sortie validée contre le schéma.- Retour : chaîne ; objet conforme au format avec
response_format; objet{"choices", "created", "model", "usage"}avecshow_details; objet{"structured_output": [{"raw_message", "type"}], "created", "model", "usage"}avec les deux. - Non reproduit : formes image et objet prompt, littéral
TYPE OBJECT(...), modèle apporté par le client. Unreturn_error_detailsTRUE sur une entrée NULL renvoie NULL (comportement Snowflake non documenté). - Références : https://docs.snowflake.com/en/sql-reference/functions/ai_complete, https://docs.snowflake.com/en/sql-reference/functions/ai_complete-single-string, https://docs.snowflake.com/en/sql-reference/functions/ai_complete-structured-outputs (lues le 2026-10-22, la dernière en partie). Pages des formes image et objet prompt lues le 2026-10-23 (non reproduites) ; reste à vérifier chez le client : limites de taille par modèle Snowflake. Note BCR-2184 lue : l'erreur passe à NULL pour
AI_COMPLETE,AI_CLASSIFY,AI_FILTER,AI_SENTIMENT,AI_TRANSLATE,AI_COUNT_TOKENS.
Fonctions de l'avenant du 2026-10-23
Pages lues le 2026-10-23 (URL dans contracts.md). Résumé des contrats et des écarts ; le détail est dans contracts.md.
| Fonction | Retour | Erreurs | Écarts principaux |
|---|---|---|---|
PROMPT(gabarit, e1 [, ... e10]) |
objet {"template", "args"} ; ne formate rien |
repère sans expression : erreur | 10 expressions au plus (A9) ; args convertis en texte ; type JSON |
AI_FILTER(input [, rd]) |
BOOLEAN | NULL ; {"value", "error"} avec rd |
forme fichier refusée ; avec rd (même FALSE) la macro renvoie JSON |
AI_EXTRACT(text, responseFormat) |
{"error": null, "response": {...}} |
erreur levée (A7) | forme fichier et config non reproduites ; scores => TRUE refusé (les scores sont GA chez Snowflake depuis le 2026-05-22) ; argument model => (modèle affiné, forme fichier) non reproduit ; limites de 100 entités et 10 tables |
AI_TRANSLATE(text, source, cible [, rd]) |
VARCHAR | NULL ; objet avec rd |
23 codes ; limite effective de la fenêtre locale |
AI_SUMMARIZE(text [, rd]) (texte) |
VARCHAR | NULL ; objet avec rd (inféré de BCR-2184, la page ne détaille rien) |
entrée de 110 caractères ou moins renvoyée telle quelle sans appel, et entrée renvoyée si le résumé est plus long ; forme fichier refusée ; limite effective de la fenêtre locale |
AI_AGG(expr, instruction), AI_SUMMARIZE_AGG(expr) |
VARCHAR | erreur levée (A7) | découpe et réduction par étapes au-delà de la fenêtre ; pas de clause OVER |
AI_EMBED(model, input [, rd]) |
vecteur de 1 024 flottants | NULL ; objet avec rd (BCR-2358, activé par défaut dans le bundle 2026_07 depuis la version 10.37) |
noms 768, image et non documentés refusés (snowflake-arctic-embed-l-v2.0-8k) ; multilingual-e5-large accepté (1 024 dimensions écrites par la page d'EMBED_TEXT_1024, servi par bge-m3, décision S2) ; FLOAT[1024] au lieu de VECTOR |
AI_SIMILARITY(a, b [, config] [, rd]) |
FLOAT de -1 à 1 | NULL ; objet avec rd (BCR-2358) |
texte seulement ; rd demande la position de la configuration (NULL admis) |
AI_REDACT(input [, categories] [, rd] [, mode]) |
VARCHAR, ou {"spans": [...]} en mode detect |
erreur levée sauf rd TRUE (A7) |
13 catégories ; le labo calcule les positions et la substitution ; pas un contrôle de sécurité |
AI_COUNT_TOKENS(fonction, [modèle,] texte [, options] [, rd]) |
entier (VARCHAR en SQL) | NULL ; objet avec rd |
estimation à 4 caractères par jeton (A8) ; chez Snowflake, compte exact pour la plupart des modèles, estimation à moins de 3 % pour Claude et Gemini, quasi exact pour OpenAI |
rd désigne return_error_details. Les erreurs levées (A7) sont celles que la note BCR-2184 de Snowflake ne fait pas passer à NULL : AI_EXTRACT, AI_AGG, AI_SUMMARIZE_AGG, AI_REDACT. Dans DuckDB, l'exception fait échouer la requête (exception_handling="default" pour ces UDF).
Fenêtre de contexte du modèle local
Le modèle local coupe en silence un prompt qui ne tient pas dans sa fenêtre (mesure du 2026-10-23, test B11) : 32 768 jetons pour les alias de chat, 2 048 pour cortex-embed (la passerelle en annonce 8 192). Le labo refuse le prompt avant l'appel : un texte plus long que la fenêtre donne une erreur par ligne (NULL, ou l'erreur levée pour les fonctions de l'écart A7), jamais une troncature. La règle est pessimiste (2 caractères ASCII par jeton, un jeton par autre caractère) et s'applique à toutes les fonctions de chat, y compris AI_CLASSIFY, AI_SENTIMENT et AI_COMPLETE. Elle refuse donc des textes que le modèle accepterait. Pour les embeddings, un contrôle après l'appel détecte aussi une coupure (jetons rapportés égaux à la fenêtre). AI_AGG et AI_SUMMARIZE_AGG découpent la liste en tranches qui tiennent dans la fenêtre puis réduisent les réponses partielles par étapes.
Les modèles locaux raisonnent : leurs jetons de réflexion comptent dans max_tokens et laissaient une réponse vide. Le client envoie donc think: false (commutateur d'Ollama, transmis par la passerelle).
Règles communes
- Forme de retour identique à Snowflake, garantie par une sortie structurée envoyée à LiteLLM (
response_formatde typejson_schema,strict: true, avec énumérations : labels fournis, valeurs de sentiment, noms de catégories) puis validée par Pydantic (ClassifyOutput,SentimentOutput) oujsonschema. Une sortie invalide (label hors liste, JSON mal formé, sentiment hors énumération, catégorie manquante ou répétée) est une erreur, jamais un résultat. - Erreurs : une ligne en erreur renvoie NULL sans bloquer la requête (sauf
AI_EXTRACT,AI_AGG,AI_SUMMARIZE_AGGetAI_REDACT, dont l'erreur est levée). Avecreturn_error_detailsà TRUE, la fonction renvoie{"value": ..., "error": ...}, exactement un des deux non NULL (cortex_lab.core.errors.run_row). Une entrée NULL donne NULL, sans erreur. - Hiérarchie d'erreurs :
CortexError, et ses sous-classesInvalidArgumentError,LimitExceededError,InvalidModelOutputError,UpstreamError. Les messages ne contiennent jamais le contenu de la requête ni la configuration. - Limites reproduites dans
cortex_lab/core/limits.pypour échouer au labo comme en production. - Température 0 par défaut pour toutes les fonctions.
SQL à deux étages (DuckDB)
- UDF internes
_ai_classify(4 arguments),_ai_sentiment(3),_ai_complete(6),_ai_filter(2),_ai_extract(3),_ai_translate(4),_ai_summarize(2),_ai_embed(2, retourFLOAT[1024]),_ai_embed_details(3),_ai_similarity(3, retour DOUBLE),_ai_similarity_details(4),_ai_agg(2, premier argumentVARCHAR[]),_ai_summarize_agg(1, argumentVARCHAR[]),_ai_redact(4),_ai_count_tokens(5) : arguments VARCHAR, les arguments structurés ou optionnels voyagent en texte JSON. Enregistrées parcortex_lab.duckdb.register(con)avectype="arrow"(lots de lignes, appels LLM concurrents jusqu'à 8 fils),null_handling="special"(un argument optionnel NULL atteint la fonction),exception_handling="return_null"(filet de dernier recours ;"default"pour les UDF dont l'erreur est levée) etside_effects=True(non déterministe, pas de réutilisation de résultat). Les erreurs sont traitées ligne par ligne : une exception ne peut pas annuler tout le lot. - Macros publiques aux noms Cortex (
cortex_lab/duckdb/macros.sql), une surcharge par nombre d'arguments, qui convertissent avecto_json()puis castent enJSON(ai_classify,ai_sentiment).ai_completerenvoie toujours un VARCHAR (chaîne, ou texte JSON d'un objet à lire avecGET).
Quand une position peut recevoir soit un objet, soit le booléen return_error_details, c'est le cœur Python qui tranche selon le type JSON reçu (split_classify_args, split_sentiment_args, split_complete_args, split_redact_args).
Les macros d'agrégat ai_agg et ai_summarize_agg enveloppent list(...) : l'UDF reçoit tout le groupe (GROUP BY ou constante). Les macros ai_extract, ai_redact et ai_summarize déclarent les noms de paramètres de Snowflake, donc text =>, responseFormat =>, categories =>, mode => fonctionnent (DuckDB 2.0.0 accepte => et := pour les paramètres nommés d'une macro, sans tenir compte de la casse, test B13). Une macro n'a qu'un type de retour par surcharge : ai_filter renvoie un BOOLEAN à un argument et un JSON avec return_error_details, ai_embed un FLOAT[1024] puis un JSON, ai_similarity un DOUBLE puis un JSON, ai_translate, ai_summarize, ai_redact et ai_count_tokens toujours un VARCHAR.
Macro get(x, field) : field chaîne (clé) ou entier (indice). Renvoie NULL pour une clé absente, une chaîne JSON sans guillemets, et le texte JSON pour un objet, un tableau ou un nombre. Elle remplace la syntaxe Snowflake x:champ, absente de DuckDB. Dans dbt, la macro get du projet dispatche selon l'adapter (voir collecte-retraitement.md).
Exemple, identique dans DuckDB au labo et dans Snowflake :
SELECT GET(AI_CLASSIFY(review, ['room', 'service', 'price']), 'labels') AS labels
FROM reviews;
Les littéraux ['a', 'b'] et {'cle': 'valeur'} s'écrivent de la même façon dans DuckDB et dans Snowflake.
Routage vers LiteLLM
cortex_lab/core/routing.py:ALIASESassocie chaque fonction à modèle géré par Snowflake, sans argument de modèle, à son alias (ai_classifyàcortex-classify,ai_filteràcortex-filter,ai_extractàcortex-extract,ai_translateàcortex-translate,ai_summarizeàcortex-summarize,ai_aggàcortex-agg,ai_summarize_aggàcortex-summarize-agg,ai_redactàcortex-redact,ai_sentimentàcortex-sentiment). PourAI_EMBEDetAI_SIMILARITY,embed_aliasassocie les noms de 1 024 dimensions (snowflake-arctic-embed-l-v2.0,voyage-multilingual-2,nv-embed-qa-4,multilingual-e5-large) à l'aliascortex-embedet refuse les autres (768 dimensions, image, dimension non documentée, inconnus) : le nom d'un modèle Snowflake n'est jamais un alias de la brique, qui porte des alias adossés au cloud sous certains de ces noms. PourAI_COMPLETE, le nom de modèle passé en argument est utilisé comme alias LiteLLM du même nom (alias_for_complete, motif^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$), mais seulement s'il figure dans la liste d'alias autorisés : les aliascortex-*connus (ALIASES,cortex-embed,cortex-complete) plus ceux de la variableCORTEX_COMPLETE_EXTRA_ALIASES(noms séparés par des virgules, lue à chaque appel, noms invalides ignorés). Un nom refusé (par exempleclaude-sonnet-4-6, alias adossé au cloud sur la brique) donne l'erreur du contrat, NULL oureturn_error_details, sans appel réseau. C'est une seconde barrière : la première reste le périmètre de la clésnowflake-lab-kit.cortex_lab/core/client.py:LiteLLMClientconstruit un clientopenai.OpenAIavecbase_urllu dansLITELLM_BASE_URL(jamais de valeur par défaut, jamais en dur), clé dansLITELLM_API_KEY, délaiLITELLM_TIMEOUT_SECONDS(défaut 120 secondes), deux nouvelles tentatives.get_client()crée un client par processus ;set_client()permet d'injecter un faux client (tests).- Aucune dépendance
litellmdans le dépôt : les versions1.82.7et1.82.8(vol d'identifiants) sont de toute façon bloquées par un hook et par un test.
Façades Python
cortex_lab.snowpark
Signatures de snowflake.snowpark.functions (snowflake-snowpark-python 1.55.3, lues dans le code du paquet le 2026-10-22) : ai_classify(expr, list_of_categories, return_error_details=None, **kwargs) (task_description, output_mode, examples en kwargs), ai_sentiment(text, categories=None, return_error_details=None), ai_complete(model, prompt, file=None, model_parameters=None, response_format=None, show_details=None, return_error_details=None), plus get(expr, field).
Chaque fonction renvoie une duckdb.FunctionExpression qui appelle la macro du même nom ; une chaîne est un littéral, une expression DuckDB est utilisée telle quelle ; les arguments None de fin sont retirés. L'indexation de colonne Snowpark (col["labels"][0]) n'existe pas sur les expressions DuckDB : utiliser get(expr, "labels"). file (forme image) lève NotImplementedError.
Signatures des fonctions de l'avenant (lues dans le même paquet le 2026-10-23) : prompt(template_string, *exprs) (une chaîne parmi exprs est un nom de colonne), ai_filter(predicate, file=None, return_error_details=None), ai_extract(input, response_format, scores=None, config=None) (scores vrai et config lèvent NotImplementedError), ai_summarize_agg(expr), ai_agg(expr, task_description), ai_similarity(input1, input2, **kwargs) (clé model), ai_embed(model, input), ai_translate(text, source_language, target_language, return_error_details=None), ai_count_tokens(function_name, input_text, model=None, options=None, return_error_details=None) et ai_redact(input, categories=None, mode=None, return_error_details=None). Pour ai_count_tokens et ai_redact, la façade remet les arguments dans l'ordre de la syntaxe SQL. Les fonctions d'agrégat s'utilisent dans rel.aggregate(...).
cortex_lab.cortex
Signatures de snowflake.cortex (snowflake-ml-python 2.6.0, lues dans le code du paquet le 2026-10-22 ; la page de documentation renvoyait 404) :
| Fonction | Retour | Différences au labo |
|---|---|---|
complete(model, prompt, *, options=None, session=None, stream=False, timeout=None, deadline=None) |
str (objet structuré sérialisé en JSON) |
stream=True et prompts de conversation refusés (NotImplementedError) ; session, timeout, deadline acceptés et ignorés |
sentiment(text, session=None) |
float entre -1 et 1 |
Comme dans le paquet Snowflake, le score vient d'un appel AI_COMPLETE avec une consigne fixe et un schéma {"score": number} ; au labo l'appel passe par l'alias cortex-sentiment |
classify_text(str_input, categories, session=None) |
chaîne JSON {"label": ...} |
Construite sur AI_CLASSIFY en mode single |
translate(text, from_language, to_language, session=None) |
str |
Construite sur AI_TRANSLATE ; langue source '' pour la détection |
summarize(text, session=None) |
str |
Construite sur AI_SUMMARIZE (forme texte) ; la page Snowflake donne « a column of string summaries », la forme str est une hypothèse ; entrée de 110 caractères ou moins renvoyée telle quelle |
embed_text_1024(model, text, session=None) |
list[float] |
Construite sur AI_EMBED ; noms de 1 024 dimensions seulement |
embed_text_768(model, text, session=None) |
list[float] |
Même signature, refuse toujours : aucun modèle de 768 dimensions derrière LiteLLM |
Ces fonctions acceptent un argument nommé client pour l'injection en test. Les erreurs lèvent une exception au lieu de renvoyer NULL. Non reproduit : extract_answer.
État chez Snowflake au 2026-10-24 : les noms CamelCase de snowflake.ml.cortex sont dépréciés au profit des noms snake_case, que seul le labo expose. BCR-2432 (bundle 2026_08, désactivé par défaut) fait échouer les fonctions SQL historiques SNOWFLAKE.CORTEX dans les comptes inactifs, sans redirection ; le module Python reste valable avec snowflake-ml-python 2.4.0 ou plus, dont le backend appelle les fonctions AI_*. SUMMARIZE a pour remplaçant AI_SUMMARIZE (reproduite au labo depuis le 2026-10-25), EXTRACT_ANSWER a pour remplaçant AI_EXTRACT.
Bascule CORTEX_BACKEND
cortex_lab/compat.py choisit le module à l'import selon CORTEX_BACKEND :
| Valeur | complete, sentiment, classify_text, translate, summarize, embed_text_768, embed_text_1024 |
snowpark_functions |
|---|---|---|
lab (défaut) |
cortex_lab.cortex |
cortex_lab.snowpark |
snowflake |
snowflake.cortex |
snowflake.snowpark.functions |
Toute autre valeur lève ValueError. Usage :
from cortex_lab.compat import complete, sentiment, classify_text
from cortex_lab.compat import snowpark_functions as F
Les paquets snowflake-ml-python et snowflake-snowpark-python ne sont pas des dépendances du labo : la valeur snowflake ne fonctionne que là où ils sont installés.
Branchement
- Connexion DuckDB du labo :
lab.duckdb_connect.connect(..., cortex=True)appelleregister. - dbt-duckdb :
plugins: [{module: cortex_lab.dbt_plugin}]dans le profil ;Plugin.configure_connectionappelleregistersur chaque connexion dbt. Le paquet est installé en mode éditable ([tool.hatch.build.targets.wheel]), doncuv run dbtle trouve sansPYTHONPATH. - Ingestion :
ingest/classify.pyappelle le cœurai_classify,ingest/mapping.pyappelle le cœurai_complete.
Ajouter une fonction
Par la commande /new-cortex-function <NOM> (.claude/skills/new-cortex-function/SKILL.md), avec un PLAN.md approuvé qui couvre la fonction :
- Lire la page de référence
https://docs.snowflake.com/en/sql-reference/functions/<nom>. - Écrire le contrat dans
references/contracts.md(signature, arguments optionnels, forme exacte du retour, erreurs, limites, modèle géré ou argument de modèle, existence en Snowpark), avec l'URL et la date de lecture. - Tests d'abord (agent
test-gate) avec le faux clienttests/cortex_lab/fake_litellm.py. - Implémentation (agent
implementer) : cœur danscortex_lab/core/functions/, alias dansrouting.pysi modèle géré, limites danslimits.py, UDF_ai_<nom>et entrée_ARITYdanscortex_lab/duckdb/__init__.py, macros dansmacros.sql, façadessnowpark.pyetcortex.pysi l'équivalent existe, alias LiteLLM créé sur le NUC. /gate.
Fonctions dont la page a été lue le 2026-10-23 mais qui ne sont pas reproduites (fonctions sur fichiers, décision A1) : AI_PARSE_DOCUMENT, TO_FILE. Vue d'ensemble : https://docs.snowflake.com/en/user-guide/snowflake-cortex/aisql (lue le 2026-10-23 ; les fenêtres de contexte par modèle n'y figurent pas).
Écarts connus
AI_COMPLETE: forme chaîne seulement ; retour VARCHAR dans DuckDB ; Cortex Guard non reproduit.AI_COMPLETE, cache des réponses : depuis le 2026-09-29, Snowflake réutilise, au mieux et dans une même requête, les réponses d'appels texte identiques et déterministes, sans les facturer. Le labo appelle le modèle à chaque ligne : écart nouveau, à traiter par avenant (le contrat de retour est inchangé, le nombre d'appels et le coût diffèrent).- Modèles
legacydu 2026-08-12 : un compte qui n'utilisait pas un modèle avant cette date ne peut plus l'appeler. Le labo n'embarque aucun nom de modèle Snowflake par défaut ; chez le client, le modèle deCORTEX_COMPLETE_MODELdoit être hors de la liste de https://docs.snowflake.com/en/release-notes/bcr-bundles/un-bundled/bcr-august-model-deprecations (lue le 2026-10-24). snowflake.cortex.sentimentrenvoie unfloat, pas l'objet d'AI_SENTIMENT(fidèle au paquet Snowflake, mais à ne pas confondre).CREATE AI FUNCTION: écart connu, aucune instruction de ce type n'est reconnue par DuckDB au labo. Description, droits et conséquence pour le code du client dans portabilite-snowflake.md, ligne «CREATE AI FUNCTIONabsente du labo ».AI_CLASSIFY: le labo refuse plus de 100 catégories pour toutes les formes, alors que la page SQL n'en fixe aucune pour le texte (décision S1, la page Snowpark impose 2 à 100) ; plus strict que Snowflake, donc sans risque de portabilité.AI_SUMMARIZE: comportement d'erreur inféré de BCR-2184 (NULL par ligne, objet avecreturn_error_details), à confirmer chez le client ; la longueur et la forme du résumé dépendent du modèle local.multilingual-e5-large: servi parbge-m3comme les autres noms de 1 024 dimensions, sans la fenêtre de 512 jetons de Snowflake ni les préfixesquery:etpassage:du modèle d'origine.- Entrées texte seulement : images et fichiers (
TO_FILE) non reproduits ; l'objetPROMPTn'est consommé que parAI_FILTER(pas parAI_CLASSIFYniAI_COMPLETE). - Fenêtre de contexte : celle du modèle local, bien plus petite que celle des modèles Snowflake pour
AI_TRANSLATE(100 000 jetons) etAI_SUMMARIZE(128K à 1M jetons selon la région) ; l'erreur est par ligne, jamais une troncature. - Vecteurs d'
AI_EMBED: mêmes dimensions que Snowflake pour les noms de 1 024, valeurs jamais comparables à celles du client ; un vecteur DuckDB est unFLOAT[1024], pas unVECTOR. AI_REDACTdu labo n'est pas un contrôle de sécurité : le modèle local manque des PII.- Types de retour des macros : une macro DuckDB n'a qu'un type de retour par surcharge, d'où des
JSONou desVARCHARlà où Snowflake renvoie un type selonreturn_error_details(voir plus haut). - Comptage de jetons estimé (4 caractères par jeton,
AI_COUNT_TOKENScomprise), pas de tokenizer du modèle géré. GETrenvoie VARCHAR au lieu deVARIANT.- La qualité des réponses diffère (modèle local
qwen3:8b) : les tests vérifient le contrat, jamais le contenu.