k96beni commited on
Commit
d6e1a71
·
verified ·
1 Parent(s): 059cdaf

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +394 -0
  2. requirements.txt +11 -0
app.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import torch
4
+ import gradio as gr
5
+ import textwrap # för chunk-exempel
6
+
7
+ from huggingface_hub import login, Repository
8
+ from sentence_transformers import SentenceTransformer, util
9
+
10
+ # ====================================================
11
+ # KONFIGURATION OCH INIT
12
+ # ====================================================
13
+ HF_TOKEN = os.getenv("HUGGINGFACE_TOKEN") # Miljövariabel med din Hugging Face-token
14
+ if not HF_TOKEN:
15
+ raise ValueError("HUGGINGFACE_TOKEN miljövariabel saknas!")
16
+
17
+ login(token=HF_TOKEN)
18
+
19
+ # Ange var du har din Space och CSV
20
+ REPO_ID = "ChargeNodeEurope/Chatbot_4o_mini" # Exempel
21
+ REPO_LOCAL_PATH = "chatbot_faq_repo"
22
+ FAQ_CSV_PATH = "faq_städad_utf8_fixed.csv"
23
+
24
+ # Klona från Space:en (OBS: repo_type="space")
25
+ repo = Repository(
26
+ local_dir=REPO_LOCAL_PATH,
27
+ clone_from=REPO_ID,
28
+ repo_type="space",
29
+ use_auth_token=HF_TOKEN
30
+ )
31
+
32
+ # Ladda FAQ
33
+ faq_path = os.path.join(REPO_LOCAL_PATH, FAQ_CSV_PATH)
34
+ try:
35
+ df = pd.read_csv(faq_path, sep=";")
36
+ df.dropna(subset=["Fråga", "Svar"], inplace=True)
37
+ except Exception as e:
38
+ raise FileNotFoundError(f"Kunde inte ladda FAQ-filen: {str(e)}")
39
+
40
+ # -- Standardparametrar vi kan ändra i Admin-UI --
41
+ CHUNK_SIZE = 0 # 0 innebär ingen chunkning som default
42
+ TEMPERATURE = 0.0 # "Heat param", 0 = ingen slump, 1 = maximal slump (demonstration)
43
+
44
+ # Ladda modell
45
+ model_name = "sentence-transformers/all-MiniLM-L6-v2"
46
+ model = SentenceTransformer(model_name)
47
+
48
+ # ------------------------------------------------
49
+ # ENKEL CHUNKING (illustrativt exempel)
50
+ # ------------------------------------------------
51
+ def chunk_text(text: str, chunk_size: int):
52
+ """
53
+ Delar upp en textsträng i bitar om 'chunk_size' tecken.
54
+ Returnerar en lista med textchunkar.
55
+ Om chunk_size <= 0, returneras texten som en enda bit.
56
+ """
57
+ if chunk_size <= 0:
58
+ return [text]
59
+ # Använder textwrap.wrap eller en egen slicing:
60
+ wrapped = textwrap.wrap(text, width=chunk_size)
61
+ return wrapped
62
+
63
+ def build_faq_embeddings(df: pd.DataFrame, chunk_size: int):
64
+ """
65
+ Bygger en lista med (fråga, svar, kategori) men chunkar
66
+ svaret om chunk_size > 0. Returnerar:
67
+ - en DataFrame expanded_df
68
+ - embeddings (tensor)
69
+ Varje "rad" i expanded_df representerar en chunk av svaret.
70
+ """
71
+ all_rows = []
72
+ for _, row in df.iterrows():
73
+ fråga = row["Fråga"]
74
+ svar = row["Svar"]
75
+ kategori = row["Kategori"]
76
+ # Chunka svaret
77
+ svar_chunks = chunk_text(str(svar), chunk_size)
78
+ # Skapa en ny rad för varje chunk
79
+ for chunk in svar_chunks:
80
+ all_rows.append((fråga, chunk, kategori))
81
+
82
+ expanded_df = pd.DataFrame(all_rows, columns=["Fråga", "Svar_chunk", "Kategori"])
83
+ # Nu skapar vi embeddings baserat på "Fråga + Svar_chunk" eller bara "Fråga"?
84
+ # Här visar vi ett exempel där vi enbart använder FAQ-frågan som embed.
85
+ # Om du vill även ta med chunkade svaret i embeddings kan du ändra här.
86
+ faq_embeddings = model.encode(expanded_df["Fråga"].tolist(), convert_to_tensor=True)
87
+ return expanded_df, faq_embeddings
88
+
89
+ # Bygg initial embeddings
90
+ expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
91
+
92
+ # ====================================================
93
+ # FUNKTIONER FÖR SÖK & FAQ-HANTERING
94
+ # ====================================================
95
+ def uppdatera_parametrar(chunk_size, temperature):
96
+ """
97
+ Uppdaterar våra globala parametrar för chunkning och "temperature".
98
+ Bygger om embeddings om chunk_size ändras.
99
+ """
100
+ global CHUNK_SIZE, TEMPERATURE, df, expanded_df, faq_embeddings
101
+
102
+ # Sätt nya värden
103
+ CHUNK_SIZE = int(chunk_size)
104
+ TEMPERATURE = float(temperature)
105
+
106
+ # Bygg om embeddings med nya chunk_size
107
+ expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
108
+
109
+ return f"Parametrar uppdaterade:\n- CHUNK_SIZE={CHUNK_SIZE}\n- TEMPERATURE={TEMPERATURE}"
110
+
111
+ def sök_faq(question, top_k, threshold):
112
+ """
113
+ Söker i expanded_df med semantisk likhet.
114
+ 'temperature' används här som en "slump-faktor" (demonstration).
115
+ - En låg temperature -> sortera normalt efter cos sim
116
+ - En högre temperature -> slumpa lite i top-k
117
+ Returnerar en DataFrame med max top_k rader.
118
+ """
119
+ question = question.strip()
120
+ if not question:
121
+ return pd.DataFrame(columns=["Liknande FAQ-fråga", "Svar_chunk", "Kategori", "Confidence"])
122
+
123
+ # Skapa embedding för query
124
+ query_emb = model.encode(question, convert_to_tensor=True)
125
+ cos_scores = util.cos_sim(query_emb, faq_embeddings)[0]
126
+
127
+ # Hämta index och scores
128
+ # Ex: top_k = 5
129
+ top_results = torch.topk(cos_scores, k=int(top_k))
130
+ indices = top_results.indices.tolist()
131
+ scores = top_results.values.tolist()
132
+
133
+ # Om TEMPERATURE > 0, "skaka om" listan lite (demonstrationssyfte).
134
+ # Ju högre temperatur, desto större slumpmoment i ordningen.
135
+ # (OBS: Detta är inte en "officiell" generativ temperatur-implementation,
136
+ # men visar hur man kan introducera variation.)
137
+ if TEMPERATURE > 0:
138
+ import random
139
+ pairs = list(zip(indices, scores))
140
+ # slumpa om med en vikt av 'TEMPERATURE'
141
+ # t.ex. multiplicera score med en slumpfaktor
142
+ new_pairs = []
143
+ for (idx, sc) in pairs:
144
+ random_factor = 1.0 + (random.random() - 0.5) * TEMPERATURE
145
+ new_pairs.append((idx, sc * random_factor))
146
+ # sortera om
147
+ new_pairs.sort(key=lambda x: x[1], reverse=True)
148
+ indices = [p[0] for p in new_pairs]
149
+ scores = [p[1] for p in new_pairs]
150
+
151
+ data = []
152
+ for i, (idx, score) in enumerate(zip(indices, scores)):
153
+ if score >= threshold:
154
+ row = expanded_df.iloc[idx]
155
+ data.append({
156
+ "Liknande FAQ-fråga": row["Fråga"],
157
+ "Svar_chunk": row["Svar_chunk"],
158
+ "Kategori": row["Kategori"],
159
+ "Confidence": round(float(score), 3)
160
+ })
161
+
162
+ # Begränsa faktiskt antal rader till top_k
163
+ data = data[:int(top_k)]
164
+ results_df = pd.DataFrame(data)
165
+ return results_df
166
+
167
+ def hitta_topp3_förslag(fråga):
168
+ """
169
+ En enkel variant som alltid hämtar top_k=3, threshold=0.0
170
+ (används i de snabba sökexemplen).
171
+ """
172
+ return sök_faq(fråga, top_k=3, threshold=0.0)
173
+
174
+ def lägg_till_faq(fråga, svar, kategori):
175
+ """
176
+ Lägger till en ny rad i FAQ-DataFrame och pushar ändringen.
177
+ """
178
+ global df, expanded_df, faq_embeddings
179
+
180
+ fråga = fråga.strip()
181
+ svar = svar.strip()
182
+ kategori = kategori.strip()
183
+
184
+ if not fråga or not svar or not kategori:
185
+ return "Fråga, svar och kategori får inte vara tomma!"
186
+
187
+ try:
188
+ # Lägg till rad
189
+ ny_rad = pd.DataFrame([[fråga, svar, kategori]], columns=["Fråga", "Svar", "Kategori"])
190
+ df = pd.concat([df, ny_rad], ignore_index=True)
191
+
192
+ # Spara CSV
193
+ df.to_csv(faq_path, index=False, sep=";")
194
+
195
+ # Bygg nya embeddings med nya df
196
+ expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
197
+
198
+ # Git commit & push
199
+ repo.git_add()
200
+ repo.git_commit(f"Lade till FAQ: {fråga[:50]}...")
201
+ repo.git_push()
202
+
203
+ return "Fråga tillagd och synkad med Hugging Face!"
204
+ except Exception as e:
205
+ return f"Fel vid uppdatering: {str(e)}"
206
+
207
+ def visa_senaste_faq(antal=10):
208
+ """Returnerar de sista N raderna från FAQ-DataFrame."""
209
+ return df.tail(antal)
210
+
211
+ def uppdatera_faq(gammal_fråga, nytt_svar, ny_kategori):
212
+ """
213
+ Uppdaterar en befintlig rad (matchar på 'gammal_fråga').
214
+ """
215
+ global df, expanded_df, faq_embeddings
216
+ gammal_fråga = gammal_fråga.strip()
217
+ nytt_svar = nytt_svar.strip()
218
+ ny_kategori = ny_kategori.strip()
219
+
220
+ if not gammal_fråga:
221
+ return "Ingen fråga vald."
222
+
223
+ match_index = df.index[df["Fråga"] == gammal_fråga]
224
+ if len(match_index) == 0:
225
+ return "Ingen matchande FAQ-fråga hittad."
226
+
227
+ # Uppdatera
228
+ if nytt_svar:
229
+ df.loc[match_index, "Svar"] = nytt_svar
230
+ if ny_kategori:
231
+ df.loc[match_index, "Kategori"] = ny_kategori
232
+
233
+ # Spara & uppdatera embeddings
234
+ try:
235
+ df.to_csv(faq_path, index=False, sep=";")
236
+ expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
237
+ repo.git_add()
238
+ repo.git_commit(f"Uppdaterade FAQ: {gammal_fråga[:50]}...")
239
+ repo.git_push()
240
+ return f"FAQ uppdaterad: {gammal_fråga}"
241
+ except Exception as e:
242
+ return f"Fel vid uppdatering: {str(e)}"
243
+
244
+ def ta_bort_faq(fråga_att_radera):
245
+ """
246
+ Tar bort en FAQ-rad baserat på exakt match på 'fråga'.
247
+ """
248
+ global df, expanded_df, faq_embeddings
249
+ fråga_att_radera = fråga_att_radera.strip()
250
+ if not fråga_att_radera:
251
+ return "Ingen fråga vald."
252
+
253
+ match_index = df.index[df["Fråga"] == fråga_att_radera]
254
+ if len(match_index) == 0:
255
+ return "Ingen matchande FAQ-fråga hittad."
256
+
257
+ # Ta bort rad
258
+ df.drop(match_index, inplace=True)
259
+
260
+ # Spara, uppdatera embeddings
261
+ try:
262
+ df.to_csv(faq_path, index=False, sep=";")
263
+ expanded_df, faq_embeddings = build_faq_embeddings(df, CHUNK_SIZE)
264
+ repo.git_add()
265
+ repo.git_commit(f"Raderade FAQ: {fråga_att_radera[:50]}...")
266
+ repo.git_push()
267
+ return f"FAQ borttagen: {fråga_att_radera}"
268
+ except Exception as e:
269
+ return f"Fel vid borttagning: {str(e)}"
270
+
271
+
272
+ # ====================================================
273
+ # GRADIO-GRÄNSSNITT
274
+ # ====================================================
275
+ with gr.Blocks() as demo:
276
+ gr.Markdown("# ChargeNode Chatbot – Utvecklarläge\n"
277
+ "Ett admin-UI där du kan hantera FAQ-poster, testa sökning och "
278
+ "leka med parametrar (chunk size & \"heat\").")
279
+
280
+ # =======================
281
+ # (A) Parametrar
282
+ # =======================
283
+ with gr.Accordion("Parametrar", open=True):
284
+ gr.Markdown("Justera chunk-storlek och \"temperature\" (heat).")
285
+ chunk_slider = gr.Slider(minimum=0, maximum=200, step=10, value=0,
286
+ label="CHUNK_SIZE (tecken per bit). 0 = av.")
287
+ temp_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.0,
288
+ label="TEMPERATURE (heat). 0 = ingen slump, 1 = max slump.")
289
+ uppdatera_btn = gr.Button("Uppdatera parametrar")
290
+ param_output = gr.Textbox(label="Status")
291
+
292
+ uppdatera_btn.click(
293
+ fn=uppdatera_parametrar,
294
+ inputs=[chunk_slider, temp_slider],
295
+ outputs=param_output
296
+ )
297
+
298
+ # =======================
299
+ # (B) SÖK I FAQ
300
+ # =======================
301
+ with gr.Accordion("Sök i FAQ", open=False):
302
+ gr.Markdown("Skriv in en fråga och se topp K träffar.")
303
+ fråga_input = gr.Textbox(label="Din fråga", placeholder="Ex: Var finns närmaste laddstation?")
304
+ topk_slider = gr.Slider(minimum=1, maximum=10, step=1, value=3, label="Antal toppträffar (top_k)")
305
+ threshold_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.0, label="Confidence threshold")
306
+ sök_knapp = gr.Button("Sök")
307
+ sök_result = gr.Dataframe(headers=["Liknande FAQ-fråga", "Svar_chunk", "Kategori", "Confidence"],
308
+ label="Sökresultat")
309
+
310
+ sök_knapp.click(
311
+ fn=sök_faq,
312
+ inputs=[fråga_input, topk_slider, threshold_slider],
313
+ outputs=sök_result
314
+ )
315
+
316
+ # =======================
317
+ # (C) KORTVARIANTE: Hitta topp 3
318
+ # =======================
319
+ with gr.Accordion("Enkel sökning (topp 3)", open=False):
320
+ gr.Markdown("Här kan du snabbt få topp 3 träffar (threshold=0) utan att ställa in parametrar.")
321
+ fråga_input2 = gr.Textbox(label="Din fråga", placeholder="Ex: Hur ändrar jag lösenord?")
322
+ knapp2 = gr.Button("Hämta topp 3")
323
+ result2 = gr.Dataframe(label="Topp 3 förslag")
324
+ knapp2.click(fn=hitta_topp3_förslag, inputs=fråga_input2, outputs=result2)
325
+
326
+ # =======================
327
+ # (D) Lägg till ny FAQ-rad
328
+ # =======================
329
+ with gr.Accordion("Lägg till en ny FAQ-rad", open=False):
330
+ ny_fråga = gr.Textbox(label="Ny fråga", placeholder="Ex: Hur registrerar jag ett konto?")
331
+ nytt_svar = gr.Textbox(label="Nytt svar", placeholder="Skriv ditt svar här...")
332
+ ny_kategori = gr.Textbox(label="Ny kategori", placeholder="Ex: Konto")
333
+
334
+ lägg_till_knapp = gr.Button("Lägg till i FAQ")
335
+ lägg_till_output = gr.Textbox(label="Status")
336
+
337
+ def clear_add_fields():
338
+ return "", "", ""
339
+
340
+ reset_ny = gr.Button("Rensa fält")
341
+
342
+ lägg_till_knapp.click(
343
+ fn=lägg_till_faq,
344
+ inputs=[ny_fråga, nytt_svar, ny_kategori],
345
+ outputs=lägg_till_output
346
+ )
347
+ reset_ny.click(
348
+ fn=clear_add_fields,
349
+ inputs=[],
350
+ outputs=[ny_fråga, nytt_svar, ny_kategori]
351
+ )
352
+
353
+ # =======================
354
+ # (E) Redigera/ta bort FAQ
355
+ # =======================
356
+ with gr.Accordion("Redigera eller ta bort befintlig FAQ", open=False):
357
+ befintliga_frågor = gr.Dropdown(choices=df["Fråga"].tolist(),
358
+ label="Välj en befintlig fråga")
359
+ nytt_svar_edit = gr.Textbox(label="Nytt svar (valfritt)")
360
+ ny_kategori_edit = gr.Textbox(label="Ny kategori (valfritt)")
361
+ uppdatera_btn2 = gr.Button("Uppdatera vald FAQ")
362
+ uppdatera_output = gr.Textbox(label="Status")
363
+
364
+ radera_btn = gr.Button("Ta bort vald FAQ")
365
+ radera_output = gr.Textbox(label="Status")
366
+
367
+ uppdatera_btn2.click(
368
+ fn=uppdatera_faq,
369
+ inputs=[befintliga_frågor, nytt_svar_edit, ny_kategori_edit],
370
+ outputs=uppdatera_output
371
+ )
372
+ radera_btn.click(
373
+ fn=ta_bort_faq,
374
+ inputs=befintliga_frågor,
375
+ outputs=radera_output
376
+ )
377
+
378
+ # =======================
379
+ # (F) Visa senaste FAQ-poster
380
+ # =======================
381
+ with gr.Accordion("Visa senaste FAQ-poster", open=False):
382
+ visa_btn = gr.Button("Visa senaste 10")
383
+ logg_output = gr.Dataframe(label="Sista 10 FAQ-rader")
384
+ visa_btn.click(fn=visa_senaste_faq, inputs=[], outputs=logg_output)
385
+
386
+ gr.Markdown("---")
387
+ gr.Markdown("**OBS:** Detta skript använder en \"Repository\"-klass som är på väg att fasas ut "
388
+ "i huggingface_hub. Det fungerar i dagsläget, men vill du framöver undvika detta "
389
+ "kan du gå över till HTTP-baserade metoder för att ladda upp/ned filer. "
390
+ "Se Hugging Face-dokumentationen för mer info.")
391
+
392
+ # Starta Gradio
393
+ if __name__ == "__main__":
394
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Kräv en äldre huggingface_hub (<0.12) som fortfarande har cached_download
2
+ huggingface-hub==0.11.1
3
+
4
+ # Din version av sentence-transformers som anropar cached_download
5
+ sentence-transformers==2.2.2
6
+
7
+ # Resten av beroenden du behöver:
8
+ pandas==1.5.3
9
+ torch==1.13.1
10
+ gradio==3.16.2
11
+ # ...