k96beni commited on
Commit
a0dd723
·
verified ·
1 Parent(s): 1515f2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -91
app.py CHANGED
@@ -16,12 +16,14 @@ from sentence_transformers import SentenceTransformer
16
  import numpy as np
17
  import faiss
18
  import re
 
19
 
20
  # --- Konfiguration ---
21
  CHARGENODE_URL = "https://www.chargenode.eu"
22
  MAX_CHUNK_SIZE = 2000
23
  CHUNK_OVERLAP = 200
24
  RETRIEVAL_K = 5
 
25
 
26
  # Kontrollera om vi kör i Hugging Face-miljön
27
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
@@ -73,12 +75,34 @@ chunks = []
73
  chunk_sources = []
74
  faq_dict = {}
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  # --- Förbättrad loggfunktion ---
77
  def safe_append_to_log(log_entry):
78
  """Säker metod för att lägga till loggdata utan att förlora historisk information."""
79
  try:
80
  with open(log_file_path, "a", encoding="utf-8") as log_file:
81
- log_json = json.dumps(log_entry)
82
  log_file.write(log_json + "\n")
83
  log_file.flush()
84
 
@@ -92,7 +116,7 @@ def safe_append_to_log(log_entry):
92
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
93
 
94
  with open(log_file_path, "a", encoding="utf-8") as log_file:
95
- log_json = json.dumps(log_entry)
96
  log_file.write(log_json + "\n")
97
 
98
  print("Loggpost tillagd efter återhämtning")
@@ -106,8 +130,27 @@ def safe_append_to_log(log_entry):
106
  def load_local_files():
107
  """Laddar alla lokala filer och returnerar som en sammanhängande text."""
108
  uploaded_text = ""
 
 
 
 
 
 
 
 
 
109
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
110
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
 
 
 
 
 
 
 
 
 
 
111
  for file in os.listdir("."):
112
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
113
  try:
@@ -125,7 +168,7 @@ def load_local_files():
125
  elif file.endswith(".csv"):
126
  content = pd.read_csv(file).to_string()
127
  elif file.endswith((".xls", ".xlsx")):
128
- if file == "FAQ stadat.xlsx":
129
  df = pd.read_excel(file)
130
  rows = []
131
  for index, row in df.iterrows():
@@ -141,8 +184,10 @@ def load_local_files():
141
  else:
142
  content = pd.read_excel(file).to_string()
143
  uploaded_text += f"\n\nFIL: {file}\n{content}"
 
144
  except Exception as e:
145
- print(f"Fel vid läsning av {file}: {str(e)}")
 
146
  return uploaded_text.strip()
147
 
148
  def load_prompt():
@@ -152,14 +197,33 @@ def load_prompt():
152
  prompt_content = f.read().strip()
153
  if not prompt_content:
154
  print("Varning: prompt.txt är tom, använder standardprompt")
155
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
 
156
  return prompt_content
157
  except FileNotFoundError:
158
  print("Varning: prompt.txt hittades inte, använder standardprompt")
159
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
160
  except Exception as e:
161
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
162
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  # --- Förbättrad chunking ---
165
  def prepare_chunks(text_data):
@@ -197,6 +261,7 @@ def prepare_chunks(text_data):
197
  answer_start = current_chunk.find("Svar:")
198
  answer_text = current_chunk[answer_start + 5:].strip()
199
 
 
200
  if any(term in question.lower() for term in ["betalsätt", "betalmetod", "betalmedel", "kort",
201
  "betalkort", "betalning", "betala"]):
202
  payment_variations = [
@@ -205,7 +270,10 @@ def prepare_chunks(text_data):
205
  "hur uppdaterar jag mitt betalkort",
206
  "hur ändrar jag betalmetod",
207
  "hur byter jag betalningsmetod",
208
- "hur ändrar jag betalkort"
 
 
 
209
  ]
210
  for variation in payment_variations:
211
  faq_dict[variation] = answer_text
@@ -247,7 +315,7 @@ def prepare_chunks(text_data):
247
  chunks = overlap_chunks
248
  sources = overlap_sources
249
 
250
- print(f"Genererade {len(chunks)} chunks med {len(faq_dict)} FAQ-par")
251
  return chunks, sources
252
 
253
  def initialize_embeddings():
@@ -255,32 +323,51 @@ def initialize_embeddings():
255
  global embedder, embeddings, index, chunks, chunk_sources, faq_dict
256
 
257
  if embedder is None:
258
- print("Initierar SentenceTransformer och FAISS-index...")
259
- print("Laddar textdata...")
260
  text_data = {"local_files": load_local_files()}
261
- print("Förbereder textsegment...")
262
  chunks, chunk_sources = prepare_chunks(text_data)
