k96beni commited on
Commit
903a885
·
verified ·
1 Parent(s): f0f58b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +376 -233
app.py CHANGED
@@ -74,7 +74,42 @@ embeddings = None
74
  index = None
75
  chunks = []
76
  chunk_sources = []
77
- faq_dict = {} # Ny: Dictionary för direktmatchning av vanliga frågor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # --- Förbättrad loggfunktion ---
80
  def safe_append_to_log(log_entry):
@@ -108,50 +143,107 @@ def safe_append_to_log(log_entry):
108
  print(f"Kritiskt fel vid loggning: {retry_error}")
109
  return False
110
 
111
- # --- Laddar textkällor ---
112
- def load_local_files():
113
- """Laddar alla lokala filer och returnerar som en sammanhängande text."""
114
- uploaded_text = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
116
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
 
117
  for file in os.listdir("."):
118
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
119
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  if file.endswith(".txt"):
121
  with open(file, "r", encoding="utf-8") as f:
122
  content = f.read()
123
  elif file.endswith(".docx"):
124
- from docx import Document # Import sker vid behov
125
  content = "\n".join([p.text for p in Document(file).paragraphs])
126
  elif file.endswith(".pdf"):
127
- import PyPDF2 # Import sker vid behov
128
  with open(file, "rb") as f:
129
  reader = PyPDF2.PdfReader(f)
130
  content = "\n".join([p.extract_text() or "" for p in reader.pages])
131
  elif file.endswith(".csv"):
132
  content = pd.read_csv(file).to_string()
133
  elif file.endswith((".xls", ".xlsx")):
134
- if file == "FAQ stadat.xlsx":
135
  df = pd.read_excel(file)
136
  rows = []
137
  for index, row in df.iterrows():
138
- # Start with the required fields
139
  row_text = f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}"
140
-
141
- # Add kategori if it exists in the dataframe
142
  if 'kategori' in df.columns:
143
  row_text += f"\nKategori: {row['kategori']}"
144
- elif 'Kategori' in df.columns: # Also check for capitalized version
145
  row_text += f"\nKategori: {row['Kategori']}"
146
-
147
  rows.append(row_text)
148
  content = "\n\n".join(rows)
149
  else:
150
  content = pd.read_excel(file).to_string()
151
- uploaded_text += f"\n\nFIL: {file}\n{content}"
 
 
 
 
 
152
  except Exception as e:
153
  print(f"Fel vid läsning av {file}: {str(e)}")
154
- return uploaded_text.strip()
 
155
 
156
  def load_prompt():
157
  """Läser in system-prompts från prompt.txt med bättre felhantering."""
@@ -169,184 +261,156 @@ def load_prompt():
169
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
170
  return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
171
 
172
- # --- Förbättrad chunking ---
173
- def prepare_chunks(text_data):
174
- """Delar upp texten i mindre segment för embedding och sökning med särskild hänsyn till FAQ-format."""
175
- chunks_list, sources_list = [], [] # Renamed to avoid conflict with global 'chunks' and 'sources'
 
176
  global faq_dict
177
 
178
  for source, text in text_data.items():
179
- # Split text into paragraph-sized chunks
180
  paragraphs = [p for p in text.split("\n") if p.strip()]
181
 
182
- # Process FAQ-specific content better
183
  i = 0
184
- current_file_chunks = []
185
- current_file_sources = []
186
  while i < len(paragraphs):
187
- # Start a new chunk
188
  current_chunk = ""
189
  start_idx = i
190
 
191
- # Check for FAQ format
192
- if i < len(paragraphs) and paragraphs[i].startswith("Fråga:"):
193
- question = paragraphs[i][7:].strip() # Extract the question text
194
  current_chunk = paragraphs[i]
195
  i += 1
196
 
197
- # Add content until we reach the next question or MAX_CHUNK_SIZE
198
  while i < len(paragraphs) and not paragraphs[i].startswith("Fråga:"):
199
- # Add this paragraph if it doesn't exceed chunk size
200
  if len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
201
  current_chunk += "\n" + paragraphs[i]
202
  else:
203
- # If we're already processing a FAQ answer, don't break mid-answer
204
  if "Svar:" in current_chunk:
205
- # We prefer to keep whole answers together, so let's break only if answer is too long
206
- if len(current_chunk) > MAX_CHUNK_SIZE * 1.5: # Allow some overflow
207
- break
208
- else:
209
- current_chunk += "\n" + paragraphs[i]
210
- else:
211
  break
 
 
212
  i += 1
213
 
214
- # Store FAQ pairs in the dictionary for direct lookup
215
  if "Svar:" in current_chunk:
216
  answer_start = current_chunk.find("Svar:")
217
  answer_text = current_chunk[answer_start + 5:].strip()
218
 
219
- # Add variations with common synonyms for payment-related questions
220
- if any(term in question.lower() for term in ["betalsätt", "betalmetod", "betalmedel", "kort",
221
- "betalkort", "betalning", "betala"]):
 
 
222
  payment_variations = [
223
  "hur ändrar jag betalmedel",
224
- "hur byter jag betalsätt",
225
  "hur uppdaterar jag mitt betalkort",
226
  "hur ändrar jag betalmetod",
227
  "hur byter jag betalningsmetod",
228
- "hur ändrar jag betalkort"
 
 
 
229
  ]
230
  for variation in payment_variations:
231
  faq_dict[variation] = answer_text
232
-
233
- # Add the original question to the dictionary
234
- faq_dict[question.lower()] = answer_text
 
235
  else:
236
- # Handle non-FAQ text using sliding window
237
  while i < len(paragraphs) and len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
238
  if current_chunk:
239
  current_chunk += " " + paragraphs[i]
240
  else:
241
  current_chunk = paragraphs[i]
242
  i += 1
 
 
243
 
244
- # Save the chunk if it has content
245
  if current_chunk.strip():
246
- current_file_chunks.append(current_chunk.strip())
247
- current_file_sources.append(source)
248
 
249
- # If we've added a chunk but haven't advanced, we need to move forward
250
  if i == start_idx:
251
  i += 1
252
-
253
- # Create overlapping chunks for better context preservation for THIS source
254
- overlap_chunks_for_file = []
255
- overlap_sources_for_file = []
256
-
257
- for j in range(len(current_file_chunks)):
258
- overlap_chunks_for_file.append(current_file_chunks[j])
259
- overlap_sources_for_file.append(current_file_sources[j])
260
-
261
- if j < len(current_file_chunks) - 1:
262
- # Calculate available space in the current chunk
263
- space_left = MAX_CHUNK_SIZE - len(current_file_chunks[j])
264
-
265
- # If there's enough space, add part of the next chunk
266
- if space_left >= CHUNK_OVERLAP:
267
- # Ensure we don't duplicate if chunks are already naturally overlapping significantly
268
- # This check could be more sophisticated, but a simple end/start check helps
269
- if not current_file_chunks[j].endswith(current_file_chunks[j+1][:CHUNK_OVERLAP]):
270
- overlap_text = current_file_chunks[j] + " " + current_file_chunks[j+1][:CHUNK_OVERLAP]
271
- if len(overlap_text) <= MAX_CHUNK_SIZE: # Ensure overlap doesn't exceed max size
272
- overlap_chunks_for_file.append(overlap_text)
273
- overlap_sources_for_file.append(current_file_sources[j]) # or a combined source if preferred
274
-
275
- chunks_list.extend(overlap_chunks_for_file)
276
- sources_list.extend(overlap_sources_for_file)
277
-
278
- print(f"Genererade {len(chunks_list)} chunks med {len(faq_dict)} FAQ-par")
279
- return chunks_list, sources_list
280
-
281
-
282
- def initialize_embeddings():
283
- """Initierar SentenceTransformer och FAISS-index vid första anrop."""
284
- global embedder, embeddings, index, chunks, chunk_sources, faq_dict
285
 
