Utoljára aktív 16 hours ago

Tres técnicas de RAG implementadas en Python para buscar en tu base de conocimiento: hybrid_search (FTS5 + cosine similarity con alpha), reranker (cross-encoder BAAI/bge-reranker-v2-m3), y HyDE (documentos hipotéticos con Ollama + Llama 3.2). Incluye CLI cerebro con argparse y pipe a fzf.

rag_hybrid.py Eredeti
1#!/usr/bin/env python3
2"""rag_hybrid.py — Hybrid search: FTS5 + cosine similarity"""
3import sqlite3, numpy as np, math, os
4from numpy.linalg import norm
5
6DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db")
7
8def blob_to_vector(blob: bytes) -> np.ndarray:
9 return np.frombuffer(blob, dtype=np.float32).copy()
10
11def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
12 return float(np.dot(a, b))
13
14def get_embedding_ollama(text: str) -> np.ndarray:
15 import requests
16 resp = requests.post("http://localhost:11434/api/embeddings",
17 json={"model": "bge-m3", "input": text})
18 return np.array(resp.json()["embeddings"][0], dtype=np.float32)
19
20def hybrid_search(query: str, alpha: float = 0.4, top_k: int = 10):
21 """
22 alpha: peso para FTS5, (1-alpha) para cosine similarity.
23 0.6 = nombres propios, 0.2 = conceptual, 0.4 = equilibrio
24 """
25 conn = sqlite3.connect(DB_PATH)
26 query_emb = get_embedding_ollama(query)
27 query_emb = query_emb / (norm(query_emb) + 1e-10)
28
29 cursor = conn.execute("""
30 SELECT c.id, c.contenido, c.embedding,
31 bm25(chunks_fts, 0.0, 0.0, 5.0, 5.0) as fts_score
32 FROM chunks_fts
33 JOIN chunks c ON chunks_fts.rowid = c.id
34 WHERE chunks_fts MATCH ?
35 ORDER BY fts_score DESC
36 LIMIT ?
37 """, (query, top_k * 2))
38
39 results = []
40 for row in cursor:
41 chunk_id, contenido, emb_blob, fts_score = row
42 fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0))
43 emb_vec = blob_to_vector(emb_blob)
44 emb_vec = emb_vec / (norm(emb_vec) + 1e-10)
45 cos_sim = cosine_similarity(query_emb, emb_vec)
46 score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim
47 results.append((score_total, contenido[:200], fts_norm, cos_sim))
48
49 results.sort(key=lambda x: x[0], reverse=True)
50 conn.close()
51 return results[:top_k]
rag_hyde.py Eredeti
1#!/usr/bin/env python3
2"""rag_hyde.py — HyDE: Hypothetical Document Embeddings"""
3import requests, numpy as np, sqlite3, os, math
4from numpy.linalg import norm
5
6DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db")
7OLLAMA_URL = "http://localhost:11434/api"
8
9def generate_hypothetical_document(query: str, model: str ="llama3.2") -> str:
10 prompt = f"""Genera un texto breve y factual que responda a la siguiente pregunta.
11El texto debe ser informativo, objetivo y escrito en español.
12
13Pregunta: {query}
14
15Texto informativo:"""
16 resp = requests.post(f"{OLLAMA_URL}/generate",
17 json={"model": model, "prompt": prompt, "stream": False})
18 return resp.json()["response"]
19
20def get_embedding(text: str, model: str ="bge-m3") -> np.ndarray:
21 resp = requests.post(f"{OLLAMA_URL}/embed",
22 json={"model": model, "input": text})
23 return np.array(resp.json()["embeddings"][0], dtype=np.float32)
24
25def blob_to_vector(blob: bytes) -> np.ndarray:
26 return np.frombuffer(blob, dtype=np.float32).copy()
27
28def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
29 return float(np.dot(a, b))
30
31def hyde_search(query: str, top_k: int = 5):
32 hypothetic_doc = generate_hypothetical_document(query)
33 print(f"[HyDE] Documento generado: {hypothetic_doc[:100]}...")
34 hyde_emb = get_embedding(hypothetic_doc)
35 hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10)
36
37 conn = sqlite3.connect(DB_PATH)
38 cursor = conn.execute("SELECT id, contenido, embedding FROM chunks")
39 results = []
40 for row in cursor:
41 contenido, emb_blob = row[1], row[2]
42 emb_vec = blob_to_vector(emb_blob)
43 emb_vec = emb_vec / (norm(emb_vec) + 1e-10)
44 cos_sim = cosine_similarity(hyde_emb, emb_vec)
45 results.append((cos_sim, contenido[:200]))
46
47 results.sort(key=lambda x: x[0], reverse=True)
48 conn.close()
49 return results[:top_k]
50
51def hyde_hybrid_search(query: str, alpha: float = 0.3, top_k: int = 5):
52 """HyDE + Hybrid search combinado. Alpha bajo porque HyDE ya captura semántica."""
53 hyde_doc = generate_hypothetical_document(query)
54 hyde_emb = get_embedding(hyde_doc)
55 hyde_emb = hyde_emb / (norm(hyde_emb) + 1e-10)
56
57 conn = sqlite3.connect(DB_PATH)
58 cursor = conn.execute("""
59 SELECT c.contenido, c.embedding,
60 bm25(chunks_fts, 0, 0, 5, 5) as fts_score
61 FROM chunks_fts JOIN chunks c ON chunks_fts.rowid = c.id
62 WHERE chunks_fts MATCH ?
63 """, (query,))
64
65 results = []
66 for row in cursor:
67 contenido, emb_blob, fts_score = row
68 emb_vec = blob_to_vector(emb_blob)
69 emb_vec = emb_vec / (norm(emb_vec) + 1e-10)
70 cos_sim = cosine_similarity(hyde_emb, emb_vec)
71 fts_norm = 1.0 / (1.0 + math.exp(-fts_score / 10.0))
72 score_total = alpha * fts_norm + (1.0 - alpha) * cos_sim
73 results.append((score_total, contenido[:200]))
74
75 results.sort(reverse=True)
76 conn.close()
77 return results[:top_k]
rag_reranker.py Eredeti
1#!/usr/bin/env python3
2"""rag_reranker.py — Re-ranking con cross-encoder"""
3from sentence_transformers import CrossEncoder
4
5class Reranker:
6 def __init__(self, model_name="BAAI/bge-reranker-v2-m3", use_fp16=True):
7 self.model = CrossEncoder(model_name, max_length=512, device="cpu")
8
9 def rerank(self, query: str, candidates: list[dict], top_k: int = 5) -> list[dict]:
10 pairs = [(query, c["contenido"]) for c in candidates]
11 scores = self.model.predict(pairs)
12 for i, score in enumerate(scores):
13 candidates[i]["rerank_score"] = float(score)
14 candidates.sort(key=lambda x: x["rerank_score"], reverse=True)
15 return candidates[:top_k]
16
17def search_with_rerank(query: str, alpha: float = 0.4,
18 top_k_hybrid: int = 20, top_k_final: int = 5):
19 from rag_hybrid import hybrid_search
20 candidates = hybrid_search(query, alpha=alpha, top_k=top_k_hybrid)
21 candidates_dict = [{"contenido": c[1], "score": c[0]} for c in candidates]
22 reranker = Reranker()
23 return reranker.rerank(query, candidates_dict, top_k=top_k_final)