最后活跃于 22 hours ago

Episodio 843 — ¿Cómo se le da memoria a una IA? Cuatro ejemplos autocontenidos (Python 3, sin dependencias, sin red) sobre las cinco capas de memoria de Valet: un solo fichero, buscar por significado, la bitácora que se escribe sola y el eco del olvido.

01-un-solo-fichero.py 原始文件
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
401 — Todo cabe en un solo fichero
5=================================
6
7Episodio «¿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
10La idea del episodio
11--------------------
12Todos los recuerdos de un asistente con memoria pueden vivir en **un único
13fichero** en tu propia máquina: lo que dijiste, las fichas de lo que pasó,
14có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
21Qué hace este ejemplo
22---------------------
23Crea ese fichero único —un `.json` con las cinco capas dentro—, lo enseña por
24pantalla, lo copia como si fuera a un *pendrive* y comprueba que la copia
25contiene exactamente lo mismo. Los recuerdos que trae son **fragmentos reales
26de una instancia de Valet en producción**, generalizados para publicarlos: sin
27nombres de terceros, sin lugares y sin datos que permitan identificarla. La
28gracia no es el formato, es que **cabe todo junto y es tuyo**.
29
30No necesita nada instalado (solo Python 3, que ya viene en casi cualquier Linux).
31
32Uso:
33 python3 01-un-solo-fichero.py # escribe ./memoria-demo.json
34 python3 01-un-solo-fichero.py /ruta/mi-memoria.json
35"""
36
37import json
38import os
39import shutil
40import 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# ─────────────────────────────────────────────────────────────────────────────
51MEMORIA_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
119def ruta_por_defecto():
120 return os.path.join(os.getcwd(), "memoria-demo.json")
121
122
123def 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
168if __name__ == "__main__":
169 main()
170
02-buscar-por-significado.py 原始文件
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
402 — Buscar por significado, no por palabra
5============================================
6
7Episodio «¿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
10La idea del episodio
11--------------------
12Cuando le preguntas algo, el asistente no busca el libro que se titula
13literalmente como tu pregunta. Busca **ideas**. Si tú preguntas por «coche» y
14la ficha dice «automóvil», un buscador normal no lo encuentra; este sí. En el
15mundo real, el sistema traduce el texto a números (lo que llaman *búsqueda
16semántica* o RAG) y compara esos números. Aquí no vamos a meter un modelo: lo
17imitamos con un diccionario de conceptos hecho a mano, para que se vea la
18diferencia sin instalar nada.
19
20Qué 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
30No necesita nada instalado (solo Python 3).
31
32Uso:
33 python3 02-buscar-por-significado.py
34"""
35
36import math
37import re
38import 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# ─────────────────────────────────────────────────────────────────────────────
49CONCEPTOS = {
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».
97STOPWORDS = {
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
138def 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
144def 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
150def pluralizar(n, singular, plural):
151 """'1 palabra' / 'N palabras'."""
152 return f"{n} {singular if n == 1 else plural}"
153
154
155def 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
165def 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
175def 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
185def 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
191def 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
228def 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
252if __name__ == "__main__":
253 main()
254
03-la-bitacora.py 原始文件
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
403 — La Bitácora: la memoria que se escribe sola
5================================================
6
7Episodio «¿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),
9la Capa E.
10
11La idea del episodio
12--------------------
13La quinta pregunta no es «qué hiciste», es «¿cómo fue el día?». No pide una
14lista: pide un **relato**. Y lo mejor es que **se escribe sola**: una vez al
15día, de madrugada, el asistente coge lo del día (los hechos, los mensajes y el
16diario de ayer para no repetirse) y escribe **una entrada por día**. Con un
17detalle de otro nivel: puede decidir que **ese día no hay nada que contar**.
18Ese «no escribo» no es un error; es una respuesta válida.
19
20Qué hace este ejemplo
21---------------------
22Lee 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
29Y respeta la regla de oro: **una sola entrada por día**. Si ya existe, no la
30duplica.
31
32En Valet el relato lo escribe un modelo. Aquí lo montamos a base de plantilla,
33sin 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
36Uso:
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
41import json
42import os
43import sys
44
45# Dónde se guardan las entradas, como en un diario real: un fichero por día.
46CARPETA_BITACORA = "bitacora"
47
48
49def 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
54def 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
79def 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
120def 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
134def 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
157if __name__ == "__main__":
158 main()
159
04-el-olvido-y-el-eco.py 原始文件
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
404 — El olvido y el eco
5=======================
6
7Episodio «¿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
10La idea del episodio
11--------------------
12Lo que el asistente **no puede olvidar**. Hoy no hay un botón de «olvida
13esto»: no existe forma de decirle «aquello del dentista, bórralo». La memoria
14solo crece. El único botón para empezar de nuevo es brutal: borra **todo**.
15Entre «lo recuerdo absolutamente todo» y «no recuerdo nada de ti» no hay
16término medio.
17
18Y hay un hallazgo precioso en el tramo final del episodio: cuando el sistema
19borra, borra el texto… pero a veces **el eco se queda**. Una huella que el buscador todavía
20puede «oler» pero que ya no tiene texto detrás. Como esa palabra en la punta de
21la lengua: sabes que estaba ahí, no la alcanzas.
22
23Qué hace este ejemplo
24---------------------
25Monta una memoria de juguete con unos recuerdos y una lista de «huellas» (lo
26que 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
33No necesita nada instalado (solo Python 3).
34
35Uso:
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# ─────────────────────────────────────────────────────────────────────────────
46class 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
72def titulo(texto):
73 print("\n" + "=" * 68)
74 print(f" {texto}")
75 print("=" * 68)
76
77
78def 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
125if __name__ == "__main__":
126 main()
127
README.md 原始文件

Ejemplos — «¿Cómo se le da memoria a una IA?»

Material de apoyo para la web y para el oyente curioso. Acompaña al episodio de atareao con Linux que cuenta —sin una palabra de programación— cómo se le da memoria a un modelo de lenguaje, usando valet-ai como caso de estudio.

Los ejemplos de aquí no son «de laboratorio»: son fragmentos reales de una instancia de Valet en producción, generalizados para poder publicarlos. Se han quitado nombres de terceros, lugares, restaurantes, proyectos internos y el nombre propio del asistente; se ha mantenido el resto —incluidas las cifras y los ejemplos de running— tal cual.

Ojo: aquí no se instala ni se despliega Valet. Eso es el episodio siguiente, «Podman, IPs reales y La Liga». Esto de aquí es solo para asomarse a las ideas: cuatro ejemplos autocontenidos que se ejecutan con Python 3 y no tocan nada de nadie.


La estructura del episodio: 7 puntos, 4 actos

El guión va de lo mecánico a lo humano en siete puntos, agrupados en cuatro actos. Solo cuatro de esos puntos llevan demo; los otros tres se apoyan en una imagen, un diagrama o un dato. Este es el orden de emisión, con el ejemplo que se ejecuta en cada punto que lo tiene:

Acto Punto Título Qué se ve Ejemplo
I · El cajón 1 Un buscador con modales imagen —
I · El cajón 2 La memoria vive alrededor diagrama —
II · Los hechos y el retrato 3 Lo que dijiste, lo que pasó y cuándo demo 02-buscar-por-significado.py
II · Los hechos y el retrato 4 El retrato dato (19 normas) —
III · El relato y lo que lo sostiene 5 El diario que se escribe solo demo 03-la-bitacora.py
III · El relato y lo que lo sostiene 6 Las tres decisiones demo 01-un-solo-fichero.py
IV · Lo que falta 7 No hay botón de olvidar demo 04-el-olvido-y-el-eco.py

Fíjate en que los nombres de los ficheros no son demo-01, demo-02…: se referencian por el nombre con el que el guión, la escaleta y la presentación los citan. El número del fichero es solo una etiqueta de taller, no el orden de emisión: el 01 se emite en el punto 6, el 02 en el punto 3, el 03 en el punto 5 y el 04 en el punto 7.


Las cinco capas del episodio

Todo el programa gira alrededor de cinco preguntas, que en Valet son cinco capas: A · B · C · D · E. De lo mecánico a lo humano.

Capa Pregunta Qué guarda Ejemplo de aquí
A ¿Qué guardo? Lo que dijiste, tal cual. La grabación en bruto. 01
B ¿Cómo lo encuentro? Fichas con el sentido de la conversación, buscadas por significado. 01, 02
C ¿Cómo sé cómo eres? Tu retrato: verdades sobre ti + cómo quieres que se comporte. 01
D ¿Qué pasó, y cuándo? Hechos sueltos con fecha: tu línea del tiempo. 01
E ¿Cómo fue el día? La Bitácora: el relato del día, escrito solo. 01, 03

Y tres decisiones que atraviesan todas las capas:

  1. Todo cabe en un solo fichero, en tu máquina → 01.
  2. Una sola transacción: las cuatro capas de la vida, de golpe → se cuenta en el episodio; el problema del «eco» se ve en 04.
  3. La edad no borra, reordena → se cuenta con la imagen de la biblioteca en el episodio.

Cifras reales de la instancia

Medido sobre la instancia de producción de Valet el 25 de octubre de 2026.

Capa / actividad Qué hay Cifra
Conversación Mensajes totales (268 usuario / 247 asistente), desde el 6-oct 515
A · indexado Mensajes ya procesados por el asistente 503
B · fichas Resúmenes de conversación guardados 81
C · persistente Una sola ficha, con normas de casa 1 fila · 19 normas
D · timeline Hechos con fecha (8 deporte, 1 social, 1 sistema) 10
E · bitácora Entradas (18–24 oct; 5 con ánimo «buen día», 1 «neutro») 6
Llamadas al modelo Chat 654 · router 237 · consolidador 192 · archivista 111 · timeline 74 · journal 6 1.274

Procedencia: instancia de producción de Valet, 25-oct-2026, consultada en solo lectura. Las cifras se mantienen íntegras; lo único que se ha generalizado es lo identificable (nombres propios, lugares y proyectos).


Los ficheros

Fichero Necesita Qué enseña
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.
02-buscar-por-significado.py nada (Python 3) La diferencia entre buscar por palabra y buscar por significado (el bibliotecario de ideas del episodio).
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.
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.
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».

Todos son de solo lectura sobre el mundo exterior: crean sus ficheros de juguete en una carpeta de trabajo y no llaman a ninguna nube.


Cómo probarlos

cd ejemplos-la-memoria-de-valet

python3 01-un-solo-fichero.py
python3 02-buscar-por-significado.py
python3 03-la-bitacora.py
python3 04-el-olvido-y-el-eco.py

El 03 escribe sus entradas en una carpeta bitacora/ del directorio donde lo ejecutes. Bórrala cuando quieras: así se ve, de paso, que borrar es borrar.


Para qué sirve cada uno, contado en lenguaje llano

02 — El bibliotecario que entiende ideas (punto 3). Un buscador normal te trae el libro que se titula literalmente como tu pregunta. El de Valet te trae el libro que habla de lo mismo, aunque no comparta ni una palabra. En el ejemplo (que es una maqueta, sin modelo) se ve cómo «quiero algo sobre el miedo a hablar en público» encuentra la ficha de «ansiedad antes de una presentación», y cómo «coche» encuentra «automóvil».

03 — Alguien llevándote el diario (punto 5). La quinta capa no cataloga: cuenta. Le das el material de un día y decide si merece una entrada. Si el día está vacío, no escribe nada, y eso es una respuesta válida. Si escribe, es una sola entrada por día, con el relato y sus matices (ánimo, logros, dificultades, etiquetas). Los días de ejemplo son reales: el 18 de octubre (lluvia, plan de entrenamiento ajustado, tirada de 40 min en vez de 13 km) y el 20 de octubre (7,51 km en 51:42).

01 — Tu memoria en un fichero (punto 6). Todo lo que sabe tu asistente —lo que le dijiste, lo que pasó, cómo eres, tu línea del tiempo y el diario— vive en una sola caja fuerte que está en tu casa. La copias a un pendrive y la llevas en el bolsillo. La abres y lees lo que sabe de ti. La borras y no queda rastro. Este ejemplo lo enseña con contenido real: una ficha de resumen de artículos de RSS, tres de las normas de casa, tres hechos de la línea del tiempo (una salida a correr de 9,00 km, una cena fuera y una corrección del nombre de Valet) y una entrada de bitácora del 18 de octubre.

04 — La pregunta difícil (punto 7). Pedirle que olvide un recuerdo concreto no funciona: ese botón no existe. Solo hay uno, y es brutal: borra todo. Y en medio, el eco —una huella que el buscador todavía huele, pero sin texto detrás—, que es la mejor metáfora de lo que es la memoria, incluso la humana. Los recuerdos usados son los tres hechos reales del timeline.


Lo que se cuenta en el episodio y no se ve aquí

Para que no haya sorpresas a quien venga buscando trucos:

  • La instalación y el despliegue de Valet no están en este episodio. Llegan en el siguiente, Podman, IPs reales y La Liga.
  • La búsqueda por significado de verdad usa un modelo que traduce el texto a números. El 02 lo imita con un diccionario a mano para que puedas ejecutarlo sin instalar nada.
  • La Bitácora de verdad la escribe un modelo a partir de tus hechos y mensajes. El 03 reproduce el mecanismo (recoger el día, decidir, escribir una única entrada), no la prosa.

El repositorio

dia-2026-10-18.json 原始文件
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}
19
dia-2026-10-19.json 原始文件
1{
2 "fecha": "2026-10-19",
3 "hechos": [],
4 "mensajes": [],
5 "animo": "sin determinar",
6 "logros": [],
7 "dificultades": [],
8 "etiquetas": []
9}
10
dia-2026-10-20.json 原始文件
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}
16