--- name: suggest_aggregations_llm kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def suggest_aggregations_llm(profile: dict, candidates: dict, max_aggs: int = 4, model: str = \"claude-haiku-4-5-20251001\") -> dict" description: "MUST-11.1 del capitulo AGREGACION del AutomaticEDA (grupo eda). Dado el TableProfile de una tabla y los candidatos cuantitativos de select_groupby_keys ({group_keys:[{col,cardinality,score}], measures:[str], pivots:[{index,columns,value}]}), con UNA sola llamada al LLM elige y ordena las K agregaciones (GROUP BY categorica x medidas numericas) y los pivots MAS INFORMATIVOS para un analisis de grupos, con una razon corta cada uno, evitando la explosion combinatoria (no todo contra todo). Privacidad/coste: NO envia filas crudas, solo el resumen AGREGADO de los candidatos (tabla, columnas categoricas con cardinalidad/score, medidas, pivots). Reusa ask_llm del grupo claude-direct (API directa con token OAuth de Claude). Impura, dict-no-throw: NUNCA lanza y SIEMPRE devuelve algo usable; si el LLM falla, el JSON no parsea o no hay seleccion valida, cae a un fallback determinista construido desde los candidatos (source='fallback'). Toda columna que el LLM invente se descarta." tags: [eda, claude-direct, llm, aggregation, groupby, pivot, datascience, automatic-eda] params: - name: profile desc: "TableProfile del grupo eda. Solo se usa profile['table'] para nombrar la tabla en el prompt; puede ir vacio o sin esa clave (se usa '(tabla sin nombre)')." - name: candidates desc: "Salida de select_groupby_keys: {group_keys:[{col, cardinality, score}], measures:[str], pivots:[{index, columns, value}]}. group_keys = columnas categoricas candidatas para GROUP BY; measures = columnas numericas a agregar (sum/avg); pivots = cruces index x columns -> value sugeridos. Cualquier columna que el LLM elija debe existir aqui o se descarta. None o no-dict se trata como vacio." - name: max_aggs desc: "Tope de agregaciones a devolver. Default 4. Valores <1 o no-int se normalizan a 4. Limita tanto la seleccion del LLM como el fallback determinista, para evitar la explosion combinatoria." - name: model desc: "id del modelo Anthropic a usar en la unica llamada. Default 'claude-haiku-4-5-20251001' (haiku, coste bajo, ~2-3s). Para razones mas finas, pasar p.ej. 'claude-opus-4-8'." output: "dict dict-no-throw: {status:'ok', source:'llm'|'fallback', aggregations:[{group_by:str, measures:[str], why:str}], pivots:[{index:str, columns:str, value:str|None, why:str}], note:str}. source=='llm' si el LLM produjo al menos una agregacion valida (columnas existentes en candidates); en cualquier otro caso (LLM caido, JSON invalido, seleccion vacia, sin candidatos) source=='fallback' y aggregations/pivots se derivan de candidates con why='selección cuantitativa (sin LLM)'. NUNCA lanza." uses_functions: [ask_llm_py_core, select_groupby_keys_py_datascience] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: true tests: ["test_extract_json_object", "test_extract_json_wrapped_in_fences_and_junk", "test_extract_json_non_json_returns_none", "test_validate_aggregations_drops_invalid_columns", "test_llm_path_uses_selection", "test_llm_path_respects_max_aggs", "test_llm_invented_column_is_discarded", "test_fallback_on_empty_llm_response", "test_fallback_on_unparseable_response", "test_fallback_respects_max_aggs", "test_fallback_when_llm_raises", "test_no_candidates_returns_empty_fallback", "test_non_dict_candidates_does_not_raise"] test_file_path: "python/functions/datascience/suggest_aggregations_llm_test.py" file_path: "python/functions/datascience/suggest_aggregations_llm.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from datascience.suggest_aggregations_llm import suggest_aggregations_llm profile = {"table": "ventas"} # candidates = salida de select_groupby_keys (aqui literal de ejemplo). candidates = { "group_keys": [ {"col": "categoria", "cardinality": 8, "score": 0.91}, {"col": "region", "cardinality": 5, "score": 0.74}, {"col": "canal", "cardinality": 3, "score": 0.60}, ], "measures": ["importe", "unidades"], "pivots": [ {"index": "categoria", "columns": "region", "value": "importe"}, ], } out = suggest_aggregations_llm(profile, candidates, max_aggs=4) # haiku por defecto print("fuente:", out["source"]) # "llm" o "fallback" si no hay red for agg in out["aggregations"]: print(f"GROUP BY {agg['group_by']} -> {agg['measures']} ({agg['why']})") for piv in out["pivots"]: print(f"pivot {piv['index']} x {piv['columns']} = {piv['value']} ({piv['why']})") ``` ## Cuando usarla Justo despues de `select_groupby_keys` en el capitulo AGREGACION del AutomaticEDA: cuando ya tienes los candidatos cuantitativos (columnas categoricas con cardinalidad, medidas numericas y pivots posibles) y quieres que un LLM se quede con las K agregaciones y pivots MAS INFORMATIVOS en vez de generar "todo contra todo". Usala para priorizar el plan de analisis de grupos antes de materializar las tablas con `aggregate_by_group` / pivots, manteniendo el coste y el ruido bajos. Si no hay red o credenciales, sigue funcionando con un fallback determinista, asi que es seguro ponerla en un pipeline. ## Gotchas - **Impura: hace 1 llamada de red al LLM.** No es determinista ni gratis. Latencia tipica ~2-3s con haiku. Una sola llamada cubre toda la seleccion. - **Requiere token OAuth de Claude** en `~/.claude/.credentials.json` (via `ask_llm` / grupo `claude-direct`). Sin token / sin red NO lanza: cae al **fallback determinista** (`source="fallback"`) construido desde `candidates` (group_keys x measures hasta `max_aggs`, pivots tal cual) con `why="selección cuantitativa (sin LLM)"`. Comprueba `out["source"]` para saber si la seleccion vino del LLM o del fallback. - **NO envia filas crudas al LLM**, solo el resumen AGREGADO de los candidatos. Esto exige que `candidates` venga ya calculado por `select_groupby_keys` (cardinalidades, scores, medidas, pivots). - **Valida columnas inventadas**: si el LLM propone un `group_by`/`measure`/`index`/ `columns` que no esta en `candidates`, esa entrada se descarta (las medidas se recortan a las validas). Si tras validar no queda ninguna agregacion, cae al fallback completo. - **`max_aggs` acota la explosion combinatoria** tanto en el camino LLM como en el fallback. Subirlo demasiado reintroduce el ruido que esta funcion evita. - **Modelo `haiku` por defecto** para coste bajo; sube a `claude-opus-4-8` si necesitas razones (`why`) mas finas (mas caro y lento).