263
- print(f"{len(chunks)} segment laddade")
264
 
265
- print("Skapar embeddings...")
266
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  embeddings = embedder.encode(chunks, convert_to_numpy=True)
268
  embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
269
  index = faiss.IndexFlatIP(embeddings.shape[1])
270
  index.add(embeddings)
271
- print("FAISS-index klart")
272
 
273
- print(f"FAQ Dictionary innehåller {len(faq_dict)} nycklar")
274
  if len(faq_dict) > 0:
275
- payment_keys = [k for k in faq_dict.keys() if any(term in k for term in ["betalsätt", "betalmetod", "betalmedel"])]
276
- print(f"Betalningsrelaterade FAQ-nycklar: {payment_keys[:5]}")
277
 
278
  def check_direct_match(query):
279
- """Kontrollerar om frågan matchar någon av våra fördefinierade FAQ-svar."""
280
- query_lower = query.lower().strip('?').strip()
281
 
282
- if any(query_lower.startswith(prefix) for prefix in ["hur ändrar jag", "hur byter jag", "hur uppdaterar jag"]) and \
283
- any(term in query_lower for term in ["betalsätt", "betalmetod", "betalmedel", "betalkort", "kort"]):
 
 
 
 
 
 
284
  payment_answer = """Så här gör du om du vill byta betalkort:
285
  1. Gå in i appen.
286
  2. Tryck på meny och mina betalsätt
@@ -293,16 +380,32 @@ def check_direct_match(query):
293
  OBS! Se till att kortet har pengar och att det är upplåst för internetbetalningar."""
294
  return payment_answer
295
 
296
- if query_lower in faq_dict:
297
- return faq_dict[query_lower]
 
 
 
 
 
298
 
299
  for key, value in faq_dict.items():
300
- if ("ändra" in query_lower or "byta" in query_lower or "uppdatera" in query_lower) and \
301
- ("ändra" in key or "byta" in key or "uppdatera" in key):
302
- query_terms = set(query_lower.split())
303
- key_terms = set(key.split())
304
- if len(query_terms.intersection(key_terms)) >= 2:
305
- return value
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  return None
308
 
@@ -312,7 +415,7 @@ def retrieve_context(query, k=RETRIEVAL_K):
312
 
313
  direct_match = check_direct_match(query)
314
  if direct_match:
315
- print(f"Direkt matchning hittad för frågan: {query}")
316
  return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
317
 
318
  query_embedding = embedder.encode([query], convert_to_numpy=True)
@@ -323,50 +426,101 @@ def retrieve_context(query, k=RETRIEVAL_K):
323
  if idx < len(chunks):
324
  retrieved.append(chunks[idx])
325
  sources.add(chunk_sources[idx])
326
- return " ".join(retrieved), list(sources)
 
 
 
 
 
 
 
 
327
 
328
  prompt_template = load_prompt()
329
 
330
- def generate_answer(query):
331
- """Genererar svar baserat på fråga och retrieval-baserad kontext med Claude Sonnet 4.5."""
332
  context, sources = retrieve_context(query)
333
 
334
  if not context.strip():
 
 
 
335
  return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI-genererat svar."
336
 
 
 
 
 
337
  system_prompt = prompt_template
 
 
 
 
338
 
