Spaces:
Runtime error
Runtime error
Download src/evidence.py from MuhammadAzfar/ResearchMind-AI: direct link, hf CLI and curl.
- Browser
- Download file 4.61 kB
-
https://huggingface.co/spaces/MuhammadAzfar/ResearchMind-AI/resolve/main/src/evidence.py
- Command line
-
hf download hf://spaces/MuhammadAzfar/ResearchMind-AI/src/evidence.py
-
curl -L -o evidence.py https://huggingface.co/spaces/MuhammadAzfar/ResearchMind-AI/resolve/main/src/evidence.py
4.61 kB
| import numpy as np | |
| from src import config | |
| from src.chunking import split_text | |
| from src.embeddings import build_cosine_index, embed_texts | |
| class EvidenceError(Exception): | |
| """Raised when there is no readable text to build evidence from.""" | |
| class EvidenceStore: | |
| """All source text, chunked and indexed in an in-memory FAISS cosine index. | |
| Used three ways: retrieve passages per sub-question for the Writer (`gather`), | |
| and pull the passages that back a specific claim for the Checker (`support_for`). | |
| """ | |
| def __init__(self, sources: list, embedder=None): | |
| self.sources = sources | |
| self.embedder = embedder | |
| self.records = [] | |
| for source_idx, source in enumerate(sources): | |
| for piece in split_text(source["content"], config.CHUNK_SIZE, config.CHUNK_OVERLAP): | |
| self.records.append({"id": len(self.records), "text": piece, "source_idx": source_idx}) | |
| if not self.records: | |
| raise EvidenceError("The sources contained no readable text.") | |
| self.vectors = embed_texts([r["text"] for r in self.records], embedder) | |
| self.index = build_cosine_index(self.vectors) | |
| self.numbering = {} # source_idx -> citation number, assigned by gather() | |
| self.selected = [] | |
| def _search(self, query: str, k: int): | |
| query_vector = embed_texts([query], self.embedder) | |
| scores, ids = self.index.search(query_vector, min(len(self.records), k)) | |
| return [(self.records[i], float(s)) for s, i in zip(scores[0], ids[0]) if i >= 0] | |
| def gather(self, questions: list, chunks_per_question: int) -> tuple: | |
| """Selects the best passages for each sub-question; returns (selected_chunks, numbered_sources).""" | |
| chosen, chosen_ids, per_source_total = [], set(), {} | |
| for q_idx, question in enumerate(questions): | |
| kept, per_source_q = 0, {} | |
| for record, score in self._search(question, chunks_per_question * 6): | |
| if kept >= chunks_per_question or len(chosen) >= config.MAX_EVIDENCE_CHUNKS: | |
| break | |
| sid = record["source_idx"] | |
| if per_source_q.get(sid, 0) >= config.MAX_PER_SOURCE_PER_QUESTION: | |
| continue | |
| if record["id"] in chosen_ids: | |
| # Already picked for an earlier question: it still covers this one, so count it | |
| # instead of padding this question with weaker passages. | |
| if score >= config.MIN_SCORE: | |
| per_source_q[sid] = per_source_q.get(sid, 0) + 1 | |
| kept += 1 | |
| continue | |
| if per_source_total.get(sid, 0) >= config.MAX_CHUNKS_PER_SOURCE: | |
| continue | |
| if score < config.MIN_SCORE and kept >= config.MIN_KEEP_PER_QUESTION: | |
| continue | |
| chosen_ids.add(record["id"]) | |
| chosen.append({**record, "score": score, "question_idx": q_idx}) | |
| per_source_q[sid] = per_source_q.get(sid, 0) + 1 | |
| per_source_total[sid] = per_source_total.get(sid, 0) + 1 | |
| kept += 1 | |
| if not chosen: | |
| raise EvidenceError("No relevant passages were found in the sources.") | |
| # Number sources by first appearance so citations read [1], [2], [3]... in order | |
| self.numbering = {} | |
| for chunk in chosen: | |
| self.numbering.setdefault(chunk["source_idx"], len(self.numbering) + 1) | |
| for chunk in chosen: | |
| chunk["cite"] = self.numbering[chunk["source_idx"]] | |
| self.selected = chosen | |
| numbered = [ | |
| { | |
| "n": n, | |
| "title": self.sources[sid]["title"], | |
| "url": self.sources[sid].get("url", ""), | |
| "kind": self.sources[sid].get("kind", "web"), | |
| } | |
| for sid, n in sorted(self.numbering.items(), key=lambda kv: kv[1]) | |
| ] | |
| return chosen, numbered | |
| def support_for(self, claim: str, cites: list, k: int = 2) -> list: | |
| """The k passages from the cited sources that are closest to a claim (what the Checker reads).""" | |
| wanted = {sid for sid, n in self.numbering.items() if n in set(cites)} | |
| candidates = [r["id"] for r in self.records if r["source_idx"] in wanted] | |
| if not candidates: | |
| return [] | |
| claim_vector = embed_texts([claim], self.embedder)[0] | |
| sims = self.vectors[candidates] @ claim_vector | |
| best = np.argsort(-sims)[:k] | |
| return [self.records[candidates[i]]["text"] for i in best] | |