Son aktivite 15 hours ago

embeddings.py Ham
1#!/usr/bin/env python3
2"""
3🚀 Embeddings para todo — Demo completa
4========================================
5Episodio 817: Un solo script que recorre todos los casos de uso.
6Búsqueda semántica, clasificación, deduplicación, recomendación y ChromaDB.
7
8Requisitos:
9 - Ollama corriendo en localhost:11434
10 - Modelo nomic-embed-text descargado: ollama pull nomic-embed-text
11 - Python 3.10+
12
13Uso:
14 python3 setup_completo.py # Ejecuta todo
15 python3 setup_completo.py --fast # Solo pasos principales, sin ChromaDB
16 python3 setup_completo.py --list # Lista los pasos disponibles
17 python3 setup_completo.py --step 3 # Ejecuta solo el paso 3
18 python3 setup_completo.py --step 2,4,6 # Ejecuta pasos específicos
19 python3 setup_completo.py --interactive # Paso a paso con confirmación
20"""
21
22import os, sys, json, math, glob, time, shutil, subprocess, textwrap, argparse
23
24# ─── Configuración ────────────────────────────────────────────────────────
25MODEL = "bge-m3"
26OLLAMA_URL = "http://localhost:11434"
27DIR_DOCS = "./docs_prueba" # Documentos de prueba para búsqueda semántica
28DIR_CHROMA = "./chroma_store" # Donde ChromaDB guarda los datos
29
30# Colores para la terminal
31VERDE = "\033[92m"
32AZUL = "\033[94m"
33AMAR = "\033[93m"
34ROJO = "\033[91m"
35FIN = "\033[0m"
36NEGR = "\033[1m"
37
38
39def ok(msg):
40 print(f" {VERDE}{FIN} {msg}")
41
42
43def info(msg):
44 print(f" {AZUL}ℹ️{FIN} {msg}")
45
46
47def warn(msg):
48 print(f" {AMAR}⚠️{FIN} {msg}")
49
50
51def error(msg):
52 print(f" {ROJO}{FIN} {msg}")
53
54
55def titulo(t):
56 print(f"\n{NEGR}{'=' * 60}{FIN}\n{NEGR} {t}{FIN}\n{'=' * 60}")
57
58
59# ─── Helpers ──────────────────────────────────────────────────────────────
60
61
62def check_ollama():
63 """Verifica que Ollama está corriendo y el modelo está disponible."""
64 try:
65 r = subprocess.run(
66 ["curl", "-s", f"{OLLAMA_URL}/api/tags"],
67 capture_output=True,
68 text=True,
69 timeout=5,
70 )
71 if r.returncode != 0:
72 error("Ollama no responde. ¿Está corriendo?")
73 info("Ejecuta: ollama serve")
74 return False
75 modelos = json.loads(r.stdout)
76 disponible = any(MODEL in m["name"] for m in modelos.get("models", []))
77 if not disponible:
78 warn(f"Modelo {MODEL} no encontrado. Descargando...")
79 subprocess.run(["ollama", "pull", MODEL], check=True)
80 ok(f"Modelo {MODEL} descargado")
81 return True
82 except Exception as e:
83 error(f"Error conectando con Ollama: {e}")
84 return False
85
86
87def emb(texto):
88 """Genera un embedding usando Ollama."""
89 payload = json.dumps({"model": MODEL, "prompt": texto})
90 r = subprocess.run(
91 ["curl", "-s", f"{OLLAMA_URL}/api/embeddings", "-d", payload],
92 capture_output=True,
93 text=True,
94 )
95 return json.loads(r.stdout)["embedding"]
96
97
98def cos_sim(a, b):
99 """Similitud coseno entre dos vectores."""
100 dot = sum(x * y for x, y in zip(a, b))
101 na = math.sqrt(sum(x * x for x in a))
102 nb = math.sqrt(sum(y * y for y in b))
103 return dot / (na * nb) if na and nb else 0
104
105
106# ─── 1. Crear documentos de prueba ────────────────────────────────────────
107
108
109def crear_documentos():
110 titulo("1️⃣ CREAR DOCUMENTOS DE PRUEBA")
111
112 if os.path.exists(DIR_DOCS):
113 shutil.rmtree(DIR_DOCS)
114 os.makedirs(DIR_DOCS)
115
116 documentos = {
117 "facturas.txt": """Factura hosting enero 2026: 12.99€
118Factura dominio atareao.es 2026: 18.50€
119Pago servidor VPS febrero: 24.99€
120Total gastos infraestructura Q1: 156.47€
121Próximo pago hosting: 1 de marzo""",
122 "apuntes-linux.txt": """Cómo montar un servidor web con Nginx en Ubuntu 24.04
123Configurar firewall con nftables: tabla inet filter con policy drop
124Instalar Docker: sudo apt install docker.io docker-compose
125Los logs del sistema están en /var/log/syslog
126systemctl enable --now para activar servicios al arranque""",
127 "recetas.txt": """Paella valenciana: arroz bomba, pollo, conejo, judía verde, garrofón
128Tortilla de patatas: 4 huevos, 2 patatas grandes, cebolla, aceite de oliva
129Pan casero: 500g harina de fuerza, 300ml agua, 10g sal, 5g levadura
130La clave de un buen arroz es el sofrito y el caldo caliente""",
131 "compras.txt": """Lista de la compra: leche, huevos, pan, arroz, tomates, lechuga
132Comprar regalo cumpleaños: libro de Rust
133Recoger paquete en Correos: número de seguimiento 123456789
134Repostar coche: gasolina 95, 40€""",
135 "ideas-proyectos.txt": """App web para gestionar gastos compartidos con amigos
136Script que monitoriza el precio de la luz y avisa cuando baja
137Bot de Telegram que resuma noticias de IA cada mañana
138Generador de menú semanal con recetas de un archivo de cocina""",
139 }
140
141 for nombre, contenido in documentos.items():
142 with open(os.path.join(DIR_DOCS, nombre), "w") as f:
143 f.write(contenido.strip())
144 ok(f"Documento creado: {nombre}")
145
146 info("5 documentos de prueba listos en ./docs_prueba/")
147 return list(documentos.keys())
148
149
150# ─── 2. Búsqueda semántica ────────────────────────────────────────────────
151
152
153def busqueda_semantica():
154 titulo("2️⃣ BÚSQUEDA SEMÁNTICA — buscar por significado")
155
156 # Indexar documentos
157 info("Generando embeddings de los documentos de prueba...")
158 archivos = sorted(glob.glob(f"{DIR_DOCS}/*.txt"))
159 index = []
160
161 for path in archivos:
162 with open(path) as f:
163 texto = f.read()
164 nombre = os.path.basename(path)
165 vec = emb(texto)
166 index.append((nombre, vec, texto))
167 print(f" {nombre}: {len(vec)} dimensiones")
168
169 ok(f"{len(index)} documentos indexados")
170
171 # Consultas de prueba
172 consultas = [
173 "¿Cuánto dinero gastamos en servidores?",
174 "¿Qué hay para cenar esta noche?",
175 "Alguna idea para un proyecto con IA",
176 ]
177
178 for consulta in consultas:
179 print(f'\n {NEGR}🔍 Consulta:{FIN} "{consulta}"')
180 vec_q = emb(consulta)
181
182 resultados = sorted(
183 [
184 (nombre, cos_sim(vec_q, vec), texto[:100])
185 for nombre, vec, texto in index
186 ],
187 key=lambda x: -x[1],
188 )
189
190 for nombre, sim, preview in resultados[:3]:
191 barra = "" * int(sim * 20) + "" * (20 - int(sim * 20))
192 print(f" {barra} {sim:.4f} {nombre}")
193
194 mejor = resultados[0]
195 print(f" → Mejor resultado: {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")
196 if mejor[1] > 0.7:
197 ok("Coincidencia semántica alta")
198 elif mejor[1] > 0.5:
199 info("Coincidencia semántica media")
200 else:
201 warn("Coincidencia baja — prueba con otra consulta")
202
203
204# ─── 3. Clasificación ─────────────────────────────────────────────────────
205
206
207def clasificacion():
208 titulo("3️⃣ CLASIFICACIÓN — sin entrenar modelos")
209
210 categorias = {
211 "urgente": [
212 "se ha caído el servidor de producción",
213 "necesito esto para mañana sin falta",
214 "error crítico en la base de datos",
215 ],
216 "normal": [
217 "te envío el documento actualizado",
218 "confirma si has recibido el email",
219 ],
220 "spam": [
221 "gana dinero rápido desde casa",
222 "has ganado un premio exclusivo",
223 ],
224 "cocina": [
225 "receta de paella valenciana",
226 "cómo hacer pan casero",
227 ],
228 }
229
230 # Pre-calcular embedding de cada categoría (promedio de ejemplos)
231 info("Calculando firmas de categorías...")
232 firmas = {}
233 for cat, ejemplos in categorias.items():
234 vecs = [emb(e) for e in ejemplos]
235 firmas[cat] = [sum(vals) / len(vals) for vals in zip(*vecs)]
236 ok(f"Categoría '{cat}' lista ({len(ejemplos)} ejemplos)")
237
238 # Textos a clasificar
239 textos_prueba = [
240 "el servidor web no responde desde las 3 de la mañana",
241 "te adjunto la factura de este mes para tu revisión",
242 "FELICIDADES HAS GANADO UN VIAJE AL CARIBE",
243 "cómo hacer una tortilla de patatas con cebolla caramelizada",
244 ]
245
246 for texto in textos_prueba:
247 vec = emb(texto)
248 resultados = sorted(
249 [(cat, cos_sim(vec, firma)) for cat, firma in firmas.items()],
250 key=lambda x: -x[1],
251 )
252 mejor = resultados[0]
253 barra = "" * int(mejor[1] * 20) + "" * (20 - int(mejor[1] * 20))
254 print(f'\n 📝 "{texto[:60]}..."')
255 print(f"{barra} {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")
256
257
258# ─── 4. Deduplicación ─────────────────────────────────────────────────────
259
260
261def deduplicacion():
262 titulo("4️⃣ DEDUPLICACIÓN — encontrar contenido repetido")
263
264 items = [
265 {"id": "doc1", "texto": "Cómo instalar Ollama en Ubuntu 24.04 paso a paso"},
266 {"id": "doc2", "texto": "Guía completa para instalar Ollama en Ubuntu 24.04"},
267 {"id": "doc3", "texto": "Receta de paella valenciana tradicional con marisco"},
268 {"id": "doc4", "texto": "Cómo configurar nftables como firewall en Linux"},
269 {"id": "doc5", "texto": "Instalación de Ollama en Ubuntu 24.04 - tutorial"},
270 ]
271
272 info("Generando embeddings...")
273 embebidos = [(item, emb(item["texto"])) for item in items]
274
275 print(f"\n {'Item':<10} {'Similitud con':<20} {'Score':<8}")
276 print(f" {'-' * 40}")
277 for i, (item_a, emb_a) in enumerate(embebidos):
278 for j, (item_b, emb_b) in enumerate(embebidos[i + 1 :], i + 1):
279 sim = cos_sim(emb_a, emb_b)
280 status = (
281 "🔴 DUPLICADO"
282 if sim > 0.90
283 else "🟡 PARECIDO"
284 if sim > 0.75
285 else "🟢 DISTINTO"
286 )
287 print(f" {item_a['id']:<10} {item_b['id']:<20} {sim:.4f} {status}")
288
289 # Detectar duplicados
290 duplicados = [
291 (item_a, item_b, sim)
292 for (item_a, emb_a), (item_b, emb_b) in [
293 ((items[i], embebidos[i][1]), (items[j], embebidos[j][1]))
294 for i in range(len(items))
295 for j in range(i + 1, len(items))
296 ]
297 if (sim := cos_sim(emb_a, emb_b)) > 0.85
298 ]
299
300 if duplicados:
301 print(
302 f"\n {NEGR}🔴 Se detectaron {len(duplicados)} grupos de duplicados:{FIN}"
303 )
304 for a, b, sim in duplicados:
305 print(f'"{a["texto"][:50]}..."')
306 print(f' "{b["texto"][:50]}..."')
307 print(f" → Similitud: {sim:.1%}")
308 else:
309 info("No se detectaron duplicados significativos")
310
311
312# ─── 5. Recomendación ─────────────────────────────────────────────────────
313
314
315def recomendacion():
316 titulo("5️⃣ RECOMENDACIÓN — más como esto")
317
318 articulos = [
319 "Linux es el mejor sistema operativo para servidores",
320 "Cómo instalar Docker en Ubuntu 24.04",
321 "Introducción a Rust: seguridad de memoria sin recolector de basura",
322 "PostgreSQL con pgvector para búsqueda semántica",
323 "Ollama: modelos de lenguaje locales en tu servidor",
324 "Configurar nftables como firewall en Linux",
325 "Rust vs Go: comparativa para backend en 2026",
326 "Embeddings: coordenadas en un mapa de significados",
327 ]
328
329 info("Generando embeddings de {len(articulos)} artículos...")
330 index = [(tit, emb(tit)) for tit in articulos]
331
332 # Elegir un artículo de referencia
333 ref = "Ollama: modelos de lenguaje locales en tu servidor"
334 print(f'\n 📖 Artículo de referencia: "{ref}"')
335 print(f"\n {NEGR}📚 Recomendaciones:{FIN}")
336
337 vec_ref = emb(ref)
338 recomendados = sorted(
339 [(tit, cos_sim(vec_ref, vec)) for tit, vec in index if tit != ref],
340 key=lambda x: -x[1],
341 )
342
343 for i, (tit, sim) in enumerate(recomendados[:4], 1):
344 barra = "" * int(sim * 20) + "" * (20 - int(sim * 20))
345 print(f" {i}. {barra} {sim:.4f} {tit}")
346
347
348# ─── 6. ChromaDB ──────────────────────────────────────────────────────────
349
350
351def chroma_demo():
352 titulo("6️⃣ CHROMADB — base de datos vectorial")
353
354 try:
355 import chromadb
356 from chromadb.utils import embedding_functions
357 except ImportError:
358 warn("chromadb no instalado. Ejecuta: pip install chromadb")
359 return
360
361 # Limpiar y crear
362 if os.path.exists(DIR_CHROMA):
363 shutil.rmtree(DIR_CHROMA)
364
365 info("Creando base de datos vectorial con ChromaDB...")
366 client = chromadb.PersistentClient(path=DIR_CHROMA)
367 collection = client.get_or_create_collection(
368 name="demo_817",
369 embedding_function=embedding_functions.OllamaEmbeddingFunction(
370 url=f"{OLLAMA_URL}/api/embeddings", model_name=MODEL
371 ),
372 )
373
374 # Documentos de prueba
375 docs = [
376 "Linux es el mejor sistema operativo para servidores por su estabilidad",
377 "Para instalar Docker en Ubuntu ejecuta sudo apt install docker.io",
378 "La paella valenciana lleva arroz, pollo, conejo, judía y garrofón",
379 "Ollama permite ejecutar modelos de lenguaje localmente en tu hardware",
380 "Rust es un lenguaje de programación de sistemas seguro y rápido",
381 "PostgreSQL con pgvector permite búsqueda semántica sobre documentos",
382 "Los embeddings convierten texto en vectores numéricos",
383 "El jamón ibérico de bellota se diferencia por la dieta del cerdo",
384 ]
385 metadatos = [
386 {"tema": "linux"},
387 {"tema": "linux"},
388 {"tema": "cocina"},
389 {"tema": "ia"},
390 {"tema": "programacion"},
391 {"tema": "ia"},
392 {"tema": "ia"},
393 {"tema": "cocina"},
394 ]
395 ids = [f"doc_{i}" for i in range(len(docs))]
396
397 collection.add(documents=docs, metadatas=metadatos, ids=ids)
398 ok(f"{len(docs)} documentos indexados en ChromaDB")
399
400 # Búsqueda
401 consultas = [
402 "¿Qué framework usar para backend?",
403 "Dime algo de cocina",
404 ]
405
406 for consulta in consultas:
407 print(f'\n {NEGR}🔍 Consulta:{FIN} "{consulta}"')
408 results = collection.query(query_texts=[consulta], n_results=3)
409
410 for doc, meta, dist in zip(
411 results["documents"][0], results["metadatas"][0], results["distances"][0]
412 ):
413 conf = 1 - dist
414 barra = "" * int(conf * 20) + "" * (20 - int(conf * 20))
415 print(f" {barra} {conf:.1%} [{meta['tema']}] {doc}")
416
417 # Filtro por metadatos
418 print(f" 🔽 Con filtro [tema=linux]:")
419 filtrados = collection.query(
420 query_texts=[consulta], n_results=2, where={"tema": "linux"}
421 )
422 for doc in filtrados["documents"][0]:
423 print(f" 🐧 {doc}")
424
425 # Persistencia
426 if os.path.exists(DIR_CHROMA):
427 ok(f"Datos persistentes en {DIR_CHROMA}/")
428 info("Cierra y abre Python — los datos siguen ahí")
429
430 # Limpiar
431 shutil.rmtree(DIR_CHROMA)
432 info("ChromaDB limpiada (demo temporal)")
433
434
435# ─── MAIN ──────────────────────────────────────────────────────────────────
436
437PASOS = {
438 1: ("Crear documentos de prueba", crear_documentos, True),
439 2: ("Búsqueda semántica", busqueda_semantica, True),
440 3: ("Clasificación", clasificacion, True),
441 4: ("Deduplicación", deduplicacion, True),
442 5: ("Recomendación", recomendacion, True),
443 6: ("ChromaDB", chroma_demo, False),
444}
445
446
447def listar_pasos():
448 print(f"\n{'=' * 60}")
449 print(f" PASOS DISPONIBLES")
450 print(f"{'=' * 60}")
451 for n in sorted(PASOS):
452 nombre, _, esencial = PASOS[n]
453 req = " (requiere chromadb)" if not esencial else ""
454 print(f" {n}. {nombre}{req}")
455 print(f"\n --fast omite el paso 6 (ChromaDB)")
456 print(f" --interactive pide confirmación entre pasos\n")
457
458
459def ejecutar_paso(n, limpiar_al_final=False):
460 if n not in PASOS:
461 warn(f"Paso {n} no válido. Usa --list para ver los pasos.")
462 return
463 nombre, fn, _ = PASOS[n]
464 info(f"Ejecutando paso {n}: {nombre}...")
465 fn()
466 if limpiar_al_final and os.path.exists(DIR_DOCS):
467 shutil.rmtree(DIR_DOCS)
468
469
470def main():
471 parser = argparse.ArgumentParser(
472 description="Episodio 817 — Embeddings para todo. Demo completa.",
473 formatter_class=argparse.RawDescriptionHelpFormatter,
474 epilog=textwrap.dedent("""\
475 Ejemplos:
476 python3 setup_completo.py # todo
477 python3 setup_completo.py --fast # sin ChromaDB
478 python3 setup_completo.py --list # listar pasos
479 python3 setup_completo.py --step 3 # solo paso 3
480 python3 setup_completo.py --step 1,3,5 # pasos 1,3,5
481 python3 setup_completo.py --interactive # paso a paso
482 """),
483 )
484 parser.add_argument("--fast", action="store_true", help="Omite ChromaDB (paso 6)")
485 parser.add_argument(
486 "--list", "-l", action="store_true", help="Lista los pasos disponibles"
487 )
488 parser.add_argument(
489 "--step",
490 "-s",
491 type=str,
492 help="Ejecuta paso(s) específicos: 3, 1,3,5, 1-4",
493 )
494 parser.add_argument(
495 "--interactive",
496 "-i",
497 action="store_true",
498 help="Ejecuta paso a paso con confirmación",
499 )
500 args = parser.parse_args()
501
502 if args.list:
503 listar_pasos()
504 return
505
506 print(f"""
507{NEGR}🚀 EPISODIO 817 — Embeddings para todo{FIN}
508{NEGR} Demo completa: búsqueda, clasificación, deduplicación, recomendación, ChromaDB{FIN}
509
510{NEGR}Requisitos:{FIN}
511 • Ollama en {OLLAMA_URL}{VERDE}ollama serve{FIN}
512 • Modelo: {MODEL}{VERDE}ollama pull {MODEL}{FIN}
513 • Python 3.10+
514 • pip install chromadb (opcional, para el bloque 6)
515""")
516
517 if not check_ollama():
518 sys.exit(1)
519
520 # Resolver qué pasos ejecutar
521 if args.step:
522 pasos = set()
523 for parte in args.step.split(","):
524 parte = parte.strip()
525 if "-" in parte:
526 a, b = parte.split("-", 1)
527 pasos.update(range(int(a), int(b) + 1))
528 else:
529 pasos.add(int(parte))
530 pasos = sorted(pasos)
531 info(f"Ejecutando pasos: {', '.join(str(p) for p in pasos)}")
532 for p in pasos:
533 ejecutar_paso(p)
534 if os.path.exists(DIR_DOCS):
535 shutil.rmtree(DIR_DOCS)
536 elif args.interactive:
537 info(
538 "Modo interactivo: presiona Enter para avanzar, 's' para saltar, 'q' para salir"
539 )
540 for n in sorted(PASOS):
541 nombre, fn, esencial = PASOS[n]
542 if args.fast and n == 6:
543 info(f"Paso {n} omitido (--fast)")
544 continue
545 r = (
546 input(f"\n{NEGR}¿Ejecutar paso {n}: {nombre}?{FIN} [Enter/s/q] ")
547 .strip()
548 .lower()
549 )
550 if r == "q":
551 info("Interrumpido por el usuario")
552 break
553 if r == "s":
554 info(f"Paso {n} saltado")
555 continue
556 fn()
557 if os.path.exists(DIR_DOCS):
558 shutil.rmtree(DIR_DOCS)
559 else:
560 crear_documentos()
561 busqueda_semantica()
562 clasificacion()
563 deduplicacion()
564 recomendacion()
565
566 if not args.fast:
567 chroma_demo()
568 else:
569 info("Modo rápido: omitiendo ChromaDB")
570 info("Ejecuta sin --fast para incluir ChromaDB")
571
572 if os.path.exists(DIR_DOCS):
573 shutil.rmtree(DIR_DOCS)
574
575 print(f"\n{NEGR}{'=' * 60}{FIN}")
576 print(f"\n{VERDE}🎉 Demo completada{FIN}")
577 print(f" • 6 casos de uso ejecutados")
578 print(f" • 1 modelo de embeddings ({MODEL})")
579 print(f" • 0 modelos entrenados, 0 GPUs, 0€")
580 print(
581 f"\n {AZUL}📖 Escaleta completa en: episodio-817-escaleta-embeddings.md{FIN}"
582 )
583 print(f" {AZUL}🔗 https://github.com/aejimmi/fail2ban-rs{FIN}")
584
585
586if __name__ == "__main__":
587 main()
588