339
- user_message = f"""Jag har en fråga om ChargeNode.
 
340
 
341
- Relevant kontext för frågan:
342
  {context}
343
 
344
- Min fråga är: {query}"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
  try:
347
  response = anthropic_client.messages.create(
348
- model="claude-sonnet-4-5-20250929", # ✅ Claude Sonnet 4.5
349
- max_tokens=1500, # Ökat från 500
350
- temperature=0.3,
351
  system=system_prompt,
352
- messages=[
353
- {"role": "user", "content": user_message}
354
- ]
355
  )
356
  answer = response.content[0].text
357
- print("✅ Använder Claude Sonnet 4.5")
 
 
 
 
358
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
359
 
360
  except RateLimitError:
361
  print("⚠️ Rate limit nådd")
 
 
362
  return "För många förfrågningar just nu. Försök igen om några sekunder.\n\nKontakta support@chargenode.eu eller 010-2051055"
363
 
364
  except APIError as e:
365
  print(f"⚠️ API-fel: {e}")
 
 
366
  return "Tekniskt fel uppstod. Vänligen försök igen.\n\nKontakta support@chargenode.eu eller 010-2051055"
367
 
368
  except Exception as e:
369
  print(f"❌ Oväntat fel: {e}")
 
 
370
  return f"Tekniskt fel: {str(e)}\n\nKontakta support@chargenode.eu eller 010-2051055"
371
 
372
  # --- Slack Integration ---
@@ -404,13 +558,13 @@ def send_to_slack(subject, content, color="#2a9d8f"):
404
  )
405
 
406
  if response.status_code == 200:
407
- print(f"Slack-meddelande skickat: {subject}")
408
  return True
409
  else:
410
- print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
411
  return False
412
  except Exception as e:
413
- print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
414
  return False
415
 
416
  # --- Feedback & Like-funktion ---
@@ -445,7 +599,7 @@ def vote(data: gr.LikeData):
445
  daemon=True
446
  ).start()
447
  except Exception as e:
448
- print(f"Kunde inte skicka feedback till Slack: {e}")
449
 
450
  return
451
 
@@ -463,13 +617,13 @@ def read_logs():
463
  log_entry = json.loads(line.strip())
464
  logs.append(log_entry)
465
  except json.JSONDecodeError as e:
466
- print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
467
  continue
468
- print(f"Läste {len(logs)} av {line_count} loggposter")
469
  else:
470
- print(f"Loggfil saknas: {log_file_path}")
471
  except Exception as e:
472
- print(f"Fel vid läsning av loggfil: {e}")
473
  return logs
474
 
475
  def get_latest_conversations(logs, limit=50):
@@ -507,7 +661,7 @@ def get_feedback_stats(logs):
507
 
508
  def generate_monthly_stats(days=30):
509
  """Genererar omfattande statistik över botanvändning för den senaste månaden."""
510
- print(f"Genererar statistik för de senaste {days} dagarna...")
511
 
512
  logs = read_logs()
513
 
@@ -578,7 +732,7 @@ def generate_monthly_stats(days=30):
578
 
579
  def simple_status_report():
580
  """Skickar en förenklad statusrapport till Slack."""
581
- print("Genererar statusrapport för Slack...")
582
 
583
  try:
584
  stats = generate_monthly_stats(days=7)
@@ -624,7 +778,7 @@ def simple_status_report():
624
  return send_to_slack(subject, content, "#2a9d8f")
625
 
626
  except Exception as e:
627
- print(f"Fel vid generering av statusrapport: {e}")
628
 
629
  error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
630
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
@@ -655,7 +809,7 @@ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
655
 
656
  return send_to_slack(subject, content, "#e76f51")
657
  except Exception as e:
658
- print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
659
  return False
660
 
661
  # --- Schemaläggning av rapporter ---
@@ -679,9 +833,9 @@ scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
679
  scheduler_thread.start()
680
 
681
  try:
682
- print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
683
  except Exception as e:
684
- print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
685
 
686
  # --- Gradio UI ---
687
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
@@ -745,7 +899,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
745
  def respond(message, chat_history, request: gr.Request):
746
  global last_log
747
  start = time.time()
748
- response = generate_answer(message)
 
 
749
  elapsed = round(time.time() - start, 2)
750
 
751
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -770,6 +926,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
770
  platform = "test"
771
  elif "app" in ref:
772
  platform = "app"
 
 
 
773
 
774
  log_data = {
775
  "timestamp": timestamp,
@@ -781,7 +940,8 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
781
  "ip": ip,
782
  "browser": browser,
783
  "os": osys,
784
- "platform": platform
 
785
  }
786
 
787
  safe_append_to_log(log_data)
@@ -791,6 +951,8 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
791
  conversation_content = f"""
792
  *Ny konversation {timestamp}*
793
 
 
 
794
  *Användare:* {message}
795
 
796
  *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
@@ -802,7 +964,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
802
  daemon=True
803
  ).start()
804
  except Exception as e:
805
- print(f"Kunde inte skicka konversation till Slack: {e}")
806
 
807
  chat_history.append({"role": "user", "content": message})
808
  chat_history.append({"role": "assistant", "content": response})
@@ -840,33 +1002,24 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
840
 
841
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
842
  """Hanterar formulärinskickningen med bättre felhantering."""
843
- print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
844
 
845
  validation_errors = []
846
 
847
- if områdeskod and not områdeskod.isdigit():
848
- print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
849
- validation_errors.append("Områdeskod måste vara numerisk.")
850
- else:
851
- print(f"Validerar områdeskod: '{områdeskod}' (ok)")
852
 
853
- if uttagsnummer and not uttagsnummer.isdigit():
854
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
855
- validation_errors.append("Uttagsnummer måste vara numerisk.")
856
- else:
857
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
858
 
859
  if not email:
860
- print("Validerar email: (saknas)")
861
  validation_errors.append("En giltig e-postadress krävs.")
862
  elif '@' not in email or '.' not in email.split('@')[1]:
863
- print(f"Validerar email: '{email}' (felaktigt format)")
864
  validation_errors.append("En giltig e-postadress krävs.")
865
- else:
866
- print(f"Validerar email: '{email}' (ok)")
867
 
