
🚀 De Notas Caóticas a Datos Estructurados: El Poder de la IA y la Filosofía de Ada Lovelace
Autor / Proyecto: IES Monterroso — MonteSteam / AIDARAC
Categoría: Inteligencia Artificial / Ingeniería de Prompts / Desarrollo Web
📌 Introducción: Por qué la estructura lo cambia todo
Vivimos sumergidos en un océano de información no estructurada. Se calcula que más del 80% de los datos que generamos a diario —notas rápidas escritas en el móvil, correos electrónicos informales, transcripciones de reuniones, partes de incidencias en el aula o reportes de errores en software— carece de un formato rígido.
Aunque los seres humanos podemos interpretar el lenguaje informal y caótico con relativa facilidad, para las computadoras y las bases de datos este tipo de texto representa una barrera impenetrable. Sin estructura no hay automatización posible, no hay búsquedas eficientes ni integración con otros sistemas.
¿Por qué la Inteligencia Artificial es el aliado perfecto para estructurar?
Hasta hace muy poco, extraer información clave de un texto libre exigía programar complejas expresiones regulares o realizar un tedioso trabajo manual de copia y pega. Hoy en día, los Modelos de Lenguaje (LLM) actúan como traductores semánticos universales:
- Comprensión contextual: Son capaces de entender la intención del texto, identificando entidades (personas, fechas, herramientas, aulas) aunque estén escritas con erratas o abreviaturas.
- Normalización instantánea: Transforman párrafos informales e inconexos en esquemas estipulados y fijos (como objetos JSON).
- Automatización de flujos: Una vez que la IA convierte el texto en datos estructurados, cualquier sistema informático puede procesarlos, filtrarlos, enviarlos por correo o guardarlos en una base de datos de manera automática.
👩💻 El Legado de Ada Lovelace: Inspiración de nuestro proyecto
En el siglo XIX, Ada Lovelace fue la primera persona en comprender que la Máquina Analítica de Charles Babbage no solo servía para realizar cálculos matemáticos complejos, sino también para manipular símbolos abstractos siguiendo reglas lógicas precisas.
En el IES Monterroso (MonteSteam / AIDARAC) hemos querido rendirle homenaje nombrando a nuestro agente de extracción estructurada app_lovelace.py. Siguiendo la visión de Ada, utilizamos Inteligencia Artificial local para convertir el lenguaje informal en datos con precisión matemática.