286
- if embedder is None:
287
- print("Initierar SentenceTransformer och FAISS-index...")
288
- # Ladda och förbered lokal data
289
- print("Laddar textdata...")
290
- text_data = {"local_files": load_local_files()}
291
- print("Förbereder textsegment...")
292
- chunks, chunk_sources = prepare_chunks(text_data) # Assign to global chunks and chunk_sources
293
- print(f"{len(chunks)} segment laddade")
294
-
295
- if not chunks:
296
- print("Varning: Inga chunks genererades. Kontrollera textkällor och chunking-logik.")
297
- # Sätt upp tomma men giltiga strukturer för att undvika fel senare
298
- embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda embedder ändå
299
- embeddings = np.array([]).reshape(0, embedder.get_sentence_embedding_dimension())
300
- index = faiss.IndexFlatIP(embedder.get_sentence_embedding_dimension())
301
- # index.add() kommer inte anropas om embeddings är tomma.
302
- print("FAISS-index initialiserat tomt då inga chunks fanns.")
303
- return
304
-
305
-
306
- print("Skapar embeddings...")
307
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
308
- embeddings = embedder.encode(chunks, convert_to_numpy=True)
309
-
310
- # Normalisera embeddings för IndexFlatIP (dot product)
311
- if embeddings.ndim == 2 and embeddings.shape[0] > 0: # Check if embeddings is not empty
312
- embeddings_norm = np.linalg.norm(embeddings, axis=1, keepdims=True)
313
- # Undvik division med noll om någon norm är noll
314
- embeddings_norm[embeddings_norm == 0] = 1e-10
315
- embeddings = embeddings / embeddings_norm
316
-
317
- index = faiss.IndexFlatIP(embeddings.shape[1])
318
- index.add(embeddings)
319
- print("FAISS-index klart")
320
- else:
321
- print("Varning: Inga embeddings genererades, FAISS-index kan vara tomt eller ogiltigt.")
322
- # Fallback: skapa ett tomt index om embeddings är tomma
323
- dimension = embedder.get_sentence_embedding_dimension() if embedder else 384 # Default dimension
324
- index = faiss.IndexFlatIP(dimension)
325
- print("FAISS-index initialiserat tomt.")
326
 