868
  if validation_errors:
869
- print(f"Valideringsfel: {validation_errors}")
870
  return {
871
  chat_interface: gr.Group(visible=False),
872
  support_interface: gr.Group(visible=True),
@@ -875,25 +1028,19 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
875
  }
876
 
877
  try:
878
- print("Försöker skicka supportförfrågan till Slack...")
879
-
880
- chat_summary = []
881
- for msg in chat_history:
882
- if 'role' in msg and 'content' in msg:
883
- chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
884
- print(f"Chatthistorik att skicka: {chat_summary}")
885
 
886
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
887
 
888
  if success:
889
- print("Support-förfrågan skickad till Slack framgångsrikt")
890
  return {
891
  chat_interface: gr.Group(visible=False),
892
  support_interface: gr.Group(visible=False),
893
  success_interface: gr.Group(visible=True)
894
  }
895
  else:
896
- print("Support-förfrågan till Slack misslyckades")
897
  return {
898
  chat_interface: gr.Group(visible=False),
899
  support_interface: gr.Group(visible=True),
@@ -901,7 +1048,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
901
  chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
902
  }
903
  except Exception as e:
904
- print(f"Oväntat fel vid hantering av support-formulär: {e}")
905
  return {
906
  chat_interface: gr.Group(visible=False),
907
  support_interface: gr.Group(visible=True),
@@ -910,7 +1057,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
910
  }
911
 
912
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
913
- clear.click(lambda: None, None, chatbot, queue=False)
914
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
915
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
916
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
@@ -920,9 +1067,9 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
920
  [chat_interface, support_interface, success_interface, chat_preview]
921
  )
922
 
923
- print("Förbereder embedding-modell och index...")
924
  initialize_embeddings()
925
- print("Embedding-modell och index redo!")
926
 
927
  if __name__ == "__main__":
928
  app.launch(share=True)
 
16
  import numpy as np
17
  import faiss
18
  import re
19
+ from difflib import SequenceMatcher
20
 
21
  # --- Konfiguration ---
22
  CHARGENODE_URL = "https://www.chargenode.eu"
23
  MAX_CHUNK_SIZE = 2000
24
  CHUNK_OVERLAP = 200
25
  RETRIEVAL_K = 5
26
+ MAX_CONTEXT_CHARS = 8000 # ~2000 tokens för kontext
27
 
28
  # Kontrollera om vi kör i Hugging Face-miljön
29
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
 
75
  chunk_sources = []
76
  faq_dict = {}
77
 
78
+ # --- Hjälpfunktioner ---
79
+ def detect_language(text):
80
+ """Enkel språkdetektering baserad på vanliga ord."""
81
+ swedish_indicators = ['hur', 'kan', 'jag', 'är', 'det', 'den', 'ett', 'och', 'som', 'på', 'för', 'med', 'av', 'till', 'om']
82
+ english_indicators = ['how', 'can', 'the', 'is', 'are', 'and', 'or', 'for', 'with', 'what', 'where', 'when']
83
+
84
+ text_lower = text.lower()
85
+ words = text_lower.split()
86
+
87
+ swedish_count = sum(1 for word in words if word in swedish_indicators)
88
+ english_count = sum(1 for word in words if word in english_indicators)
89
+
90
+ if english_count > swedish_count and english_count > 2:
91
+ return 'en'
92
+ return 'sv'
93
+
94
+ def validate_numeric_field(value, field_name):
95
+ """Validerar att ett fält är numeriskt."""
96
+ if value and not value.isdigit():
97
+ return f"{field_name} måste vara numerisk."
98
+ return None
99
+
100
  # --- Förbättrad loggfunktion ---
101
  def safe_append_to_log(log_entry):
102
  """Säker metod för att lägga till loggdata utan att förlora historisk information."""
103
  try:
104
  with open(log_file_path, "a", encoding="utf-8") as log_file:
105
+ log_json = json.dumps(log_entry, ensure_ascii=False)
106
  log_file.write(log_json + "\n")
107
  log_file.flush()
108
 
 
116
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
117
 
118
  with open(log_file_path, "a", encoding="utf-8") as log_file:
119
+ log_json = json.dumps(log_entry, ensure_ascii=False)
120
  log_file.write(log_json + "\n")
121
 
122
  print("Loggpost tillagd efter återhämtning")
 
130
  def load_local_files():
131
  """Laddar alla lokala filer och returnerar som en sammanhängande text."""
132
  uploaded_text = ""