⚙️ ¿Cómo funciona nuestra aplicación? (app_lovelace.py)
La aplicación, desarrollada con FastAPI y conectada localmente con LM Studio (puerto 1234), demuestra cómo la ingeniería de prompts y el control de calidad sintáctico garantizan resultados infalibles.
1. Entrada de Texto Libre
El usuario puede escribir o pegar cualquier texto desordenado. Por ejemplo:
“El profe Juan explicó ecuaciones en 3º ESO A. Falta por entregar el trabajo María Gómez. Examen el viernes 20. El proyector parpadea en verde.”
2. Prompt de Sistema Estricto
La IA recibe instrucciones matemáticamente delimitadas (System Prompt) que le prohíben responder con texto conversacional o saludos, forzándola a generar un único objeto JSON con la siguiente estructura:
titulo_resumen: Título corto y descriptivo.categoria: Clasificación del texto (Educación, Incidencia Técnica, Bug, etc.).entidades: Desglose de personas, fechas/plazos y elementos técnicos o lugares.puntos_clave: Puntos principales del texto.acciones_pendientes: Tareas con responsable, descripción y prioridad.incidencias_o_errores: Registro explícito de problemas detectados.
3. Validación y Resiliencia en Frontend
Para evitar errores típicos de los navegadores al manipular plantillas o copiar fragmentos, la interfaz utiliza contenedores nativos seguros e invisibles (<textarea style="display:none;">) que permiten cargar ejemplos al instante sin fallos de compatibilidad.
"""
Proyecto Ada Lovelace — Extracción Estructurada de Datos (AI Agents)
IES Monterroso · MonteSteam / AIDARAC
Puerto 8060
Inspirado en Ada Lovelace, quien creó el primer algoritmo para una máquina.
Transforma texto desordenado e informal (notas de clase, partes de incidencias,
reportes de bugs) en datos JSON estructurados con esquemas fijos y validación estricta.
"""
import json
import re
import time
from typing import Dict, Any
import requests
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, JSONResponse
app = FastAPI(title="Ada Lovelace — Extracción Estructurada")
# --- Configuración del servidor LM Studio ---
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
# --- Textos de ejemplo predeterminados ---
EJEMPLO_CLASE = (
"Nota rápida de clase de Matemáticas - 3º ESO A (Martes 10 Oct):\n"
"El profe Juan explicó ecuaciones de segundo grado. Falta por entregar el trabajo "
"María Gómez y Carlos Ruiz. Examen fijado para el próximo viernes 20.\n"
"Incidencia registrada: la pantalla del proyector no funciona bien, se ve parpadeando en verde.\n"
"Tarea para casa: páginas 45 a 47 ejercicios 1 al 8."
)
EJEMPLO_PROGRAMACION = (
"Informe de bug en producción - Módulo Autenticación:\n"
"En el commit 8f3a9d2 del desarrollador Marcos, la función loginUser() en auth.js lanza un "
"UnhandledPromiseRejection si la base de datos MySQL no responde a tiempo. Afecta al endpoint "
"/api/v1/login. Severidad: Alta. Prioridad: Urgente para el Sprint 4. Asignado a Ana Martínez.\n"
"Acción pendiente: añadir bloque try/catch y refactorizar el middleware de conexión."
)
def extraer_y_validar_json(texto: str) -> tuple[Dict[str, Any], bool, str]:
"""
Extrae el bloque JSON del texto generado por el LLM y valida su sintaxis.
Retorna el diccionario parsed, un booleano de validez y un mensaje de estado.
"""
patron_markdown = re.search(r"`{3}(?:json)?\s*(\{[\s\S]*?\})\s*`{3}", texto)
if patron_markdown:
texto_json = patron_markdown.group(1)
else:
inicio = texto.find("{")
fin = texto.rfind("}")
if inicio != -1 and fin != -1 and fin > inicio:
texto_json = texto[inicio : fin + 1]
else:
texto_json = texto
try:
datos = json.loads(texto_json)
return datos, True, "✅ Esquema JSON válido y parseado correctamente."
except json.JSONDecodeError as e:
return (
{"error_raw": texto},
False,
f"❌ Error de sintaxis JSON: {str(e)}",
)
def solicitar_extraccion_a_lmstudio(texto_bruto: str) -> tuple[Dict[str, Any], bool, str, float]:
system_prompt = (
"Eres Ada Lovelace, la primera algoritmista de la historia. Tu tarea es analizar "
"textos desordenados, informales o sin formato y convertirlos en datos estructurados con "
"precisión matemática absoluta.\n"
"Debes responder EXCLUSIVAMENTE con un objeto JSON válido. No incluyas ningún texto "
"de introducción, explicación ni conclusión fuera del objeto JSON.\n\n"
"El JSON DEBE seguir strictly esta estructura de campos:\n"
"{\n"
' "titulo_resumen": "Título corto y descriptivo del texto",\n'
' "categoria": "Categoría (ej: Educación, Programación, Incidencia Técnica, Gestión)",\n'
' "entidades": {\n'
' "personas": ["nombre 1", "nombre 2"],\n'
' "fechas_o_plazos": ["fecha 1", "plazo 2"],\n'
' "elementos_tecnicos_o_lugares": ["proyector", "auth.js", "MySQL", "3º ESO A"]\n'
" },\n"
' "puntos_clave": ["punto clave 1", "punto clave 2"],\n'
' "acciones_pendientes": [\n'
" {\n"
' "responsable": "Nombre o Rol",\n'
' "tarea": "Descripción de la tarea",\n'
' "prioridad": "Alta / Media / Baja"\n'
" }\n"
" ],\n"
' "incidencias_o_errores": ["descripción del problema o bug"]\n'
"}"
)
user_prompt = f"Analiza y extrae los datos del siguiente texto bruto:\n\n{texto_bruto}"
payload = {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": 0.1, # Temperatura baja para máxima consistencia estructurada
"stream": False,
}
inicio = time.perf_counter()
try:
respuesta = requests.post(
LM_STUDIO_URL,
json=payload,
headers={"Content-Type": "application/json"},
timeout=180,
)
tiempo_transcurrido = time.perf_counter() - inicio
if respuesta.status_code != 200:
raise RuntimeError(f"Error HTTP {respuesta.status_code} desde LM Studio")
datos_respuesta = respuesta.json()
contenido_texto = datos_respuesta["choices"][0]["message"]["content"]
datos_json, es_valido, msg_estado = extraer_y_validar_json(contenido_texto)
return datos_json, es_valido, msg_estado, tiempo_transcurrido
except requests.exceptions.ConnectionError:
raise ConnectionError(
"No se pudo conectar con LM Studio. Asegúrate de que el servidor local está "
"activo en http://localhost:1234 (Local Server ON)."
)
except Exception as e:
raise RuntimeError(f"Error durante el procesamiento: {str(e)}")
HTML_PLANTILLA = """
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ada Lovelace — Extracción Estructurada de Datos</title>
<style>
:root {
--fondo: #f4f8f5;
--panel: #ffffff;
--borde: #c8e0d0;
--texto: #1c3525;
--texto-secundario: #4b6855;
--acento: #008539; /* Verde corporativo Educación */
--acento-hover: #00662b;
--acento-suave: #e8f5ed;
--alerta-fondo: #fff8e6;
--alerta-borde: #f0d695;
--alerta-texto: #7a5200;
--exito-fondo: #e6f4ea;
--exito-borde: #a8dab5;
--exito-texto: #137333;
}
* { box-sizing: border-box; }
body {
background: var(--fondo);
color: var(--texto);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
margin: 0;
padding: 2rem;
}
.cabecera {
max-width: 950px;
margin: 0 auto 1.5rem auto;
}
.cabecera h1 {
font-size: 1.8rem;
margin-bottom: 0.3rem;
color: var(--acento);
}
.cabecera p {
color: var(--texto-secundario);
font-size: 0.95rem;
line-height: 1.5;
}
.aviso-lovelace {
background: var(--acento-suave);
border-left: 4px solid var(--acento);
padding: 1rem 1.2rem;
border-radius: 6px;
margin: 1rem 0;
font-size: 0.9rem;
line-height: 1.5;
color: var(--texto);
}
.guia-instrucciones {
background: #ffffff;
border: 1px solid var(--borde);
border-radius: 8px;
padding: 1rem 1.2rem;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.guia-instrucciones h4 {
margin: 0 0 0.5rem 0;
color: var(--acento);
}
.contenedor-principal {
max-width: 950px;
margin: 0 auto;
}
.panel-entrada {
background: var(--panel);
border: 1px solid var(--borde);
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 133, 57, 0.05);
}
label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
font-size: 0.9rem;
color: var(--texto);
}
.barra-ejemplos {
display: flex;
gap: 0.5rem;
margin-bottom: 0.8rem;
flex-wrap: wrap;
}
.btn-ejemplo {
background: var(--acento-suave);
color: var(--acento);
border: 1px solid var(--borde);
border-radius: 4px;
padding: 0.4rem 0.8rem;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-ejemplo:hover {
background: var(--acento);
color: #ffffff;
}
textarea {
width: 100%;
background: #fafdfb;
color: var(--texto);
border: 1px solid var(--borde);
border-radius: 6px;
padding: 0.8rem;
font-family: inherit;
font-size: 0.95rem;
margin-bottom: 1rem;
min-height: 120px;
resize: vertical;
}
textarea:focus {
outline: none;
border-color: var(--acento);
box-shadow: 0 0 0 3px rgba(0, 133, 57, 0.15);
}
button.btn-principal {
background: var(--acento);
color: #ffffff;
border: none;
border-radius: 6px;
padding: 0.8rem 1.8rem;
font-weight: 600;
cursor: pointer;
font-size: 0.95rem;
transition: background 0.2s ease;
}
button.btn-principal:hover { background: var(--acento-hover); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.grid-resultados {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.panel {
background: var(--panel);
border: 1px solid var(--borde);
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 133, 57, 0.04);
}
.panel h3 {
margin-top: 0;
font-size: 1rem;
color: var(--acento);
border-bottom: 1px solid var(--borde);
padding-bottom: 0.5rem;
}
.card-dato {
background: #ffffff;
border: 1px solid var(--borde);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.card-dato h5 {
margin: 0 0 0.4rem 0;
color: var(--acento);
font-size: 0.88rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.etiqueta {
display: inline-block;
background: var(--acento-suave);
color: var(--texto);
border: 1px solid #d0e8d9;
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
border-radius: 4px;
margin-right: 0.4rem;
margin-bottom: 0.4rem;
}
.alerta-box {
background: var(--alerta-fondo);
border: 1px solid var(--alerta-borde);
color: var(--alerta-texto);
font-size: 0.85rem;
padding: 0.6rem;
border-radius: 4px;
margin-top: 0.4rem;
}
.exito-box {
background: var(--exito-fondo);
border: 1px solid var(--exito-borde);
color: var(--exito-texto);
font-size: 0.85rem;
padding: 0.6rem;
border-radius: 4px;
margin-bottom: 1rem;
font-weight: 600;
}
.metrica {
display: flex;
justify-content: space-between;
padding: 0.4rem 0;
border-bottom: 1px solid var(--borde);
font-size: 0.88rem;
}
.metrica span:first-child { color: var(--texto-secundario); }
.metrica span:last-child {
font-family: 'Courier New', monospace;
font-weight: bold;
color: var(--acento);
}
pre {
background: #f0f7f2;
border: 1px solid var(--borde);
border-radius: 6px;
padding: 0.8rem;
font-size: 0.82rem;
max-height: 320px;
overflow: auto;
color: #0d5c27;
}
.estado {
color: var(--texto-secundario);
font-style: italic;
text-align: center;
padding: 2rem 0;
}
</style>
</head>
<body>
<div class="cabecera">
<h1>⚙️ Ada Lovelace — Extracción Estructurada de Datos</h1>
<p>
Ada Lovelace fue la primera en comprender que las máquinas podían procesar no solo números,
sino símbolos complejos siguiendo reglas precisas. En la ingeniería de IA actual, transformar
texto caótico en JSON estructurado es la base de la automatización de procesos.
</p>
<div class="aviso-lovelace">
<strong>Fundamento de AI Engineering:</strong><br>
Los LLM son generadores de texto libre. Para conectarlos con bases de datos o APIs,
debemos forzar al modelo a extraer información y devolver un esquema de datos estricto e inalterable.
</div>
</div>
<div class="contenedor-principal">
<div class="panel-entrada">
<div class="guia-instrucciones">
<h4>💡 ¿Qué debes introducir aquí?</h4>
<p style="margin:0 0 0.5rem 0; color:var(--texto-secundario);">
Introduce cualquier texto libre, desordenado e informal. Puede ser una nota rápida tomada a mano,
el transcrito de una reunión, un correo con varias peticiones o un reporte de error de programación.
</p>
<p style="margin:0; font-size:0.85rem; color:var(--texto);">
👉 <strong>Prueba con los botones de ejemplo rápido para ver cómo la IA extrae los campos clave:</strong>
</p>
</div>
<div class="barra-ejemplos">
<button type="button" class="btn-ejemplo" onclick="cargarEjemplo(1)">📝 Cargar Ejemplo 1: Nota de clase e Incidencia</button>
<button type="button" class="btn-ejemplo" onclick="cargarEjemplo(2)">💻 Cargar Ejemplo 2: Reporte de Bug en Código</button>
</div>
<label for="texto-input">Texto Desordenado de Entrada:</label>
<textarea id="texto-input" placeholder="Pega o escribe aquí el texto bruto..."></textarea>
<button id="boton-extraer" type="button" class="btn-principal" onclick="extraerDatos()">✨ Procesar y Estructurar con IA</button>
</div>
<div class="grid-resultados">
<div class="panel">
<h3>📊 Datos Estructurados Extraídos</h3>
<div id="contenedor-visual">
<p class="estado">Haz clic en "Procesar y Estructurar con IA" para ejecutar la extracción</p>
</div>
</div>
<div class="panel">
<h3>🔍 Validación de Esquema y Salida JSON</h3>
<div id="contenedor-debug">
<p class="estado">Aquí se mostrará la validación de formato y el JSON procesado</p>
</div>
</div>
</div>
</div>
<script>
const textoEjemplo1 = __EJEMPLO_CLASE_JSON__;
const textoEjemplo2 = __EJEMPLO_PROGRAMACION_JSON__;
function cargarEjemplo(num) {
const input = document.getElementById('texto-input');
if (num === 1) {
input.value = textoEjemplo1;
} else if (num === 2) {
input.value = textoEjemplo2;
}
}
async function extraerDatos() {
const textoBruto = document.getElementById('texto-input').value.trim();
const boton = document.getElementById('boton-extraer');
const contenedorVisual = document.getElementById('contenedor-visual');
const contenedorDebug = document.getElementById('contenedor-debug');
if (!textoBruto) {
alert('Por favor, introduce o carga un texto antes de procesar.');
return;
}
boton.disabled = true;
boton.textContent = '⏳ Ejecutando algoritmo de extracción...';
contenedorVisual.innerHTML = '<p class="estado">Analizando sintaxis y extrayendo entidades...</p>';
contenedorDebug.innerHTML = '<p class="estado">Validando conformidad del JSON...</p>';
try {
const formData = new FormData();
formData.append('texto_bruto', textoBruto);
const respuesta = await fetch('/extraer', {
method: 'POST',
body: formData
});
const datos = await respuesta.json();
if (!respuesta.ok) {
throw new Error(datos.error || 'Error procesando la solicitud');
}
const json = datos.datos_json;
// Renderizado Visual
let htmlVisual = `
<div class="card-dato">
<span class="etiqueta" style="background:var(--acento); color:#fff; font-weight:bold;">${json.categoria || 'Sin Categoría'}</span>
<h4 style="margin:0.5rem 0 0 0; color:var(--acento); font-size:1.1rem;">${json.titulo_resumen || 'Resumen no disponible'}</h4>
</div>
`;
// Entidades
if (json.entidades) {
htmlVisual += `<div class="card-dato"><h5>🏷️ Entidades Identificadas</h5>`;
if (json.entidades.personas && json.entidades.personas.length > 0) {
htmlVisual += `<p style="margin:0.2rem 0; font-size:0.85rem;"><strong>Personas:</strong> ${json.entidades.personas.map(p => `<span class="etiqueta">${p}</span>`).join('')}</p>`;
}
if (json.entidades.fechas_o_plazos && json.entidades.fechas_o_plazos.length > 0) {
htmlVisual += `<p style="margin:0.2rem 0; font-size:0.85rem;"><strong>Fechas / Plazos:</strong> ${json.entidades.fechas_o_plazos.map(f => `<span class="etiqueta">${f}</span>`).join('')}</p>`;
}
if (json.entidades.elementos_tecnicos_o_lugares && json.entidades.elementos_tecnicos_o_lugares.length > 0) {
htmlVisual += `<p style="margin:0.2rem 0; font-size:0.85rem;"><strong>Elementos Téc. / Lugares:</strong> ${json.entidades.elementos_tecnicos_o_lugares.map(e => `<span class="etiqueta">${e}</span>`).join('')}</p>`;
}
htmlVisual += `</div>`;
}
// Puntos Clave
if (json.puntos_clave && json.puntos_clave.length > 0) {
htmlVisual += `<div class="card-dato"><h5>📌 Puntos Clave</h5><ul style="margin:0; padding-left:1.2rem; font-size:0.88rem;">`;
json.puntos_clave.forEach(pt => { htmlVisual += `<li>${pt}</li>`; });
htmlVisual += `</ul></div>`;
}
// Acciones Pendientes
if (json.acciones_pendientes && json.acciones_pendientes.length > 0) {
htmlVisual += `<div class="card-dato"><h5>✅ Acciones Pendientes</h5>`;
json.acciones_pendientes.forEach(acc => {
htmlVisual += `
<div style="background:#f9fbf9; border:1px solid var(--borde); padding:0.5rem; border-radius:4px; margin-bottom:0.4rem; font-size:0.85rem;">
<strong>${acc.responsable || 'Sin asignado'}:</strong> ${acc.tarea}
<span class="etiqueta" style="float:right; font-size:0.75rem;">Prioridad: ${acc.prioridad || 'Media'}</span>
</div>
`;
});
htmlVisual += `</div>`;
}
// Incidencias o Errores
if (json.incidencias_o_errores && json.incidencias_o_errores.length > 0) {
htmlVisual += `<div class="card-dato"><h5>⚠️ Incidencias / Errores Detectados</h5>`;
json.incidencias_o_errores.forEach(inc => {
htmlVisual += `<div class="alerta-box">🚨 ${inc}</div>`;
});
htmlVisual += `</div>`;
}
contenedorVisual.innerHTML = htmlVisual;
// Renderizado Debug y JSON
let htmlDebug = `
<div class="${datos.es_valido ? 'exito-box' : 'alerta-box'}">${datos.mensaje_estado}</div>
<div class="metrica"><span>⏱️ Tiempo de extracción</span><span>${datos.tiempo_segundos.toFixed(2)} s</span></div>
<div class="metrica"><span> Servidor de Inferencia</span><span>LM Studio (Local)</span></div>
<label style="font-size:0.85rem; color:var(--texto-secundario); margin-top:1rem; display:block;">Objeto JSON Resultante:</label>
<pre>${JSON.stringify(json, null, 2)}</pre>
`;
contenedorDebug.innerHTML = htmlDebug;
} catch (error) {
contenedorVisual.innerHTML = `<p style="color:#c53030;">❌ Error: ${error.message}</p>`;
contenedorDebug.innerHTML = `<p style="color:#c53030;">Verifica que LM Studio esté activo en el puerto 1234.</p>`;
} finally {
boton.disabled = false;
boton.textContent = '✨ Procesar y Estructurar con IA';
}
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def pagina_principal():
contenido = (
HTML_PLANTILLA
.replace("__EJEMPLO_CLASE_JSON__", json.dumps(EJEMPLO_CLASE))
.replace("__EJEMPLO_PROGRAMACION_JSON__", json.dumps(EJEMPLO_PROGRAMACION))
)
return contenido
@app.post("/extraer")
def endpoint_extraer(texto_bruto: str = Form(...)):
if not texto_bruto.strip():
return JSONResponse(status_code=400, content={"error": "El texto de entrada no puede estar vacío."})
try:
datos_json, es_valido, msg_estado, tiempo_seg = solicitar_extraccion_a_lmstudio(texto_bruto)
return JSONResponse(
content={
"datos_json": datos_json,
"es_valido": es_valido,
"mensaje_estado": msg_estado,
"tiempo_segundos": tiempo_seg,
}
)
except ConnectionError as e:
return JSONResponse(status_code=503, content={"error": str(e)})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
💡 Casos de Uso Prácticos
🏫 1. En la Gestión Educativa y del Aula
Transforma partes de incidencias o notas de clase tomadas a mano en tareas asignadas automáticamente a tutores o al departamento de mantenimiento informático.
💻 2. En el Desarrollo de Software
Convierte un reporte informal de bug en Slack o WhatsApp (“Oye, a Marcos le falla el loginUser en auth.js cuando la BD no responde”) en un ticket estructurado listo para integrarse en tableros de gestión como Jira o Trello.
🎯 Conclusión
La Inteligencia Artificial estructuradora no está diseñada para sustituir el criterio humano, sino para liberarnos de la burocracia digital y la entrada manual de datos. Convertir el caos en orden es el primer paso para construir sistemas más inteligentes, eficientes y humanos.
¡Prueba la aplicación localmente en el puerto 8060 y empieza a estructurar tu información con el espíritu de Ada Lovelace!



