TzepChris commited on
Commit
a183987
·
verified ·
1 Parent(s): 27d780b

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +284 -0
  2. requirements.txt +15 -0
app.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import torch
3
+ import unicodedata
4
+ import re
5
+ import numpy as np
6
+ from pathlib import Path
7
+ from transformers import AutoTokenizer, AutoModel
8
+ from sklearn.feature_extraction.text import HashingVectorizer
9
+ from sklearn.preprocessing import normalize as sk_normalize
10
+ import chromadb
11
+ import joblib
12
+ import pickle
13
+ import scipy.sparse
14
+ import textwrap
15
+ import os
16
+
17
+ # --------------------------- CONFIG -----------------------------------
18
+ DB_DIR = Path("./chroma_db_greekbertChatbotVol106")
19
+ ASSETS_DIR = Path("./assets")
20
+
21
+ # --- ΝΕΑ ΜΕΤΑΒΛΗΤΗ ΓΙΑ ΕΞΩΤΕΡΙΚΑ PDF ---
22
+ # !!! ΣΗΜΑΝΤΙΚΟ: Αντικαταστήστε το παρακάτω URL με το πραγματικό βασικό URL
23
+ # όπου θα φιλοξενείτε τα PDF σας. Πρέπει να τελειώνει με '/'.
24
+ EXTERNAL_PDF_BASE_URL = "https://your-pdf-hosting-service.com/path/to/your/pdfs/"
25
+ # Παράδειγμα: "https://storage.googleapis.com/my-bucket-name/pdfs/"
26
+ # Αν δεν έχετε ακόμα επιλέξει υπηρεσία, μπορείτε να το αφήσετε κενό προς το παρόν,
27
+ # αλλά οι σύνδεσμοι PDF δεν θα λειτουργούν σωστά.
28
+ # print(f"DEBUG: External PDF Base URL set to: {EXTERNAL_PDF_BASE_URL}") # Για έλεγχο
29
+
30
+ # Οι παρακάτω μεταβλητές μπορεί να μην χρησιμοποιούνται πλέον άμεσα για την εξυπηρέτηση PDF,
31
+ # αλλά τις αφήνουμε καθώς το Gradio μπορεί να τις χρειάζεται για άλλες λειτουργίες (π.χ. caching).
32
+
33
+ GCS_BUCKET_NAME = "chatbotthesisihu" # <<<--- ΑΛΛΑΞΤΕ ΤΟ ΑΥΤΟ!
34
+ GCS_PUBLIC_URL_PREFIX = f"https://storage.googleapis.com/{GCS_BUCKET_NAME}/"
35
+
36
+
37
+ STATIC_PDF_DIR = Path("./static_pdfs")
38
+ STATIC_PDF_DIR_NAME = "static_pdfs"
39
+
40
+ COL_NAME = "dataset14_grbert_charword"
41
+ MODEL_NAME = "sentence-transformers/paraphrase-xlm-r-multilingual-v1"
42
+ CHUNK_SIZE = 512
43
+ ALPHA_BASE = 0.2
44
+ ALPHA_LONGQ = 0.5
45
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
46
+
47
+ print(f"Running on device: {DEVICE}")
48
+
49
+ # ----------------------- PRE-/POST HELPERS ----------------------------
50
+ def strip_acc(s: str) -> str:
51
+ return ''.join(ch for ch in unicodedata.normalize('NFD', s)
52
+ if not unicodedata.combining(ch))
53
+
54
+ STOP = {"σχετικο", "σχετικα", "με", "και"}
55
+
56
+ def preprocess(txt: str) -> str:
57
+ txt = strip_acc(txt.lower())
58
+ txt = re.sub(r"[^a-zα-ω0-9 ]", " ", txt)
59
+ txt = re.sub(r"\s+", " ", txt).strip()
60
+ return " ".join(w for w in txt.split() if w not in STOP)
61
+
62
+ def cls_embed(texts, tok, model):
63
+ out = []
64
+ enc = tok(texts, padding=True, truncation=True,
65
+ max_length=CHUNK_SIZE, return_tensors="pt").to(DEVICE)
66
+ with torch.no_grad():
67
+ hs = model(**enc, output_hidden_states=True).hidden_states
68
+ cls = torch.stack(hs[-4:],0).mean(0)[:,0,:]
69
+ cls = torch.nn.functional.normalize(cls, p=2, dim=1)
70
+ out.append(cls.cpu())
71
+ return torch.cat(out).numpy()
72
+
73
+ # ---------------------- LOAD MODELS & DATA (Μία φορά κατά την εκκίνηση) --------------------
74
+ print("⏳ Loading Model and Tokenizer...")
75
+ try:
76
+ tok = AutoTokenizer.from_pretrained(MODEL_NAME)
77
+ model = AutoModel.from_pretrained(MODEL_NAME).to(DEVICE).eval()
78
+ print("✓ Model and tokenizer loaded.")
79
+ except Exception as e:
80
+ print(f"CRITICAL ERROR loading model/tokenizer: {e}")
81
+ raise
82
+
83
+ print("⏳ Loading TF-IDF vectorizers and SPARSE matrices...")
84
+ try:
85
+ char_vec = joblib.load(ASSETS_DIR / "char_vectorizer.joblib")
86
+ word_vec = joblib.load(ASSETS_DIR / "word_vectorizer.joblib")
87
+ X_char = scipy.sparse.load_npz(ASSETS_DIR / "X_char_sparse.npz")
88
+ X_word = scipy.sparse.load_npz(ASSETS_DIR / "X_word_sparse.npz")
89
+ print("✓ TF-IDF components loaded (sparse matrices).")
90
+ print(f" → X_char shape: {X_char.shape}, type: {type(X_char)}")
91
+ print(f" → X_word shape: {X_word.shape}, type: {type(X_word)}")
92
+ except Exception as e:
93
+ print(f"CRITICAL ERROR loading TF-IDF components: {e}")
94
+ raise
95
+
96
+ print("⏳ Loading chunk data (pre_chunks, raw_chunks, ids, metas)...")
97
+ try:
98
+ with open(ASSETS_DIR / "pre_chunks.pkl", "rb") as f:
99
+ pre_chunks = pickle.load(f)
100
+ with open(ASSETS_DIR / "raw_chunks.pkl", "rb") as f:
101
+ raw_chunks = pickle.load(f)
102
+ with open(ASSETS_DIR / "ids.pkl", "rb") as f:
103
+ ids = pickle.load(f)
104
+ with open(ASSETS_DIR / "metas.pkl", "rb") as f:
105
+ metas = pickle.load(f)
106
+ print(f"✓ Chunk data loaded. Total chunks from ids: {len(ids):,}")
107
+ if not all([pre_chunks, raw_chunks, ids, metas]):
108
+ print("WARNING: One or more chunk data lists are empty!")
109
+ except Exception as e:
110
+ print(f"CRITICAL ERROR loading chunk data: {e}")
111
+ raise
112
+
113
+ print("⏳ Connecting to ChromaDB...")
114
+ try:
115
+ client = chromadb.PersistentClient(path=str(DB_DIR.resolve()))
116
+ col = client.get_collection(COL_NAME)
117
+ print(f"✓ Connected to ChromaDB. Collection '{COL_NAME}' count: {col.count()}")
118
+ if col.count() == 0:
119
+ print("WARNING: ChromaDB collection is empty or not found correctly!")
120
+ except Exception as e:
121
+ print(f"CRITICAL ERROR connecting to ChromaDB or getting collection: {e}")
122
+ print(f"Attempted DB path for PersistentClient: {str(DB_DIR.resolve())}")
123
+ print("Ensure the ChromaDB directory structure is correct in your Hugging Face Space repository.")
124
+ raise
125
+
126
+ # ---------------------- HYBRID SEARCH (Κύρια Λογική) ---------------------------------
127
+ def hybrid_search_gradio(query, k=5):
128
+ if not query.strip():
129
+ return "Παρακαλώ εισάγετε μια ερώτηση."
130
+
131
+ if not ids:
132
+ return "Σφάλμα: Τα δεδομένα αναζήτησης (ids) δεν έχουν φορτωθεί. Επικοινωνήστε με τον διαχειριστή."
133
+
134
+ q_pre = preprocess(query)
135
+ words = q_pre.split()
136
+ alpha = ALPHA_LONGQ if len(words) > 30 else ALPHA_BASE
137
+
138
+ exact_ids_set = {ids[i] for i, t in enumerate(pre_chunks) if q_pre in t}
139
+
140
+ q_emb_np = cls_embed([q_pre], tok, model)
141
+ q_emb_list = q_emb_np.tolist()
142
+
143
+ try:
144
+ sem_results = col.query(
145
+ query_embeddings=q_emb_list,
146
+ n_results=min(k * 30, len(ids)),
147
+ include=["distances", "metadatas", "documents"]
148
+ )
149
+ except Exception as e:
150
+ print(f"ERROR during ChromaDB query: {e}")
151
+ return "Σφάλμα κατά την σημασιολογική αναζήτηση."
152
+
153
+ sem_sims = {doc_id: 1 - dist for doc_id, dist in zip(sem_results["ids"][0], sem_results["distances"][0])}
154
+
155
+ q_char_sparse = char_vec.transform([q_pre])
156
+ q_char_normalized = sk_normalize(q_char_sparse)
157
+ char_sim_scores = (q_char_normalized @ X_char.T).toarray().flatten()
158
+
159
+ q_word_sparse = word_vec.transform([q_pre])
160
+ q_word_normalized = sk_normalize(q_word_sparse)
161
+ word_sim_scores = (q_word_normalized @ X_word.T).toarray().flatten()
162
+
163
+ lex_sims = {}
164
+ for idx, (c_score, w_score) in enumerate(zip(char_sim_scores, word_sim_scores)):
165
+ if c_score > 0 or w_score > 0:
166
+ if idx < len(ids):
167
+ lex_sims[ids[idx]] = 0.85 * c_score + 0.15 * w_score
168
+ else:
169
+ print(f"Warning: Lexical score index {idx} out of bounds for ids list (len: {len(ids)}).")
170
+
171
+ all_chunk_ids_set = set(sem_sims.keys()) | set(lex_sims.keys()) | exact_ids_set
172
+ scored = []
173
+ for chunk_id_key in all_chunk_ids_set:
174
+ s = alpha * sem_sims.get(chunk_id_key, 0.0) + \
175
+ (1 - alpha) * lex_sims.get(chunk_id_key, 0.0)
176
+ if chunk_id_key in exact_ids_set:
177
+ s = 1.0
178
+ scored.append((chunk_id_key, s))
179
+
180
+ scored.sort(key=lambda x: x[1], reverse=True)
181
+
182
+ hits_output = []
183
+ seen_doc_main_ids = set()
184
+ for chunk_id_val, score_val in scored:
185
+ try:
186
+ idx_in_lists = ids.index(chunk_id_val)
187
+ except ValueError:
188
+ print(f"Warning: chunk_id '{chunk_id_val}' from search results not found in global 'ids' list. Skipping.")
189
+ continue
190
+
191
+ doc_meta = metas[idx_in_lists]
192
+ doc_main_id = doc_meta['id']
193
+
194
+ if doc_main_id in seen_doc_main_ids:
195
+ continue
196
+
197
+ original_url_from_meta = doc_meta.get('url', '#')
198
+
199
+ pdf_gcs_url = "#"
200
+ pdf_filename_display = "N/A"
201
+ pdf_filename_extracted = None
202
+
203
+ if original_url_from_meta and original_url_from_meta != '#':
204
+ pdf_filename_extracted = os.path.basename(original_url_from_meta)
205
+ print(f"--- Debug: Original URL from meta: {original_url_from_meta}, Extracted filename: {pdf_filename_extracted}")
206
+
207
+ if pdf_filename_extracted and pdf_filename_extracted.lower().endswith(".pdf"):
208
+ # Κατασκευή του δημόσιου URL για το GCS
209
+ # Βεβαιωθείτε ότι τα ονόματα αρχείων στο GCS ταιριάζουν ακριβώς με το pdf_filename_extracted
210
+ pdf_gcs_url = f"{GCS_PUBLIC_URL_PREFIX}{pdf_filename_extracted}"
211
+ pdf_filename_display = pdf_filename_extracted # Εμφάνιση του ονόματος αρχείου
212
+ print(f"--- Debug: Constructed GCS URL: {pdf_gcs_url}")
213
+
214
+ # Δεν χρειάζεται πλέον να ελέγχουμε τοπική ύπαρξη ή να προσπαθούμε να ανοίξουμε τοπικά το αρχείο
215
+ # για τους σκοπούς της δημιουργίας του συνδέσμου.
216
+ # Η πηγή της αλήθειας είναι τώρα το GCS.
217
+ else:
218
+ if not pdf_filename_extracted:
219
+ print(f"--- Debug: pdf_filename_extracted is empty or None from os.path.basename.")
220
+ else:
221
+ print(f"--- Debug: Extracted filename '{pdf_filename_extracted}' is not a PDF.")
222
+ pdf_filename_display = "Not a valid PDF link"
223
+ else:
224
+ print(f"--- Debug: No valid original_url_from_meta found. URL was: '{original_url_from_meta}'")
225
+ pdf_filename_display = "No source URL"
226
+
227
+ hits_output.append({
228
+ "score": score_val,
229
+ "title": doc_meta.get('title', 'N/A'),
230
+ "snippet": raw_chunks[idx_in_lists][:500] + " ...",
231
+ "original_url_meta": original_url_from_meta,
232
+ # Τώρα χρησιμοποιούμε το GCS URL για εξυπηρέτηση
233
+ "pdf_serve_url": pdf_gcs_url,
234
+ "pdf_filename_display": pdf_filename_display
235
+ })
236
+ seen_doc_main_ids.add(doc_main_id)
237
+ if len(hits_output) >= k:
238
+ break
239
+
240
+ if not hits_output:
241
+ return "Δεν βρέθηκαν σχετικά αποτελέσματα."
242
+
243
+ # Δημιουργία της εξόδου Markdown
244
+ output_md = f"Βρέθηκαν **{len(hits_output)}** σχετικά αποτελέσματα:\n\n"
245
+ for hit in hits_output:
246
+ output_md += f"### {hit['title']} (Score: {hit['score']:.3f})\n"
247
+ snippet_wrapped = textwrap.fill(hit['snippet'].replace("\n", " "), width=100)
248
+ output_md += f"**Απόσπασμα:** {snippet_wrapped}\n"
249
+
250
+ if hit['pdf_serve_url'] and hit['pdf_serve_url'] != '#':
251
+ # Ο σύνδεσμος θα χρησιμοποιήσει το εξωτερικό URL
252
+ output_md += f"**Πηγή (PDF):** <a href='{hit['pdf_serve_url']}' target='_blank'>{hit['pdf_filename_display']}</a>\n"
253
+ elif hit['original_url_meta'] and hit['original_url_meta'] != '#':
254
+ # Εναλλακτικά, αν δεν υπάρχει εξωτερικό PDF URL, δείχνουμε το αρχικό URL από τα metadata
255
+ output_md += f"**Πηγή (αρχικό από metadata):** [{hit['original_url_meta']}]({hit['original_url_meta']})\n"
256
+
257
+ # Προαιρετική προσθήκη συνδέσμου για το αρχείο txt (αφαιρέστε ή κρατήστε το σχολιασμένο αν δεν το χρειάζεστε πλέον)
258
+ # output_md += "\n\n---\n**Δοκιμαστικός Σύνδεσμος Κειμένου:** <a href='/file/static_pdfs/test_text_file.txt' target='_blank'>Άνοιγμα test_text_file.txt</a>\n"
259
+
260
+ output_md += "---\n"
261
+
262
+ return output_md
263
+
264
+
265
+ # ---------------------- GRADIO INTERFACE -----------------------------------
266
+ print("🚀 Launching Gradio Interface...")
267
+ iface = gr.Interface(
268
+ fn=hybrid_search_gradio,
269
+ inputs=gr.Textbox(lines=3, placeholder="Γράψε την ερώτησή σου εδώ...", label="Ερώτηση προς τον βοηθό:"),
270
+ outputs=gr.Markdown(label="Απαντήσεις από τα έγγραφα:", rtl=False, sanitize_html=False),
271
+ title="🏛️ Ελληνικό Chatbot Υβριδικής Αναζήτησης (v1.1.0 - External PDFs)",
272
+ description="Πληκτρολογήστε την ερώτησή σας για αναζήτηση στα διαθέσιμα έγγραφα. Η αναζήτηση συνδυάζει σημασιολογική ομοιότητα (GreekBERT) και ομοιότητα λέξεων/χαρακτήρων (TF-IDF).\nΧρησιμοποιεί το μοντέλο: sentence-transformers/paraphrase-xlm-r-multilingual-v1.\nΤα PDF (αν είναι διαμορφωμένα) ανοίγουν από εξωτερική πηγή σε νέα καρτέλα.",
273
+ allow_flagging="never",
274
+ examples=[
275
+ ["Ποια είναι τα μέτρα για τον κορονοϊό;", 5],
276
+ ["Πληροφορίες για άδεια ειδικού σκοπού", 3],
277
+ ["Τι προβλέπεται για τις μετακινήσεις εκτός νομού;", 5]
278
+ ],
279
+ )
280
+
281
+ if __name__ == '__main__':
282
+ # Η παράμετρος allowed_paths μπορεί να παραμείνει για άλλες λειτουργίες του Gradio,
283
+ # ακόμα κι αν δεν εξυπηρετούμε πλέον τα κύρια PDF μέσω αυτής.
284
+ iface.launch(allowed_paths=[STATIC_PDF_DIR_NAME])
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cu121
2
+ torch==2.2.1+cu121
3
+ torchaudio==2.2.1+cu121
4
+ torchvision==0.17.1+cu121
5
+ # -----------------------------
6
+ transformers==4.38.2 # Ή όποια έκδοση αποφασίσεις
7
+ chromadb==0.4.24 # <<<--- Η "κανονική" έκδοση που θα χρησιμοποιήσεις παντού
8
+ scikit-learn==1.6.1 # <<<--- Από την προηγούμενη προειδοποίηση
9
+ numpy==1.26.4
10
+ sentence-transformers==2.6.1 # Ή όποια έκδοση αποφασίσεις
11
+ gradio==4.20.0
12
+ joblib==1.3.2
13
+ unicodedata2
14
+ tqdm
15
+ scipy