133
+
134
+ # Definiera obligatoriska filer
135
+ required_files = [
136
+ "FAQ_stadat.xlsx",
137
+ "Foretagskonto.txt",
138
+ "ChargeNode_App.txt",
139
+ "ChargeNode_Portal.txt"
140
+ ]
141
+
142
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
143
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
144
+
145
+ # Kontrollera att alla obligatoriska filer finns
146
+ missing_files = []
147
+ for req_file in required_files:
148
+ if not os.path.exists(req_file):
149
+ missing_files.append(req_file)
150
+
151
+ if missing_files:
152
+ print(f"⚠️ VARNING: Följande obligatoriska filer saknas: {', '.join(missing_files)}")
153
+
154
  for file in os.listdir("."):
155
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
156
  try:
 
168
  elif file.endswith(".csv"):
169
  content = pd.read_csv(file).to_string()
170
  elif file.endswith((".xls", ".xlsx")):
171
+ if file == "FAQ_stadat.xlsx" or file == "FAQ stadat.xlsx":
172
  df = pd.read_excel(file)
173
  rows = []
174
  for index, row in df.iterrows():
 
184
  else:
185
  content = pd.read_excel(file).to_string()
186
  uploaded_text += f"\n\nFIL: {file}\n{content}"
187
+ print(f"✅ Laddade fil: {file}")
188
  except Exception as e:
189
+ print(f"Fel vid läsning av {file}: {str(e)}")
190
+
191
  return uploaded_text.strip()
192
 
193
  def load_prompt():
 
197
  prompt_content = f.read().strip()
198
  if not prompt_content:
199
  print("Varning: prompt.txt är tom, använder standardprompt")
200
+ return get_default_prompt()
201
+ print("✅ Laddade prompt.txt")
202
  return prompt_content
203
  except FileNotFoundError:
204
  print("Varning: prompt.txt hittades inte, använder standardprompt")
205
+ return get_default_prompt()
206
  except Exception as e:
207
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
208
+ return get_default_prompt()
209
+
210
+ def get_default_prompt():
211
+ """Returnerar standardprompt om prompt.txt saknas."""
212
+ return """Du är ChargeNode's AI-assistent som hjälper användare med frågor om ChargeNode's produkter och tjänster.
213
+
214
+ SPRÅK: Svara alltid på samma språk som användaren skriver på (svenska eller engelska).
215
+
216
+ SVARSSTIL:
217
+ - Var vänlig, professionell och hjälpsam
218
+ - Ge konkreta, tydliga svar baserat på den tillhandahållna informationen
219
+ - Om informationen inte finns i kontexten, säg det tydligt
220
+ - När du ger navigationsinstruktioner, använd numrerade steg
221
+ - Håll svaren koncisa men kompletta
222
+
223
+ KONTAKTINFORMATION:
224
+ Om användaren behöver ytterligare hjälp, hänvisa till:
225
+ - Email: support@chargenode.eu
226
+ - Telefon: 010-2051055"""
227
 
228
  # --- Förbättrad chunking ---
229
  def prepare_chunks(text_data):
 
261
  answer_start = current_chunk.find("Svar:")
262
  answer_text = current_chunk[answer_start + 5:].strip()
263
 
264
+ # Lägg till betalningsrelaterade variationer
265
  if any(term in question.lower() for term in ["betalsätt", "betalmetod", "betalmedel", "kort",
266
  "betalkort", "betalning", "betala"]):
267
  payment_variations = [
 
270
  "hur uppdaterar jag mitt betalkort",
271
  "hur ändrar jag betalmetod",
272
  "hur byter jag betalningsmetod",
273
+ "hur ändrar jag betalkort",
274
+ "how do i change payment method",
275
+ "how to update credit card",
276
+ "how to change payment card"
277
  ]
278
  for variation in payment_variations:
279
  faq_dict[variation] = answer_text
 
315
  chunks = overlap_chunks
316
  sources = overlap_sources
317
 
318
+ print(f"Genererade {len(chunks)} chunks med {len(faq_dict)} FAQ-par")
319
  return chunks, sources
320
 
321
  def initialize_embeddings():
 
323
  global embedder, embeddings, index, chunks, chunk_sources, faq_dict
324
 
325
  if embedder is None:
326
+ print("🔄 Initierar SentenceTransformer och FAISS-index...")
327
+ print("📁 Laddar textdata...")
328
  text_data = {"local_files": load_local_files()}
329
+ print("✂️ Förbereder textsegment...")
330
  chunks, chunk_sources = prepare_chunks(text_data)
331
+ print(f"{len(chunks)} segment laddade")
332
 
333
+ print("🧠 Skapar embeddings...")
334
+ try:
335
+ # Försök först med svensk modell
336
+ embedder = SentenceTransformer('KBLab/sentence-bert-swedish-cased')
337
+ print("✅ Använder svensk embeddings-modell")
338
+ except:
339
+ try:
340
+ # Fallback till multilingual
341
+ embedder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
342
+ print("✅ Använder multilingual embeddings-modell")
343
+ except:
344
+ # Sista fallback
345
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
346
+ print("✅ Använder engelsk embeddings-modell")
347
+
348
  embeddings = embedder.encode(chunks, convert_to_numpy=True)
