atareao revisou este gist 1 day ago. Ir para a revisão
8 files changed, 917 insertions
01-un-solo-fichero.py(arquivo criado)
| @@ -0,0 +1,169 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | # -*- coding: utf-8 -*- | |
| 3 | + | """ | |
| 4 | + | 01 — Todo cabe en un solo fichero | |
| 5 | + | ================================= | |
| 6 | + | ||
| 7 | + | Episodio «¿Cómo se le da memoria a una IA?» — Punto 6 del guion, | |
| 8 | + | «Las tres decisiones» (acto III — El relato y lo que lo sostiene). | |
| 9 | + | ||
| 10 | + | La idea del episodio | |
| 11 | + | -------------------- | |
| 12 | + | Todos los recuerdos de un asistente con memoria pueden vivir en **un único | |
| 13 | + | fichero** en tu propia máquina: lo que dijiste, las fichas de lo que pasó, | |
| 14 | + | cómo eres, qué pasó y cuándo, y la bitácora del día. Consecuencias: | |
| 15 | + | ||
| 16 | + | * Tus recuerdos **no salen de tu casa**: no hay nube a la que subir nada. | |
| 17 | + | * Lo copias a un *pendrive* y tienes tu memoria entera en el bolsillo. | |
| 18 | + | * Lo borras y no queda rastro en ningún sitio. | |
| 19 | + | * Lo abres y lees qué sabe el asistente de ti. | |
| 20 | + | ||
| 21 | + | Qué hace este ejemplo | |
| 22 | + | --------------------- | |
| 23 | + | Crea ese fichero único —un `.json` con las cinco capas dentro—, lo enseña por | |
| 24 | + | pantalla, lo copia como si fuera a un *pendrive* y comprueba que la copia | |
| 25 | + | contiene exactamente lo mismo. Los recuerdos que trae son **fragmentos reales | |
| 26 | + | de una instancia de Valet en producción**, generalizados para publicarlos: sin | |
| 27 | + | nombres de terceros, sin lugares y sin datos que permitan identificarla. La | |
| 28 | + | gracia no es el formato, es que **cabe todo junto y es tuyo**. | |
| 29 | + | ||
| 30 | + | No necesita nada instalado (solo Python 3, que ya viene en casi cualquier Linux). | |
| 31 | + | ||
| 32 | + | Uso: | |
| 33 | + | python3 01-un-solo-fichero.py # escribe ./memoria-demo.json | |
| 34 | + | python3 01-un-solo-fichero.py /ruta/mi-memoria.json | |
| 35 | + | """ | |
| 36 | + | ||
| 37 | + | import json | |
| 38 | + | import os | |
| 39 | + | import shutil | |
| 40 | + | import sys | |
| 41 | + | ||
| 42 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 43 | + | # Las cinco capas del episodio, dentro de un solo diccionario. Son fragmentos | |
| 44 | + | # reales de una instancia de Valet en producción, generalizados para publicar. | |
| 45 | + | # Capa A: lo que dijiste (la grabación en bruto) | |
| 46 | + | # Capa B: lo que pasó (fichas con el sentido de la conversación) | |
| 47 | + | # Capa C: cómo eres (tu retrato + el tono del asistente; en producción, 19 normas) | |
| 48 | + | # Capa D: qué y cuándo (hechos sueltos con fecha) | |
| 49 | + | # Capa E: la bitácora (el relato del día, escrito solo) | |
| 50 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 51 | + | MEMORIA_DE_EJEMPLO = { | |
| 52 | + | "A_lo_que_dijiste": [ | |
| 53 | + | {"quien": "tú", "texto": "Resúmeme los artículos de RSS que tengo pendientes."}, | |
| 54 | + | { | |
| 55 | + | "quien": "asistente", | |
| 56 | + | "texto": "Hecho. Te dejo por aquí los temas y las decisiones que saco en limpio.", | |
| 57 | + | }, | |
| 58 | + | ], | |
| 59 | + | "B_lo_que_paso": [ | |
| 60 | + | { | |
| 61 | + | "fecha": "2026-10-24", | |
| 62 | + | "temas": [ | |
| 63 | + | "RSS", | |
| 64 | + | "Docker Compose", | |
| 65 | + | "cifrado de disco", | |
| 66 | + | "monitorización de servidores", | |
| 67 | + | "alternativas a Dropbox", | |
| 68 | + | ], | |
| 69 | + | # Fragmento real de la ficha (≈257 tokens en producción). | |
| 70 | + | "sintesis": "Resumen y selección de artículos de RSS pendientes; " | |
| 71 | + | "el usuario pidió un resumen y una selección de lo más relevante.", | |
| 72 | + | } | |
| 73 | + | ], | |
| 74 | + | "C_como_eres": { | |
| 75 | + | # El retrato: verdades que se destilan con el tiempo, no hechos de un día. | |
| 76 | + | "quien_eres": [ | |
| 77 | + | "Sales a correr con regularidad y te gusta anotar distancias y ritmos.", | |
| 78 | + | "Prefieres resolver las cosas por la mañana.", | |
| 79 | + | ], | |
| 80 | + | # Las normas de casa: cómo quieres que se comporte tu asistente. | |
| 81 | + | # Estas tres están citadas tal cual; en producción hay 19. | |
| 82 | + | "normas_de_casa": [ | |
| 83 | + | "No inventes ni rellenes contenidos no disponibles; informa con " | |
| 84 | + | "honestidad sobre limitaciones de acceso.", | |
| 85 | + | "Verifica antes y después de crear recordatorios o eventos.", | |
| 86 | + | "No registres afirmaciones subjetivas o no verificables como hechos " | |
| 87 | + | "en el timeline.", | |
| 88 | + | ], | |
| 89 | + | }, | |
| 90 | + | "D_que_y_cuando": [ | |
| 91 | + | { | |
| 92 | + | # Cambiado de 2026-10-20 a 2026-10-17: el 20 es una de las jornadas | |
| 93 | + | # de la bitácora (18, 19 y 20) y no puede llevar dos cifras distintas. | |
| 94 | + | # El 17 no colisiona con ningún día de la bitácora y mantiene el | |
| 95 | + | # orden ascendente de la línea del tiempo (17 · 21 · 22). | |
| 96 | + | "fecha": "2026-10-17", | |
| 97 | + | "hecho": "Corrí 9,00 km con 119 m de desnivel y ritmo medio de 6:44 min/km.", | |
| 98 | + | }, | |
| 99 | + | {"fecha": "2026-10-21", "hecho": "Cené fuera."}, | |
| 100 | + | {"fecha": "2026-10-22", "hecho": "Corregí el nombre en la ficha de Valet."}, | |
| 101 | + | ], | |
| 102 | + | "E_la_bitacora": [ | |
| 103 | + | { | |
| 104 | + | "fecha": "2026-10-18", | |
| 105 | + | "relato": "Hoy ha sido un día de esos en los que la lluvia y las " | |
| 106 | + | "decisiones se entrelazan de manera inesperada.", | |
| 107 | + | "animo": "buen día", | |
| 108 | + | "logros": [ | |
| 109 | + | "Ajustar el plan de entrenamiento a la lluvia", | |
| 110 | + | "Decidir una tirada de 40 min en vez de 13 km", | |
| 111 | + | ], | |
| 112 | + | "dificultades": ["Condiciones adversas"], | |
| 113 | + | "etiquetas": ["entrenamiento", "meteorología", "decisiones"], | |
| 114 | + | } | |
| 115 | + | ], | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | ||
| 119 | + | def ruta_por_defecto(): | |
| 120 | + | return os.path.join(os.getcwd(), "memoria-demo.json") | |
| 121 | + | ||
| 122 | + | ||
| 123 | + | def main(): | |
| 124 | + | ruta = sys.argv[1] if len(sys.argv) > 1 else ruta_por_defecto() | |
| 125 | + | ||
| 126 | + | # 1) Escribir el fichero único. | |
| 127 | + | with open(ruta, "w", encoding="utf-8") as f: | |
| 128 | + | json.dump(MEMORIA_DE_EJEMPLO, f, ensure_ascii=False, indent=2) | |
| 129 | + | ||
| 130 | + | print("=" * 68) | |
| 131 | + | print(" UN SOLO FICHERO CON TODA LA MEMORIA") | |
| 132 | + | print("=" * 68) | |
| 133 | + | print(f"\n Fichero: {ruta}") | |
| 134 | + | print(f" Tamaño : {os.path.getsize(ruta)} bytes (ocupa nada, ya lo ves)") | |
| 135 | + | ||
| 136 | + | # 2) Enseñar las cinco capas que viven dentro, sin entrar en tripas. | |
| 137 | + | print("\n Dentro de ese único fichero:\n") | |
| 138 | + | etiquetas = { | |
| 139 | + | "A_lo_que_dijiste": "A · lo que dijiste", | |
| 140 | + | "B_lo_que_paso": "B · lo que pasó", | |
| 141 | + | "C_como_eres": "C · cómo eres", | |
| 142 | + | "D_que_y_cuando": "D · qué y cuándo", | |
| 143 | + | "E_la_bitacora": "E · la bitácora", | |
| 144 | + | } | |
| 145 | + | for clave, etiqueta in etiquetas.items(): | |
| 146 | + | contenido = MEMORIA_DE_EJEMPLO[clave] | |
| 147 | + | n = len(contenido) if isinstance(contenido, list) else len(contenido.keys()) | |
| 148 | + | print(f" • {etiqueta:<20} → {n} elemento(s)") | |
| 149 | + | ||
| 150 | + | # 3) "Copiarlo al pendrive": es un fichero, se copia y punto. | |
| 151 | + | destino = ruta + ".copia-pendrive.json" | |
| 152 | + | shutil.copyfile(ruta, destino) | |
| 153 | + | iguales = open(ruta, "rb").read() == open(destino, "rb").read() | |
| 154 | + | print(f"\n Copia al pendrive: {destino}") | |
| 155 | + | print(f" ¿La copia es idéntica? {'sí ✔' if iguales else 'no ✘'}") | |
| 156 | + | ||
| 157 | + | # 4) "Abrirlo y leer qué sabe de ti": aquí lo enseñamos en crudo. | |
| 158 | + | print("\n Y si lo abres, esto es lo que el asistente sabe de ti:") | |
| 159 | + | print(" " + "-" * 64) | |
| 160 | + | perfil = MEMORIA_DE_EJEMPLO["C_como_eres"]["quien_eres"] | |
| 161 | + | for verdad in perfil: | |
| 162 | + | print(f" · {verdad}") | |
| 163 | + | print(" " + "-" * 64) | |
| 164 | + | ||
| 165 | + | print("\n Lo borras y no queda rastro en ningún sitio. Eso es la gracia.\n") | |
| 166 | + | ||
| 167 | + | ||
| 168 | + | if __name__ == "__main__": | |
| 169 | + | main() | |
02-buscar-por-significado.py(arquivo criado)
| @@ -0,0 +1,253 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | # -*- coding: utf-8 -*- | |
| 3 | + | """ | |
| 4 | + | 02 — Buscar por significado, no por palabra | |
| 5 | + | ============================================ | |
| 6 | + | ||
| 7 | + | Episodio «¿Cómo se le da memoria a una IA?» — Punto 3 del guion, | |
| 8 | + | «Lo que dijiste, lo que pasó y cuándo» (acto II — Los hechos y el retrato). | |
| 9 | + | ||
| 10 | + | La idea del episodio | |
| 11 | + | -------------------- | |
| 12 | + | Cuando le preguntas algo, el asistente no busca el libro que se titula | |
| 13 | + | literalmente como tu pregunta. Busca **ideas**. Si tú preguntas por «coche» y | |
| 14 | + | la ficha dice «automóvil», un buscador normal no lo encuentra; este sí. En el | |
| 15 | + | mundo real, el sistema traduce el texto a números (lo que llaman *búsqueda | |
| 16 | + | semántica* o RAG) y compara esos números. Aquí no vamos a meter un modelo: lo | |
| 17 | + | imitamos con un diccionario de conceptos hecho a mano, para que se vea la | |
| 18 | + | diferencia sin instalar nada. | |
| 19 | + | ||
| 20 | + | Qué hace este ejemplo | |
| 21 | + | --------------------- | |
| 22 | + | 1. Compara un **buscador por palabra** (cuenta letras en común) con un | |
| 23 | + | **buscador por significado** (compara conceptos). Misma pregunta, mismos | |
| 24 | + | recuerdos, resultados distintos. | |
| 25 | + | 2. Busca «quiero algo sobre el miedo a hablar en público» y enseña cómo el | |
| 26 | + | buscador de significado trae la ficha que habla de «ansiedad antes de una | |
| 27 | + | presentación», aunque no compartan ni una palabra. | |
| 28 | + | 3. De propina, reproduce el caso «coche / automóvil». | |
| 29 | + | ||
| 30 | + | No necesita nada instalado (solo Python 3). | |
| 31 | + | ||
| 32 | + | Uso: | |
| 33 | + | python3 02-buscar-por-significado.py | |
| 34 | + | """ | |
| 35 | + | ||
| 36 | + | import math | |
| 37 | + | import re | |
| 38 | + | import unicodedata | |
| 39 | + | ||
| 40 | + | ||
| 41 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 42 | + | # El "traductor a significado" de juguete. | |
| 43 | + | # | |
| 44 | + | # En un sistema real, un modelo convierte cada texto en una lista de números | |
| 45 | + | # (un vector) de forma que los textos que hablan de lo mismo acaban con números | |
| 46 | + | # parecidos. Aquí no hay modelo: definimos a mano unos cuantos CONCEPTOS y | |
| 47 | + | # decimos qué palabras activan cada uno. Es una maqueta, pero enseña la idea. | |
| 48 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 49 | + | CONCEPTOS = { | |
| 50 | + | "miedo_escenario": [ | |
| 51 | + | "miedo", | |
| 52 | + | "panico", | |
| 53 | + | "nervios", | |
| 54 | + | "ansiedad", | |
| 55 | + | "presentacion", | |
| 56 | + | "presentar", | |
| 57 | + | "publico", | |
| 58 | + | "escenario", | |
| 59 | + | "hablar en publico", | |
| 60 | + | "audiencia", | |
| 61 | + | "examen", | |
| 62 | + | ], | |
| 63 | + | "vehiculo": [ | |
| 64 | + | "coche", | |
| 65 | + | "automovil", | |
| 66 | + | "carro", | |
| 67 | + | "turismo", | |
| 68 | + | "vehiculo", | |
| 69 | + | "conducir", | |
| 70 | + | "volante", | |
| 71 | + | ], | |
| 72 | + | "comida": [ | |
| 73 | + | "comida", | |
| 74 | + | "cena", | |
| 75 | + | "receta", | |
| 76 | + | "cocina", | |
| 77 | + | "carbonara", | |
| 78 | + | "restaurante", | |
| 79 | + | "ingrediente", | |
| 80 | + | ], | |
| 81 | + | "salud": ["dentista", "medico", "salud", "diente", "muela", "consulta", "revision"], | |
| 82 | + | "tecnologia": [ | |
| 83 | + | "disco", | |
| 84 | + | "duro", | |
| 85 | + | "ordenador", | |
| 86 | + | "backup", | |
| 87 | + | "copia", | |
| 88 | + | "linux", | |
| 89 | + | "programa", | |
| 90 | + | ], | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | ||
| 94 | + | # Palabras vacías del castellano: las que aparecen en todas partes y no | |
| 95 | + | # significan nada por sí solas. Un buscador serio las ignora; si no, cualquier | |
| 96 | + | # pregunta haría "coincidir" con cualquier recuerdo solo por compartir un «el». | |
| 97 | + | STOPWORDS = { | |
| 98 | + | "el", | |
| 99 | + | "la", | |
| 100 | + | "los", | |
| 101 | + | "las", | |
| 102 | + | "un", | |
| 103 | + | "una", | |
| 104 | + | "unos", | |
| 105 | + | "unas", | |
| 106 | + | "de", | |
| 107 | + | "del", | |
| 108 | + | "a", | |
| 109 | + | "al", | |
| 110 | + | "en", | |
| 111 | + | "y", | |
| 112 | + | "o", | |
| 113 | + | "u", | |
| 114 | + | "que", | |
| 115 | + | "por", | |
| 116 | + | "para", | |
| 117 | + | "con", | |
| 118 | + | "sin", | |
| 119 | + | "se", | |
| 120 | + | "su", | |
| 121 | + | "sus", | |
| 122 | + | "mi", | |
| 123 | + | "mis", | |
| 124 | + | "me", | |
| 125 | + | "te", | |
| 126 | + | "tu", | |
| 127 | + | "tus", | |
| 128 | + | "lo", | |
| 129 | + | "es", | |
| 130 | + | "son", | |
| 131 | + | "fue", | |
| 132 | + | "fui", | |
| 133 | + | "soy", | |
| 134 | + | "era", | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | ||
| 138 | + | def normalizar(texto): | |
| 139 | + | """Minúsculas y sin acentos, para que 'público' y 'publico' se encuentren.""" | |
| 140 | + | texto = unicodedata.normalize("NFKD", texto.lower()) | |
| 141 | + | return "".join(c for c in texto if not unicodedata.combining(c)) | |
| 142 | + | ||
| 143 | + | ||
| 144 | + | def palabras_con_peso(texto): | |
| 145 | + | """Trocea un texto en palabras con significado, tirando las vacías (stopwords).""" | |
| 146 | + | todas = re.findall(r"\w+", normalizar(texto)) | |
| 147 | + | return {p for p in todas if p not in STOPWORDS} | |
| 148 | + | ||
| 149 | + | ||
| 150 | + | def pluralizar(n, singular, plural): | |
| 151 | + | """'1 palabra' / 'N palabras'.""" | |
| 152 | + | return f"{n} {singular if n == 1 else plural}" | |
| 153 | + | ||
| 154 | + | ||
| 155 | + | def vector_de_conceptos(texto): | |
| 156 | + | """Convierte un texto en una pequeña lista de números: cuánto toca cada concepto.""" | |
| 157 | + | limpio = normalizar(texto) | |
| 158 | + | vector = [] | |
| 159 | + | for palabras in CONCEPTOS.values(): | |
| 160 | + | cuenta = sum(1 for p in palabras if p in limpio) | |
| 161 | + | vector.append(float(cuenta)) | |
| 162 | + | return vector | |
| 163 | + | ||
| 164 | + | ||
| 165 | + | def similitud_coseno(a, b): | |
| 166 | + | """Cuánto se parecen dos listas de números. Va de 0 (nada) a 1 (idénticas).""" | |
| 167 | + | producto = sum(x * y for x, y in zip(a, b)) | |
| 168 | + | norma_a = math.sqrt(sum(x * x for x in a)) | |
| 169 | + | norma_b = math.sqrt(sum(y * y for y in b)) | |
| 170 | + | if norma_a == 0 or norma_b == 0: | |
| 171 | + | return 0.0 | |
| 172 | + | return producto / (norma_a * norma_b) | |
| 173 | + | ||
| 174 | + | ||
| 175 | + | def buscar_por_palabra(pregunta, recuerdos): | |
| 176 | + | """Buscador clásico: cuenta cuántas palabras CON SIGNIFICADO comparten.""" | |
| 177 | + | palabras_pregunta = palabras_con_peso(pregunta) | |
| 178 | + | puntuaciones = [] | |
| 179 | + | for r in recuerdos: | |
| 180 | + | comunes = palabras_pregunta & palabras_con_peso(r) | |
| 181 | + | puntuaciones.append(len(comunes)) | |
| 182 | + | return puntuaciones | |
| 183 | + | ||
| 184 | + | ||
| 185 | + | def buscar_por_significado(pregunta, recuerdos): | |
| 186 | + | """Buscador moderno: compara el 'significado' (los conceptos) de cada texto.""" | |
| 187 | + | v_pregunta = vector_de_conceptos(pregunta) | |
| 188 | + | return [similitud_coseno(v_pregunta, vector_de_conceptos(r)) for r in recuerdos] | |
| 189 | + | ||
| 190 | + | ||
| 191 | + | def batalla(pregunta, recuerdos): | |
| 192 | + | print("=" * 68) | |
| 193 | + | print(" BUSCAR POR SIGNIFICADO, NO POR PALABRA") | |
| 194 | + | print("=" * 68) | |
| 195 | + | print(f"\n Pregunta: «{pregunta}»\n") | |
| 196 | + | print(" Recuerdos disponibles:") | |
| 197 | + | for i, r in enumerate(recuerdos, 1): | |
| 198 | + | print(f" {i}. {r}") | |
| 199 | + | ||
| 200 | + | por_palabra = buscar_por_palabra(pregunta, recuerdos) | |
| 201 | + | por_significado = buscar_por_significado(pregunta, recuerdos) | |
| 202 | + | ||
| 203 | + | mejor_palabra = max(range(len(recuerdos)), key=lambda i: por_palabra[i]) | |
| 204 | + | mejor_sig = max(range(len(recuerdos)), key=lambda i: por_significado[i]) | |
| 205 | + | ||
| 206 | + | print("\n ── Buscador por PALABRA ─────────────────────────────────────") | |
| 207 | + | if max(por_palabra) == 0: | |
| 208 | + | print(" ✗ No encuentra nada: ningún recuerdo comparte palabras con") | |
| 209 | + | print(" la pregunta. Aquí se queda sin respuesta.") | |
| 210 | + | else: | |
| 211 | + | for i, r in enumerate(recuerdos): | |
| 212 | + | marca = " ← el primero" if i == mejor_palabra else "" | |
| 213 | + | etiqueta = pluralizar( | |
| 214 | + | por_palabra[i], "palabra en común", "palabras en común" | |
| 215 | + | ) | |
| 216 | + | print(f" {etiqueta} · {r}{marca}") | |
| 217 | + | ||
| 218 | + | print("\n ── Buscador por SIGNIFICADO ─────────────────────────────────") | |
| 219 | + | for i, r in enumerate(recuerdos): | |
| 220 | + | marca = " ← el primero" if i == mejor_sig else "" | |
| 221 | + | print(f" {por_significado[i]:.2f} de parecido · {r}{marca}") | |
| 222 | + | ||
| 223 | + | print("\n Fíjate: el buscador de significado trae la ficha que HABLA DE LO") | |
| 224 | + | print(" MISMO aunque no comparta ni una palabra con tu pregunta. Eso es") | |
| 225 | + | print(" lo que hace que el asistente parezca que te recuerda.\n") | |
| 226 | + | ||
| 227 | + | ||
| 228 | + | def main(): | |
| 229 | + | # Caso 1: el ejemplo estrella del episodio. | |
| 230 | + | batalla( | |
| 231 | + | "quiero algo sobre el miedo a hablar en público", | |
| 232 | + | [ | |
| 233 | + | "Ansiedad antes de una presentación y consejos para respirar.", | |
| 234 | + | "Receta de carbonara para la cena del sábado.", | |
| 235 | + | "El martes fui al dentista por la mañana.", | |
| 236 | + | ], | |
| 237 | + | ) | |
| 238 | + | ||
| 239 | + | # Caso 2: el «coche / automóvil» que cuenta el episodio. | |
| 240 | + | batalla( | |
| 241 | + | "¿qué te conté del coche?", | |
| 242 | + | [ | |
| 243 | + | "Hablamos de comprar un automóvil de segunda mano.", | |
| 244 | + | "Ayer preparé una carbonara bastante decente.", | |
| 245 | + | ], | |
| 246 | + | ) | |
| 247 | + | ||
| 248 | + | print(" Nota: en Valet esto es de verdad, con un modelo que traduce el") | |
| 249 | + | print(" texto a números. Aquí es una maqueta para que se vea la idea.\n") | |
| 250 | + | ||
| 251 | + | ||
| 252 | + | if __name__ == "__main__": | |
| 253 | + | main() | |
03-la-bitacora.py(arquivo criado)
| @@ -0,0 +1,158 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | # -*- coding: utf-8 -*- | |
| 3 | + | """ | |
| 4 | + | 03 — La Bitácora: la memoria que se escribe sola | |
| 5 | + | ================================================ | |
| 6 | + | ||
| 7 | + | Episodio «¿Cómo se le da memoria a una IA?» — Punto 5 del guion, | |
| 8 | + | «El diario que se escribe solo» (acto III — El relato y lo que lo sostiene), | |
| 9 | + | la Capa E. | |
| 10 | + | ||
| 11 | + | La idea del episodio | |
| 12 | + | -------------------- | |
| 13 | + | La quinta pregunta no es «qué hiciste», es «¿cómo fue el día?». No pide una | |
| 14 | + | lista: pide un **relato**. Y lo mejor es que **se escribe sola**: una vez al | |
| 15 | + | día, de madrugada, el asistente coge lo del día (los hechos, los mensajes y el | |
| 16 | + | diario de ayer para no repetirse) y escribe **una entrada por día**. Con un | |
| 17 | + | detalle de otro nivel: puede decidir que **ese día no hay nada que contar**. | |
| 18 | + | Ese «no escribo» no es un error; es una respuesta válida. | |
| 19 | + | ||
| 20 | + | Qué hace este ejemplo | |
| 21 | + | --------------------- | |
| 22 | + | Lee un fichero con el «material del día» y hace lo mismo que haría Valet: | |
| 23 | + | ||
| 24 | + | * Si el día trae algo que merece memoria → escribe una entrada en | |
| 25 | + | `bitacora/AAAA-MM-DD.md`, en primera persona, y le añade los matices | |
| 26 | + | (ánimo, logros, dificultades, etiquetas). | |
| 27 | + | * Si el día está vacío → **no escribe nada** y lo dice en voz alta. | |
| 28 | + | ||
| 29 | + | Y respeta la regla de oro: **una sola entrada por día**. Si ya existe, no la | |
| 30 | + | duplica. | |
| 31 | + | ||
| 32 | + | En Valet el relato lo escribe un modelo. Aquí lo montamos a base de plantilla, | |
| 33 | + | sin instalar nada, porque lo que queremos enseñar no es la prosa, sino el | |
| 34 | + | **mecanismo**: recoger el día, decidir, y escribir una única entrada. | |
| 35 | + | ||
| 36 | + | Uso: | |
| 37 | + | python3 03-la-bitacora.py # enseña los días de ejemplo | |
| 38 | + | python3 03-la-bitacora.py datos/dia-2026-10-18.json | |
| 39 | + | """ | |
| 40 | + | ||
| 41 | + | import json | |
| 42 | + | import os | |
| 43 | + | import sys | |
| 44 | + | ||
| 45 | + | # Dónde se guardan las entradas, como en un diario real: un fichero por día. | |
| 46 | + | CARPETA_BITACORA = "bitacora" | |
| 47 | + | ||
| 48 | + | ||
| 49 | + | def dia_vacio(material): | |
| 50 | + | """Un día sin hechos, sin mensajes y sin nada que contar no merece entrada.""" | |
| 51 | + | return not material.get("hechos") and not material.get("mensajes") | |
| 52 | + | ||
| 53 | + | ||
| 54 | + | def componer_relato(material): | |
| 55 | + | """ | |
| 56 | + | Escribe el relato del día, en primera persona y con nuestras palabras. | |
| 57 | + | ||
| 58 | + | El episodio lo pide así: «no enumeres los hechos otra vez, descríbelos con | |
| 59 | + | tus palabras. Describe en vez de diagnosticar.» Aquí, sin modelo, hacemos | |
| 60 | + | una versión sencilla de esa idea: unimos los hechos en un párrafo que suene | |
| 61 | + | a cómo lo contarías tú, no a un informe. | |
| 62 | + | """ | |
| 63 | + | hechos = material.get("hechos", []) | |
| 64 | + | frases = [] | |
| 65 | + | ||
| 66 | + | # Abrimos con el primer hecho como arranque del día. | |
| 67 | + | if hechos: | |
| 68 | + | frases.append("Hoy " + hechos[0].rstrip(".").lower() + ".") | |
| 69 | + | # Y seguimos con el resto, enganchándolos con naturalidad. | |
| 70 | + | for hecho in hechos[1:]: | |
| 71 | + | frases.append("También " + hecho.rstrip(".").lower() + ".") | |
| 72 | + | ||
| 73 | + | if material.get("mensajes"): | |
| 74 | + | frases.append("Entre medias he ido hablando con el asistente de lo de siempre.") | |
| 75 | + | ||
| 76 | + | return " ".join(frases) | |
| 77 | + | ||
| 78 | + | ||
| 79 | + | def escribir_entrada(ruta, material): | |
| 80 | + | fecha = material["fecha"] | |
| 81 | + | os.makedirs(CARPETA_BITACORA, exist_ok=True) | |
| 82 | + | destino = os.path.join(CARPETA_BITACORA, f"{fecha}.md") | |
| 83 | + | ||
| 84 | + | # Regla de oro: una sola entrada por día. | |
| 85 | + | if os.path.exists(destino): | |
| 86 | + | print(f" {fecha}: ya había una entrada, no se duplica → {destino}") | |
| 87 | + | return | |
| 88 | + | ||
| 89 | + | relato = componer_relato(material) | |
| 90 | + | animo = material.get("animo", "sin determinar") | |
| 91 | + | logros = material.get("logros", []) | |
| 92 | + | dificultades = material.get("dificultades", []) | |
| 93 | + | etiquetas = material.get("etiquetas", []) | |
| 94 | + | ||
| 95 | + | lineas = [ | |
| 96 | + | f"# {fecha}", | |
| 97 | + | "", | |
| 98 | + | relato, | |
| 99 | + | "", | |
| 100 | + | f"- **Ánimo:** {animo}", | |
| 101 | + | ] | |
| 102 | + | if logros: | |
| 103 | + | lineas.append("- **Logros:** " + "; ".join(logros)) | |
| 104 | + | if dificultades: | |
| 105 | + | lineas.append("- **Dificultades:** " + "; ".join(dificultades)) | |
| 106 | + | if etiquetas: | |
| 107 | + | lineas.append("- **Etiquetas:** " + ", ".join(etiquetas)) | |
| 108 | + | lineas.append("") | |
| 109 | + | ||
| 110 | + | with open(destino, "w", encoding="utf-8") as f: | |
| 111 | + | f.write("\n".join(lineas)) | |
| 112 | + | ||
| 113 | + | print(f" {fecha}: se escribe la entrada → {destino}") | |
| 114 | + | print(" " + "-" * 62) | |
| 115 | + | for linea in lineas: | |
| 116 | + | print(" " + linea) | |
| 117 | + | print(" " + "-" * 62) | |
| 118 | + | ||
| 119 | + | ||
| 120 | + | def procesar(material): | |
| 121 | + | fecha = material.get("fecha", "sin fecha") | |
| 122 | + | print(f"\n Material del día {fecha}:") | |
| 123 | + | print(f" hechos : {len(material.get('hechos', []))}") | |
| 124 | + | print(f" mensajes : {len(material.get('mensajes', []))}") | |
| 125 | + | ||
| 126 | + | if dia_vacio(material): | |
| 127 | + | # Este es el «no escribo» explícito: una respuesta válida, no un error. | |
| 128 | + | print(" → NO se escribe entrada: ese día no hay nada que merezca memoria.") | |
| 129 | + | return | |
| 130 | + | ||
| 131 | + | escribir_entrada(CARPETA_BITACORA, material) | |
| 132 | + | ||
| 133 | + | ||
| 134 | + | def main(): | |
| 135 | + | print("=" * 68) | |
| 136 | + | print(" LA BITÁCORA: LA MEMORIA QUE SE ESCRIBE SOLA") | |
| 137 | + | print("=" * 68) | |
| 138 | + | ||
| 139 | + | if len(sys.argv) > 1: | |
| 140 | + | with open(sys.argv[1], encoding="utf-8") as f: | |
| 141 | + | procesar(json.load(f)) | |
| 142 | + | else: | |
| 143 | + | # Sin argumentos, enseña los días de ejemplo que trae el episodio: | |
| 144 | + | # dos jornadas reales (18 y 20 de octubre) y un día vacío, para ver | |
| 145 | + | # el «no escribo» en acción. | |
| 146 | + | aqui = os.path.dirname(os.path.abspath(__file__)) | |
| 147 | + | carpeta = os.path.join(aqui, "datos") | |
| 148 | + | for nombre in sorted(os.listdir(carpeta)): | |
| 149 | + | if nombre.startswith("dia-") and nombre.endswith(".json"): | |
| 150 | + | with open(os.path.join(carpeta, nombre), encoding="utf-8") as f: | |
| 151 | + | procesar(json.load(f)) | |
| 152 | + | ||
| 153 | + | print("\n Y así, sin que se lo pidas, el asistente te lleva el diario.") | |
| 154 | + | print(" De madrugada, una vez al día, decide y escribe. O decide no escribir.\n") | |
| 155 | + | ||
| 156 | + | ||
| 157 | + | if __name__ == "__main__": | |
| 158 | + | main() | |
04-el-olvido-y-el-eco.py(arquivo criado)
| @@ -0,0 +1,126 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | # -*- coding: utf-8 -*- | |
| 3 | + | """ | |
| 4 | + | 04 — El olvido y el eco | |
| 5 | + | ======================= | |
| 6 | + | ||
| 7 | + | Episodio «¿Cómo se le da memoria a una IA?» — Punto 7 del guion, | |
| 8 | + | «No hay botón de olvidar» (acto IV — Lo que falta) y cierre. | |
| 9 | + | ||
| 10 | + | La idea del episodio | |
| 11 | + | -------------------- | |
| 12 | + | Lo que el asistente **no puede olvidar**. Hoy no hay un botón de «olvida | |
| 13 | + | esto»: no existe forma de decirle «aquello del dentista, bórralo». La memoria | |
| 14 | + | solo crece. El único botón para empezar de nuevo es brutal: borra **todo**. | |
| 15 | + | Entre «lo recuerdo absolutamente todo» y «no recuerdo nada de ti» no hay | |
| 16 | + | término medio. | |
| 17 | + | ||
| 18 | + | Y hay un hallazgo precioso en el tramo final del episodio: cuando el sistema | |
| 19 | + | borra, borra el texto… pero a veces **el eco se queda**. Una huella que el buscador todavía | |
| 20 | + | puede «oler» pero que ya no tiene texto detrás. Como esa palabra en la punta de | |
| 21 | + | la lengua: sabes que estaba ahí, no la alcanzas. | |
| 22 | + | ||
| 23 | + | Qué hace este ejemplo | |
| 24 | + | --------------------- | |
| 25 | + | Monta una memoria de juguete con unos recuerdos y una lista de «huellas» (lo | |
| 26 | + | que el buscador tiene anotado para poder encontrarlos) y prueba tres cosas: | |
| 27 | + | ||
| 28 | + | 1. Intentar olvidar **un** recuerdo → no existe ese botón. Se niega. | |
| 29 | + | 2. Un borrado chapucero que se lleva el texto pero deja la huella → aparece | |
| 30 | + | **el eco**: el buscador lo huele, pero no hay nada que leer. | |
| 31 | + | 3. El botón brutal de «empezar de nuevo» → borra todo de verdad. | |
| 32 | + | ||
| 33 | + | No necesita nada instalado (solo Python 3). | |
| 34 | + | ||
| 35 | + | Uso: | |
| 36 | + | python3 04-el-olvido-y-el-eco.py | |
| 37 | + | """ | |
| 38 | + | ||
| 39 | + | ||
| 40 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 41 | + | # La memoria de juguete. Cada recuerdo tiene su texto… y el buscador guarda | |
| 42 | + | # aparte una "huella" para poder encontrarlo por significado (como en el | |
| 43 | + | # ejemplo 02). Texto y huella viven juntos pero no son lo mismo: ahí está la | |
| 44 | + | # trampa del eco. | |
| 45 | + | # ───────────────────────────────────────────────────────────────────────────── | |
| 46 | + | class Memoria: | |
| 47 | + | def __init__(self): | |
| 48 | + | self.recuerdos = { | |
| 49 | + | 1: { | |
| 50 | + | "texto": "Corrí 9,00 km con 119 m de desnivel y ritmo medio de 6:44 min/km.", | |
| 51 | + | "huella": "deporte running entrenamiento kilómetros", | |
| 52 | + | }, | |
| 53 | + | 2: { | |
| 54 | + | "texto": "Cené fuera.", | |
| 55 | + | "huella": "social cena fuera", | |
| 56 | + | }, | |
| 57 | + | 3: { | |
| 58 | + | "texto": "Corregí el nombre en la ficha de Valet.", | |
| 59 | + | "huella": "sistema asistente ficha nombre", | |
| 60 | + | }, | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | def buscar(self, consulta): | |
| 64 | + | """Buscador de andar por casa: mira las huellas, no el texto.""" | |
| 65 | + | resultados = [] | |
| 66 | + | for rid, r in self.recuerdos.items(): | |
| 67 | + | if consulta.lower() in r["huella"].lower(): | |
| 68 | + | resultados.append((rid, r)) | |
| 69 | + | return resultados | |
| 70 | + | ||
| 71 | + | ||
| 72 | + | def titulo(texto): | |
| 73 | + | print("\n" + "=" * 68) | |
| 74 | + | print(f" {texto}") | |
| 75 | + | print("=" * 68) | |
| 76 | + | ||
| 77 | + | ||
| 78 | + | def main(): | |
| 79 | + | memoria = Memoria() | |
| 80 | + | ||
| 81 | + | titulo("1. Intentar olvidar UN recuerdo concreto") | |
| 82 | + | print("\n Le pedimos: «olvida lo de la salida de ayer, bórralo».\n") | |
| 83 | + | print(" …") | |
| 84 | + | print(" No hay un botón de «olvida esto». No existe.") | |
| 85 | + | print(" La memoria solo crece: lo único que caduca solo son las estadísticas.") | |
| 86 | + | print(" Y el único botón para empezar de nuevo borra TODO, no un recuerdo.") | |
| 87 | + | ||
| 88 | + | titulo("2. El eco: borrar el texto y dejar la huella") | |
| 89 | + | print("\n Imaginemos un borrado chapucero: se lleva el texto del recuerdo,") | |
| 90 | + | print(" pero la huella del buscador se queda. Mirad lo que pasa.\n") | |
| 91 | + | ||
| 92 | + | # Borrado chapucero: quitamos el texto pero dejamos la huella. | |
| 93 | + | del memoria.recuerdos[1]["texto"] | |
| 94 | + | ||
| 95 | + | print(" Recuerdo 1 después del borrado chapucero:") | |
| 96 | + | print(f" texto : {memoria.recuerdos[1].get('texto', '(ya no hay texto)')}") | |
| 97 | + | print(f" huella : {memoria.recuerdos[1]['huella']}") | |
| 98 | + | ||
| 99 | + | print("\n Ahora el buscador intenta encontrarlo por «running»:\n") | |
| 100 | + | for rid, r in memoria.buscar("running"): | |
| 101 | + | print( | |
| 102 | + | f" · recuerdo {rid}: huele a «{r['huella']}»… " | |
| 103 | + | f"pero texto = {r.get('texto', '(nada que leer)')}" | |
| 104 | + | ) | |
| 105 | + | print("\n Ahí está el eco: una huella que el buscador puede oler, pero que") | |
| 106 | + | print(" ya no tiene texto detrás. Un recuerdo al que has dejado de poder") | |
| 107 | + | print(" llegar, pero que sabes que estaba ahí.") | |
| 108 | + | print(" Como la palabra en la punta de la lengua.") | |
| 109 | + | ||
| 110 | + | titulo("3. El botón brutal: empezar de nuevo") | |
| 111 | + | print("\n No hay término medio. El único «empezar de nuevo» borra la memoria") | |
| 112 | + | print(" entera. Como si acabaras de instalar el asistente y nunca le") | |
| 113 | + | print(" hubieras contado nada.\n") | |
| 114 | + | memoria.recuerdos.clear() | |
| 115 | + | print(f" Recuerdos que quedan: {len(memoria.recuerdos)}.") | |
| 116 | + | print(" Ni el eco: nada. Eso sí es olvidar… demasiado.") | |
| 117 | + | ||
| 118 | + | titulo("Para llevarse") | |
| 119 | + | print("\n Un asistente que lo recuerda todo pero no sabe olvidar nada no es") | |
| 120 | + | print(" un buen asistente: es alguien que nunca te perdona nada.") | |
| 121 | + | print(" Y si esto algún día sale de tu máquina, el primer cambio no sería") | |
| 122 | + | print(" una capa nueva: sería un botón de OLVIDAR DE VERDAD.\n") | |
| 123 | + | ||
| 124 | + | ||
| 125 | + | if __name__ == "__main__": | |
| 126 | + | main() | |
README.md(arquivo criado)
| @@ -0,0 +1,169 @@ | |||
| 1 | + | # Ejemplos — «¿Cómo se le da memoria a una IA?» | |
| 2 | + | ||
| 3 | + | Material de apoyo para la web y para el oyente curioso. Acompaña al episodio de | |
| 4 | + | *atareao con Linux* que cuenta —sin una palabra de programación— cómo se le da | |
| 5 | + | memoria a un modelo de lenguaje, usando | |
| 6 | + | [`valet-ai`](https://github.com/atareao/valet-ai) como caso de estudio. | |
| 7 | + | ||
| 8 | + | Los ejemplos de aquí no son «de laboratorio»: son **fragmentos reales de una | |
| 9 | + | instancia de Valet en producción**, generalizados para poder publicarlos. Se han | |
| 10 | + | quitado nombres de terceros, lugares, restaurantes, proyectos internos y el | |
| 11 | + | nombre propio del asistente; se ha mantenido el resto —incluidas las cifras y | |
| 12 | + | los ejemplos de *running*— tal cual. | |
| 13 | + | ||
| 14 | + | > **Ojo:** aquí **no se instala ni se despliega Valet**. Eso es el episodio | |
| 15 | + | > siguiente, *«Podman, IPs reales y La Liga»*. Esto de aquí es solo para | |
| 16 | + | > **asomarse a las ideas**: cuatro ejemplos autocontenidos que se ejecutan con | |
| 17 | + | > Python 3 y no tocan nada de nadie. | |
| 18 | + | ||
| 19 | + | --- | |
| 20 | + | ||
| 21 | + | ## La estructura del episodio: 7 puntos, 4 actos | |
| 22 | + | ||
| 23 | + | El guión va de lo mecánico a lo humano en siete puntos, agrupados en cuatro | |
| 24 | + | actos. **Solo cuatro de esos puntos llevan demo**; los otros tres se apoyan en | |
| 25 | + | una imagen, un diagrama o un dato. Este es el orden de emisión, con el ejemplo | |
| 26 | + | que se ejecuta en cada punto que lo tiene: | |
| 27 | + | ||
| 28 | + | | Acto | Punto | Título | Qué se ve | Ejemplo | | |
| 29 | + | |---|---|---|---|---| | |
| 30 | + | | **I · El cajón** | 1 | Un buscador con modales | imagen | — | | |
| 31 | + | | **I · El cajón** | 2 | La memoria vive alrededor | diagrama | — | | |
| 32 | + | | **II · Los hechos y el retrato** | 3 | Lo que dijiste, lo que pasó y cuándo | **demo** | `02-buscar-por-significado.py` | | |
| 33 | + | | **II · Los hechos y el retrato** | 4 | El retrato | dato (19 normas) | — | | |
| 34 | + | | **III · El relato y lo que lo sostiene** | 5 | El diario que se escribe solo | **demo** | `03-la-bitacora.py` | | |
| 35 | + | | **III · El relato y lo que lo sostiene** | 6 | Las tres decisiones | **demo** | `01-un-solo-fichero.py` | | |
| 36 | + | | **IV · Lo que falta** | 7 | No hay botón de olvidar | **demo** | `04-el-olvido-y-el-eco.py` | | |
| 37 | + | ||
| 38 | + | Fíjate en que los nombres de los ficheros **no son `demo-01`, `demo-02`…**: se | |
| 39 | + | referencian por el nombre con el que el guión, la escaleta y la presentación los | |
| 40 | + | citan. El número del fichero es solo una etiqueta de taller, no el orden de | |
| 41 | + | emisión: el `01` se emite en el punto 6, el `02` en el punto 3, el `03` en el | |
| 42 | + | punto 5 y el `04` en el punto 7. | |
| 43 | + | ||
| 44 | + | --- | |
| 45 | + | ||
| 46 | + | ## Las cinco capas del episodio | |
| 47 | + | ||
| 48 | + | Todo el programa gira alrededor de cinco preguntas, que en Valet son cinco | |
| 49 | + | capas: **A · B · C · D · E**. De lo mecánico a lo humano. | |
| 50 | + | ||
| 51 | + | | Capa | Pregunta | Qué guarda | Ejemplo de aquí | | |
| 52 | + | |---|---|---|---| | |
| 53 | + | | **A** | ¿Qué guardo? | Lo que dijiste, tal cual. La grabación en bruto. | `01` | | |
| 54 | + | | **B** | ¿Cómo lo encuentro? | Fichas con el sentido de la conversación, buscadas **por significado**. | `01`, `02` | | |
| 55 | + | | **C** | ¿Cómo sé cómo eres? | Tu retrato: verdades sobre ti + cómo quieres que se comporte. | `01` | | |
| 56 | + | | **D** | ¿Qué pasó, y cuándo? | Hechos sueltos con fecha: tu línea del tiempo. | `01` | | |
| 57 | + | | **E** | ¿Cómo fue el día? | **La Bitácora**: el relato del día, escrito solo. | `01`, `03` | | |
| 58 | + | ||
| 59 | + | Y tres decisiones que atraviesan todas las capas: | |
| 60 | + | ||
| 61 | + | 1. **Todo cabe en un solo fichero**, en tu máquina → `01`. | |
| 62 | + | 2. **Una sola transacción**: las cuatro capas de la vida, de golpe → se cuenta | |
| 63 | + | en el episodio; el problema del «eco» se ve en `04`. | |
| 64 | + | 3. **La edad no borra, reordena** → se cuenta con la imagen de la biblioteca | |
| 65 | + | en el episodio. | |
| 66 | + | ||
| 67 | + | --- | |
| 68 | + | ||
| 69 | + | ## Cifras reales de la instancia | |
| 70 | + | ||
| 71 | + | Medido sobre la instancia de producción de Valet el **25 de octubre de 2026**. | |
| 72 | + | ||
| 73 | + | | Capa / actividad | Qué hay | Cifra | | |
| 74 | + | |---|---|---| | |
| 75 | + | | **Conversación** | Mensajes totales (268 usuario / 247 asistente), desde el 6-oct | **515** | | |
| 76 | + | | **A · indexado** | Mensajes ya procesados por el asistente | **503** | | |
| 77 | + | | **B · fichas** | Resúmenes de conversación guardados | **81** | | |
| 78 | + | | **C · persistente** | Una sola ficha, con normas de casa | **1 fila · 19 normas** | | |
| 79 | + | | **D · timeline** | Hechos con fecha (8 deporte, 1 social, 1 sistema) | **10** | | |
| 80 | + | | **E · bitácora** | Entradas (18–24 oct; 5 con ánimo «buen día», 1 «neutro») | **6** | | |
| 81 | + | | **Llamadas al modelo** | Chat 654 · router 237 · consolidador 192 · archivista 111 · timeline 74 · journal 6 | **1.274** | | |
| 82 | + | ||
| 83 | + | Procedencia: **instancia de producción de Valet, 25-oct-2026**, consultada en | |
| 84 | + | solo lectura. Las cifras se mantienen íntegras; lo único que se ha generalizado | |
| 85 | + | es lo identificable (nombres propios, lugares y proyectos). | |
| 86 | + | ||
| 87 | + | --- | |
| 88 | + | ||
| 89 | + | ## Los ficheros | |
| 90 | + | ||
| 91 | + | | Fichero | Necesita | Qué enseña | | |
| 92 | + | |---|---|---| | |
| 93 | + | | `01-un-solo-fichero.py` | nada (Python 3) | Cómo las cinco capas caben en **un único fichero** que es tuyo, con fragmentos reales de las capas A–E. Lo copias al pendrive, lo abres y lees qué sabe de ti. | | |
| 94 | + | | `02-buscar-por-significado.py` | nada (Python 3) | La diferencia entre buscar **por palabra** y buscar **por significado** (el bibliotecario de ideas del episodio). | | |
| 95 | + | | `03-la-bitacora.py` | nada (Python 3) | La Capa E: una entrada **por día**, escrita sola… o el «no escribo» cuando el día no da para nada. | | |
| 96 | + | | `04-el-olvido-y-el-eco.py` | nada (Python 3) | Que no hay botón de «olvida esto», que el único «empezar de nuevo» borra todo, y el **eco** de un recuerdo mal borrado. | | |
| 97 | + | | `datos/` | — | Tres jornadas de ejemplo para el `03`: el 18 y el 20 de octubre (reales) y un día vacío, para ver el «no escribo». | | |
| 98 | + | ||
| 99 | + | Todos son de **solo lectura** sobre el mundo exterior: crean sus ficheros de | |
| 100 | + | juguete en una carpeta de trabajo y no llaman a ninguna nube. | |
| 101 | + | ||
| 102 | + | --- | |
| 103 | + | ||
| 104 | + | ## Cómo probarlos | |
| 105 | + | ||
| 106 | + | ```bash | |
| 107 | + | cd ejemplos-la-memoria-de-valet | |
| 108 | + | ||
| 109 | + | python3 01-un-solo-fichero.py | |
| 110 | + | python3 02-buscar-por-significado.py | |
| 111 | + | python3 03-la-bitacora.py | |
| 112 | + | python3 04-el-olvido-y-el-eco.py | |
| 113 | + | ``` | |
| 114 | + | ||
| 115 | + | El `03` escribe sus entradas en una carpeta `bitacora/` del directorio donde lo | |
| 116 | + | ejecutes. Bórrala cuando quieras: así se ve, de paso, que borrar es borrar. | |
| 117 | + | ||
| 118 | + | --- | |
| 119 | + | ||
| 120 | + | ## Para qué sirve cada uno, contado en lenguaje llano | |
| 121 | + | ||
| 122 | + | **`02` — El bibliotecario que entiende ideas** *(punto 3)*. Un buscador normal | |
| 123 | + | te trae el libro que se titula literalmente como tu pregunta. El de Valet te | |
| 124 | + | trae **el libro que habla de lo mismo**, aunque no comparta ni una palabra. En | |
| 125 | + | el ejemplo (que es una maqueta, sin modelo) se ve cómo «quiero algo sobre el | |
| 126 | + | miedo a hablar en público» encuentra la ficha de «ansiedad antes de una | |
| 127 | + | presentación», y cómo «coche» encuentra «automóvil». | |
| 128 | + | ||
| 129 | + | **`03` — Alguien llevándote el diario** *(punto 5)*. La quinta capa no cataloga: | |
| 130 | + | cuenta. Le das el material de un día y decide si merece una entrada. Si el día | |
| 131 | + | está vacío, **no escribe nada**, y eso es una respuesta válida. Si escribe, es | |
| 132 | + | **una sola entrada por día**, con el relato y sus matices (ánimo, logros, | |
| 133 | + | dificultades, etiquetas). Los días de ejemplo son reales: el 18 de octubre | |
| 134 | + | (lluvia, plan de entrenamiento ajustado, tirada de 40 min en vez de 13 km) y el | |
| 135 | + | 20 de octubre (7,51 km en 51:42). | |
| 136 | + | ||
| 137 | + | **`01` — Tu memoria en un fichero** *(punto 6)*. Todo lo que sabe tu asistente | |
| 138 | + | —lo que le dijiste, lo que pasó, cómo eres, tu línea del tiempo y el diario— | |
| 139 | + | vive en **una sola caja fuerte** que está en tu casa. La copias a un pendrive y | |
| 140 | + | la llevas en el bolsillo. La abres y lees lo que sabe de ti. La borras y no queda | |
| 141 | + | rastro. Este ejemplo lo enseña con contenido real: una ficha de resumen de | |
| 142 | + | artículos de RSS, tres de las normas de casa, tres hechos de la línea del tiempo | |
| 143 | + | (una salida a correr de 9,00 km, una cena fuera y una corrección del nombre de | |
| 144 | + | Valet) y una entrada de bitácora del 18 de octubre. | |
| 145 | + | ||
| 146 | + | **`04` — La pregunta difícil** *(punto 7)*. Pedirle que olvide un recuerdo | |
| 147 | + | concreto no funciona: ese botón no existe. Solo hay uno, y es brutal: borra | |
| 148 | + | todo. Y en medio, el **eco** —una huella que el buscador todavía huele, pero sin | |
| 149 | + | texto detrás—, que es la mejor metáfora de lo que es la memoria, incluso la | |
| 150 | + | humana. Los recuerdos usados son los tres hechos reales del timeline. | |
| 151 | + | ||
| 152 | + | --- | |
| 153 | + | ||
| 154 | + | ## Lo que se cuenta en el episodio y no se ve aquí | |
| 155 | + | ||
| 156 | + | Para que no haya sorpresas a quien venga buscando trucos: | |
| 157 | + | ||
| 158 | + | - **La instalación y el despliegue de Valet no están en este episodio.** Llegan | |
| 159 | + | en el siguiente, *Podman, IPs reales y La Liga*. | |
| 160 | + | - La **búsqueda por significado** de verdad usa un modelo que traduce el texto | |
| 161 | + | a números. El `02` lo imita con un diccionario a mano para que puedas | |
| 162 | + | ejecutarlo sin instalar nada. | |
| 163 | + | - La **Bitácora** de verdad la escribe un modelo a partir de tus hechos y | |
| 164 | + | mensajes. El `03` reproduce el **mecanismo** (recoger el día, decidir, | |
| 165 | + | escribir una única entrada), no la prosa. | |
| 166 | + | ||
| 167 | + | ## El repositorio | |
| 168 | + | ||
| 169 | + | - **Valet** — <https://github.com/atareao/valet-ai> (público, licencia MIT) | |
dia-2026-10-18.json(arquivo criado)
| @@ -0,0 +1,18 @@ | |||
| 1 | + | { | |
| 2 | + | "fecha": "2026-10-18", | |
| 3 | + | "hechos": [ | |
| 4 | + | "Ajusté el plan de entrenamiento a la lluvia.", | |
| 5 | + | "Decidí una tirada de 40 min en vez de 13 km." | |
| 6 | + | ], | |
| 7 | + | "mensajes": [ | |
| 8 | + | "Con esta lluvia, ¿qué me recomiendas para el entrenamiento de hoy?", | |
| 9 | + | "Apúntame que al final he cambiado la tirada larga por 40 minutos." | |
| 10 | + | ], | |
| 11 | + | "animo": "buen día", | |
| 12 | + | "logros": [ | |
| 13 | + | "Ajustar el plan de entrenamiento a la lluvia", | |
| 14 | + | "Decidir una tirada de 40 min en vez de 13 km" | |
| 15 | + | ], | |
| 16 | + | "dificultades": ["Condiciones adversas"], | |
| 17 | + | "etiquetas": ["entrenamiento", "meteorología", "decisiones"] | |
| 18 | + | } | |
dia-2026-10-19.json(arquivo criado)
| @@ -0,0 +1,9 @@ | |||
| 1 | + | { | |
| 2 | + | "fecha": "2026-10-19", | |
| 3 | + | "hechos": [], | |
| 4 | + | "mensajes": [], | |
| 5 | + | "animo": "sin determinar", | |
| 6 | + | "logros": [], | |
| 7 | + | "dificultades": [], | |
| 8 | + | "etiquetas": [] | |
| 9 | + | } | |
dia-2026-10-20.json(arquivo criado)
| @@ -0,0 +1,15 @@ | |||
| 1 | + | { | |
| 2 | + | "fecha": "2026-10-20", | |
| 3 | + | "hechos": [ | |
| 4 | + | "Finalmente pude salir a correr tras varios días de lluvia.", | |
| 5 | + | "Completé 7,51 km en 51:42." | |
| 6 | + | ], | |
| 7 | + | "mensajes": [ | |
| 8 | + | "¿Cómo llevo la semana de entrenamiento?", | |
| 9 | + | "Apunta la salida de hoy: 7,51 km en 51:42." | |
| 10 | + | ], | |
| 11 | + | "animo": "buen día", | |
| 12 | + | "logros": ["Retomar el entrenamiento tras la lluvia"], | |
| 13 | + | "dificultades": [], | |
| 14 | + | "etiquetas": ["entrenamiento", "running"] | |
| 15 | + | } | |