Bhaskar Ram commited on
Commit
55953aa
·
0 Parent(s):

Fix Gradio 6.x compatibility errors

Browse files
Files changed (8) hide show
  1. README.md +115 -0
  2. app.py +191 -0
  3. rag/__init__.py +0 -0
  4. rag/chain.py +71 -0
  5. rag/document_loader.py +62 -0
  6. rag/embedder.py +81 -0
  7. rag/retriever.py +37 -0
  8. requirements.txt +7 -0
README.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Enterprise Document Q&A (RAG)
3
+ emoji: 🏢
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: "6.6.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - rag
13
+ - document-qa
14
+ - enterprise
15
+ - llama
16
+ - langchain
17
+ - faiss
18
+ - gradio
19
+ - nlp
20
+ - question-answering
21
+ ---
22
+
23
+ # 🏢 Enterprise Document Q&A — RAG System
24
+
25
+ > **Upload your company documents. Ask questions. Get answers — strictly from your data.**
26
+
27
+ A production-ready **Retrieval-Augmented Generation (RAG)** system built for businesses, enterprises, and private-sector organizations. Powered by **Llama 3** and **FAISS**, it lets your teams query internal documents through a clean chat interface — with zero hallucination from outside knowledge.
28
+
29
+ ---
30
+
31
+ ## ✨ Features
32
+
33
+ | Feature | Details |
34
+ | ----------------------------- | ---------------------------------------------------------- |
35
+ | 📄 **Multi-format ingestion** | PDF, DOCX, TXT, MD, CSV |
36
+ | 🧠 **Open-source LLM** | `meta-llama/Llama-3.1-8B-Instruct` via HF Inference API |
37
+ | 🔒 **Strictly grounded** | Answers only from your uploaded documents |
38
+ | 📦 **Multi-document** | Upload and query across multiple files simultaneously |
39
+ | 💬 **Multi-turn chat** | Maintains conversation context across questions |
40
+ | ⚡ **Fast** | CPU-friendly embeddings (`all-MiniLM-L6-v2` + FAISS) |
41
+ | 🔑 **Secure** | Files processed in-session only — never stored permanently |
42
+
43
+ ---
44
+
45
+ ## 🚀 How to Use
46
+
47
+ ### On Hugging Face Spaces
48
+
49
+ 1. Upload your documents (PDF, DOCX, TXT) using the left panel
50
+ 2. Click **Index Documents**
51
+ 3. Enter your [Hugging Face API token](https://huggingface.co/settings/tokens) _(Write access required for Llama 3)_
52
+ 4. Ask questions in the chat!
53
+
54
+ ### Self-Hosted / Local
55
+
56
+ ```bash
57
+ git clone https://huggingface.co/kerdosdotio/Custom-LLM-Chat
58
+ cd Custom-LLM-Chat
59
+ pip install -r requirements.txt
60
+ HF_TOKEN=hf_your_token python app.py
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🏗️ Architecture
66
+
67
+ ```
68
+ User Uploads Files
69
+
70
+ Document Parser (PDF / DOCX / TXT)
71
+
72
+ Text Chunking (512 chars, 64 overlap)
73
+
74
+ Embeddings (all-MiniLM-L6-v2)
75
+
76
+ FAISS Vector Index (in-memory)
77
+
78
+ User Question → Similarity Search → Top-K Chunks
79
+
80
+ Llama 3.1 8B — answers ONLY from retrieved chunks
81
+
82
+ Response + Source Citations
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 🔧 Tech Stack
88
+
89
+ - **UI**: [Gradio](https://gradio.app)
90
+ - **LLM**: `meta-llama/Llama-3.1-8B-Instruct`
91
+ - **Embeddings**: `sentence-transformers/all-MiniLM-L6-v2`
92
+ - **Vector Store**: [FAISS](https://github.com/facebookresearch/faiss)
93
+ - **Document Parsing**: PyMuPDF, python-docx
94
+
95
+ ---
96
+
97
+ ## 💼 Use Cases
98
+
99
+ - **Customer Support**: Index your product manuals, FAQs, and policies
100
+ - **HR & Legal**: Query employee handbooks, contracts, and compliance docs
101
+ - **Sales Enablement**: Search product specs, case studies, and pricing docs
102
+ - **IT Helpdesk**: Query runbooks, troubleshooting guides, and SOPs
103
+
104
+ ---
105
+
106
+ ## 🔐 Privacy
107
+
108
+ - Uploaded documents are **processed in-memory** and **not stored** after your session ends
109
+ - For persistent storage or on-premise deployment, clone and self-host this repository
110
+
111
+ ---
112
+
113
+ ## 📄 License
114
+
115
+ MIT License — free for commercial and private use.
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py — Enterprise Document Q&A (RAG)
3
+ Powered by Llama 3 + FAISS + Sentence Transformers
4
+ Hosted on Hugging Face Spaces
5
+ """
6
+
7
+ import os
8
+ import gradio as gr
9
+ from rag.document_loader import load_documents
10
+ from rag.embedder import build_index, add_to_index
11
+ from rag.retriever import retrieve
12
+ from rag.chain import answer
13
+
14
+ # ─────────────────────────────────────────────
15
+ # State helpers
16
+ # ─────────────────────────────────────────────
17
+
18
+ def get_hf_token(user_token: str) -> str:
19
+ """Prefer user-supplied token; fall back to Space secret."""
20
+ t = user_token.strip() if user_token else ""
21
+ return t or os.environ.get("HF_TOKEN", "")
22
+
23
+
24
+ # ─────────────────────────────────────────────
25
+ # Gradio handlers
26
+ # ─────────────────────────────────────────────
27
+
28
+ def process_files(files, current_index, status_box):
29
+ """Parse uploaded files and build / extend the FAISS index."""
30
+ if not files:
31
+ return current_index, "⚠️ No files uploaded."
32
+
33
+ file_paths = [f.name for f in files] if hasattr(files[0], "name") else files
34
+ docs = load_documents(file_paths)
35
+
36
+ if not docs:
37
+ return current_index, "❌ Could not extract text from the uploaded files. Please upload PDF, DOCX, or TXT files."
38
+
39
+ try:
40
+ if current_index is None:
41
+ idx = build_index(docs)
42
+ else:
43
+ idx = add_to_index(current_index, docs)
44
+ except Exception as e:
45
+ return current_index, f"❌ Failed to build index: {e}"
46
+
47
+ sources = list({d["source"] for d in docs})
48
+ total_chunks = idx.index.ntotal
49
+ msg = (
50
+ f"✅ Indexed {len(docs)} file(s): {', '.join(sources)}\n"
51
+ f"📦 Total chunks in knowledge base: {total_chunks}"
52
+ )
53
+ return idx, msg
54
+
55
+
56
+ def chat(user_message, history, vector_index, hf_token_input, top_k):
57
+ """Main chat handler — retrieves context and calls the LLM."""
58
+ if not user_message.strip():
59
+ return history, ""
60
+
61
+ hf_token = get_hf_token(hf_token_input)
62
+ if not hf_token:
63
+ history = history + [(user_message, "⚠️ Please provide a Hugging Face API token to use the chat.")]
64
+ return history, ""
65
+
66
+ if vector_index is None:
67
+ history = history + [(user_message, "⚠️ Please upload at least one document first.")]
68
+ return history, ""
69
+
70
+ try:
71
+ chunks = retrieve(user_message, vector_index, top_k=int(top_k))
72
+ bot_reply = answer(user_message, chunks, hf_token, chat_history=history)
73
+ except Exception as e:
74
+ bot_reply = f"❌ Error: {e}"
75
+
76
+ history = history + [(user_message, bot_reply)]
77
+ return history, ""
78
+
79
+
80
+ def reset_all():
81
+ """Clear index and chat."""
82
+ return None, [], "🗑️ Knowledge base and chat cleared.", ""
83
+
84
+
85
+ # ─────────────────────────────────────────────
86
+ # UI
87
+ # ─────────────────────────────────────────────
88
+
89
+ CSS = """
90
+ #title { text-align: center; }
91
+ #subtitle { text-align: center; color: #666; margin-bottom: 8px; }
92
+ .upload-box { border: 2px dashed #4f8ef7 !important; border-radius: 12px !important; }
93
+ #status-box { font-size: 0.9em; }
94
+ footer { display: none !important; }
95
+ """
96
+
97
+ with gr.Blocks(title="Enterprise Doc Q&A") as demo:
98
+
99
+ # ── Header ───────────────────────────────
100
+ gr.Markdown("# 🏢 Enterprise Document Q&A", elem_id="title")
101
+ gr.Markdown(
102
+ "Upload your company documents (PDF, DOCX, TXT) and ask questions. "
103
+ "The AI answers **only from your data** — never from outside knowledge.",
104
+ elem_id="subtitle",
105
+ )
106
+
107
+ # ── Shared state ─────────────────────────
108
+ vector_index = gr.State(None)
109
+
110
+ with gr.Row():
111
+ # ── Left panel: Upload + config ──────
112
+ with gr.Column(scale=1, min_width=300):
113
+ gr.Markdown("### 📂 Upload Documents")
114
+ file_upload = gr.File(
115
+ file_count="multiple",
116
+ file_types=[".pdf", ".docx", ".txt", ".md", ".csv"],
117
+ label="Drag & drop or click to upload",
118
+ elem_classes=["upload-box"],
119
+ )
120
+ index_btn = gr.Button("📥 Index Documents", variant="primary")
121
+ status_box = gr.Textbox(
122
+ label="Status",
123
+ interactive=False,
124
+ lines=3,
125
+ elem_id="status-box",
126
+ )
127
+
128
+ gr.Markdown("### ⚙️ Settings")
129
+ hf_token_input = gr.Textbox(
130
+ label="Hugging Face Token (optional if Space secret is set)",
131
+ placeholder="hf_...",
132
+ type="password",
133
+ value="",
134
+ )
135
+ top_k_slider = gr.Slider(
136
+ minimum=1, maximum=10, value=5, step=1,
137
+ label="Chunks to retrieve (top-K)",
138
+ )
139
+ reset_btn = gr.Button("🗑️ Clear All", variant="stop")
140
+
141
+ # ── Right panel: Chat ─────────────────
142
+ with gr.Column(scale=2):
143
+ gr.Markdown("### 💬 Ask Questions")
144
+ chatbot = gr.Chatbot(height=460, show_label=False)
145
+ with gr.Row():
146
+ user_input = gr.Textbox(
147
+ placeholder="Ask a question about your documents...",
148
+ show_label=False,
149
+ scale=5,
150
+ container=False,
151
+ )
152
+ send_btn = gr.Button("Send ▶", variant="primary", scale=1)
153
+
154
+ # ── Examples ─────────────────────────────
155
+ gr.Examples(
156
+ examples=[
157
+ ["What is the refund policy?"],
158
+ ["Summarize the key points of this document."],
159
+ ["What are the terms of service?"],
160
+ ["Who is the contact person for support?"],
161
+ ],
162
+ inputs=user_input,
163
+ )
164
+
165
+ # ── Event wiring ──────────────────────────
166
+ index_btn.click(
167
+ fn=process_files,
168
+ inputs=[file_upload, vector_index, status_box],
169
+ outputs=[vector_index, status_box],
170
+ )
171
+
172
+ send_btn.click(
173
+ fn=chat,
174
+ inputs=[user_input, chatbot, vector_index, hf_token_input, top_k_slider],
175
+ outputs=[chatbot, user_input],
176
+ )
177
+
178
+ user_input.submit(
179
+ fn=chat,
180
+ inputs=[user_input, chatbot, vector_index, hf_token_input, top_k_slider],
181
+ outputs=[chatbot, user_input],
182
+ )
183
+
184
+ reset_btn.click(
185
+ fn=reset_all,
186
+ inputs=[],
187
+ outputs=[vector_index, chatbot, status_box, user_input],
188
+ )
189
+
190
+ if __name__ == "__main__":
191
+ demo.launch(show_api=False, css=CSS, theme=gr.themes.Soft())
rag/__init__.py ADDED
File without changes
rag/chain.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ chain.py
3
+ Calls the LLM via HF Inference API with a strict RAG prompt.
4
+ Only answers from the retrieved context — never from general knowledge.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from huggingface_hub import InferenceClient
9
+
10
+ SYSTEM_PROMPT = """You are an enterprise document assistant. Your ONLY job is to answer questions using the provided document context below.
11
+
12
+ STRICT RULES:
13
+ 1. Answer ONLY using information explicitly found in the provided context.
14
+ 2. Do NOT use any outside knowledge or assumptions.
15
+ 3. If the answer is not found in the context, respond EXACTLY with: "I don't have that information in the uploaded documents."
16
+ 4. Always cite the source document name(s) in your answer using [Source: <filename>].
17
+ 5. Be concise and professional.
18
+
19
+ Context from uploaded documents:
20
+ ---
21
+ {context}
22
+ ---
23
+ """
24
+
25
+ LLM_MODEL = "meta-llama/Llama-3.1-8B-Instruct"
26
+ MAX_NEW_TOKENS = 1024
27
+ TEMPERATURE = 0.1 # Low temperature for factual, grounded responses
28
+
29
+
30
+ def build_context(chunks: list[dict]) -> str:
31
+ """Format retrieved chunks into a readable context block."""
32
+ parts = []
33
+ for i, chunk in enumerate(chunks, 1):
34
+ parts.append(f"[{i}] (Source: {chunk['source']})\n{chunk['text']}")
35
+ return "\n\n".join(parts)
36
+
37
+
38
+ def answer(
39
+ query: str,
40
+ context_chunks: list[dict],
41
+ hf_token: str,
42
+ chat_history: list[tuple[str, str]] | None = None,
43
+ ) -> str:
44
+ """
45
+ Call Llama 3 via HF Inference API to answer the query
46
+ grounded strictly in context_chunks.
47
+ """
48
+ if not context_chunks:
49
+ return "I don't have that information in the uploaded documents."
50
+
51
+ context = build_context(context_chunks)
52
+ system_msg = SYSTEM_PROMPT.format(context=context)
53
+
54
+ # Build message history for multi-turn conversation
55
+ messages = [{"role": "system", "content": system_msg}]
56
+ if chat_history:
57
+ for user_msg, bot_msg in chat_history[-4:]: # keep last 4 turns for context
58
+ if user_msg:
59
+ messages.append({"role": "user", "content": user_msg})
60
+ if bot_msg:
61
+ messages.append({"role": "assistant", "content": bot_msg})
62
+ messages.append({"role": "user", "content": query})
63
+
64
+ client = InferenceClient(token=hf_token)
65
+ response = client.chat_completion(
66
+ model=LLM_MODEL,
67
+ messages=messages,
68
+ max_tokens=MAX_NEW_TOKENS,
69
+ temperature=TEMPERATURE,
70
+ )
71
+ return response.choices[0].message.content.strip()
rag/document_loader.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ document_loader.py
3
+ Parses uploaded files (PDF, DOCX, TXT/MD) into plain text.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ def load_documents(file_paths: list[str]) -> list[dict]:
11
+ """
12
+ Given a list of file paths, parse each into a dict:
13
+ { "source": filename, "text": full text content }
14
+ Supports: .pdf, .docx, .txt, .md
15
+ """
16
+ docs = []
17
+ for path in file_paths:
18
+ if path is None:
19
+ continue
20
+ ext = Path(path).suffix.lower()
21
+ name = Path(path).name
22
+ try:
23
+ if ext == ".pdf":
24
+ text = _load_pdf(path)
25
+ elif ext == ".docx":
26
+ text = _load_docx(path)
27
+ elif ext in (".txt", ".md", ".csv"):
28
+ text = _load_text(path)
29
+ else:
30
+ print(f"[Loader] Unsupported file type: {ext} — skipping {name}")
31
+ continue
32
+
33
+ if text.strip():
34
+ docs.append({"source": name, "text": text})
35
+ else:
36
+ print(f"[Loader] Empty content from {name} — skipping")
37
+ except Exception as e:
38
+ print(f"[Loader] Failed to load {name}: {e}")
39
+
40
+ return docs
41
+
42
+
43
+ def _load_pdf(path: str) -> str:
44
+ import fitz # PyMuPDF
45
+ doc = fitz.open(path)
46
+ pages = []
47
+ for page in doc:
48
+ pages.append(page.get_text("text"))
49
+ doc.close()
50
+ return "\n".join(pages)
51
+
52
+
53
+ def _load_docx(path: str) -> str:
54
+ from docx import Document
55
+ doc = Document(path)
56
+ paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
57
+ return "\n".join(paragraphs)
58
+
59
+
60
+ def _load_text(path: str) -> str:
61
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
62
+ return f.read()
rag/embedder.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ embedder.py
3
+ Chunks raw text documents and builds an in-memory FAISS vector index.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import numpy as np
8
+ from dataclasses import dataclass, field
9
+
10
+ CHUNK_SIZE = 512 # characters
11
+ CHUNK_OVERLAP = 64 # characters
12
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
13
+
14
+
15
+ @dataclass
16
+ class VectorIndex:
17
+ """Holds chunks, their embeddings, and the FAISS index."""
18
+ chunks: list[dict] = field(default_factory=list) # {"source", "text"}
19
+ index: object = None # faiss.IndexFlatL2
20
+ embedder: object = None # SentenceTransformer
21
+
22
+
23
+ def _chunk_text(source: str, text: str) -> list[dict]:
24
+ """Split text into overlapping chunks."""
25
+ chunks = []
26
+ start = 0
27
+ while start < len(text):
28
+ end = start + CHUNK_SIZE
29
+ chunk_text = text[start:end]
30
+ if chunk_text.strip():
31
+ chunks.append({"source": source, "text": chunk_text})
32
+ start += CHUNK_SIZE - CHUNK_OVERLAP
33
+ return chunks
34
+
35
+
36
+ def build_index(docs: list[dict]) -> VectorIndex:
37
+ """
38
+ Takes list of {"source", "text"} dicts.
39
+ Returns a VectorIndex with embeddings stored in FAISS.
40
+ """
41
+ import faiss
42
+ from sentence_transformers import SentenceTransformer
43
+
44
+ # Chunk all documents
45
+ all_chunks = []
46
+ for doc in docs:
47
+ all_chunks.extend(_chunk_text(doc["source"], doc["text"]))
48
+
49
+ if not all_chunks:
50
+ raise ValueError("No text chunks could be extracted from the uploaded files.")
51
+
52
+ print(f"[Embedder] Embedding {len(all_chunks)} chunks...")
53
+ model = SentenceTransformer(EMBEDDING_MODEL)
54
+ texts = [c["text"] for c in all_chunks]
55
+ embeddings = model.encode(texts, show_progress_bar=False, batch_size=32)
56
+ embeddings = np.array(embeddings, dtype="float32")
57
+
58
+ dim = embeddings.shape[1]
59
+ index = faiss.IndexFlatL2(dim)
60
+ index.add(embeddings)
61
+
62
+ print(f"[Embedder] Index built: {index.ntotal} vectors, dim={dim}")
63
+ return VectorIndex(chunks=all_chunks, index=index, embedder=model)
64
+
65
+
66
+ def add_to_index(vector_index: VectorIndex, docs: list[dict]) -> VectorIndex:
67
+ """Incrementally add new docs to an existing index."""
68
+ import faiss
69
+ import numpy as np
70
+
71
+ new_chunks = []
72
+ for doc in docs:
73
+ new_chunks.extend(_chunk_text(doc["source"], doc["text"]))
74
+
75
+ texts = [c["text"] for c in new_chunks]
76
+ embeddings = vector_index.embedder.encode(texts, show_progress_bar=False, batch_size=32)
77
+ embeddings = np.array(embeddings, dtype="float32")
78
+
79
+ vector_index.index.add(embeddings)
80
+ vector_index.chunks.extend(new_chunks)
81
+ return vector_index
rag/retriever.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ retriever.py
3
+ Performs similarity search against the FAISS index.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import numpy as np
8
+ from rag.embedder import VectorIndex
9
+
10
+ DEFAULT_TOP_K = 5
11
+
12
+
13
+ def retrieve(query: str, vector_index: VectorIndex, top_k: int = DEFAULT_TOP_K) -> list[dict]:
14
+ """
15
+ Embed the query and return top_k most similar chunks.
16
+ Each result: {"source": str, "text": str, "score": float}
17
+ """
18
+ if vector_index is None or vector_index.index is None:
19
+ return []
20
+
21
+ query_embedding = vector_index.embedder.encode([query], show_progress_bar=False)
22
+ query_embedding = np.array(query_embedding, dtype="float32")
23
+
24
+ n_results = min(top_k, vector_index.index.ntotal)
25
+ distances, indices = vector_index.index.search(query_embedding, n_results)
26
+
27
+ results = []
28
+ for dist, idx in zip(distances[0], indices[0]):
29
+ if idx == -1:
30
+ continue
31
+ chunk = vector_index.chunks[idx]
32
+ results.append({
33
+ "source": chunk["source"],
34
+ "text": chunk["text"],
35
+ "score": float(dist),
36
+ })
37
+ return results
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=6.6.0
2
+ sentence-transformers>=2.7.0
3
+ faiss-cpu>=1.7.4
4
+ PyMuPDF>=1.24.0
5
+ python-docx>=1.1.0
6
+ huggingface-hub>=0.23.0
7
+ numpy>=1.24.0