349
  embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
350
  index = faiss.IndexFlatIP(embeddings.shape[1])
351
  index.add(embeddings)
352
+ print("FAISS-index klart")
353
 
354
+ print(f"📚 FAQ Dictionary innehåller {len(faq_dict)} nycklar")
355
  if len(faq_dict) > 0:
356
+ payment_keys = [k for k in faq_dict.keys() if any(term in k for term in ["betalsätt", "betalmetod", "betalmedel", "payment"])]
357
+ print(f"💳 Betalningsrelaterade FAQ-nycklar: {len(payment_keys)}")
358
 
359
  def check_direct_match(query):
360
+ """Kontrollerar om frågan matchar någon av våra fördefinierade FAQ-svar med fuzzy matching."""
361
+ query_lower = query.lower().strip('?!.').strip()
362
 
363
+ # Normalisera vanliga variationer
364
+ query_normalized = query_lower
365
+ query_normalized = re.sub(r'\bbyt(a|er)\b', 'ändra', query_normalized)
366
+ query_normalized = re.sub(r'\buppdat(era|erar)\b', 'ändra', query_normalized)
367
+
368
+ # Exakt matchning för betalningsfrågor
369
+ if any(query_normalized.startswith(prefix) for prefix in ["hur ändrar jag", "hur byter jag", "hur uppdaterar jag", "how do i change", "how to change", "how to update"]) and \
370
+ any(term in query_normalized for term in ["betalsätt", "betalmetod", "betalmedel", "betalkort", "kort", "payment", "credit card", "card"]):
371
  payment_answer = """Så här gör du om du vill byta betalkort:
372
  1. Gå in i appen.
373
  2. Tryck på meny och mina betalsätt
 
380
  OBS! Se till att kortet har pengar och att det är upplåst för internetbetalningar."""
381
  return payment_answer
382
 
383
+ # Exakt matchning i FAQ dictionary
384
+ if query_normalized in faq_dict:
385
+ return faq_dict[query_normalized]
386
+
387
+ # Fuzzy matching för liknande frågor
388
+ best_match = None
389
+ best_score = 0.0
390
 
391
  for key, value in faq_dict.items():
392
+ similarity = SequenceMatcher(None, query_normalized, key).ratio()
393
+
394
+ # Ge bonus för matchande nyckelord
395
+ query_terms = set(query_normalized.split())
396
+ key_terms = set(key.split())
397
+ common_terms = query_terms.intersection(key_terms)
398
+
399
+ if len(common_terms) >= 2:
400
+ similarity += 0.1
401
+
402
+ if similarity > best_score and similarity > 0.75: # 75% likhet krävs
403
+ best_score = similarity
404
+ best_match = value
405
+
406
+ if best_match:
407
+ print(f"🎯 Fuzzy match hittad (score: {best_score:.2f})")
408
+ return best_match
409
 
410
  return None
411
 
 
415
 
416
  direct_match = check_direct_match(query)
417
  if direct_match:
418
+ print(f"Direkt matchning hittad för frågan: {query[:50]}...")
419
  return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
420
 
421
  query_embedding = embedder.encode([query], convert_to_numpy=True)
 
426
  if idx < len(chunks):
427
  retrieved.append(chunks[idx])
428
  sources.add(chunk_sources[idx])
429
+
430
+ context = " ".join(retrieved)
431
+
432
+ # Truncate om för långt
433
+ if len(context) > MAX_CONTEXT_CHARS:
434
+ print(f"⚠️ Kontext trunkerad från {len(context)} till {MAX_CONTEXT_CHARS} tecken")
435
+ context = context[:MAX_CONTEXT_CHARS] + "..."
436
+
437
+ return context, list(sources)
438
 
439
  prompt_template = load_prompt()
440
 
441
+ def generate_answer(query, chat_history=None):
442
+ """Genererar svar baserat på fråga, chatthistorik och retrieval-baserad kontext med Claude Sonnet 4.5."""
443
  context, sources = retrieve_context(query)
444
 
445
  if not context.strip():
446
+ lang = detect_language(query)
447
+ if lang == 'en':
448
+ return "I couldn't find any relevant information in my sources.\n\nThis is an AI-generated response."
449
  return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI-genererat svar."
450
 
451
+ # Detektera språk
452
+ lang = detect_language(query)
453
+
454
+ # Förbered system prompt med språkinstruktion
455
  system_prompt = prompt_template
