k96beni commited on
Commit
d2ed5a8
·
verified ·
1 Parent(s): c2d420c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -137
app.py CHANGED
@@ -24,7 +24,7 @@ CHUNK_OVERLAP = 200 # Nytt: Overlapping chunks för att inte tappa kontext
24
  RETRIEVAL_K = 5 # Antal chunker att hämta vid varje sökning
25
 
26
  # Uppdaterad modell till Sonnet 4
27
- MODEL_NAME = "claude-sonnet-4-20250514"
28
 
29
  # Kontrollera om vi kör i Hugging Face-miljön
30
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
@@ -172,7 +172,7 @@ def load_prompt():
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, sources = [], []
176
  global faq_dict
177
 
178
  for source, text in text_data.items():
@@ -181,6 +181,8 @@ def prepare_chunks(text_data):
181
 
182
  # Process FAQ-specific content better
183
  i = 0
 
 
184
  while i < len(paragraphs):
185
  # Start a new chunk
186
  current_chunk = ""
@@ -216,7 +218,7 @@ def prepare_chunks(text_data):
216
 
217
  # Add variations with common synonyms for payment-related questions
218
  if any(term in question.lower() for term in ["betalsätt", "betalmetod", "betalmedel", "kort",
219
- "betalkort", "betalning", "betala"]):
220
  payment_variations = [
221
  "hur ändrar jag betalmedel",
222
  "hur byter jag betalsätt",
@@ -241,41 +243,41 @@ def prepare_chunks(text_data):
241
 
242
  # Save the chunk if it has content
243
  if current_chunk.strip():
244
- chunks.append(current_chunk.strip())
245
- sources.append(source)
246
 
247
  # If we've added a chunk but haven't advanced, we need to move forward
248
  if i == start_idx:
249
  i += 1
250
 
251
- # Create overlapping chunks for better context preservation
252
- overlap_chunks = []
253
- overlap_sources = []
254
 
255
- for j in range(0, len(chunks)):
256
- overlap_chunks.append(chunks[j])
257
- overlap_sources.append(sources[j])
258
 
259
- # Create an overlapping chunk with the next chunk if it exists
260
- if j < len(chunks) - 1 and chunks[j].endswith(chunks[j+1][:CHUNK_OVERLAP]):
261
- # Skip if there's already significant overlap
262
- continue
263
-
264
- if j < len(chunks) - 1:
265
  # Calculate available space in the current chunk
266
- space_left = MAX_CHUNK_SIZE - len(chunks[j])
267
 
268
  # If there's enough space, add part of the next chunk
269
  if space_left >= CHUNK_OVERLAP:
270
- overlap_text = chunks[j] + " " + chunks[j+1][:CHUNK_OVERLAP]
271
- overlap_chunks.append(overlap_text)
272
- overlap_sources.append(sources[j])
 
 
 
 
 
 
 
273
 
274
- chunks = overlap_chunks
275
- sources = overlap_sources
276
-
277
- print(f"Genererade {len(chunks)} chunks med {len(faq_dict)} FAQ-par")
278
- return chunks, sources
279
 
280
  def initialize_embeddings():
281
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
@@ -287,17 +289,41 @@ def initialize_embeddings():
287
  print("Laddar textdata...")
288
  text_data = {"local_files": load_local_files()}
289
  print("Förbereder textsegment...")
290
- chunks, chunk_sources = prepare_chunks(text_data)
291
  print(f"{len(chunks)} segment laddade")
292
 
 
 
 
 
 
 
 
 
 
 
 
293
  print("Skapar embeddings...")
294
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
295
  embeddings = embedder.encode(chunks, convert_to_numpy=True)
296
- embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
297
- index = faiss.IndexFlatIP(embeddings.shape[1])
298
- index.add(embeddings)
299
- print("FAISS-index klart")
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  # Print FAQ dictionary keys for debugging
302
  print(f"FAQ Dictionary innehåller {len(faq_dict)} nycklar")
303
  if len(faq_dict) > 0:
@@ -334,8 +360,8 @@ OBS! Se till att kortet har pengar och att det är upplåst för internetbetalni
334
  if ("ändra" in query_lower or "byta" in query_lower or "uppdatera" in query_lower) and \
335
  ("ändra" in key or "byta" in key or "uppdatera" in key):
336
  # Check if key and query share important terms
337
- query_terms = set(query_lower.split())
338
- key_terms = set(key.split())
339
  if len(query_terms.intersection(key_terms)) >= 2: # At least 2 words in common
340
  return value
341
 
@@ -353,15 +379,24 @@ def retrieve_context(query, k=RETRIEVAL_K):
353
  return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
354
 
355
  # Om ingen direktmatchning, använd vanlig embedding-sökning
 
 
 
 
356
  query_embedding = embedder.encode([query], convert_to_numpy=True)
357
- query_embedding /= np.linalg.norm(query_embedding)
 
 
 
 
358
  D, I = index.search(query_embedding, k)
359
- retrieved, sources = [], set()
360
  for idx in I[0]:
361
- if idx < len(chunks):
362
  retrieved.append(chunks[idx])
363
- sources.add(chunk_sources[idx])
364
- return " ".join(retrieved), list(sources)
 
365
 
366
  # Ladda prompt template
367
  prompt_template = load_prompt()
@@ -369,6 +404,7 @@ prompt_template = load_prompt()
369
  def format_chat_history_for_claude(chat_history):
370
  """Formaterar chatthistoriken för Claude API med endast de senaste meddelandena för att undvika token-gränser."""
371
  # Ta endast de senaste 10 meddelandena för att hålla kontexten hanterbar
 
372
  recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
373
 
374
  messages = []
@@ -384,39 +420,40 @@ def format_chat_history_for_claude(chat_history):
384
  def generate_answer(query, chat_history=None):
385
  """Genererar svar baserat på fråga, chatthistorik och retrieval-baserad kontext med Claude Sonnet 4."""
386
  # Hämta relevant kontext via RAG istället för hela databasen
387
- context, sources = retrieve_context(query)
388
-
389
- if not context.strip():
390
- return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
391
 
 
 
 
 
 
 
392
  # System-prompts
393
  system_prompt = prompt_template
394
 
395
  # Förbered meddelanden för Claude API
396
- messages = []
397
 
398
- # Lägg till chatthistorik om den finns
399
- if chat_history and len(chat_history) > 1:
400
- # Formatera tidigare meddelanden (exkludera det senaste som är den aktuella frågan)
 
 
401
  formatted_history = format_chat_history_for_claude(chat_history[:-1])
402
  messages.extend(formatted_history)
403
 
404
  # Skapa användarmeddelandet med kontext och aktuell fråga
405
- user_message = f"""Jag har en fråga om ChargeNode.
406
-
407
- Relevant kontext för frågan:
408
- {context}
409
-
410
- Min fråga är: {query}"""
411
-
412
- # Lägg till det aktuella användarmeddelandet
413
- messages.append({"role": "user", "content": user_message})
414
 
415
  try:
416
  # Använd Claude Sonnet 4 med RAG-baserad kontext och chatthistorik
417
  response = anthropic_client.messages.create(
418
  model=MODEL_NAME,
419
- max_tokens=500,
420
  temperature=0.3,
421
  system=system_prompt,
422
  messages=messages
@@ -476,18 +513,18 @@ def send_to_slack(subject, content, color="#2a9d8f"):
476
  def vote(data: gr.LikeData):
477
  """
478
  Hanterar feedback från Gradio's inbyggda like-funktion.
479
- data.liked är True om uppvote, annars False.
480
  data.value innehåller information om meddelandet.
481
  """
482
  feedback_type = "up" if data.liked else "down"
483
- global last_log
484
  log_entry = {
485
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
486
  "feedback": feedback_type,
487
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
488
  }
489
  # Om global logdata finns, lägg till ytterligare metadata.
490
- if last_log:
491
  log_entry.update({
492
  "session_id": last_log.get("session_id"),
493
  "user_message": last_log.get("user_message"),
@@ -498,7 +535,7 @@ def vote(data: gr.LikeData):
498
 
499
  # Skicka feedback till Slack
500
  try:
501
- if feedback_type == "down": # Skicka bara negativ feedback
502
  feedback_message = f"""
503
  *⚠️ Negativ feedback registrerad*
504
 
@@ -597,8 +634,10 @@ def generate_monthly_stats(days=30):
597
  except:
598
  pass # Hoppa över poster med ogiltigt datum
599
 
600
- logs = filtered_logs
601
-
 
 
602
  # Basstatistik
603
  total_conversations = sum(1 for log in logs if 'user_message' in log)
604
  unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
@@ -611,7 +650,7 @@ def generate_monthly_stats(days=30):
611
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
612
 
613
  # Svarstidsstatistik
614
- response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
615
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
616
 
617
  # Plattformsstatistik
@@ -660,8 +699,8 @@ def simple_status_report():
660
  stats = generate_monthly_stats(days=7) # Senaste veckan
661
 
662
  # Skapa innehåll för Slack
663
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
664
- subject = f"ChargeNode AI Bot - Status {now}"
665
 
666
  if 'error' in stats:
667
  content = f"*Fel vid generering av statistik:* {stats['error']}"
@@ -673,7 +712,7 @@ def simple_status_report():
673
  perf = stats["performance"]
674
 
675
  content = f"""
676
- *ChargeNode AI Bot - Statusrapport {now}*
677
 
678
  *Basstatistik* (senaste 7 dagarna)
679
  - Totalt antal konversationer: {basic['total_conversations']}
@@ -688,8 +727,8 @@ def simple_status_report():
688
  """
689
 
690
  # Lägg till de senaste konversationerna
691
- logs = read_logs()
692
- conversations = get_latest_conversations(logs, 3)
693
 
694
  if conversations:
695
  content += "\n*Senaste konversationer*\n"
@@ -711,12 +750,12 @@ def simple_status_report():
711
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
712
  return send_to_slack(error_subject, error_content, "#ff0000")
713
 
714
- def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
715
  """Skickar en supportförfrågan till Slack."""
716
  try:
717
  # Formatera chat-historiken
718
  chat_content = ""
719
- for msg in chat_history:
720
  if msg['role'] == 'user':
721
  chat_content += f">*Användare:* {msg['content']}\n\n"
722
  elif msg['role'] == 'assistant':
@@ -769,6 +808,8 @@ scheduler_thread.start()
769
  try:
770
  print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
771
  # Anropa inte direkt här - sker i schemaläggaren
 
 
772
  except Exception as e:
773
  print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
774
 
@@ -803,7 +844,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
803
  # Chat interface
804
  with gr.Group(visible=True) as chat_interface:
805
  chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
806
- chatbot.like(vote, None, None)
807
 
808
  with gr.Row():
809
  msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
@@ -835,26 +876,30 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
835
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
836
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
837
 
838
- def respond(message, chat_history, request: gr.Request):
 
839
  global last_log
840
- start = time.time()
841
 
842
- # Skicka hela chatthistoriken till generate_answer
843
- response = generate_answer(message, chat_history)
844
- elapsed = round(time.time() - start, 2)
 
 
 
845
 
846
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
847
  session_id = str(uuid.uuid4())
848
 
849
  # Använd session_id från tidigare logg om det finns
850
- if last_log and 'session_id' in last_log:
851
  session_id = last_log.get('session_id')
852
 
853
  user_id = request.client.host if request else "okänd"
854
 
855
  ua_str = request.headers.get("user-agent", "")
856
  ref = request.headers.get("referer", "")
857
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
858
  ua = parse_ua(ua_str)
859
  browser = f"{ua.browser.family} {ua.browser.version_string}"
860
  osys = f"{ua.os.family} {ua.os.version_string}"
@@ -862,9 +907,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
862
  platform = "webb"
863
  if "chargenode.eu" in ref:
864
  platform = "chargenode.eu"
865
- elif "localhost" in ref:
866
  platform = "test"
867
- elif "app" in ref:
868
  platform = "app"
869
 
870
  log_data = {
@@ -872,32 +917,29 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
872
  "user_id": user_id,
873
  "session_id": session_id,
874
  "user_message": message,
875
- "bot_reply": response,
876
  "response_time": elapsed,
877
  "ip": ip,
878
  "browser": browser,
879
  "os": osys,
880
  "platform": platform,
881
- "chat_history_length": len(chat_history) # Lägg till information om chatthistorikens längd
882
  }
883
 
884
- # Använd den förbättrade loggfunktionen
885
  safe_append_to_log(log_data)
886
- last_log = log_data
887
 
888
  # Skicka varje konversation direkt till Slack
889
  try:
890
- # Konversationsinnehåll
891
  conversation_content = f"""
892
  *Ny konversation {timestamp}*
893
 
894
  *Användare:* {message}
895
 
896
- *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
897
 
898
- *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform} | Chat längd: {len(chat_history)} meddelanden
899
  """
900
- # Skicka asynkront för att inte blockera svarstiden
901
  threading.Thread(
902
  target=lambda: send_to_slack(f"Ny konversation", conversation_content),
903
  daemon=True
@@ -905,26 +947,26 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
905
  except Exception as e:
906
  print(f"Kunde inte skicka konversation till Slack: {e}")
907
 
908
- chat_history.append({"role": "user", "content": message})
909
- chat_history.append({"role": "assistant", "content": response})
910
- return "", chat_history
911
 
912
- def format_chat_preview(chat_history):
913
- if not chat_history:
914
  return "Ingen chatthistorik att visa."
915
 
916
  preview = ""
917
- for msg in chat_history:
918
- sender = "Användare" if msg["role"] == "user" else "Bot"
919
- content = msg["content"]
920
  if len(content) > 100: # Truncate long messages
921
  content = content[:100] + "..."
922
  preview += f"**{sender}:** {content}\n\n"
923
 
924
  return preview
925
 
926
- def show_support_form(chat_history):
927
- preview = format_chat_preview(chat_history)
928
  return {
929
  chat_interface: gr.Group(visible=False),
930
  support_interface: gr.Group(visible=True),
@@ -939,84 +981,83 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
939
  success_interface: gr.Group(visible=False)
940
  }
941
 
942
- def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
943
  """Hanterar formulärinskickningen med bättre felhantering."""
944
- print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
945
 
946
- # Validera input med tydligare loggning
947
  validation_errors = []
948
 
949
- if områdeskod and not områdeskod.isdigit():
950
- print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
951
  validation_errors.append("Områdeskod måste vara numerisk.")
952
  else:
953
- print(f"Validerar områdeskod: '{områdeskod}' (ok)")
954
 
955
- if uttagsnummer and not uttagsnummer.isdigit():
956
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
957
  validation_errors.append("Uttagsnummer måste vara numerisk.")
958
  else:
959
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
960
 
961
- if not email:
962
  print("Validerar email: (saknas)")
963
  validation_errors.append("En giltig e-postadress krävs.")
964
- elif '@' not in email or '.' not in email.split('@')[1]:
965
- print(f"Validerar email: '{email}' (felaktigt format)")
966
  validation_errors.append("En giltig e-postadress krävs.")
967
  else:
968
- print(f"Validerar email: '{email}' (ok)")
969
 
970
- # Om det finns valideringsfel
971
  if validation_errors:
972
  print(f"Valideringsfel: {validation_errors}")
 
 
973
  return {
974
- chat_interface: gr.Group(visible=False),
975
- support_interface: gr.Group(visible=True),
976
- success_interface: gr.Group(visible=False),
977
- chat_preview: "\n".join(["**Fel:**"] + validation_errors)
978
  }
979
 
980
- # Om formuläret klarade valideringen, försök skicka till Slack
981
  try:
982
  print("Försöker skicka supportförfrågan till Slack...")
983
 
984
- # Skapa en förenklad chathistorik för loggning
985
  chat_summary = []
986
- for msg in chat_history:
987
- if 'role' in msg and 'content' in msg:
988
- chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
989
  print(f"Chatthistorik att skicka: {chat_summary}")
990
 
991
- # Skicka till Slack
992
- success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
993
 
994
  if success:
995
  print("Support-förfrågan skickad till Slack framgångsrikt")
996
  return {
997
- chat_interface: gr.Group(visible=False),
998
- support_interface: gr.Group(visible=False),
999
- success_interface: gr.Group(visible=True)
1000
  }
1001
  else:
1002
  print("Support-förfrågan till Slack misslyckades")
 
1003
  return {
1004
- chat_interface: gr.Group(visible=False),
1005
- support_interface: gr.Group(visible=True),
1006
- success_interface: gr.Group(visible=False),
1007
- chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
1008
  }
1009
  except Exception as e:
1010
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
 
1011
  return {
1012
- chat_interface: gr.Group(visible=False),
1013
- support_interface: gr.Group(visible=True),
1014
- success_interface: gr.Group(visible=False),
1015
- chat_preview: f"**Ett fel uppstod: {str(e)}**"
1016
  }
1017
 
1018
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
1019
- clear.click(lambda: initial_chat, None, chatbot, queue=False)
1020
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
1021
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1022
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
@@ -1028,8 +1069,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1028
 
1029
  # Initialisera embeddings vid uppstart
1030
  print("Förbereder embedding-modell och index...")
1031
- initialize_embeddings()
1032
  print("Embedding-modell och index redo!")
1033
 
1034
  if __name__ == "__main__":
1035
- app.launch(share=True)
 
 
24
  RETRIEVAL_K = 5 # Antal chunker att hämta vid varje sökning
25
 
26
  # Uppdaterad modell till Sonnet 4
27
+ MODEL_NAME = "claude-3-sonnet-20240229" # Behåller din specificerade Sonnet, Claude-3.5-Sonnet är "claude-3-5-sonnet-20240620"
28
 
29
  # Kontrollera om vi kör i Hugging Face-miljön
30
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
 
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():
 
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 = ""
 
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",
 
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."""
 
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:
 
360
  if ("ändra" in query_lower or "byta" in query_lower or "uppdatera" in query_lower) and \
361
  ("ändra" in key or "byta" in key or "uppdatera" in key):
362
  # Check if key and query share important terms
363
+ query_terms = set(re.findall(r'\w+', query_lower)) # Use regex to get words
364
+ key_terms = set(re.findall(r'\w+', key))
365
  if len(query_terms.intersection(key_terms)) >= 2: # At least 2 words in common
366
  return value
367
 
 
379
  return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
380
 
381
  # Om ingen direktmatchning, använd vanlig embedding-sökning
382
+ if embedder is None or index is None or index.ntotal == 0: # Check if index is usable
383
+ print("Varning: Embedder eller FAISS-index är inte korrekt initierat eller är tomt. Returnerar tom kontext.")
384
+ return "", []
385
+
386
  query_embedding = embedder.encode([query], convert_to_numpy=True)
387
+ # Normalisera query_embedding på samma sätt som indexets embeddings
388
+ query_embedding_norm = np.linalg.norm(query_embedding)
389
+ if query_embedding_norm == 0: query_embedding_norm = 1e-10 # Undvik division med noll
390
+ query_embedding = query_embedding / query_embedding_norm
391
+
392
  D, I = index.search(query_embedding, k)
393
+ retrieved, sources_set = [], set() # Renamed to avoid conflict
394
  for idx in I[0]:
395
+ if 0 <= idx < len(chunks): # Ensure index is valid
396
  retrieved.append(chunks[idx])
397
+ sources_set.add(chunk_sources[idx]) # Use chunk_sources which is global
398
+ return " ".join(retrieved), list(sources_set)
399
+
400
 
401
  # Ladda prompt template
402
  prompt_template = load_prompt()
 
404
  def format_chat_history_for_claude(chat_history):
405
  """Formaterar chatthistoriken för Claude API med endast de senaste meddelandena för att undvika token-gränser."""
406
  # Ta endast de senaste 10 meddelandena för att hålla kontexten hanterbar
407
+ # En "tur" är en user + assistant. Så 10 meddelanden = 5 turns.
408
  recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
409
 
410
  messages = []
 
420
  def generate_answer(query, chat_history=None):
421
  """Genererar svar baserat på fråga, chatthistorik och retrieval-baserad kontext med Claude Sonnet 4."""
422
  # Hämta relevant kontext via RAG istället för hela databasen
423
+ context, sources = retrieve_context(query) # sources är en lista med källor
 
 
 
424
 
425
+ if not context.strip(): # Om context är tom, efterfråga mer info eller ge standard svar
426
+ # Detta kan hända om RAG inte hittar något relevant.
427
+ # Vi kan fortfarande försöka svara med Claude, men utan RAG-kontext.
428
+ print("Ingen RAG-kontext hittades. Försöker svara utan.")
429
+ # 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."
430
+
431
  # System-prompts
432
  system_prompt = prompt_template
433
 
434
  # Förbered meddelanden för Claude API
435
+ messages = [] # Starta med en tom lista för varje nytt anrop
436
 
437
+ # Lägg till chatthistorik om den finns och är meningsfull
438
+ # chat_history inkluderar nu den aktuella användarfrågan som sista element.
439
+ # Vi vill skicka historiken *före* den aktuella frågan till format_chat_history_for_claude.
440
+ if chat_history and len(chat_history) > 1: # Minst en tidigare tur (user + assistant) + aktuell user
441
+ # chat_history[:-1] kommer att exkludera den aktuella användarens fråga, vilket är korrekt här.
442
  formatted_history = format_chat_history_for_claude(chat_history[:-1])
443
  messages.extend(formatted_history)
444
 
445
  # Skapa användarmeddelandet med kontext och aktuell fråga
446
+ user_message_content = f"Relevant kontext för frågan:\n{context}\n\nMin fråga är: {query}"
447
+ if not context.strip(): # Om ingen kontext, förenkla prompten
448
+ user_message_content = f"Min fråga är: {query}"
449
+
450
+ messages.append({"role": "user", "content": user_message_content})
 
 
 
 
451
 
452
  try:
453
  # Använd Claude Sonnet 4 med RAG-baserad kontext och chatthistorik
454
  response = anthropic_client.messages.create(
455
  model=MODEL_NAME,
456
+ max_tokens=1024, # Ökat något för att tillåta längre svar om det behövs
457
  temperature=0.3,
458
  system=system_prompt,
459
  messages=messages
 
513
  def vote(data: gr.LikeData):
514
  """
515
  Hanterar feedback från Gradio's inbyggda like-funktion.
516
+ data.liked är True om upvote, annars False.
517
  data.value innehåller information om meddelandet.
518
  """
519
  feedback_type = "up" if data.liked else "down"
520
+ global last_log # Använd den globala variabeln
521
  log_entry = {
522
  "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
523
  "feedback": feedback_type,
524
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
525
  }
526
  # Om global logdata finns, lägg till ytterligare metadata.
527
+ if last_log: # Kontrollera att last_log inte är None
528
  log_entry.update({
529
  "session_id": last_log.get("session_id"),
530
  "user_message": last_log.get("user_message"),
 
535
 
536
  # Skicka feedback till Slack
537
  try:
538
+ if feedback_type == "down" and last_log: # Skicka bara negativ feedback och om last_log finns
539
  feedback_message = f"""
540
  *⚠️ Negativ feedback registrerad*
541
 
 
634
  except:
635
  pass # Hoppa över poster med ogiltigt datum
636
 
637
+ logs = filtered_logs # Använd filtrerade loggar
638
+ if not logs: # Kontrollera igen efter filtrering
639
+ return {"error": f"Inga loggar hittades för de senaste {days} dagarna"}
640
+
641
  # Basstatistik
642
  total_conversations = sum(1 for log in logs if 'user_message' in log)
643
  unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
 
650
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
651
 
652
  # Svarstidsstatistik
653
+ response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log and isinstance(log.get('response_time'), (int, float))]
654
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
655
 
656
  # Plattformsstatistik
 
699
  stats = generate_monthly_stats(days=7) # Senaste veckan
700
 
701
  # Skapa innehåll för Slack
702
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Renamed to avoid conflict
703
+ subject = f"ChargeNode AI Bot - Status {now_str}"
704
 
705
  if 'error' in stats:
706
  content = f"*Fel vid generering av statistik:* {stats['error']}"
 
712
  perf = stats["performance"]
713
 
714
  content = f"""
715
+ *ChargeNode AI Bot - Statusrapport {now_str}*
716
 
717
  *Basstatistik* (senaste 7 dagarna)
718
  - Totalt antal konversationer: {basic['total_conversations']}
 
727
  """
728
 
729
  # Lägg till de senaste konversationerna
730
+ all_logs = read_logs() # Renamed to avoid conflict with global logs
731
+ conversations = get_latest_conversations(all_logs, 3)
732
 
733
  if conversations:
734
  content += "\n*Senaste konversationer*\n"
 
750
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
751
  return send_to_slack(error_subject, error_content, "#ff0000")
752
 
753
+ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history_list): # Renamed to avoid confusion
754
  """Skickar en supportförfrågan till Slack."""
755
  try:
756
  # Formatera chat-historiken
757
  chat_content = ""
758
+ for msg in chat_history_list: # Use the renamed parameter
759
  if msg['role'] == 'user':
760
  chat_content += f">*Användare:* {msg['content']}\n\n"
761
  elif msg['role'] == 'assistant':
 
808
  try:
809
  print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
810
  # Anropa inte direkt här - sker i schemaläggaren
811
+ # Om du vill testa direkt vid uppstart kan du anropa simple_status_report() här
812
+ # simple_status_report()
813
  except Exception as e:
814
  print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
815
 
 
844
  # Chat interface
845
  with gr.Group(visible=True) as chat_interface:
846
  chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
847
+ chatbot.like(vote, None, None) # vote-funktionen anropas med data från chatbot-komponenten
848
 
849
  with gr.Row():
850
  msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
 
876
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
877
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
878
 
879
+ # KORRIGERAD respond-funktion
880
+ def respond(message, chat_history_list, request: gr.Request): # Renamed chat_history to chat_history_list for clarity
881
  global last_log
882
+ start_time = time.time() # Renamed to avoid conflict
883
 
884
+ # Lägg till användarens nuvarande meddelande i historiken FÖRE anrop till generate_answer
885
+ chat_history_list.append({"role": "user", "content": message})
886
+
887
+ # Skicka den uppdaterade chatthistoriken till generate_answer
888
+ response_text = generate_answer(message, chat_history_list)
889
+ elapsed = round(time.time() - start_time, 2)
890
 
891
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
892
  session_id = str(uuid.uuid4())
893
 
894
  # Använd session_id från tidigare logg om det finns
895
+ if last_log and 'session_id' in last_log: # Check last_log is not None
896
  session_id = last_log.get('session_id')
897
 
898
  user_id = request.client.host if request else "okänd"
899
 
900
  ua_str = request.headers.get("user-agent", "")
901
  ref = request.headers.get("referer", "")
902
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0].strip() # Added strip()
903
  ua = parse_ua(ua_str)
904
  browser = f"{ua.browser.family} {ua.browser.version_string}"
905
  osys = f"{ua.os.family} {ua.os.version_string}"
 
907
  platform = "webb"
908
  if "chargenode.eu" in ref:
909
  platform = "chargenode.eu"
910
+ elif "localhost" in ref or "127.0.0.1" in ref : # Added check for 127.0.0.1
911
  platform = "test"
912
+ elif "app" in ref: # This might need a more robust check
913
  platform = "app"
914
 
915
  log_data = {
 
917
  "user_id": user_id,
918
  "session_id": session_id,
919
  "user_message": message,
920
+ "bot_reply": response_text,
921
  "response_time": elapsed,
922
  "ip": ip,
923
  "browser": browser,
924
  "os": osys,
925
  "platform": platform,
926
+ "chat_history_length": len(chat_history_list)
927
  }
928
 
 
929
  safe_append_to_log(log_data)
930
+ last_log = log_data # Uppdatera last_log med aktuell konversationsdata
931
 
932
  # Skicka varje konversation direkt till Slack
933
  try:
 
934
  conversation_content = f"""
935
  *Ny konversation {timestamp}*
936
 
937
  *Användare:* {message}
938
 
939
+ *Bot:* {response_text[:300]}{'...' if len(response_text) > 300 else ''}
940
 
941
+ *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform} | Chat längd: {len(chat_history_list)} meddelanden
942
  """
 
943
  threading.Thread(
944
  target=lambda: send_to_slack(f"Ny konversation", conversation_content),
945
  daemon=True
 
947
  except Exception as e:
948
  print(f"Kunde inte skicka konversation till Slack: {e}")
949
 
950
+ # Användarens meddelande är redan tillagt, lägg bara till assistentens svar.
951
+ chat_history_list.append({"role": "assistant", "content": response_text})
952
+ return "", chat_history_list
953
 
954
+ def format_chat_preview(chat_history_list): # Renamed
955
+ if not chat_history_list:
956
  return "Ingen chatthistorik att visa."
957
 
958
  preview = ""
959
+ for msg_item in chat_history_list: # Renamed msg to msg_item
960
+ sender = "Användare" if msg_item["role"] == "user" else "Bot"
961
+ content = msg_item["content"]
962
  if len(content) > 100: # Truncate long messages
963
  content = content[:100] + "..."
964
  preview += f"**{sender}:** {content}\n\n"
965
 
966
  return preview
967
 
968
+ def show_support_form(chat_history_list): # Renamed
969
+ preview = format_chat_preview(chat_history_list)
970
  return {
971
  chat_interface: gr.Group(visible=False),
972
  support_interface: gr.Group(visible=True),
 
981
  success_interface: gr.Group(visible=False)
982
  }
983
 
984
+ def submit_support_form(omr_kod, uttags_nr, email_addr, chat_history_list): # Renamed parameters
985
  """Hanterar formulärinskickningen med bättre felhantering."""
986
+ print(f"Support-förfrågan: områdeskod={omr_kod}, uttagsnummer={uttags_nr}, email={email_addr}")
987
 
 
988
  validation_errors = []
989
 
990
+ if omr_kod and not omr_kod.isdigit():
991
+ print(f"Validerar områdeskod: '{omr_kod}' (felaktig)")
992
  validation_errors.append("Områdeskod måste vara numerisk.")
993
  else:
994
+ print(f"Validerar områdeskod: '{omr_kod}' (ok)")
995
 
996
+ if uttags_nr and not uttags_nr.isdigit():
997
+ print(f"Validerar uttagsnummer: '{uttags_nr}' (felaktig)")
998
  validation_errors.append("Uttagsnummer måste vara numerisk.")
999
  else:
1000
+ print(f"Validerar uttagsnummer: '{uttags_nr}' (ok)")
1001
 
1002
+ if not email_addr:
1003
  print("Validerar email: (saknas)")
1004
  validation_errors.append("En giltig e-postadress krävs.")
1005
+ elif '@' not in email_addr or '.' not in email_addr.split('@')[-1]: # Improved email validation slightly
1006
+ print(f"Validerar email: '{email_addr}' (felaktigt format)")
1007
  validation_errors.append("En giltig e-postadress krävs.")
1008
  else:
1009
+ print(f"Validerar email: '{email_addr}' (ok)")
1010
 
 
1011
  if validation_errors:
1012
  print(f"Valideringsfel: {validation_errors}")
1013
+ # Uppdatera chat_preview med felmeddelanden istället för att returnera en sträng direkt.
1014
+ error_message_md = "**Fel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
1015
  return {
1016
+ chat_interface: gr.update(visible=False),
1017
+ support_interface: gr.update(visible=True),
1018
+ success_interface: gr.update(visible=False),
1019
+ chat_preview: gr.update(value=error_message_md) # Uppdatera chat_preview komponenten
1020
  }
1021
 
 
1022
  try:
1023
  print("Försöker skicka supportförfrågan till Slack...")
1024
 
 
1025
  chat_summary = []
1026
+ for msg_item in chat_history_list:
1027
+ if 'role' in msg_item and 'content' in msg_item:
1028
+ chat_summary.append(f"{msg_item['role']}: {msg_item['content'][:30]}...")
1029
  print(f"Chatthistorik att skicka: {chat_summary}")
1030
 
1031
+ success = send_support_to_slack(omr_kod, uttags_nr, email_addr, chat_history_list)
 
1032
 
1033
  if success:
1034
  print("Support-förfrågan skickad till Slack framgångsrikt")
1035
  return {
1036
+ chat_interface: gr.update(visible=False),
1037
+ support_interface: gr.update(visible=False),
1038
+ success_interface: gr.update(visible=True)
1039
  }
1040
  else:
1041
  print("Support-förfrågan till Slack misslyckades")
1042
+ error_message_md = "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
1043
  return {
1044
+ chat_interface: gr.update(visible=False),
1045
+ support_interface: gr.update(visible=True),
1046
+ success_interface: gr.update(visible=False),
1047
+ chat_preview: gr.update(value=error_message_md)
1048
  }
1049
  except Exception as e:
1050
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
1051
+ error_message_md = f"**Ett oväntat fel uppstod: {str(e)}**"
1052
  return {
1053
+ chat_interface: gr.update(visible=False),
1054
+ support_interface: gr.update(visible=True),
1055
+ success_interface: gr.update(visible=False),
1056
+ chat_preview: gr.update(value=error_message_md)
1057
  }
1058
 
1059
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
1060
+ clear.click(lambda: initial_chat, None, chatbot, queue=False) # Använd initial_chat för att återställa
1061
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
1062
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
1063
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
 
1069
 
1070
  # Initialisera embeddings vid uppstart
1071
  print("Förbereder embedding-modell och index...")
1072
+ initialize_embeddings() # Detta anropas nu för att ladda allt
1073
  print("Embedding-modell och index redo!")
1074
 
1075
  if __name__ == "__main__":
1076
+ app.launch(share=IS_HUGGINGFACE) # share=True om du vill ha en publik länk, False för lokal körning
1077
+ # IS_HUGGINGFACE kan användas för att styra detta.