327
- # Print FAQ dictionary keys for debugging
328
- print(f"FAQ Dictionary innehåller {len(faq_dict)} nycklar")
329
- if len(faq_dict) > 0:
330
- payment_keys = [k for k in faq_dict.keys() if any(term in k for term in ["betalsätt", "betalmetod", "betalmedel"])]
331
- print(f"Betalningsrelaterade FAQ-nycklar: {payment_keys[:5]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- def check_direct_match(query):
334
- """Kontrollerar om frågan matchar någon av våra fördefinierade FAQ-svar."""
335
  query_lower = query.lower().strip('?').strip()
336
 
337
- # Utökad lista med prefix för betalningsrelaterade frågor
 
 
 
 
 
 
 
 
338
  payment_prefixes = [
339
- "hur ändrar jag",
340
- "hur byter jag",
341
- "hur uppdaterar jag",
342
- "hur lägger jag till", # NYTT
343
- "hur adderar jag", # NYTT
344
- "hur registrerar jag" # NYTT
345
  ]
346
 
347
- # Explicit check for payment method question
 
 
 
 
 
 
 
348
  if any(query_lower.startswith(prefix) for prefix in payment_prefixes) and \
349
- any(term in query_lower for term in ["betalsätt", "betalmetod", "betalmedel", "betalkort", "kort"]):
 
350
  payment_answer = """Så här lägger du till/ändrar betalkort:
351
  1. Öppna ChargeNode-appen
352
  2. Tryck på 'Meny' (hamburgerikon) i nedre menyn
@@ -359,56 +423,146 @@ def check_direct_match(query):
359
  8. Bekräfta med BankID
360
 
361
  OBS! Se till att kortet har pengar och att det är upplåst för internetbetalningar."""
362
- return payment_answer # <-- Detta ska vara INUTI if-blocket!
363
 
364
- # Check if query directly matches a FAQ
365
  if query_lower in faq_dict:
366
  return faq_dict[query_lower]
367
 
368
- # Check for close matches using pattern matching
369
  for key, value in faq_dict.items():
370
- # Find questions about changing things with synonyms
371
- if ("ändra" in query_lower or "byta" in query_lower or "uppdatera" in query_lower or
372
- "lägger till" in query_lower or "adderar" in query_lower) and \
373
- ("ändra" in key or "byta" in key or "uppdatera" in key or "lägger till" in key):
374
- # Check if key and query share important terms
375
- query_terms = set(re.findall(r'\w+', query_lower))
376
- key_terms = set(re.findall(r'\w+', key))
377
- if len(query_terms.intersection(key_terms)) >= 2: # At least 2 words in common
378
- return value
379
 
380
  return None
381
 
382
- def retrieve_context(query, k=RETRIEVAL_K):
383
- """Hämtar relevant kontext för frågor med direkt matchning för vanliga frågor."""
384
- # Säkerställ att modeller är laddade
385
- initialize_embeddings()
386
 
387
- # Först, kolla efter direktmatchningar för vanliga frågor
388
- direct_match = check_direct_match(query)
389
  if direct_match:
390
- print(f"Direkt matchning hittad för frågan: {query}")
391
- return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
392
 
393
- # Om ingen direktmatchning, använd vanlig embedding-sökning
394
- if embedder is None or index is None or index.ntotal == 0: # Check if index is usable
395
- print("Varning: Embedder eller FAISS-index är inte korrekt initierat eller är tomt. Returnerar tom kontext.")
396
  return "", []
397
-
 
 
 
 
 
398
  query_embedding = embedder.encode([query], convert_to_numpy=True)
399
- # Normalisera query_embedding på samma sätt som indexets embeddings
400
  query_embedding_norm = np.linalg.norm(query_embedding)
401
- if query_embedding_norm == 0: query_embedding_norm = 1e-10 # Undvik division med noll
 
402
  query_embedding = query_embedding / query_embedding_norm
403
 
404
- D, I = index.search(query_embedding, k)
405
- retrieved, sources_set = [], set() # Renamed to avoid conflict
406
- for idx in I[0]:
407
- if 0 <= idx < len(chunks): # Ensure index is valid
408
- retrieved.append(chunks[idx])
409
- sources_set.add(chunk_sources[idx]) # Use chunk_sources which is global
410
- return " ".join(retrieved), list(sources_set)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
  # Ladda prompt template
414
  prompt_template = load_prompt()
@@ -429,34 +583,28 @@ def format_chat_history_for_claude(chat_history):
429
 
430
  return messages
431
 
432
- def generate_answer(query, chat_history=None):
433
- """Genererar svar baserat på fråga, chatthistorik och retrieval-baserad kontext med Claude Sonnet 4."""
434
- # Hämta relevant kontext via RAG istället för hela databasen
435
- context, sources = retrieve_context(query) # sources är en lista med källor
436
-
437
- if not context.strip(): # Om context är tom, efterfråga mer info eller ge standard svar
438
- # Detta kan hända om RAG inte hittar något relevant.
439
- # Vi kan fortfarande försöka svara med Claude, men utan RAG-kontext.
440
- print("Ingen RAG-kontext hittades. Försöker svara utan.")
441
- # return "Jag hittar ingen relevant information i mina källor för att svara på din fråga. Kan du omformulera eller ge mer detaljer?\n\nDetta är ett AI genererat svar."
442
-
443
  # System-prompts
444
  system_prompt = prompt_template
445
 
446
  # Förbered meddelanden för Claude API
447
- messages = [] # Starta med en tom lista för varje nytt anrop
448
 
449
  # Lägg till chatthistorik om den finns och är meningsfull
450
- # chat_history inkluderar nu den aktuella användarfrågan som sista element.
451
- # Vi vill skicka historiken *före* den aktuella frågan till format_chat_history_for_claude.
452
- if chat_history and len(chat_history) > 1: # Minst en tidigare tur (user + assistant) + aktuell user
453
- # chat_history[:-1] kommer att exkludera den aktuella användarens fråga, vilket är korrekt här.
454
  formatted_history = format_chat_history_for_claude(chat_history[:-1])
455
  messages.extend(formatted_history)
456
 
457
  # Skapa användarmeddelandet med kontext och aktuell fråga
458
  user_message_content = f"Relevant kontext för frågan:\n{context}\n\nMin fråga är: {query}"
459
- if not context.strip(): # Om ingen kontext, förenkla prompten
460
  user_message_content = f"Min fråga är: {query}"
461
 
462
  messages.append({"role": "user", "content": user_message_content})
@@ -465,15 +613,21 @@ def generate_answer(query, chat_history=None):
465
  # Använd Claude Sonnet 4 med RAG-baserad kontext och chatthistorik
466
  response = anthropic_client.messages.create(
467
  model=MODEL_NAME,
468
- max_tokens=1024, # Ökat något för att tillåta längre svar om det behövs
469
  temperature=0.3,
470
  system=system_prompt,
471
  messages=messages
472
  )
473
  answer = response.content[0].text
 
 
 
 
 
 
474
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
475
  except Exception as e:
476
- print(f"Fel vid API-anrop: {str(e)}")
477
  return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
478
 
479
  # --- Slack Integration ---
@@ -529,14 +683,14 @@ def vote(data: gr.LikeData):
529
  data.value innehåller information om meddelandet.
530
  """
531
  feedback_type = "up" if data.liked else "down"
532
- global last_log # Använd den globala variabeln
533
  log_entry = {
534
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
535
  "feedback": feedback_type,
536
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
537
  }
538
  # Om global logdata finns, lägg till ytterligare metadata.
539
- if last_log: # Kontrollera att last_log inte är None
540
  log_entry.update({
541
  "session_id": last_log.get("session_id"),
542
  "user_message": last_log.get("user_message"),
@@ -547,7 +701,7 @@ def vote(data: gr.LikeData):
547
 
548
  # Skicka feedback till Slack
549
  try:
550
- if feedback_type == "down" and last_log: # Skicka bara negativ feedback och om last_log finns
551
  feedback_message = f"""
552
  *⚠️ Negativ feedback registrerad*
553
 
@@ -644,10 +798,10 @@ def generate_monthly_stats(days=30):
644
  if log_date >= cutoff_date:
645
  filtered_logs.append(log)
646
  except:
647
- pass # Hoppa över poster med ogiltigt datum
648
 
649
- logs = filtered_logs # Använd filtrerade loggar
650
- if not logs: # Kontrollera igen efter filtrering
651
  return {"error": f"Inga loggar hittades för de senaste {days} dagarna"}
652
 
653
  # Basstatistik
@@ -708,10 +862,10 @@ def simple_status_report():
708
 
709
  try:
710
  # Generera statistik
711
- stats = generate_monthly_stats(days=7) # Senaste veckan
712
 
713
  # Skapa innehåll för Slack
714
- now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Renamed to avoid conflict
715
  subject = f"ChargeNode AI Bot - Status {now_str}"
716
 
717
  if 'error' in stats:
@@ -739,7 +893,7 @@ def simple_status_report():
739
  """
740
 
741
  # Lägg till de senaste konversationerna
742
- all_logs = read_logs() # Renamed to avoid conflict with global logs
743
  conversations = get_latest_conversations(all_logs, 3)
744
 
745
  if conversations:
@@ -762,12 +916,12 @@ def simple_status_report():
762
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
763
  return send_to_slack(error_subject, error_content, "#ff0000")
764
 
765
- def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history_list): # Renamed to avoid confusion
766
  """Skickar en supportförfrågan till Slack."""
767
  try:
768
  # Formatera chat-historiken
769
  chat_content = ""
770
- for msg in chat_history_list: # Use the renamed parameter
771
  if msg['role'] == 'user':
772
  chat_content += f">*Användare:* {msg['content']}\n\n"
773
  elif msg['role'] == 'assistant':
@@ -810,21 +964,12 @@ def run_scheduler():
810
 
811
  while True:
812
  schedule.run_pending()
813
- time.sleep(60) # Kontrollera varje minut
814
 
815
  # Starta schemaläggaren i en separat tråd
816
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
817
  scheduler_thread.start()
818
 
819
- # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
820
- try:
821
- print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
822
- # Anropa inte direkt här - sker i schemaläggaren
823
- # Om du vill testa direkt vid uppstart kan du anropa simple_status_report() här
824
- # simple_status_report()
825
- except Exception as e:
826
- print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
827
-
828
  # --- Gradio UI ---
829
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
830
 
@@ -856,7 +1001,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
856
  # Chat interface
857
  with gr.Group(visible=True) as chat_interface:
858
  chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
859
- chatbot.like(vote, None, None) # vote-funktionen anropas med data från chatbot-komponenten
860
 
861
  with gr.Row():
862
  msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
@@ -889,29 +1034,29 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
889
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
890
 
891
  # KORRIGERAD respond-funktion
892
- def respond(message, chat_history_list, request: gr.Request): # Renamed chat_history to chat_history_list for clarity
893
  global last_log
894
- start_time = time.time() # Renamed to avoid conflict
895
 
896
  # Lägg till användarens nuvarande meddelande i historiken FÖRE anrop till generate_answer
897
  chat_history_list.append({"role": "user", "content": message})
898
 
899
  # Skicka den uppdaterade chatthistoriken till generate_answer
900
- response_text = generate_answer(message, chat_history_list)
901
  elapsed = round(time.time() - start_time, 2)
902
 
903
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
904
  session_id = str(uuid.uuid4())
905
 
906
  # Använd session_id från tidigare logg om det finns
907
- if last_log and 'session_id' in last_log: # Check last_log is not None
908
  session_id = last_log.get('session_id')
909
 
910
  user_id = request.client.host if request else "okänd"
911
 
912
  ua_str = request.headers.get("user-agent", "")
913
  ref = request.headers.get("referer", "")
914
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0].strip() # Added strip()
915
  ua = parse_ua(ua_str)
916
  browser = f"{ua.browser.family} {ua.browser.version_string}"
917
  osys = f"{ua.os.family} {ua.os.version_string}"
@@ -919,9 +1064,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
919
  platform = "webb"
920
  if "chargenode.eu" in ref:
921
  platform = "chargenode.eu"
922
- elif "localhost" in ref or "127.0.0.1" in ref : # Added check for 127.0.0.1
923
  platform = "test"
924
- elif "app" in ref: # This might need a more robust check
925
  platform = "app"
926
 
927
  log_data = {
@@ -939,7 +1084,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
939
  }
940
 
941
  safe_append_to_log(log_data)
942
- last_log = log_data # Uppdatera last_log med aktuell konversationsdata
943
 
944
  # Skicka varje konversation direkt till Slack
945
  try:
@@ -963,21 +1108,21 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
963
  chat_history_list.append({"role": "assistant", "content": response_text})
964
  return "", chat_history_list
965
 
966
- def format_chat_preview(chat_history_list): # Renamed
967
  if not chat_history_list:
968
  return "Ingen chatthistorik att visa."
969
 
970
  preview = ""
971
- for msg_item in chat_history_list: # Renamed msg to msg_item
972
  sender = "Användare" if msg_item["role"] == "user" else "Bot"
973
  content = msg_item["content"]
974
- if len(content) > 100: # Truncate long messages
975
  content = content[:100] + "..."
976
  preview += f"**{sender}:** {content}\n\n"
977
 
978
  return preview
979
 
980
- def show_support_form(chat_history_list): # Renamed
981
  preview = format_chat_preview(chat_history_list)
982
  return {
983
  chat_interface: gr.Group(visible=False),
@@ -993,7 +1138,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
993
  success_interface: gr.Group(visible=False)
994
  }
995
 
996
- def submit_support_form(omr_kod, uttags_nr, email_addr, chat_history_list): # Renamed parameters
997
  """Hanterar formulärinskickningen med bättre felhantering."""
998
  print(f"Support-förfrågan: områdeskod={omr_kod}, uttagsnummer={uttags_nr}, email={email_addr}")
999
 
@@ -1014,7 +1159,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1014
  if not email_addr:
1015
  print("Validerar email: (saknas)")
1016
  validation_errors.append("En giltig e-postadress krävs.")
1017
- elif '@' not in email_addr or '.' not in email_addr.split('@')[-1]: # Improved email validation slightly
1018
  print(f"Validerar email: '{email_addr}' (felaktigt format)")
1019
  validation_errors.append("En giltig e-postadress krävs.")
1020
  else:
@@ -1022,13 +1167,12 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1022
 
1023
  if validation_errors:
1024
  print(f"Valideringsfel: {validation_errors}")
1025
- # Uppdatera chat_preview med felmeddelanden istället för att returnera en sträng direkt.
1026
  error_message_md = "**Fel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
1027
  return {
1028
  chat_interface: gr.update(visible=False),
1029
  support_interface: gr.update(visible=True),
1030
  success_interface: gr.update(visible=False),
1031
- chat_preview: gr.update(value=error_message_md) # Uppdatera chat_preview komponenten
1032
  }
1033
 
1034
  try:
@@ -1069,7 +1213,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1069
  }
1070
 
1071
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
1072
- clear.click(lambda: initial_chat, None, chatbot, queue=False) # Använd initial_chat för att återställa
1073
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
1074
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1075
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
@@ -1081,9 +1225,8 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1081
 
1082
  # Initialisera embeddings vid uppstart
1083
  print("Förbereder embedding-modell och index...")
1084
- initialize_embeddings() # Detta anropas nu för att ladda allt
1085
  print("Embedding-modell och index redo!")
1086
 
1087
  if __name__ == "__main__":
1088
- app.launch(share=IS_HUGGINGFACE) # share=True om du vill ha en publik länk, False för lokal körning
1089
- # IS_HUGGINGFACE kan användas för att styra detta.
 
74
  index = None
75
  chunks = []
76
  chunk_sources = []
77
+ chunk_priorities = [] # Ny: Prioritet för varje chunk
78
+ faq_dict = {} # Dictionary för direktmatchning av vanliga frågor
79
+
80
+ # Nya globala variabler för källprioriterng
81
+ source_priorities = {
82
+ "FAQ": 1.0, # Högsta prioritet
83
+ "ChargeNode App": 0.8,
84
+ "ChargeNode Portal": 0.8,
85
+ "Företagskonto": 0.8,
86
+ "local_files": 0.6 # Lägsta prioritet för övriga filer
87
+ }
88
+
89
+ # Förbättrade nyckelord med organisationskontext
90
+ system_keywords = {
91
+ "app": ["app", "mobil", "telefon", "ladda bil", "ladda fordon", "qr-kod", "skanna", "kartvy", "favoriter", "laddningar", "nedre meny", "mitt privata"],
92
+ "portal": [
93
+ "portal", "dashboard", "medlemmar", "statistik", "priser", "avtal", "felanmälan", "hjälpcenter",
94
+ "samfällighet", "samfällighetsförening", "bostadsrättsförening", "förening", "organisation",
95
+ "vi är", "vi har avtal", "våra uppgifter", "vårt avtal", "vår förening", "adminpanel",
96
+ "administrera", "hantera medlemmar", "logga in portal", "portal.chargenode"
97
+ ],
98
+ "företagskonto": [
99
+ "företag", "företagskonto", "administratör", "fakturor", "behörighet", "användare",
100
+ "org.nummer", "organisationsnummer", "företagsavtal", "mitt företag", "vårt företag"
101
+ ],
102
+ "betalning_privat": [
103
+ "mitt betalsätt", "min betalmetod", "mitt kort", "min betalning", "privat betalkort",
104
+ "personlig betalning", "mitt privata kort"
105
+ ]
106
+ }
107
+
108
+ # Nya organisationsspecifika regler
109
+ organization_types = [
110
+ "samfällighet", "samfällighetsförening", "bostadsrättsförening", "förening",
111
+ "organisation", "kommun", "kommunal", "offentlig", "vi är", "vi har avtal"
112
+ ]
113
 
114
  # --- Förbättrad loggfunktion ---
115
  def safe_append_to_log(log_entry):
 
143
  print(f"Kritiskt fel vid loggning: {retry_error}")
144
  return False
145
 
146
+ def identify_source_context(query):
147
+ """Förbättrad identifiering med organisationsmedvetenhet."""
148
+ query_lower = query.lower()
149
+
150
+ # FÖRST: Kontrollera om det är en organisationsfråga
151
+ is_organization = any(org_type in query_lower for org_type in organization_types)
152
+ has_collective_pronouns = any(word in query_lower for word in ["vi", "våra", "vårt", "vår"])
153
+ mentions_agreement = any(word in query_lower for word in ["avtal", "avtalet", "överenskommelse"])
154
+
155
+ if is_organization or (has_collective_pronouns and mentions_agreement):
156
+ print(f"🏢 Identifierad som organisationsfråga: {query}")
157
+
158
+ # Om det handlar om fakturor/betalningar för företag -> Företagskonto
159
+ if any(word in query_lower for word in ["faktura", "betalning", "kostnad", "avgift", "företagskonto"]):
160
+ return "Företagskonto"
161
+ else:
162
+ # Annars Portal för allmän administration
163
+ return "ChargeNode Portal"
164
+
165
+ # ANDRA: Kontrollera för specifika betalningsfrågor (endast privata)
166
+ if any(keyword in query_lower for keyword in system_keywords["betalning_privat"]):
167
+ return "FAQ"
168
+
169
+ # TREDJE: Kontrollera för andra system
170
+ for system, keywords in system_keywords.items():
171
+ if system == "betalning_privat": # Redan hanterat ovan
172
+ continue
173
+
174
+ if any(keyword in query_lower for keyword in keywords):
175
+ if system == "app":
176
+ return "ChargeNode App"
177
+ elif system == "portal":
178
+ return "ChargeNode Portal"
179
+ elif system == "företagskonto":
180
+ return "Företagskonto"
181
+
182
+ # Standard fallback - men inte FAQ för organisationer
183
+ if is_organization or has_collective_pronouns:
184
+ return "ChargeNode Portal"
185
+ else:
186
+ return "FAQ"
187
+
188
+ def load_local_files_with_source_separation():
189
+ """Laddar filer med tydlig källseparation."""
190
+ sources_data = {}
191
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
192
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
193
+
194
  for file in os.listdir("."):
195
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
196
  try:
197
+ # Bestäm källtyp baserat på filnamn
198
+ source_key = "local_files" # Default
199
+
200
+ if "FAQ" in file or "faq" in file.lower():
201
+ source_key = "FAQ"
202
+ elif "App" in file or "app" in file.lower():
203
+ source_key = "ChargeNode App"
204
+ elif "Portal" in file or "portal" in file.lower():
205
+ source_key = "ChargeNode Portal"
206
+ elif "Foretagskonto" in file or "företag" in file.lower():
207
+ source_key = "Företagskonto"
208
+
209
+ # Läs filinnehåll
210
  if file.endswith(".txt"):
211
  with open(file, "r", encoding="utf-8") as f:
212
  content = f.read()
213
  elif file.endswith(".docx"):
214
+ from docx import Document
215
  content = "\n".join([p.text for p in Document(file).paragraphs])
216
  elif file.endswith(".pdf"):
217
+ import PyPDF2
218
  with open(file, "rb") as f:
219
  reader = PyPDF2.PdfReader(f)
220
  content = "\n".join([p.extract_text() or "" for p in reader.pages])
221
  elif file.endswith(".csv"):
222
  content = pd.read_csv(file).to_string()
223
  elif file.endswith((".xls", ".xlsx")):
224
+ if "FAQ" in file or "faq" in file.lower():
225
  df = pd.read_excel(file)
226
  rows = []
227
  for index, row in df.iterrows():
 
228
  row_text = f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}"
 
 
229
  if 'kategori' in df.columns:
230
  row_text += f"\nKategori: {row['kategori']}"
231
+ elif 'Kategori' in df.columns:
232
  row_text += f"\nKategori: {row['Kategori']}"
 
233
  rows.append(row_text)
234
  content = "\n\n".join(rows)
235
  else:
236
  content = pd.read_excel(file).to_string()
237
+
238
+ # Lägg till i sources_data med källidentifiering
239
+ if source_key not in sources_data:
240
+ sources_data[source_key] = ""
241
+ sources_data[source_key] += f"\n\nFIL: {file}\n{content}"
242
+
243
  except Exception as e:
244
  print(f"Fel vid läsning av {file}: {str(e)}")
245
+
246
+ return sources_data
247
 
248
  def load_prompt():
249
  """Läser in system-prompts från prompt.txt med bättre felhantering."""
 
261
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
262
  return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
263
 
264
+ def enhanced_prepare_chunks(text_data):
265
+ """Förbättrad chunking med källmedvetenhet och prioritering."""
266
+ chunks_list = []
267
+ sources_list = []
268
+ chunk_priorities_list = [] # Ny: Prioritet för varje chunk
269
  global faq_dict
270
 
271
  for source, text in text_data.items():
272
+ source_priority = source_priorities.get(source, 0.5)
273
  paragraphs = [p for p in text.split("\n") if p.strip()]
274
 
 
275
  i = 0
 
 
276
  while i < len(paragraphs):
 
277
  current_chunk = ""
278
  start_idx = i
279
 
280
+ # Särskild behandling för FAQ
281
+ if source == "FAQ" and i < len(paragraphs) and paragraphs[i].startswith("Fråga:"):
282
+ question = paragraphs[i][7:].strip()
283
  current_chunk = paragraphs[i]
284
  i += 1
285
 
286
+ # Lägg till svar och annan info
287
  while i < len(paragraphs) and not paragraphs[i].startswith("Fråga:"):
 
288
  if len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
289
  current_chunk += "\n" + paragraphs[i]
290
  else:
 
291
  if "Svar:" in current_chunk:
 
 
 
 
 
 
292
  break
293
+ else:
294
+ current_chunk += "\n" + paragraphs[i]
295
  i += 1
296
 
297
+ # Lagra FAQ i dictionary med förbättrade variationer
298
  if "Svar:" in current_chunk:
299
  answer_start = current_chunk.find("Svar:")
300
  answer_text = current_chunk[answer_start + 5:].strip()
301
 
302
+ # Grundläggande FAQ-lagring
303
+ faq_dict[question.lower()] = answer_text
304
+
305
+ # Skapa variationer för betalningsfrågor
306
+ if any(term in question.lower() for term in ["betalsätt", "betalmetod", "betalmedel", "kort", "betalkort", "betalning", "betala"]):
307
  payment_variations = [
308
  "hur ändrar jag betalmedel",
309
+ "hur byter jag betalsätt",
310
  "hur uppdaterar jag mitt betalkort",
311
  "hur ändrar jag betalmetod",
312
  "hur byter jag betalningsmetod",
313
+ "hur ändrar jag betalkort",
314
+ "hur lägger jag till nytt kort",
315
+ "hur adderar jag betalkort",
316
+ "hur registrerar jag betalkort"
317
  ]
318
  for variation in payment_variations:
319
  faq_dict[variation] = answer_text
320
+
321
+ # Ge FAQ chunks högsta prioritet
322
+ chunk_priorities_list.append(1.0)
323
+
324
  else:
325
+ # Hantera icke-FAQ innehåll
326
  while i < len(paragraphs) and len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
327
  if current_chunk:
328
  current_chunk += " " + paragraphs[i]
329
  else:
330
  current_chunk = paragraphs[i]
331
  i += 1
332
+
333
+ chunk_priorities_list.append(source_priority)
334
 
335
+ # Spara chunk om den har innehåll
336
  if current_chunk.strip():
337
+ chunks_list.append(current_chunk.strip())
338
+ sources_list.append(source)
339
 
340
+ # Säkerställ framsteg
341
  if i == start_idx:
342
  i += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
+ print(f"Genererade {len(chunks_list)} chunks från {len(text_data)} källor")
345
+ print(f"FAQ Dictionary innehåller {len(faq_dict)} nycklar")
346
+ print(f"Källfördelning: {[(source, len([s for s in sources_list if s == source])) for source in set(sources_list)]}")
347
+
348
+ return chunks_list, sources_list, chunk_priorities_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ def add_organization_faqs():
351
+ """Lägger till specifika FAQ-svar för organisationer."""
352
+ global faq_dict
353
+
354
+ # Samfällighets- och föreningsfrågor
355
+ samfallighet_svar = """För samfällighetsföreningar och andra organisationer med avtal:
356
+
357
+ 1. Gå till portal.chargenode.eu
358
+ 2. Logga in med de uppgifter ni fick när avtalet tecknades
359
+ 3. I portalen kan ni:
360
+ - Se Dashboard med översikt
361
+ - Hantera medlemmar under 'Medlemmar'
362
+ - Se statistik under 'Statistik'
363
+ - Ändra priser under 'Priser'
364
+ - Hantera avtal under 'Avtal'
365
+ - Se fakturering under 'Fakturering'
366
+
367
+ Om ni inte har inloggningsuppgifter, kontakta support@chargenode.eu eller 010-2051055."""
368
+
369
+ organization_keys = [
370
+ "samfällighetsförening logga in",
371
+ "förening logga in portal",
372
+ "vi har avtal hur loggar vi in",
373
+ "ändra våra uppgifter",
374
+ "hantera vår organisation",
375
+ "organisationsuppgifter",
376
+ "samfällighet portal",
377
+ "bostadsrättsförening portal",
378
+ "vi är en förening"
379
+ ]
380
+
381
+ for key in organization_keys:
382
+ faq_dict[key.lower()] = samfallighet_svar
383
 
384
+ def enhanced_check_direct_match(query):
385
+ """Förbättrad direktmatchning som undviker fel för organisationer."""
386
  query_lower = query.lower().strip('?').strip()
387
 
388
+ # VIKTIG FIX: Kontrollera först om det är organisationsfråga
389
+ is_organization = any(org_type in query_lower for org_type in organization_types)
390
+ has_collective_pronouns = any(word in query_lower for word in ["vi", "våra", "vårt", "vår"])
391
+
392
+ if is_organization or has_collective_pronouns:
393
+ print(f"🏢 Organisationsfråga detekterad - hoppar över FAQ direktmatchning")
394
+ return None # Låt RAG hantera detta istället
395
+
396
+ # Endast för PRIVATA betalningsfrågor
397
  payment_prefixes = [
398
+ "hur ändrar jag", "hur byter jag", "hur uppdaterar jag",
399
+ "hur lägger jag till", "hur adderar jag", "hur registrerar jag",
400
+ "hur tar jag bort", "kan jag ändra", "går det att byta"
 
 
 
401
  ]
402
 
403
+ # Lägg till extra kontroll för privata termer
404
+ private_payment_terms = ["mitt betalsätt", "min betalmetod", "mitt betalkort", "mitt kort", "min betalning"]
405
+ general_payment_terms = ["betalsätt", "betalmetod", "betalmedel", "betalkort", "kort", "betalning", "kortuppgifter"]
406
+
407
+ # Bara matcha om det BÅDE har prefix OCH är tydligt privat ELLER allmän betalning utan organisationskontext
408
+ is_private_payment = any(term in query_lower for term in private_payment_terms)
409
+ is_general_payment = any(term in query_lower for term in general_payment_terms)
410
+
411
  if any(query_lower.startswith(prefix) for prefix in payment_prefixes) and \
412
+ (is_private_payment or (is_general_payment and not has_collective_pronouns)):
413
+
414
  payment_answer = """Så här lägger du till/ändrar betalkort:
415
  1. Öppna ChargeNode-appen
416
  2. Tryck på 'Meny' (hamburgerikon) i nedre menyn
 
423
  8. Bekräfta med BankID
424
 
425
  OBS! Se till att kortet har pengar och att det är upplåst för internetbetalningar."""
426
+ return payment_answer
427
 
428
+ # Kontrollera exakt matchning i FAQ dictionary (endast för icke-organisationer)
429
  if query_lower in faq_dict:
430
  return faq_dict[query_lower]
431
 
432
+ # Förbättrad fuzzy matching (endast för icke-organisationer)
433
  for key, value in faq_dict.items():
434
+ query_words = set(re.findall(r'\w+', query_lower))
435
+ key_words = set(re.findall(r'\w+', key))
436
+
437
+ common_words = query_words.intersection(key_words)
438
+ required_overlap = 0.6 if len(query_words) <= 4 else 0.4
439
+ overlap_ratio = len(common_words) / len(query_words) if query_words else 0
440
+
441
+ if overlap_ratio >= required_overlap and len(common_words) >= 2:
442
+ return value
443
 
444
  return None
445
 
446
+ def enhanced_retrieve_context(query, k=RETRIEVAL_K):
447
+ """Förbättrad kontexthämtning med källprioriterng och smart viktning."""
448
+ initialize_enhanced_embeddings()
 
449
 
450
+ # Steg 1: Kontrollera direktmatchning (högsta prioritet)
451
+ direct_match = enhanced_check_direct_match(query)
452
  if direct_match:
453
+ print(f"Direkt FAQ-matchning hittad för: {query}")
454
+ return f"Fråga: {query}\nSvar: {direct_match}", ["FAQ_direct_match"]
455
 
456
+ if embedder is None or index is None or index.ntotal == 0:
457
+ print("Varning: Embedder eller FAISS-index inte tillgängligt")
 
458
  return "", []
459
+
460
+ # Steg 2: Identifiera förväntad källkontext
461
+ preferred_source = identify_source_context(query)
462
+ print(f"📍 Identifierad förväntad källa: {preferred_source}")
463
+
464
+ # Steg 3: Utför embedding-sökning
465
  query_embedding = embedder.encode([query], convert_to_numpy=True)
 
466
  query_embedding_norm = np.linalg.norm(query_embedding)
467
+ if query_embedding_norm == 0:
468
+ query_embedding_norm = 1e-10
469
  query_embedding = query_embedding / query_embedding_norm
470
 
471
+ # Hämta fler kandidater för bättre filtrering
472
+ search_k = min(k * 3, len(chunks))
473
+ D, I = index.search(query_embedding, search_k)
474
+
475
+ # Steg 4: Viktad ranking baserat på både relevans och källprioritet
476
+ candidates = []
477
+ for idx, score in zip(I[0], D[0]):
478
+ if 0 <= idx < len(chunks):
479
+ source = chunk_sources[idx]
480
+ chunk_priority = chunk_priorities[idx] if idx < len(chunk_priorities) else 0.5
481
+
482
+ # Bonuspoäng om det matchar förväntad källa
483
+ source_bonus = 0.3 if source == preferred_source else 0.0
484
+
485
+ # Kombinera semantic similarity med source priority
486
+ combined_score = score + chunk_priority + source_bonus
487
+
488
+ candidates.append({
489
+ 'chunk': chunks[idx],
490
+ 'source': source,
491
+ 'score': combined_score,
492
+ 'semantic_score': score,
493
+ 'priority': chunk_priority
494
+ })
495
+
496
+ # Steg 5: Sortera och välj topp k resultat
497
+ candidates.sort(key=lambda x: x['score'], reverse=True)
498
+ top_candidates = candidates[:k]
499
+
500
+ # Steg 6: Logga vad som valdes för debugging
501
+ print(f"🔍 Valda källor: {[c['source'] for c in top_candidates]}")
502
+ print(f"📊 Scores: {[(c['source'], round(c['score'], 3)) for c in top_candidates]}")
503
+
504
+ # Steg 7: Bygg kontextsvar
505
+ retrieved_chunks = [c['chunk'] for c in top_candidates]
506
+ unique_sources = list(set(c['source'] for c in top_candidates))
507
+
508
+ context = " ".join(retrieved_chunks)
509
+ return context, unique_sources
510
 
511
+ def initialize_enhanced_embeddings():
512
+ """Initierar embeddings med förbättrad källhantering."""
513
+ global embedder, embeddings, index, chunks, chunk_sources, chunk_priorities, faq_dict
514
+
515
+ if embedder is None:
516
+ print("🚀 Initierar förbättrad RAG med källprioriterng...")
517
+
518
+ # Ladda data med källseparation
519
+ print("📁 Laddar källseparerad textdata...")
520
+ text_data = load_local_files_with_source_separation()
521
+ print(f"📚 Laddade {len(text_data)} källor: {list(text_data.keys())}")
522
+
523
+ # Förbättrad chunking
524
+ print("✂️ Skapar chunks med källmedvetenhet...")
525
+ chunks, chunk_sources, chunk_priorities = enhanced_prepare_chunks(text_data)
526
+ print(f"📦 {len(chunks)} chunks skapade")
527
+
528
+ # NYTT: Lägg till organisationsspecifika FAQ
529
+ add_organization_faqs()
530
+
531
+ if not chunks:
532
+ print("⚠️ Inga chunks genererades")
533
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
534
+ embeddings = np.array([]).reshape(0, embedder.get_sentence_embedding_dimension())
535
+ index = faiss.IndexFlatIP(embedder.get_sentence_embedding_dimension())
536
+ chunk_priorities = []
537
+ return
538
+
539
+ # Skapa embeddings
540
+ print("🧮 Skapar embeddings...")
541
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
542
+ embeddings = embedder.encode(chunks, convert_to_numpy=True)
543
+
544
+ if embeddings.ndim == 2 and embeddings.shape[0] > 0:
545
+ embeddings_norm = np.linalg.norm(embeddings, axis=1, keepdims=True)
546
+ embeddings_norm[embeddings_norm == 0] = 1e-10
547
+ embeddings = embeddings / embeddings_norm
548
+
549
+ index = faiss.IndexFlatIP(embeddings.shape[1])
550
+ index.add(embeddings)
551
+ print("✅ FAISS-index klart")
552
+ else:
553
+ print("⚠️ Tomma embeddings, skapar tomt index")
554
+ dimension = embedder.get_sentence_embedding_dimension()
555
+ index = faiss.IndexFlatIP(dimension)
556
+ chunk_priorities = []
557
+
558
+ # Sammanfattning
559
+ source_summary = {}
560
+ for source in chunk_sources:
561
+ source_summary[source] = source_summary.get(source, 0) + 1
562
+
563
+ print(f"📈 Källsammanfattning: {source_summary}")
564
+ print(f"❓ FAQ entries: {len(faq_dict)}")
565
+ print(f"🏢 Organisationsfrågor inkluderade")
566
 
567
  # Ladda prompt template
568
  prompt_template = load_prompt()
 
583
 
584
  return messages
585
 
586
+ def enhanced_generate_answer(query, chat_history=None):
587
+ """Genererar svar med förbättrad källprioriterng."""
588
+ # Använd förbättrad kontexthämtning
589
+ context, sources = enhanced_retrieve_context(query)
590
+
591
+ if not context.strip():
592
+ print("ℹ️ Ingen specifik kontext hittad, använder allmän kunskap")
593
+
 
 
 
594
  # System-prompts
595
  system_prompt = prompt_template
596
 
597
  # Förbered meddelanden för Claude API
598
+ messages = []
599
 
600
  # Lägg till chatthistorik om den finns och är meningsfull
601
+ if chat_history and len(chat_history) > 1:
 
 
 
602
  formatted_history = format_chat_history_for_claude(chat_history[:-1])
603
  messages.extend(formatted_history)
604
 
605
  # Skapa användarmeddelandet med kontext och aktuell fråga
606
  user_message_content = f"Relevant kontext för frågan:\n{context}\n\nMin fråga är: {query}"
607
+ if not context.strip():
608
  user_message_content = f"Min fråga är: {query}"
609
 
610
  messages.append({"role": "user", "content": user_message_content})
 
613
  # Använd Claude Sonnet 4 med RAG-baserad kontext och chatthistorik
614
  response = anthropic_client.messages.create(
615
  model=MODEL_NAME,
616
+ max_tokens=1024,
617
  temperature=0.3,
618
  system=system_prompt,
619
  messages=messages
620
  )
621
  answer = response.content[0].text
622
+
623
+ # Lägg till källinformation i svaret för transparens
624
+ if sources and any(source != "FAQ_direct_match" for source in sources):
625
+ source_info = f"\n\n📚 Källor: {', '.join(set(sources))}"
626
+ answer += source_info
627
+
628
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
629
  except Exception as e:
630
+ print(f"Fel vid API-anrop: {str(e)}")
631
  return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
632
 
633
  # --- Slack Integration ---
 
683
  data.value innehåller information om meddelandet.
684
  """
685
  feedback_type = "up" if data.liked else "down"
686
+ global last_log
687
  log_entry = {
688
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
689
  "feedback": feedback_type,
690
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
691
  }
692
  # Om global logdata finns, lägg till ytterligare metadata.
693
+ if last_log:
694
  log_entry.update({
695
  "session_id": last_log.get("session_id"),
696
  "user_message": last_log.get("user_message"),
 
701
 
702
  # Skicka feedback till Slack
703
  try:
704
+ if feedback_type == "down" and last_log:
705
  feedback_message = f"""
706
  *⚠️ Negativ feedback registrerad*
707
 
 
798
  if log_date >= cutoff_date:
799
  filtered_logs.append(log)
800
  except:
801
+ pass
802
 
803
+ logs = filtered_logs
804
+ if not logs:
805
  return {"error": f"Inga loggar hittades för de senaste {days} dagarna"}
806
 
807
  # Basstatistik
 
862
 
863
  try:
864
  # Generera statistik
865
+ stats = generate_monthly_stats(days=7)
866
 
867
  # Skapa innehåll för Slack
868
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
869
  subject = f"ChargeNode AI Bot - Status {now_str}"
870
 
871
  if 'error' in stats:
 
893
  """
894
 
895
  # Lägg till de senaste konversationerna
896
+ all_logs = read_logs()
897
  conversations = get_latest_conversations(all_logs, 3)
898
 
899
  if conversations:
 
916
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
917
  return send_to_slack(error_subject, error_content, "#ff0000")
918
 
919
+ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history_list):
920
  """Skickar en supportförfrågan till Slack."""
921
  try:
922
  # Formatera chat-historiken
923
  chat_content = ""
924
+ for msg in chat_history_list:
925
  if msg['role'] == 'user':
926
  chat_content += f">*Användare:* {msg['content']}\n\n"
927
  elif msg['role'] == 'assistant':
 
964
 
965
  while True:
966
  schedule.run_pending()
967
+ time.sleep(60)
968
 
969
  # Starta schemaläggaren i en separat tråd
970
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
971
  scheduler_thread.start()
972
 
 
 
 
 
 
 
 
 
 
973
  # --- Gradio UI ---
974
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
975
 
 
1001
  # Chat interface
1002
  with gr.Group(visible=True) as chat_interface:
1003
  chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
1004
+ chatbot.like(vote, None, None)
1005
 
1006
  with gr.Row():
1007
  msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
 
1034
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
1035
 
1036
  # KORRIGERAD respond-funktion
1037
+ def respond(message, chat_history_list, request: gr.Request):
1038
  global last_log
1039
+ start_time = time.time()
1040
 
1041
  # Lägg till användarens nuvarande meddelande i historiken FÖRE anrop till generate_answer
1042
  chat_history_list.append({"role": "user", "content": message})
1043
 
1044
  # Skicka den uppdaterade chatthistoriken till generate_answer
1045
+ response_text = enhanced_generate_answer(message, chat_history_list)
1046
  elapsed = round(time.time() - start_time, 2)
1047
 
1048
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1049
  session_id = str(uuid.uuid4())
1050
 
1051
  # Använd session_id från tidigare logg om det finns
1052
+ if last_log and 'session_id' in last_log:
1053
  session_id = last_log.get('session_id')
1054
 
1055
  user_id = request.client.host if request else "okänd"
1056
 
1057
  ua_str = request.headers.get("user-agent", "")
1058
  ref = request.headers.get("referer", "")
1059
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0].strip()
1060
  ua = parse_ua(ua_str)
1061
  browser = f"{ua.browser.family} {ua.browser.version_string}"
1062
  osys = f"{ua.os.family} {ua.os.version_string}"
 
1064
  platform = "webb"
1065
  if "chargenode.eu" in ref:
1066
  platform = "chargenode.eu"
1067
+ elif "localhost" in ref or "127.0.0.1" in ref:
1068
  platform = "test"
1069
+ elif "app" in ref:
1070
  platform = "app"
1071
 
1072
  log_data = {
 
1084
  }
1085
 
1086
  safe_append_to_log(log_data)
1087
+ last_log = log_data
1088
 
1089
  # Skicka varje konversation direkt till Slack
1090
  try:
 
1108
  chat_history_list.append({"role": "assistant", "content": response_text})
1109
  return "", chat_history_list
1110
 
1111
+ def format_chat_preview(chat_history_list):
1112
  if not chat_history_list:
1113
  return "Ingen chatthistorik att visa."
1114
 
1115
  preview = ""
1116
+ for msg_item in chat_history_list:
1117
  sender = "Användare" if msg_item["role"] == "user" else "Bot"
1118
  content = msg_item["content"]
1119
+ if len(content) > 100:
1120
  content = content[:100] + "..."
1121
  preview += f"**{sender}:** {content}\n\n"
1122
 
1123
  return preview
1124
 
1125
+ def show_support_form(chat_history_list):
1126
  preview = format_chat_preview(chat_history_list)
1127
  return {
1128
  chat_interface: gr.Group(visible=False),
 
1138
  success_interface: gr.Group(visible=False)
1139
  }
1140
 
1141
+ def submit_support_form(omr_kod, uttags_nr, email_addr, chat_history_list):
1142
  """Hanterar formulärinskickningen med bättre felhantering."""
1143
  print(f"Support-förfrågan: områdeskod={omr_kod}, uttagsnummer={uttags_nr}, email={email_addr}")
1144
 
 
1159
  if not email_addr:
1160
  print("Validerar email: (saknas)")
1161
  validation_errors.append("En giltig e-postadress krävs.")
1162
+ elif '@' not in email_addr or '.' not in email_addr.split('@')[-1]:
1163
  print(f"Validerar email: '{email_addr}' (felaktigt format)")
1164
  validation_errors.append("En giltig e-postadress krävs.")
1165
  else:
 
1167
 
1168
  if validation_errors:
1169
  print(f"Valideringsfel: {validation_errors}")
 
1170
  error_message_md = "**Fel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
1171
  return {
1172
  chat_interface: gr.update(visible=False),
1173
  support_interface: gr.update(visible=True),
1174
  success_interface: gr.update(visible=False),
1175
+ chat_preview: gr.update(value=error_message_md)
1176
  }
1177
 
1178
  try:
 
1213
  }
1214
 
1215
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
1216
+ clear.click(lambda: initial_chat, None, chatbot, queue=False)
1217
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
1218
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1219
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
 
1225
 
1226
  # Initialisera embeddings vid uppstart
1227
  print("Förbereder embedding-modell och index...")
1228
+ initialize_enhanced_embeddings()
1229
  print("Embedding-modell och index redo!")
1230
 
1231
  if __name__ == "__main__":
1232
+ app.launch(share=IS_HUGGINGFACE)