456
+ if lang == 'en':
457
+ system_prompt += "\n\nIMPORTANT: The user is writing in ENGLISH. Respond in ENGLISH."
458
+ else:
459
+ system_prompt += "\n\nVIKTIGT: Användaren skriver på SVENSKA. Svara på SVENSKA."
460
 
461
+ # Strukturerad user message
462
+ user_message = f"""Baserat på följande information om ChargeNode, svara på användarens fråga.
463
 
464
+ === RELEVANT KONTEXT ===
465
  {context}
466
 
467
+ === ANVÄNDARENS FRÅGA ===
468
+ {query}
469
+
470
+ === INSTRUKTIONER ===
471
+ - Svara på samma språk som frågan ({('engelska' if lang == 'en' else 'svenska')})
472
+ - Var koncis men komplett
473
+ - Om informationen inte finns i kontexten, säg det tydligt
474
+ - Hänvisa till specifika funktioner i appen/portalen när relevant
475
+ - Om frågan gäller navigation, ge steg-för-steg instruktioner med numrerade punkter"""
476
+
477
+ # Bygg messages array med historik
478
+ messages = []
479
+
480
+ # Lägg till relevanta historiska meddelanden (max senaste 6 meddelanden = 3 par)
481
+ if chat_history:
482
+ for msg in chat_history[-6:]:
483
+ if msg.get('role') in ['user', 'assistant']:
484
+ messages.append({
485
+ "role": msg['role'],
486
+ "content": msg['content']
487
+ })
488
+
489
+ # Lägg till aktuell fråga
490
+ messages.append({"role": "user", "content": user_message})
491
 
492
  try:
493
  response = anthropic_client.messages.create(
494
+ model="claude-sonnet-4-5-20250929",
495
+ max_tokens=3000, # Ökat från 1500
496
+ temperature=0.1, # Sänkt från 0.3 för mer konsekventa svar
497
  system=system_prompt,
498
+ messages=messages
 
 
499
  )
500
  answer = response.content[0].text
501
+ print("✅ Svar genererat med Claude Sonnet 4.5")
502
+
503
+ # Språkspecifik footer
504
+ if lang == 'en':
505
+ return answer + "\n\nAI-generated. Need more help? Contact support@chargenode.eu or call 010-2051055"
506
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
507
 
508
  except RateLimitError:
509
  print("⚠️ Rate limit nådd")
510
+ if lang == 'en':
511
+ return "Too many requests right now. Please try again in a few seconds.\n\nContact support@chargenode.eu or 010-2051055"
512
  return "För många förfrågningar just nu. Försök igen om några sekunder.\n\nKontakta support@chargenode.eu eller 010-2051055"
513
 
514
  except APIError as e:
515
  print(f"⚠️ API-fel: {e}")
516
+ if lang == 'en':
517
+ return "A technical error occurred. Please try again.\n\nContact support@chargenode.eu or 010-2051055"
518
  return "Tekniskt fel uppstod. Vänligen försök igen.\n\nKontakta support@chargenode.eu eller 010-2051055"
519
 
520
  except Exception as e:
521
  print(f"❌ Oväntat fel: {e}")
522
+ if lang == 'en':
523
+ return f"Technical error: {str(e)}\n\nContact support@chargenode.eu or 010-2051055"
524
  return f"Tekniskt fel: {str(e)}\n\nKontakta support@chargenode.eu eller 010-2051055"
525
 
526
  # --- Slack Integration ---
 
558
  )
559
 
560
  if response.status_code == 200:
561
+ print(f"Slack-meddelande skickat: {subject}")
562
  return True
563
  else:
564
+ print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
565
  return False
566
  except Exception as e:
567
+ print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
568
  return False
569
 
570
  # --- Feedback & Like-funktion ---
 
599
  daemon=True
600
  ).start()
601
  except Exception as e:
602
+ print(f"Kunde inte skicka feedback till Slack: {e}")
603
 
604
  return
605
 
 
617
  log_entry = json.loads(line.strip())
618
  logs.append(log_entry)
619
  except json.JSONDecodeError as e:
620
+ print(f"⚠️ Varning: Kunde inte tolka rad {line_count}: {e}")
621
  continue
622
+ print(f"Läste {len(logs)} av {line_count} loggposter")
623
  else:
624
+ print(f"⚠️ Loggfil saknas: {log_file_path}")
625
  except Exception as e:
626
+ print(f"Fel vid läsning av loggfil: {e}")
627
  return logs
628
 
629
  def get_latest_conversations(logs, limit=50):
 
661
 
662
  def generate_monthly_stats(days=30):
663
  """Genererar omfattande statistik över botanvändning för den senaste månaden."""
664
+ print(f"📊 Genererar statistik för de senaste {days} dagarna...")
665
 
666
  logs = read_logs()
