Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| from pathlib import Path | |
| from typing import List, Dict | |
| from dotenv import load_dotenv | |
| from openai import OpenAI | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from ingestion import ingest_directory | |
| from search import HybridSearchIndex | |
| DATA_DIR = Path(__file__).parent.parent / "data" | |
| CHROMA_DIR = Path(__file__).parent.parent / "chroma_store" | |
| RERANK_TOP_K = 5 | |
| SYSTEM_PROMPT = ( | |
| "You are a precise assistant. Answer the user's question using ONLY the provided context. " | |
| "If the context does not contain enough information, say so explicitly. " | |
| "Cite which chunk(s) you are drawing from by referencing their number." | |
| ) | |
| def build_index(index: HybridSearchIndex, force_reingest: bool = False) -> None: | |
| count = index._collection.count() | |
| if count > 0 and not force_reingest: | |
| print(f"Using existing index ({count} chunks). Rebuilding BM25 from ChromaDB...") | |
| index.build_bm25_from_collection() | |
| return | |
| if not DATA_DIR.exists(): | |
| print(f"ERROR: Data directory not found: {DATA_DIR}") | |
| sys.exit(1) | |
| chunks = ingest_directory(DATA_DIR) | |
| if not chunks: | |
| print("ERROR: No chunks produced. Add PDF files to the data/ directory.") | |
| sys.exit(1) | |
| print(f"\nAdding {len(chunks)} chunks to index...") | |
| index.add_documents(chunks) | |
| print(f"Index built: ChromaDB ({index._collection.count()} docs), BM25 ({len(chunks)} docs).") | |
| def generate_answer(query: str, context_chunks: List[Dict], client: OpenAI) -> str: | |
| context_parts = [] | |
| for i, chunk in enumerate(context_chunks, 1): | |
| context_parts.append( | |
| f"[Chunk {i} | {chunk['source']} p.{chunk['page_num']} | rerank_score={chunk.get('rerank_score', 0):.3f}]\n{chunk['text']}" | |
| ) | |
| context = "\n\n---\n\n".join(context_parts) | |
| user_message = f"Context:\n{context}\n\nQuestion: {query}" | |
| try: | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_message}, | |
| ], | |
| temperature=0, | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as exc: | |
| return f"[LLM error: {exc}]" | |
| def display_results(chunks: List[Dict]) -> None: | |
| print(f"\n--- Top {len(chunks)} Retrieved Chunks ---") | |
| for i, chunk in enumerate(chunks, 1): | |
| rerank = chunk.get("rerank_score", 0.0) | |
| rtype = chunk.get("retrieval_type", "?") | |
| source = chunk.get("source", "?") | |
| page = chunk.get("page_num", "?") | |
| tokens = chunk.get("token_count", "?") | |
| preview = chunk["text"][:120].replace("\n", " ") | |
| print(f"[{i}] rerank={rerank:.3f} | type={rtype:<6} | {source} p.{page} | tokens={tokens}") | |
| print(f" \"{preview}...\"") | |
| print() | |
| def main() -> None: | |
| load_dotenv() | |
| api_key = os.getenv("OPENAI_API_KEY") | |
| if not api_key: | |
| print("ERROR: OPENAI_API_KEY not set. Copy .env.example to .env and add your key.") | |
| sys.exit(1) | |
| enable_llm = os.getenv("ENABLE_LLM_ANSWER", "true").lower() == "true" | |
| openai_client = OpenAI(api_key=api_key) if enable_llm else None | |
| cross_encoder_model = os.getenv("CROSS_ENCODER_MODEL", "cross-encoder/ms-marco-TinyBERT-L-2-v2") | |
| dense_k = int(os.getenv("DENSE_K", "10")) | |
| sparse_k = int(os.getenv("SPARSE_K", "10")) | |
| rerank_top_k = int(os.getenv("RERANK_TOP_K", str(RERANK_TOP_K))) | |
| index = HybridSearchIndex( | |
| persist_directory=str(CHROMA_DIR), | |
| openai_api_key=api_key, | |
| cross_encoder_model=cross_encoder_model, | |
| rerank_top_k=rerank_top_k, | |
| ) | |
| force = "--reingest" in sys.argv | |
| build_index(index, force_reingest=force) | |
| print(f"\nProduction RAG ready. Model: {cross_encoder_model}") | |
| print(f"Search: dense_k={dense_k}, sparse_k={sparse_k}, rerank_top_k={rerank_top_k}") | |
| print("Type your query (or 'quit' to exit).\n") | |
| while True: | |
| try: | |
| query = input("Query> ").strip() | |
| except (KeyboardInterrupt, EOFError): | |
| print("\nExiting.") | |
| break | |
| if query.lower() in ("quit", "exit", "q"): | |
| break | |
| if not query: | |
| continue | |
| candidates = index.hybrid_search(query, dense_k=dense_k, sparse_k=sparse_k) | |
| print(f" Hybrid search: {len(candidates)} unique candidates (dense + BM25)") | |
| top_chunks = index.re_rank(query, candidates) | |
| display_results(top_chunks) | |
| if enable_llm and openai_client: | |
| print("Generating answer...\n") | |
| answer = generate_answer(query, top_chunks, openai_client) | |
| print(f"Answer:\n{answer}\n") | |
| if __name__ == "__main__": | |
| main() | |