---
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).