667
 
 
732
 
733
  def simple_status_report():
734
  """Skickar en förenklad statusrapport till Slack."""
735
+ print("📊 Genererar statusrapport för Slack...")
736
 
737
  try:
738
  stats = generate_monthly_stats(days=7)
 
778
  return send_to_slack(subject, content, "#2a9d8f")
779
 
780
  except Exception as e:
781
+ print(f"Fel vid generering av statusrapport: {e}")
782
 
783
  error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
784
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
 
809
 
810
  return send_to_slack(subject, content, "#e76f51")
811
  except Exception as e:
812
+ print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
813
  return False
814
 
815
  # --- Schemaläggning av rapporter ---
 
833
  scheduler_thread.start()
834
 
835
  try:
836
+ print("📤 Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
837
  except Exception as e:
838
+ print(f"ℹ️ Information: Statusrapport kommer att skickas enligt schema: {e}")
839
 
840
  # --- Gradio UI ---
841
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
 
899
  def respond(message, chat_history, request: gr.Request):
900
  global last_log
901
  start = time.time()
902
+
903
+ # Skicka chatthistorik till generate_answer
904
+ response = generate_answer(message, chat_history)
905
  elapsed = round(time.time() - start, 2)
906
 
907
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
926
  platform = "test"
927
  elif "app" in ref:
928
  platform = "app"
929
+
930
+ # Detektera språk för loggning
931
+ lang = detect_language(message)
932
 
933
  log_data = {
934
  "timestamp": timestamp,
 
940
  "ip": ip,
941
  "browser": browser,
942
  "os": osys,
943
+ "platform": platform,
944
+ "language": lang
945
  }
946
 
947
  safe_append_to_log(log_data)
 
951
  conversation_content = f"""
952
  *Ny konversation {timestamp}*
953
 
954
+ *Språk:* {('English' if lang == 'en' else 'Svenska')}
955
+
956
  *Användare:* {message}
957
 
958
  *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
 
964
  daemon=True
965
  ).start()
966
  except Exception as e:
967
+ print(f"Kunde inte skicka konversation till Slack: {e}")
968
 
969
  chat_history.append({"role": "user", "content": message})
970
  chat_history.append({"role": "assistant", "content": response})
 
1002
 
1003
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
1004
  """Hanterar formulärinskickningen med bättre felhantering."""
1005
+ print(f"📝 Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
1006
 
1007
  validation_errors = []
1008
 
1009
+ # Använd validerings-hjälpfunktionen
1010
+ if error := validate_numeric_field(områdeskod, "Områdeskod"):
1011
+ validation_errors.append(error)
 
 
1012
 
1013
+ if error := validate_numeric_field(uttagsnummer, "Uttagsnummer"):
1014
+ validation_errors.append(error)
 
 
 
1015
 
1016
  if not email:
 
1017
  validation_errors.append("En giltig e-postadress krävs.")
1018
  elif '@' not in email or '.' not in email.split('@')[1]:
 
1019
  validation_errors.append("En giltig e-postadress krävs.")
 
 
1020
 
1021
  if validation_errors:
1022
+ print(f"Valideringsfel: {validation_errors}")
1023
  return {
1024
  chat_interface: gr.Group(visible=False),
1025
  support_interface: gr.Group(visible=True),
 
1028
  }
1029
 
1030
  try:
1031
+ print("📤 Försöker skicka supportförfrågan till Slack...")
 
 
 
 
 
 
1032
 
1033
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
1034
 
1035
  if success:
1036
+ print("Support-förfrågan skickad till Slack framgångsrikt")
1037
  return {
1038
  chat_interface: gr.Group(visible=False),
1039
  support_interface: gr.Group(visible=False),
1040
  success_interface: gr.Group(visible=True)
1041
  }
1042
  else:
1043
+ print("Support-förfrågan till Slack misslyckades")
1044
  return {
1045
  chat_interface: gr.Group(visible=False),
1046
  support_interface: gr.Group(visible=True),
 
1048
  chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
1049
  }
1050
  except Exception as e:
1051
+ print(f"Oväntat fel vid hantering av support-formulär: {e}")
1052
  return {
1053
  chat_interface: gr.Group(visible=False),
1054
  support_interface: gr.Group(visible=True),
 
1057
  }
1058
 
1059
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
1060
+ clear.click(lambda: initial_chat.copy(), None, chatbot, queue=False)
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])
 
1067
  [chat_interface, support_interface, success_interface, chat_preview]
1068
  )
1069
 
1070
+ print("🚀 Förbereder embedding-modell och index...")
1071
  initialize_embeddings()
1072
+ print("Embedding-modell och index redo!")
1073
 
1074
  if __name__ == "__main__":
1075
  app.launch(share=True)