feat(infra): auto-commit con 56 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:22:55 +02:00
parent c1071a82b3
commit 32c7336bf6
56 changed files with 5307 additions and 100 deletions
+3 -1
View File
@@ -42,6 +42,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [sink](sink.md) | 11 | Funciones que escriben datos a destino externo (BD, dashboard, alerta, email). Nodos output |
| [validator](validator.md) | 6 | Funciones que verifican datos/config contra reglas. Pre-flight de sinks y gates en DAGs |
| [navegator](navegator.md) | 4 | Automatización de browser via CDP + AX tree + LLM: obtener, limpiar, chunkear AX tree y llamar a Claude CLI |
| [img-to-3d](img-to-3d.md) | 3 | Imagen 2D -> modelo 3D: profundidad monocular (Depth-Anything-V2) + malla de relieve texturizada exportada a .glb, con pipeline one-shot. Produce el glb que mesh-3d consume/renderiza |
| [whatsapp](whatsapp.md) | 3 | Operar WhatsApp Web por CDP sobre la pestaña existente (sin ventana ni foco): buscar/abrir chat, leer conversacion, enviar texto. Compone 4 primitivas CDP-Python (cdp_eval/type_chars/press_key/click_xy). No HTTP: WhatsApp usa WebSocket + cifrado E2E |
| [cpp-dashboard-viz](cpp-dashboard-viz.md) | 10 | Primitivas C++ ImGui para dashboards: kpi_card, sparkline, line/bar/scatter/pie/heatmap/histogram, panel containers |
| [agents](agents.md) | 3 | Orquestar agentes Claude headless en git worktrees: launch, cleanup, DoD evidence schema audit |
@@ -64,8 +65,9 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [consent](consent.md) | 3 | CMP / IAB TCF / data brokers: detectar el CMP de un sitio (Didomi/OneTrust/Sourcepoint/Quantcast), leer `__tcfapi` para contar vendors y propositos, aceptar el banner (selectores + fallback LLM con haiku que localiza Aceptar/Ver socios), y descargar la GVL de IAB para nominar cada broker y que datos recopila. Nacio de `projects/databrokers/` |
| [onlyoffice](onlyoffice.md) | 3 | Operar ONLYOFFICE Desktop Editors (binario onlyoffice-desktopeditors) en Linux/X11 desde terminal via instancia aislada (slot HOME=/tmp/oo_<instance>): abrir un archivo en ventana propia, cerrar+reabrir para mostrar datos editados en disco (no hay reload nativo, Issue #2313), y matar el proceso del slot. Solo gestiona la ventana, NO edita ni crea archivos. Requiere X11 + wmctrl + xdotool. No confundir con el Document Server (web/Docker) |
| [email](email.md) | 21 | Gestionar cuentas de correo por IMAP+SMTP directo (Python stdlib, sin browser ni MCP Gmail): conectar/listar/buscar/leer (imap_*), mutar estado (mark_seen/move/delete/save_draft) por UID, y construir+enviar (email_build_html/smtp_send). Auth user+app-password (NO OAuth; Outlook fuera). Credenciales desde pass, resueltas por la capa app. Complementa al browser (interactivo) — no lo reemplaza |
| [eda](eda.md) | 8 | Exploratory Data Analysis por tabla con motor DuckDB push-down: perfil base SQL (SUMMARIZE), estadística numérica/categórica sobre muestra, tipo semántico por regex, score de calidad, render markdown con sparklines y el orquestador one-shot `profile_table` (promueve VARCHAR→numeric/datetime, emite TableProfile + report md/json). Fases siguientes: correlaciones, relaciones inter-tabla, modelos baratos, LLM, notebook |
| [eda](eda.md) | 27 | Exploratory Data Analysis por tabla y base con motor DuckDB + PostgreSQL push-down: perfil base SQL (SUMMARIZE + distinct exacto), estadística numérica/categórica, tipo semántico regex, calidad, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK containment + join graph mermaid), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM (dictionary/PII/limpieza/análisis) y generación de notebook. Orquestadores `profile_table` (backend duckdb/postgres, flags run_models/run_llm) y `profile_database` |
| [seo](seo.md) | 3 | SEO orientado a datos sobre Google Search Console: autenticar con service account (`gsc_auth`), extraer Search Analytics paginado (`pull_gsc_search_analytics`) y el pipeline de ingesta a DuckDB + espejo Postgres para Metabase (`ingest_gsc_search_analytics`). Cadena de ingesta del proyecto `seo_analytics`; alimenta dashboards de striking distance, CTR opportunities y content decay |
| [local-hub](local-hub.md) | 4 | Exponer los procesos locales como subdominios `*.localhost` (via Caddy, sin DNS) y reunirlos en una pantalla principal Glance con estado en vivo, refrescada a diario por dag_engine. Descubre servicios (manifiesto + registry), renderiza Caddyfile + config Glance (puras), y el pipeline `refresh_local_hub` regenera+recarga. Fuente de verdad: `apps/local_hub/local_services.yaml` |
## Como anadir grupo
+2 -1
View File
@@ -19,6 +19,7 @@ Pieza central del patron **BD como fuente de verdad + Obsidian como vista** (pro
| `duckdb_table_schema_py_infra` | `duckdb_table_schema(db_path, table) -> dict` | Introspección read-only: schema de una tabla (`DESCRIBE`). Devuelve `{status, table, columns:[{name,type}]}`. Útil para mapear tipos a otro motor (p.ej. PostgreSQL). |
| `excel_to_duckdb_py_infra` | `excel_to_duckdb(xlsx_path, duckdb_path, table, sheet=None, mode='replace') -> dict` | **Puente de entrada Excel→DuckDB**: ingiere una hoja `.xlsx` a una tabla con la extensión nativa `excel` de DuckDB. `replace`/`append`. Devuelve `{status, table, row_count}`. |
| `duckdb_to_postgres_py_pipelines` | `duckdb_to_postgres(duckdb_path, table, pg_dsn, pg_table=None, mode='replace', key_cols=None, batch_size=5000) -> dict` | **Puente de salida DuckDB→Postgres**: mapea tipos, crea la tabla y sincroniza filas. Desbloquea que Metabase/Grafana/Superset (que no hablan DuckDB) lean los datos. Devuelve `{status, pg_table, rows_synced, created}`. |
| `query_osint_db_py_datascience` | `query_osint_db(sql, base_url='http://127.0.0.1:8771', timeout=30) -> dict` | **Cliente HTTP del service `osint_db`**: hace `POST {base_url}/api/query` con `{"sql": sql}` y devuelve `{status, columns, rows, row_count, truncated}` sin lanzar (mismo estilo que `duckdb_query_readonly`). Vía correcta para leer la DuckDB maestra del proyecto `osint` desde otro proceso sin abrir el archivo (respeta el single-writer). Service caído → `{status:'error', error}` claro. Solo stdlib. |
## Puentes: Excel → DuckDB → Postgres → visualización
@@ -79,7 +80,7 @@ Conversion CSV -> Parquet en una linea:
## Gotchas del grupo
- **Single-writer**: DuckDB permite UN solo proceso escritor por archivo. Si un service (ej. `osint_db`) posee la base, el resto de procesos deben leer con `read_only=True` (`duckdb_query_readonly` ya lo hace) o pasar por la API HTTP del service. Las funciones de escritura (`duckdb_execute`, `duckdb_upsert`) abren en read-write y SOLO debe usarlas el proceso dueño de la base (dentro de su write lock), nunca un cliente concurrente.
- **Single-writer**: DuckDB permite UN solo proceso escritor por archivo. Si un service (ej. `osint_db`) posee la base, el resto de procesos deben leer con `read_only=True` (`duckdb_query_readonly` ya lo hace) o pasar por la API HTTP del service (`query_osint_db` para `osint_db`). Las funciones de escritura (`duckdb_execute`, `duckdb_upsert`) abren en read-write y SOLO debe usarlas el proceso dueño de la base (dentro de su write lock), nunca un cliente concurrente.
- **Version del motor**: el formato de archivo puede cambiar entre versiones mayores de DuckDB. El venv del registry lleva `duckdb` 1.5.x; no mezclar con CLIs/WASM antiguos sobre el mismo archivo.
- `read_only=True` exige que el archivo exista — no crea bases nuevas.
+100 -48
View File
@@ -1,80 +1,132 @@
# eda — Exploratory Data Analysis por tabla
# eda — Exploratory Data Analysis por tabla y base
Grupo de capacidad para perfilar tablas y entender datasets nuevos rápido, repetible y sin reinventar lógica. Motor **DuckDB SQL push-down**: los agregados (`SUMMARIZE`, `COUNT DISTINCT`, percentiles) se calculan en SQL sin traer las filas a RAM; solo una muestra pequeña baja a Python para lo estadístico fino (skew, kurtosis, histograma, outliers).
Grupo de capacidad para perfilar tablas y bases de datos completas y entender datasets nuevos rápido, repetible y sin reinventar lógica. Motor **DuckDB SQL push-down**: los agregados (`SUMMARIZE`, `COUNT DISTINCT`, `corr()`, percentiles) se calculan en SQL sin traer las filas a RAM; solo una muestra pequeña baja a Python para lo estadístico fino (skew, kurtosis, histograma, correlación mixta, modelos).
El orquestador one-shot es `profile_table_py_pipelines`: "hazme un EDA de esta tabla" → un `TableProfile` completo + report markdown + JSON sidecar en `reports/`.
Orquestadores one-shot:
- `profile_table_py_pipelines` — "hazme un EDA de esta tabla" → `TableProfile` completo + report markdown + JSON. Flags `run_models` (modelos baratos) y `run_llm` (interpretación LLM).
- `profile_database_py_pipelines` — "hazme un EDA de esta base" → perfila todas las tablas + infiere FK + join graph (mermaid).
> Cuando Enmanuel pide un EDA, el flujo acordado es: perfilar con este grupo, escribir el report, y **generar un analysis Jupyter lanzado en el navegador colaborativo y ejecutado por Claude** para verlo en vivo. Ver la memoria `eda-workflow-registry` y la regla `notebook_collaboration.md`.
## Funciones
### Perfilado base (tabla y columna)
| ID | Pureza | Qué hace |
|---|---|---|
| `summarize_table_duckdb_py_datascience` | impure | Corazón: `SUMMARIZE` push-down → esqueleto del `TableProfile` con perfil base por columna (tipo inferido, nulls, distinct exacto ≤200k filas, flags). Reusa `duckdb_query_readonly`. |
| `describe_numeric_py_datascience` | pure | Bloque `numeric` sobre una muestra: min/max/mean/median/mode/std/cv, percentiles p1-p99, IQR, skew, kurtosis, outliers, %zeros/%neg, tipo de distribución, histograma. |
| `summarize_categorical_py_datascience` | pure | Bloque `categorical`: top-k frecuencias, mode, distinct, entropía de Shannon (bits), imbalance, longitudes. |
| `infer_semantic_type_py_datascience` | pure | Tipo semántico por regex (email/url/ip/uuid/iban/currency/datetime/integer/decimal/...) sin LLM. Primera pasada barata. |
| `column_quality_score_py_datascience` | pure | Score de calidad 0-100 (completeness/validity/consistency) + issues legibles para un `ColumnProfile`. |
| `render_eda_markdown_py_datascience` | pure | `TableProfile` → report markdown autosuficiente (Overview, Columnas, Numéricas con sparkline ASCII, Categóricas, Calidad). |
| `summary_stats_py_datascience` | pure | Descriptiva mínima (n, mean, median, p25, p75) de una lista de floats. |
| `profile_table_py_pipelines` | pipeline | Orquestador end-to-end: compone todo lo anterior, promueve tipos VARCHAR→numeric/datetime por contenido, y emite `TableProfile` + report markdown + JSON. |
| `summarize_table_duckdb_py_datascience` | impure | Corazón (DuckDB): `SUMMARIZE` push-down + `COUNT DISTINCT` exacto (≤200k filas) → esqueleto del `TableProfile`. |
| `summarize_table_pg_py_datascience` | impure | Adaptador PostgreSQL: mismo esqueleto `TableProfile` vía SQL push-down (information_schema + count/distinct/min/max/avg/stddev/percentile_cont). |
| `describe_numeric_py_datascience` | pure | Bloque numérico: min/max/mean/median/std/cv, p1-p99, IQR, skew, kurtosis, outliers, distribución, histograma. |
| `summarize_categorical_py_datascience` | pure | top-k frecuencias, mode, distinct, entropía, imbalance, longitudes. |
| `infer_semantic_type_py_datascience` | pure | Tipo semántico por regex (email/url/ip/uuid/iban/currency/datetime/...). |
| `column_quality_score_py_datascience` | pure | Score 0-100 (completeness/validity/consistency) + issues. |
| `render_eda_markdown_py_datascience` | pure | `TableProfile` → report markdown con sparklines ASCII. |
| `summary_stats_py_datascience` | pure | Descriptiva mínima (n, mean, median, p25, p75). |
### Correlación / asociación
| ID | Pureza | Qué hace |
|---|---|---|
| `pearson_py_datascience` | pure | Correlación lineal num↔num (preexistente). |
| `spearman_corr_py_datascience` | pure | Correlación de rangos (monotónica no lineal) num↔num. |
| `cramers_v_py_datascience` | pure | Asociación simétrica cat↔cat (corrección Bergsma-Wicher). |
| `theils_u_py_datascience` | pure | Asociación direccional U(a\|b) cat↔cat. |
| `correlation_ratio_py_datascience` | pure | η: cuánto explica una categórica a una numérica. |
| `mutual_info_columns_py_datascience` | pure | Información mutua (no lineal, general) entre cualquier par. |
| `association_matrix_py_datascience` | pure | Matriz unificada: elige métrica por par de tipos + pares fuertes. |
| `correlation_matrix_duckdb_py_datascience` | impure | Matriz Pearson push-down (`corr()` SQL) para muchas filas. |
### Relaciones inter-tabla
| ID | Pureza | Qué hace |
|---|---|---|
| `infer_fk_containment_duckdb_py_datascience` | impure | Infiere FK candidatas por containment de valores (inclusion coefficient). |
| `build_join_graph_py_datascience` | pure | FK candidates → grafo (roles fact/dimension) + diagrama Mermaid. |
### Modelos baratos (flag `run_models`)
| ID | Pureza | Qué hace |
|---|---|---|
| `pca_explained_py_datascience` | pure | PCA: varianza explicada + loadings + proyección. |
| `kmeans_segments_py_datascience` | pure | Segmentos naturales, auto-k por silhouette. |
| `isolation_forest_outliers_py_datascience` | pure | Outliers multivariante (filas anómalas). |
| `normality_tests_py_datascience` | pure | Jarque-Bera + D'Agostino + Shapiro → ¿normal? |
| `trend_slope_py_datascience` | pure | Tendencia de una serie (up/down/flat) por regresión lineal. |
| `run_eda_models_py_datascience` | pure | Wrapper: compone PCA + KMeans + IsolationForest + normalidad → bloque `models`. |
### Capa LLM y entrega
| ID | Pureza | Qué hace |
|---|---|---|
| `eda_llm_insights_py_datascience` | impure | 1 call LLM sobre el perfil agregado (no filas crudas): data dictionary, resumen, granularidad de fila, PII/RGPD, limpieza, análisis sugeridos. |
| `build_eda_notebook_py_datascience` | impure | Genera un `.ipynb` (nbformat v4) que perfila la tabla, listo para lanzar en Jupyter colaborativo. |
### Orquestadores (pipelines)
| ID | Qué hace |
|---|---|
| `profile_table_py_pipelines` | EDA de una tabla end-to-end, `backend="duckdb"` (default) o `"postgres"` (base + correlación + `run_models` + `run_llm`) → JSON + markdown. |
| `profile_database_py_pipelines` | EDA de una base entera: todas las tablas + FK + join graph. |
## Contrato de datos
Todas las funciones producen/consumen el mismo shape (dict JSON), lo que desacopla cálculo, render y (futuro) LLM:
```
TableProfile = {
table, source, profiled_at, n_rows, n_cols, size_bytes,
duplicate_rows, duplicate_pct, constant_cols:[str], all_null_cols:[str],
null_cell_pct, type_breakdown:{numeric,categorical,datetime,text,boolean},
columns:[ColumnProfile], correlations, key_candidates:[str],
quality_score, llm, models
}
TableProfile = {table, source, profiled_at, n_rows, n_cols, size_bytes,
duplicate_rows, duplicate_pct, constant_cols, all_null_cols, null_cell_pct,
type_breakdown:{numeric,categorical,datetime,text,boolean},
columns:[ColumnProfile], correlations, key_candidates, quality_score, llm, models}
ColumnProfile = {
name, physical_type, inferred_type, # numeric|categorical|datetime|boolean|text|id
semantic_type, count, n_rows, null_count, null_pct, empty_count, empty_pct,
distinct_count, unique_pct, # *_pct son FRACCIONES 0-1; el render las muestra ×100
flags:[constant|possible_id|high_cardinality|mostly_null],
quality_score,
numeric: {min,max,mean,median,mode,std,variance,cv,p1,p5,p25,p50,p75,p95,p99,iqr,
skew,kurtosis,n_outliers,outlier_pct,zero_pct,negative_pct,distribution_type,
histogram:[{lo,hi,count}]} | None,
categorical: {top:[{value,count,pct}],mode,mode_pct,n_distinct,entropy,imbalance,
len_mean,len_min,len_max} | None,
datetime: {min,max,range_days,granularity,n_gaps,future_pct,monotonic} | None
}
ColumnProfile = {name, physical_type, inferred_type, semantic_type, count, n_rows,
null_count, null_pct, empty_count, empty_pct, distinct_count, unique_pct,
flags:[constant|possible_id|high_cardinality|mostly_null], quality_score,
numeric:{...}|None, categorical:{...}|None, datetime:{...}|None}
# *_pct son FRACCIONES 0-1; el render las muestra ×100
correlations = {pairs:[{a,b,a_type,b_type,method,value,extra}], strong:[...], methods_legend}
models = {n_numeric_cols, pca, kmeans, outliers, normality, note}
llm = {summary, row_meaning, dictionary:[{column,description,business_meaning,unit}],
pii:[{column,kind,severity}], cleaning:[str], analyses:[str]}
```
## Ejemplo canónico
EDA de una tabla DuckDB en una línea (escribe `reports/eda_<table>_<ts>.md` + `.json`):
EDA completo de una tabla (estadística + correlación + modelos + LLM + report):
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from pipelines.profile_table import profile_table
r = profile_table(os.path.expanduser("~/.fn_freelance/freelance.duckdb"), "freelance_projects")
print(r["status"], r["report_md_path"])
r = profile_table("/ruta/datos.duckdb", "clientes", run_models=True, run_llm=True)
prof = r["profile"]
print(prof["type_breakdown"], "key_candidates:", prof["key_candidates"], "calidad:", prof["quality_score"])
print(r["report_md_path"]) # reports/eda_clientes_<ts>.md
print(prof["correlations"]["strong"]) # pares correlacionados
print(prof["models"]["kmeans"]["best_k"]) # segmentos
print(prof["llm"]["row_meaning"]) # qué representa 1 fila
```
La promoción de tipo por contenido resuelve el caso típico de scrapers/CSV donde los números y fechas llegan como `VARCHAR`: `bids` ('10','20') se detecta `integer` y se perfila como numérica (mean/median/percentiles); `scraped_at` se detecta `datetime_iso`.
EDA de una base entera con relaciones:
```python
from pipelines.profile_database import profile_database
r = profile_database("/ruta/datos.duckdb") # todas las tablas
print(r["db_profile"]["join_graph"]["mermaid"]) # diagrama de relaciones FK
```
Notebook ejecutable:
```python
from datascience import build_eda_notebook
build_eda_notebook("/ruta/datos.duckdb", "clientes", "/tmp/eda.ipynb", run_models=True)
```
## Fronteras
- **NO carga la tabla entera a RAM**: solo metadata SQL + una muestra (`sample`, default 5000) por columna. Para distribución exacta de una columna enorme, sube `sample` o consulta SQL directa.
- **Distinct exacto solo hasta 200k filas**; por encima usa aproximado (HyperLogLog) capado a nº de filas.
- **Solo DuckDB** por ahora (CSV/Parquet/Excel entran gratis vía `read_csv_auto`/`read_parquet`/`read_xlsx` cargándolos antes a DuckDB). PostgreSQL y BigQuery requieren adaptador (pendiente).
- **No es estadística inferencial ni modelado**: es perfilado descriptivo. Correlaciones, modelos baratos (PCA/KMeans/IsolationForest) y capa LLM son fases siguientes del grupo.
- **NO carga la tabla entera a RAM**: metadata SQL + muestra por columna/filas (`sample`, default 5000).
- **Distinct exacto hasta 200k filas**; por encima aproximado capado.
- **Correlación de tabla** se calcula sobre la muestra de filas alineadas; excluye columnas id-like (alta cardinalidad) para evitar asociación espuria. `correlation_matrix_duckdb` ofrece Pearson push-down exacto a escala si hace falta.
- **Modelos** (`run_models`) requieren ≥2 columnas numéricas para PCA/KMeans/IsolationForest; normalidad funciona con 1.
- **LLM** (`run_llm`) hace 1 llamada (haiku) y envía solo el perfil agregado, nunca filas crudas; requiere token OAuth de Claude.
- **Fuentes**: DuckDB nativo (CSV/Parquet/Excel cargándolos antes a DuckDB) y **PostgreSQL** (`backend="postgres"`, DSN vía `resolve_pg_dsn`). BigQuery pendiente. `profile_database` (multi-tabla + FK) es solo DuckDB por ahora.
## Roadmap (fases siguientes)
## Estado
- **Correlación / asociación**: Spearman, Cramér's V, Theil's U, correlation ratio η², Mutual Information, VIF → `correlations` del `TableProfile`.
- **Relaciones inter-tabla**: FK inference por containment, cardinalidad de relación, join graph (mermaid), star-schema hints → `profile_database`.
- **Modelos baratos** (flag `--models`, sklearn/scipy): PCA 2D, KMeans + silhouette, Isolation Forest, feature importance, tests de normalidad, tendencia temporal.
- **Capa LLM** (flag `--llm`, grupo `claude-direct`): data dictionary, resumen ejecutivo (qué es 1 fila + granularidad), flag PII/RGPD, limpieza sugerida, análisis sugeridos.
- **Entrega notebook**: analysis Jupyter auto-generado y ejecutado en el navegador colaborativo.
Implementado y validado end-to-end (152 tests verdes): perfilado base, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK + join graph), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM y generación de notebook.
Validado sobre PostgreSQL real (tablas del Metabase local del proyecto captacion_clientes).
Pendiente: adaptador BigQuery; `profile_database` multi-tabla para PostgreSQL (hoy solo DuckDB); perfil fino de columnas datetime (`profile_datetime`); excluir columnas numéricas `possible_id` de la matriz de asociación (hoy solo se excluyen las categóricas id-like).
+84
View File
@@ -0,0 +1,84 @@
---
group: img-to-3d
description: "Convertir una imagen 2D en un modelo 3D: estimacion de profundidad monocular (Depth-Anything-V2) + reconstruccion de una malla de relieve texturizada exportada a glTF binario (.glb)."
---
# img-to-3d — Capability Group
Cluster de funciones Python (dominio `datascience`) para el flujo **imagen 2D → modelo 3D**. A
partir de una sola foto se estima un mapa de profundidad monocular con un modelo de vision y se
reconstruye una malla de relieve (heightmap) texturizada con la imagen original, exportada como
`.glb` cargable por cualquier visor glTF (three.js `useGLTF`/`GLTFLoader`, Babylon, model-viewer).
Promovido desde la app `img_to_3d_webapp` (su backend incrustaba estas dos funciones; ver su
`backend/depth.py`). El flujo canonico es de **dos pasos encadenados**:
```
estimate_image_depth (imagen -> depth+image) -> depth_to_relief_glb (depth+image -> .glb)
```
## Funciones
| ID | Firma corta | Que hace |
|---|---|---|
| `estimate_image_depth_py_datascience` | `estimate_image_depth(image_path, model_name?, device?, use_cache?) -> dict` | Estima profundidad monocular con Depth-Anything-V2 (GPU/CPU). Devuelve `depth` ndarray [0,1] + `image` PIL. Paso 1. |
| `depth_to_relief_glb_py_datascience` | `depth_to_relief_glb(image, depth, out_glb_path, z_scale?, max_dim?) -> dict` | Convierte `depth`+`image` en una malla de relieve texturizada y la exporta a `.glb`. Paso 2. |
| `build_relief_glb_from_image_py_pipelines` | `build_relief_glb_from_image(image_path, out_glb_path, model_name?, device?, z_scale?, max_dim?) -> dict` | **Pipeline one-shot**: compone los dos pasos en una sola llamada (imagen -> .glb). Salida JSON-serializable, apta para `fn run`. |
Las tres son **impuras** (cargan modelo / GPU / escriben archivo), devuelven `dict` con `status`
(`ok`/`error`) y **nunca lanzan**: los fallos vuelven como `{status:'error', error:str}`. El
pipeline ademas marca `stage` (`estimate`/`relief`) en el error.
## Ejemplo canonico (end-to-end imagen → glb)
```python
# Requiere un venv con torch + transformers + trimesh + pillow + numpy.
# Import PLANO: el paquete datascience.__init__ arrastra deps de otros dominios (bs4, duckdb...)
# ausentes en el venv de vision. Ver "Fronteras / gotchas".
import sys
sys.path.insert(0, "python/functions/datascience")
from estimate_image_depth import estimate_image_depth
from depth_to_relief_glb import depth_to_relief_glb
IMG = "apps/img_to_3d_webapp/samples/cats.jpg"
OUT = "/tmp/cats_relief.glb"
est = estimate_image_depth(IMG) # device='auto' -> GPU si hay
assert est["status"] == "ok"
# est["depth"]: ndarray HxW float32 [0,1] (1=mas cerca) | est["image"]: PIL.Image RGB
res = depth_to_relief_glb(est["image"], est["depth"], OUT, z_scale=0.35, max_dim=220)
assert res["status"] == "ok"
print(res["glb_path"], res["vertices"], res["faces"]) # /tmp/cats_relief.glb 36300 71832
# OUT es un glTF binario valido: trimesh.load(OUT) devuelve una Scene texturizada.
```
O en una sola llamada con el pipeline (recomendado para fn run / Launcher TUI):
```bash
./fn run build_relief_glb_from_image_py_pipelines apps/img_to_3d_webapp/samples/cats.jpg /tmp/cats_relief.glb
# {"status": "ok", "glb_path": "/tmp/cats_relief.glb", "vertices": 36300, "faces": 71832, ...}
```
## Fronteras
- **Es relieve 2.5D, no reconstruccion volumetrica.** Deforma un plano segun la profundidad
(heightmap); no recupera caras ocultas ni el volumen trasero del objeto. Para 3D real
multivista/fotogrametria, NSP/Gaussian Splatting, esto NO aplica.
- **Profundidad relativa, no metrica.** Depth-Anything devuelve disparidad normalizada a [0,1];
no comparable entre imagenes ni en unidades del mundo real.
- **No cubre el render/visualizacion.** Producir el `.glb` es el limite del grupo. Cargarlo y
subirlo a GPU (OpenGL) en una app C++/ImGui es el grupo **`mesh-3d`** (`gltf_load_mesh_cpp_gfx`
carga justamente este tipo de `.glb`). img-to-3d **produce**; mesh-3d **consume/renderiza**.
- **Deps pesadas y de dos mundos.** Requiere `torch`+`transformers` (vision) y `trimesh` (mesh),
que hoy viven en el venv de `img_to_3d_webapp`, NO en el venv del registry. Ademas el
`datascience.__init__` arrastra deps de scrapers (`bs4`...) que no estan en el venv de vision,
por eso el import es **plano** (al modulo) y no via el paquete. `fn run` de estas funciones
exige un venv que combine ambos mundos (torch + transformers + trimesh + las deps del dominio
datascience). Ver gotchas en cada `.md`.
## Prerequisitos
- GPU NVIDIA + CUDA recomendada (corre en CPU pero lento). Primera ejecucion descarga los pesos
del modelo a `~/.cache/huggingface/` (cientos de MB segun la variante).
- Paquetes: `torch`, `transformers`, `trimesh`, `pillow`, `numpy`.
+77
View File
@@ -0,0 +1,77 @@
# Capability: local-hub
Exponer los procesos locales de la maquina como subdominios `*.localhost` (via Caddy) y reunirlos
en una pantalla principal (Glance) con estado online/offline en vivo, refrescada a diario por
`dag_engine`. Cubre el ciclo: descubrir servicios -> renderizar config de proxy -> renderizar
config de dashboard -> recargar y reiniciar (pipeline `refresh_local_hub`).
Fuente de verdad de los servicios: `apps/local_hub/local_services.yaml`.
## Por que existe
Una maquina con muchos procesos locales (Metabase :3030, Portainer :9000, Grafana, Jupyter,
dag_engine, registry_api...) obliga a recordar puerto por puerto. Este grupo los pone detras de
nombres legibles (`metabase.localhost`, `portainer.localhost`) sin tocar DNS ni `/etc/hosts`
(systemd-resolved resuelve `*.localhost` a 127.0.0.1 por defecto, RFC 6761) y los lista en una
sola pagina con su salud en vivo.
## Funciones
| ID | Firma | Que hace |
|---|---|---|
| `discover_local_services_py_infra` | `discover_local_services(manifest_path, include_registry=True) -> list[dict]` | Lee el manifiesto `local_services.yaml`, normaliza cada servicio (name, subdomain, port, health_path, title, icon, category) y resuelve `up` por chequeo TCP. Con `include_registry` anade los servicios del registry (via `fn doctor services-spec`) deduplicados. |
| `render_caddyfile_py_infra` | `render_caddyfile(services, dashboard=None) -> str` | PURA. Convierte la lista de servicios en el texto de un fragmento de Caddyfile (`http://<sub>.localhost { reverse_proxy 127.0.0.1:<port> }`), ordenado y determinista. El dashboard va primero. |
| `render_glance_config_py_infra` | `render_glance_config(services, title="Procesos locales", host_suffix="localhost") -> str` | PURA. Convierte la lista en YAML de Glance: una pagina con un widget `monitor` por categoria, cada site apuntando a `http://<sub>.localhost`. |
| `refresh_local_hub_py_pipelines` | `refresh_local_hub(manifest_path=..., reload=True) -> dict` | PIPELINE. Compone las 3 anteriores: descubre, renderiza Caddyfile + Glance, los escribe (`/etc/caddy/conf.d/local_hub.caddy` via ACL + `apps/local_hub/glance/glance.yml`), recarga Caddy (admin API :2019, sin sudo) y reinicia la user-unit `glance`. |
## Ejemplo canonico
```bash
# Refrescar todo el hub (descubrir + regenerar configs + recargar):
./fn run refresh_local_hub
# Acceder a un servicio por su subdominio (cualquier navegador del host):
# http://metabase.localhost
# http://portainer.localhost
# http://home.localhost <- la pantalla principal (Glance)
# Anadir un servicio nuevo: editar el manifiesto y refrescar
$EDITOR apps/local_hub/local_services.yaml # name, subdomain, port, health_path, title, icon, category
./fn run refresh_local_hub
```
Composicion ad-hoc (heredoc) si se necesita solo una parte:
```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from infra.discover_local_services import discover_local_services
from infra.render_caddyfile import render_caddyfile
services = discover_local_services("apps/local_hub/local_services.yaml")
print(render_caddyfile(services, dashboard={"subdomain": "home", "port": 8585}))
```
## Infraestructura (one-time, ya provisionada)
- **Caddy** (`apt`, systemd system service, puerto 80): `/etc/caddy/Caddyfile` hace
`import /etc/caddy/conf.d/*.caddy`. El usuario tiene ACL de escritura sobre `conf.d/` para que
el pipeline regenere sin sudo. Reload via admin API en `localhost:2019`.
- **Glance** (binario nativo en `~/.local/bin/glance`, systemd user service `glance.service`,
`127.0.0.1:8585`). Corre como binario del host —no contenedor— para que `*.localhost` resuelva
igual que en el resto del sistema.
- **dag_engine**: DAG `refresh_local_hub` diario que ejecuta el pipeline.
## Fronteras
- **NO gestiona TLS**: sirve HTTP plano (`http://`) porque es trafico loopback. Para HTTPS con CA
interna habria que quitar el prefijo `http://` en `render_caddyfile` y dejar que Caddy emita
certs internos.
- **NO arranca ni para los servicios** que expone: asume que ya corren. Solo crea el mapeo de
subdominio y los lista. Encender/apagar un servicio es trabajo de su propia unit / `systemd`.
- **NO hace el health-check en vivo**: eso lo hace Glance client-side. El pipeline solo resuelve
un `up/down` puntual al regenerar (snapshot del momento).
- **Servicios no-HTTP** (Postgres :5433, etc.) quedan fuera del proxy y del dashboard: Caddy y el
widget `monitor` de Glance son HTTP.
- **Solo loopback / un PC**: el manifiesto y los subdominios son locales a la maquina. No expone
nada a la red. Para acceso remoto se usa SSH port-forward o el grupo `ssh`/`wireguard`.
+19 -1
View File
@@ -1,6 +1,6 @@
# Capability: metabase
Operar Metabase 100% via API REST. Cubre: auth (`metabase_auth`), CRUD de cards/dashboards/collections/snippets/permissions/databases, ejecucion de queries (`metabase_execute_card`, `metabase_query`), refresh metadata + result_metadata, listado y archivado, gestion de pulses, y composiciones (`init_metabase`, `setup_metabase_volume`). 106 funciones Go+Py. Cliente reutilizable: `MetabaseClient` (Go: `metabase_client_go_infra`; Py: `MetabaseClient_py_infra`).
Operar Metabase 100% via API REST. Cubre: auth (`metabase_auth` con email/password, `metabase_client_from_pass` cargando la credencial desde `pass` — sesión o API-key), CRUD de cards/dashboards/collections/snippets/permissions/databases, ejecucion de queries (`metabase_execute_card`, `metabase_query`), refresh metadata + result_metadata, listado y archivado, gestion de pulses, y composiciones (`init_metabase`, `setup_metabase_volume`). 108 funciones Go+Py. Cliente reutilizable: `MetabaseClient` (Go: `metabase_client_go_infra`; Py: `MetabaseClient_py_infra`) — el cliente Py detecta el prefijo `mb_` y autentica por header `X-API-KEY`.
## Funciones
@@ -15,6 +15,8 @@ Operar Metabase 100% via API REST. Cubre: auth (`metabase_auth`), CRUD de cards/
| `metabase_archive_snippet_py_infra` | `def metabase_archive_snippet(client: MetabaseClient, snippet_id: int) -> dict` | Archiva un SQL snippet en Metabase. Wrapper sobre metabase_update_snippet con archived=True. |
| `metabase_auth_go_infra` | `func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)` | Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session. |
| `metabase_auth_py_infra` | `def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient` | Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias. Endpoint: POST /api/session. |
| `metabase_client_from_pass_py_infra` | `def metabase_client_from_pass(pass_key: str, base_url: str, mode: str = 'auto') -> MetabaseClient \| dict` | Construye un `MetabaseClient` autenticado leyendo la credencial desde `pass`. `mode='session'` (secreto multi-línea: L1 password, línea `email:`) usa `metabase_auth`; `mode='api_key'` (secreto de una línea tipo `mb_...`) autentica por header; `mode='auto'` detecta por la forma del secreto. Compone `pass_get_secret` + `parse_metabase_secret` + `metabase_auth`. Devuelve el cliente o `{status:'error', error}` sin lanzar. Cubre Aurgi (API-key) y captación (sesión) sin reescribir la carga de credenciales. |
| `parse_metabase_secret_py_infra` | `def parse_metabase_secret(secret_text: str, mode: str = 'auto') -> dict` | Núcleo **puro** y testeable de `metabase_client_from_pass`: parsea el texto del secreto de `pass` y devuelve `{mode, email, password}` (sesión) o `{mode, api_key}` (API-key). `mode='auto'` clasifica: una sola línea sin `email:`/`login:` → api_key; multi-línea con email → session. Sin I/O. |
| `metabase_copy_card_py_infra` | `def metabase_copy_card(client: MetabaseClient, card_id: int, name: str \| None = None, collection_id: int \| None = None, description: str \| None = None) -> dict` | Crea una copia de una card/pregunta en Metabase via el endpoint nativo POST /api/card/:id/copy. Permite sobrescribir nombre, coleccion y descripcion en la copia. |
| `metabase_copy_dashboard_py_infra` | `def metabase_copy_dashboard(client: MetabaseClient, dashboard_id: int, name: str \| None = None, collection_id: int \| None = None, description: str \| None = None, is_deep_copy: bool = False) -> dict` | Crea una copia de un dashboard en Metabase via POST /api/dashboard/:id/copy. Con is_deep_copy=True tambien clona las cards referenciadas. |
| `metabase_copy_dashcard_mappings_py_infra` | `def metabase_copy_dashcard_mappings(client: MetabaseClient, *, dashboard_id: int, source_card_id: int, dest_card_id: int) -> list[dict]` | Copia los parameter_mappings del primer dashcard con source_card_id al card destino (dest_card_id), devolviendo una lista nueva de mappings sin mutar el original. Util para replicar filtros de dashboard a cards nuevas sin copiar manualmente cada mapping. |
@@ -134,6 +136,22 @@ dash = metabase_get_dashboard(client, dashboard_id=42)
cards = metabase_list_cards(client, collection_id=dash["collection_id"])
```
### Cliente autenticado desde `pass` (sin manejar credenciales a mano)
```python
import os, sys
sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))
from metabase import metabase_client_from_pass, metabase_get_dashboard
# Aurgi: API-key de una línea en pass (mb_...)
client = metabase_client_from_pass("metabase/aurgi-api-key", "https://reports.autingo.es", mode="api_key")
# Captación: secreto multi-línea (password + email:) → sesión
# client = metabase_client_from_pass("captacion/metabase", "http://localhost:3030", mode="session")
dash = metabase_get_dashboard(client, dashboard_id=734)
```
### Crear card + dashboard + ejecutar (Go)
```bash
+12 -2
View File
@@ -15,12 +15,22 @@ Postgres es la **capa que sirve datos a las herramientas de BI** del stack (`Exc
| `pg_create_table_from_rows_py_infra` | `pg_create_table_from_rows(dsn, table, rows, primary_key=None) -> dict` | `CREATE TABLE IF NOT EXISTS` infiriendo columnas y tipos desde los valores (bool→BOOLEAN, int→BIGINT, float→DOUBLE PRECISION, datetime→TIMESTAMP, date→DATE, resto→TEXT). Idempotente. Devuelve `{status, created, table, columns}`. |
| `pg_list_tables_py_infra` | `pg_list_tables(dsn, schema='public') -> dict` | Introspección read-only: tablas base con sus columnas vía `information_schema`. Devuelve `{status, schema, tables:[{name, columns:[{name,type,nullable}]}]}`. |
| `pg_apply_sql_py_infra` | `pg_apply_sql(dsn, sql_path) -> int` | Ejecuta un archivo `.sql` completo (multi-statement, una transacción). Para migraciones idempotentes (`IF NOT EXISTS`). |
| `resolve_pg_dsn_py_infra` | `resolve_pg_dsn(project) -> dict` | Resuelve el DSN de Postgres de un proyecto conocido (`captacion`/`captacion_clientes` vía `CAPTACION_DSN`, `seo`/`seo_analytics` vía `SEO_DSN`) en este orden: (1) variable de entorno, (2) línea `<ENV_VAR>=` del `.env` del proyecto, (3) construido desde `pass` en runtime. Devuelve `{status, project, dsn, source}` (`source` = `env`\|`dotenv`\|`pass`) sin lanzar. Mapa de proyectos explícito en el código — añadir un proyecto = editar `_PROJECTS`. Nunca hardcodea el password. |
| `query_project_pg_py_pipelines` | `query_project_pg(project, sql, max_rows=10000) -> dict` | **Pipeline one-shot**: compone `resolve_pg_dsn` + `pg_query`. Lee el DSN del proyecto y ejecuta el SELECT en un solo paso, sin que el caller toque el DSN. Devuelve lo de `pg_query` (`{status, columns, rows, row_count, truncated}`) o propaga el error de resolución. Reemplaza el patrón inline de resolver el DSN a mano antes de consultar. |
Relacionadas (otros grupos): `duckdb_to_postgres_py_pipelines` (sincroniza una tabla DuckDB a Postgres) e `init_metabase_go_infra` (despliega el stack Metabase + Postgres en Docker).
## Ejemplo canónico
Crear una tabla inferida, hacer upsert idempotente y consultar (DSN desde `pass`):
Atajo de un paso — consultar un proyecto conocido sin tocar el DSN (resuelto desde `.env`/`pass`):
```bash
cd /home/enmanuel/fn_registry
./fn run query_project_pg captacion "SELECT COUNT(*) AS n FROM product_opportunities"
# {"status":"ok","columns":["n"],"rows":[{"n":19}],"row_count":1,"truncated":false}
```
Camino completo — crear una tabla inferida, hacer upsert idempotente y consultar (DSN desde `pass`):
```bash
cd /home/enmanuel/fn_registry
@@ -42,7 +52,7 @@ PYEOF
## Gotchas del grupo
- **El DSN lleva credenciales — nunca hardcodear.** Resuélvelo desde `pass` (ej. `pass captacion/postgres`: L1 = password, resto `user/host/port/datadb`). No imprimas el DSN en logs.
- **El DSN lleva credenciales — nunca hardcodear.** Resuélvelo desde `pass` (ej. `pass captacion/postgres`: L1 = password, resto `user/host/port/datadb`), o mejor con `resolve_pg_dsn(project)` que centraliza la convención por proyecto. No imprimas el DSN en logs. Para proyectos no mapeados en `resolve_pg_dsn`, pasa el DSN a `pg_query` directamente.
- **`pg_query`/`pg_list_tables` son read-only por convención** (`SET TRANSACTION READ ONLY` + rollback), protegen la base pero NO son sandbox; los identificadores (tabla/schema) NO se parametrizan — los valores sí (`%s`). Las funciones validan identificadores con `^[A-Za-z_][A-Za-z0-9_]*$`.
- **`pg_upsert` cuenta insert vs update con el pseudo-columna `xmax`** (`RETURNING (xmax = 0)`). Fiable en el caso normal (single-writer, sin triggers raros). Con `update_cols=[]` (DO NOTHING) las filas en conflicto no se devuelven, así que solo se cuentan las nuevas. BEFORE-triggers / REPLICA IDENTITY pueden desviar el conteo.
- **`pg_create_table_from_rows` no reconcilia schema:** si la tabla ya existe, `columns` reporta los tipos inferidos de las filas, no los reales. Inferencia best-effort sin NUMERIC/escala — para dinero define el schema a mano con `pg_apply_sql`.
+10
View File
@@ -7,6 +7,7 @@ Operar hosts remotos via SSH. Cubre: CRUD de `~/.ssh/config` (`ssh_config_add_en
| ID | Firma | Que hace |
|---|---|---|
| `audit_ssh_config_bash_cybersecurity` | `audit_ssh_config(config_path: string) -> void` | Audita la configuración de sshd_config evaluando parámetros de seguridad críticos (PermitRootLogin, PasswordAuthentication, Port, MaxAuthTries, X11Forwarding, AllowUsers). También revisa intentos de login fallidos en los logs y lista las claves autorizadas del usuario actual. |
| `check_service_health_via_ssh_bash_infra` | `check_service_health_via_ssh <ssh_host> <local_url> [--token-from-env <remote_env> <VAR>] [--token <literal>] [--expect-status 200]` | Comprueba la salud de un service HTTP que solo escucha en loopback de un host remoto: entra por SSH, lee opcionalmente un bearer token de un `.env` remoto, y hace `curl` al endpoint local con `Authorization: Bearer`. Emite JSON (`{status, host, url, http_code, healthy}`), exit 0 si sano. El token nunca se imprime; prefiere `--token-from-env` sobre `--token` (este deja el secreto en argv local). |
| `docker_compose_remote_deploy_bash_infra` | `docker_compose_remote_deploy(host: string, remote_dir: string, branch: string, compose_files: string) -> json` | Despliega un stack Docker Compose en un host remoto via SSH. Verifica conectividad, hace git pull del branch indicado, actualiza imagenes con docker-compose pull y levanta/recrea los servicios modificados con docker-compose up -d. Soporta compose files adicionales. Retorna JSON con status, containers corriendo y duracion. |
| `rsync_deploy_bash_infra` | `rsync_deploy(local_dir: string, ssh_alias: string, remote_dir: string) -> json` | Sincroniza un directorio local a un host remoto via rsync+SSH. Excluye archivos de desarrollo y bases de datos locales. Crea el directorio remoto si no existe. |
| `setup_registry_api_bash_infra` | `setup_registry_api(ssh_host: string, api_token: string, basic_auth_user: string, basic_auth_pass: string) -> json` | Deploy completo de registry_api en VPS con Docker + Traefik (Coolify proxy). Sincroniza el repo via rsync, genera el hash bcrypt para basicAuth, sube el traefik-dynamic.yml, crea el .env con el token, hace docker compose build+up y verifica el health check. |
@@ -50,6 +51,15 @@ Operar hosts remotos via SSH. Cubre: CRUD de `~/.ssh/config` (`ssh_config_add_en
./fn run wait_for_http https://myapp.example.com/health 30
```
### Health-check de un service que solo escucha en loopback del host remoto
```bash
./fn run check_service_health_via_ssh om "http://127.0.0.1:8487/agents" \
--token-from-env /home/ubuntu/CodeProyects/agents_and_robots/.env AGENTS_API_KEY \
--expect-status 200
# {"status":"ok","host":"om","url":"http://127.0.0.1:8487/agents","http_code":200,"healthy":true}
```
## Fronteras
- **NO genera ni rota llaves SSH automaticamente**. Asume llave ya generada con `ssh-keygen`.