TzepChris commited on
Commit
900a6a6
·
verified ·
1 Parent(s): e758887

Upload 3 files

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