Haldi247 commited on
Commit
16b90ff
Β·
verified Β·
1 Parent(s): e9648a7

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +211 -0
  2. chunks_recursive.json +0 -0
  3. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, re, time
2
+ import numpy as np
3
+ import gradio as gr
4
+ from sentence_transformers import SentenceTransformer, CrossEncoder
5
+ from rank_bm25 import BM25Okapi
6
+ from pinecone import Pinecone
7
+ from huggingface_hub import InferenceClient
8
+
9
+ # CONFIG
10
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
11
+ HF_TOKEN = os.getenv("HF_TOKEN")
12
+ PINECONE_INDEX = "rag-nlp-project"
13
+ LLM_MODEL = "meta-llama/Meta-Llama-3-8B-Instruct"
14
+
15
+ # ── LOAD RESOURCES ──
16
+ print("Loading resources...")
17
+
18
+ with open("chunks_recursive.json") as f:
19
+ ALL_CHUNKS = json.load(f)
20
+
21
+ tokenized = [c["text"].lower().split() for c in ALL_CHUNKS]
22
+ bm25 = BM25Okapi(tokenized)
23
+
24
+ embedder = SentenceTransformer("all-MiniLM-L6-v2")
25
+ reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
26
+
27
+ pc = Pinecone(api_key=PINECONE_API_KEY)
28
+ pine_index = pc.Index(PINECONE_INDEX)
29
+
30
+ llm = InferenceClient(token=HF_TOKEN)
31
+ print("All ready!")
32
+
33
+ # ── RETRIEVAL ──
34
+ def semantic_search(query, namespace="recursive", top_k=20):
35
+ qvec = embedder.encode(query).tolist()
36
+ res = pine_index.query(vector=qvec, top_k=top_k,
37
+ include_metadata=True, namespace=namespace)
38
+ return [{"id": m["id"], "text": m["metadata"]["text"],
39
+ "title": m["metadata"]["title"], "score": m["score"]}
40
+ for m in res["matches"]]
41
+
42
+ def bm25_search(query, top_k=20):
43
+ tokens = query.lower().split()
44
+ scores = bm25.get_scores(tokens)
45
+ top_idx = np.argsort(scores)[::-1][:top_k]
46
+ return [{"id": ALL_CHUNKS[i]["id"], "text": ALL_CHUNKS[i]["text"],
47
+ "title": ALL_CHUNKS[i]["title"], "score": float(scores[i])}
48
+ for i in top_idx if scores[i] > 0]
49
+
50
+ def rrf_fuse(lists_of_results, k=60):
51
+ scores, data = {}, {}
52
+ for results in lists_of_results:
53
+ for rank, item in enumerate(results):
54
+ did = item["id"]
55
+ scores[did] = scores.get(did, 0) + 1.0 / (k + rank + 1)
56
+ data[did] = {"text": item["text"], "title": item["title"]}
57
+ ranked = sorted(scores, key=lambda x: scores[x], reverse=True)
58
+ return [{"id": d, "rrf_score": scores[d], **data[d]} for d in ranked]
59
+
60
+ def cross_encoder_rerank(query, candidates, top_k=5):
61
+ if not candidates:
62
+ return []
63
+ pool = candidates[:30]
64
+ pairs = [(query, c["text"]) for c in pool]
65
+ ce_scores = reranker.predict(pairs)
66
+ for i, s in enumerate(ce_scores):
67
+ pool[i]["ce_score"] = float(s)
68
+ pool.sort(key=lambda x: x["ce_score"], reverse=True)
69
+ return pool[:top_k]
70
+
71
+ # ── LLM ──
72
+ def call_llm(prompt, max_tokens=512, temperature=0.3):
73
+ try:
74
+ resp = llm.chat_completion(
75
+ model=LLM_MODEL,
76
+ messages=[{"role": "user", "content": prompt}],
77
+ max_tokens=max_tokens, temperature=temperature
78
+ )
79
+ return resp.choices[0].message.content.strip()
80
+ except Exception as e:
81
+ return f"[LLM Error: {e}]"
82
+
83
+ def generate_answer(query, contexts):
84
+ ctx = "\n\n".join([f"{i+1}. [{c['title']}] {c['text']}" for i, c in enumerate(contexts)])
85
+ prompt = f"""Based on the following information:
86
+
87
+ {ctx}
88
+
89
+ Please provide a detailed answer to the question: {query}.
90
+ Your answer should integrate the diverse perspectives or data points provided by the retrieved passages.
91
+ If the passages are irrelevant to the question, say that you couldn't find a good response in the database."""
92
+ return call_llm(prompt)
93
+
94
+ # ── EVALUATION ──
95
+ def eval_faithfulness(answer, contexts):
96
+ context_str = "\n".join([c["text"] for c in contexts])[:3000]
97
+ claims_raw = call_llm(
98
+ f"Extract all factual claims as a numbered list.\n\nAnswer: {answer}\n\nClaims:",
99
+ max_tokens=400, temperature=0.1
100
+ )
101
+ claims = [re.sub(r"^[\d]+[\.\)]\s*", "", l.strip())
102
+ for l in claims_raw.split("\n")
103
+ if len(re.sub(r"^[\d]+[\.\)]\s*", "", l.strip())) > 15]
104
+ if not claims:
105
+ return 1.0, "No claims extracted."
106
+
107
+ supported = 0
108
+ details = []
109
+ for claim in claims[:8]:
110
+ verdict = call_llm(
111
+ f"Is this claim supported by the context? Reply ONLY 'SUPPORTED' or 'NOT SUPPORTED'.\n\n"
112
+ f"Context: {context_str}\n\nClaim: {claim}\n\nVerdict:",
113
+ max_tokens=10, temperature=0.1
114
+ ).upper()
115
+ ok = "SUPPORTED" in verdict and "NOT" not in verdict
116
+ if ok:
117
+ supported += 1
118
+ details.append(f"{'[Y]' if ok else '[N]'} {claim}")
119
+ score = supported / len(claims[:8])
120
+ return score, "\n".join(details)
121
+
122
+ def eval_relevancy(query, answer):
123
+ qs_raw = call_llm(
124
+ f"Generate exactly 3 questions that this answer directly addresses. "
125
+ f"One per line, no numbering.\n\nAnswer: {answer}\n\nQuestions:",
126
+ max_tokens=200, temperature=0.3
127
+ )
128
+ questions = [re.sub(r"^[\d]+[\.\)]\s*", "", l.strip())
129
+ for l in qs_raw.split("\n")
130
+ if len(re.sub(r"^[\d]+[\.\)]\s*", "", l.strip())) > 10][:3]
131
+ if not questions:
132
+ return 0.0, "Could not generate questions."
133
+
134
+ embs = embedder.encode([query] + questions)
135
+ q_emb = embs[0]
136
+ sims, detail_lines = [], []
137
+ for i, q in enumerate(questions):
138
+ sim = float(np.dot(q_emb, embs[i+1]) /
139
+ (np.linalg.norm(q_emb) * np.linalg.norm(embs[i+1])))
140
+ sims.append(sim)
141
+ detail_lines.append(f" Q{i+1}: {q} (sim={sim:.3f})")
142
+ return float(np.mean(sims)), "\n".join(detail_lines)
143
+
144
+ # ── MAIN PIPELINE ──
145
+ def run_query(query, run_eval):
146
+ if not query.strip():
147
+ return "Please enter a question.", "", "", ""
148
+
149
+ t0 = time.time()
150
+ sem = semantic_search(query)
151
+ kw = bm25_search(query)
152
+ fused = rrf_fuse([sem, kw])
153
+ reranked = cross_encoder_rerank(query, fused)
154
+ t_retrieve = time.time() - t0
155
+
156
+ t1 = time.time()
157
+ answer = generate_answer(query, reranked)
158
+ t_generate = time.time() - t1
159
+
160
+ ctx_display = ""
161
+ for i, c in enumerate(reranked):
162
+ ctx_display += f"**[{i+1}] {c['title']}** (score: {c.get('ce_score', 0):.3f})\n"
163
+ ctx_display += f"{c['text']}\n\n---\n\n"
164
+
165
+ scores_display = ""
166
+ t_eval = 0
167
+ if run_eval:
168
+ t2 = time.time()
169
+ faith_score, faith_detail = eval_faithfulness(answer, reranked)
170
+ rel_score, rel_detail = eval_relevancy(query, answer)
171
+ t_eval = time.time() - t2
172
+ scores_display = (
173
+ f"### Faithfulness: {faith_score:.0%}\n{faith_detail}\n\n"
174
+ f"### Relevancy: {rel_score:.0%}\n{rel_detail}"
175
+ )
176
+ else:
177
+ scores_display = "*(Check the box to run evaluation)*"
178
+
179
+ timing = (f"Retrieval: {t_retrieve:.2f}s | Generation: {t_generate:.2f}s | "
180
+ f"Evaluation: {t_eval:.2f}s | Total: {t_retrieve + t_generate + t_eval:.2f}s")
181
+ return answer, ctx_display, scores_display, timing
182
+
183
+ # ── GRADIO UI ──
184
+ with gr.Blocks(title="RAG Q&A β€” AI/ML Domain", theme=gr.themes.Soft()) as demo:
185
+ gr.Markdown(
186
+ "# RAG Question-Answering System\n"
187
+ "*AI/ML Domain - Hybrid Search (BM25 + Semantic + RRF) - Cross-Encoder Reranking - LLM-as-a-Judge*"
188
+ )
189
+ with gr.Row():
190
+ query_box = gr.Textbox(label="Your Question",
191
+ placeholder="e.g. What is backpropagation?", scale=4)
192
+ eval_check = gr.Checkbox(label="Run Evaluation (slower)", value=True)
193
+ btn = gr.Button("Ask", variant="primary", scale=1)
194
+
195
+ with gr.Tabs():
196
+ with gr.TabItem("Answer"):
197
+ answer_out = gr.Markdown()
198
+ with gr.TabItem("Retrieved Context"):
199
+ context_out = gr.Markdown()
200
+ with gr.TabItem("Evaluation Scores"):
201
+ scores_out = gr.Markdown()
202
+
203
+ timing_out = gr.Textbox(label="Timing", interactive=False)
204
+ btn.click(fn=run_query, inputs=[query_box, eval_check],
205
+ outputs=[answer_out, context_out, scores_out, timing_out])
206
+
207
+ gr.Markdown("---\n*Embedding: all-MiniLM-L6-v2 | Reranker: ms-marco-MiniLM | "
208
+ "LLM: Meta-Llama-3-8B-Instruct | Vector DB: Pinecone*")
209
+
210
+ if __name__ == "__main__":
211
+ demo.launch()
chunks_recursive.json ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ sentence-transformers
3
+ rank-bm25
4
+ pinecone
5
+ huggingface-hub
6
+ numpy