最后活跃于 1 day 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.

atareao's Avatar atareao 修订了这个 Gist 1 day ago. 转到此修订

8 files changed, 917 insertions

01-un-solo-fichero.py(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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(文件已创建)

@@ -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 + }
上一页 下一页