Zuletzt aktiv 17 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.

atareao's Avatar atareao hat die Gist bearbeitet 17 hours ago. Zu Änderung gehen

3 files changed, 151 insertions

rag_hybrid.py(Datei erstellt)

@@ -0,0 +1,51 @@
1 + #!/usr/bin/env python3
2 + """rag_hybrid.py — Hybrid search: FTS5 + cosine similarity"""
3 + import sqlite3, numpy as np, math, os
4 + from numpy.linalg import norm
5 +
6 + DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db")
7 +
8 + def blob_to_vector(blob: bytes) -> np.ndarray:
9 + return np.frombuffer(blob, dtype=np.float32).copy()
10 +
11 + def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
12 + return float(np.dot(a, b))
13 +
14 + def 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 +
20 + def 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(Datei erstellt)

@@ -0,0 +1,77 @@
1 + #!/usr/bin/env python3
2 + """rag_hyde.py — HyDE: Hypothetical Document Embeddings"""
3 + import requests, numpy as np, sqlite3, os, math
4 + from numpy.linalg import norm
5 +
6 + DB_PATH = os.path.expanduser("~/.cerebro/rag_conocimiento.db")
7 + OLLAMA_URL = "http://localhost:11434/api"
8 +
9 + def 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.
11 + El texto debe ser informativo, objetivo y escrito en español.
12 +
13 + Pregunta: {query}
14 +
15 + Texto informativo:"""
16 + resp = requests.post(f"{OLLAMA_URL}/generate",
17 + json={"model": model, "prompt": prompt, "stream": False})
18 + return resp.json()["response"]
19 +
20 + def 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 +
25 + def blob_to_vector(blob: bytes) -> np.ndarray:
26 + return np.frombuffer(blob, dtype=np.float32).copy()
27 +
28 + def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
29 + return float(np.dot(a, b))
30 +
31 + def 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 +
51 + def 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(Datei erstellt)

@@ -0,0 +1,23 @@
1 + #!/usr/bin/env python3
2 + """rag_reranker.py — Re-ranking con cross-encoder"""
3 + from sentence_transformers import CrossEncoder
4 +
5 + class 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 +
17 + def 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)
Neuer Älter