k96beni commited on
Commit
8cd16e6
·
verified ·
1 Parent(s): 28715bf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +440 -403
app.py CHANGED
@@ -74,30 +74,21 @@ chunk_sources = []
74
  def safe_append_to_log(log_entry):
75
  """Säker metod för att lägga till loggdata utan att förlora historisk information."""
76
  try:
77
- # Öppna filen i append-läge
78
  with open(log_file_path, "a", encoding="utf-8") as log_file:
79
  log_json = json.dumps(log_entry)
80
  log_file.write(log_json + "\n")
81
- log_file.flush() # Säkerställ att data skrivs till disk omedelbart
82
-
83
  print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
84
  return True
85
-
86
  except Exception as e:
87
  print(f"Fel vid loggning: {e}")
88
-
89
- # Försök skapa mappen om den inte finns
90
  try:
91
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
92
-
93
- # Försök igen
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")
99
  return True
100
-
101
  except Exception as retry_error:
102
  print(f"Kritiskt fel vid loggning: {retry_error}")
103
  return False
@@ -115,29 +106,25 @@ def load_local_files():
115
  with open(file, "r", encoding="utf-8") as f:
116
  content = f.read()
117
  elif file.endswith(".docx"):
118
- from docx import Document # Import sker vid behov
119
  content = "\n".join([p.text for p in Document(file).paragraphs])
120
  elif file.endswith(".pdf"):
121
- import PyPDF2 # Import sker vid behov
122
  with open(file, "rb") as f:
123
  reader = PyPDF2.PdfReader(f)
124
  content = "\n".join([p.extract_text() or "" for p in reader.pages])
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():
132
- # Start with the required fields
133
  row_text = f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}"
134
-
135
- # Add kategori if it exists in the dataframe
136
- if 'kategori' in df.columns:
137
  row_text += f"\nKategori: {row['kategori']}"
138
- elif 'Kategori' in df.columns: # Also check for capitalized version
139
  row_text += f"\nKategori: {row['Kategori']}"
140
-
141
  rows.append(row_text)
142
  content = "\n\n".join(rows)
143
  else:
@@ -149,39 +136,41 @@ def load_local_files():
149
 
150
  def load_prompt():
151
  """Läser in system-prompts från prompt.txt med bättre felhantering."""
 
152
  try:
153
  with open("prompt.txt", "r", encoding="utf-8") as f:
154
  prompt_content = f.read().strip()
155
  if not prompt_content:
156
  print("Varning: prompt.txt är tom, använder standardprompt")
157
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
158
  return prompt_content
159
  except FileNotFoundError:
160
  print("Varning: prompt.txt hittades inte, använder standardprompt")
161
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
162
  except Exception as e:
163
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
164
- return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
165
 
166
  # Förbered textsegment
167
  def prepare_chunks(text_data):
168
  """Delar upp texten i mindre segment för embedding och sökning."""
169
- chunks, sources = [], []
170
  for source, text in text_data.items():
171
  paragraphs = [p for p in text.split("\n") if p.strip()]
172
- chunk = ""
173
  for para in paragraphs:
174
- if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
175
- chunk += " " + para
176
  else:
177
- if chunk.strip():
178
- chunks.append(chunk.strip())
179
- sources.append(source)
180
- chunk = para
181
- if chunk.strip():
182
- chunks.append(chunk.strip())
183
- sources.append(source)
184
- return chunks, sources
 
185
 
186
  def initialize_embeddings():
187
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
@@ -189,73 +178,123 @@ def initialize_embeddings():
189
 
190
  if embedder is None:
191
  print("Initierar SentenceTransformer och FAISS-index...")
192
- # Ladda och förbered lokal data
193
  print("Laddar textdata...")
194
  text_data = {"local_files": load_local_files()}
195
  print("Förbereder textsegment...")
196
- chunks, chunk_sources = prepare_chunks(text_data)
197
  print(f"{len(chunks)} segment laddade")
198
 
 
 
 
 
 
 
199
  print("Skapar embeddings...")
200
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
201
- embeddings = embedder.encode(chunks, convert_to_numpy=True)
202
- embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
203
- index = faiss.IndexFlatIP(embeddings.shape[1])
204
- index.add(embeddings)
205
- print("FAISS-index klart")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  def retrieve_context(query, k=RETRIEVAL_K):
208
  """Hämtar relevant kontext för frågor."""
209
- # Säkerställ att modeller är laddade
210
- initialize_embeddings()
211
 
 
 
 
 
212
  query_embedding = embedder.encode([query], convert_to_numpy=True)
213
- query_embedding /= np.linalg.norm(query_embedding)
214
- D, I = index.search(query_embedding, k)
215
- retrieved, sources = [], set()
 
 
 
 
 
 
 
 
 
 
 
 
216
  for idx in I[0]:
217
- if idx < len(chunks):
218
- retrieved.append(chunks[idx])
219
- sources.add(chunk_sources[idx])
220
- return " ".join(retrieved), list(sources)
221
 
222
  # Ladda prompt template
223
  prompt_template = load_prompt()
224
 
225
  def generate_answer(query):
226
- """Genererar svar baserat på fråga och retrieval-baserad kontext med Claude Haiku."""
227
- # Hämta relevant kontext via RAG istället för hela databasen
228
  context, sources = retrieve_context(query)
229
 
230
  if not context.strip():
231
- return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
232
-
233
- # System-prompts och användarfråga
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  system_prompt = prompt_template
235
-
236
- # Skapa ett renare användarmeddelande med bara den relevanta kontexten
237
- user_message = f"""Jag har en fråga om ChargeNode.
238
 
239
- Relevant kontext för frågan:
 
240
  {context}
 
241
 
242
  Min fråga är: {query}"""
243
 
244
  try:
245
- # Använd Claude Haiku med RAG-baserad kontext
246
  response = anthropic_client.messages.create(
247
- model="claude-3-7-sonnet-20250219",
248
  max_tokens=500,
249
  temperature=0.3,
250
  system=system_prompt,
251
- messages=[
252
- {"role": "user", "content": user_message}
253
- ]
254
  )
255
  answer = response.content[0].text
256
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
257
  except Exception as e:
258
- return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
259
 
260
  # --- Slack Integration ---
261
  def send_to_slack(subject, content, color="#2a9d8f"):
@@ -266,31 +305,13 @@ def send_to_slack(subject, content, color="#2a9d8f"):
266
  return False
267
 
268
  try:
269
- # Formatera meddelandet för Slack
270
  payload = {
271
  "blocks": [
272
- {
273
- "type": "header",
274
- "text": {
275
- "type": "plain_text",
276
- "text": subject
277
- }
278
- },
279
- {
280
- "type": "section",
281
- "text": {
282
- "type": "mrkdwn",
283
- "text": content
284
- }
285
- }
286
  ]
287
  }
288
-
289
- response = requests.post(
290
- webhook_url,
291
- json=payload,
292
- headers={"Content-Type": "application/json"}
293
- )
294
 
295
  if response.status_code == 200:
296
  print(f"Slack-meddelande skickat: {subject}")
@@ -304,11 +325,7 @@ def send_to_slack(subject, content, color="#2a9d8f"):
304
 
305
  # --- Feedback & Like-funktion ---
306
  def vote(data: gr.LikeData):
307
- """
308
- Hanterar feedback från Gradio's inbyggda like-funktion.
309
- data.liked är True om uppvote, annars False.
310
- data.value innehåller information om meddelandet.
311
- """
312
  feedback_type = "up" if data.liked else "down"
313
  global last_log
314
  log_entry = {
@@ -316,39 +333,33 @@ def vote(data: gr.LikeData):
316
  "feedback": feedback_type,
317
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
318
  }
319
- # Om global logdata finns, lägg till ytterligare metadata.
320
- if last_log:
321
  log_entry.update({
322
  "session_id": last_log.get("session_id"),
323
  "user_message": last_log.get("user_message"),
324
  })
325
 
326
- # Använd den förbättrade loggfunktionen
327
  safe_append_to_log(log_entry)
328
 
329
- # Skicka feedback till Slack
330
  try:
331
- if feedback_type == "down": # Skicka bara negativ feedback
332
  feedback_message = f"""
333
  *⚠️ Negativ feedback registrerad*
334
 
335
  *Fråga:* {last_log.get('user_message', 'Okänd fråga')}
336
 
337
- *Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''}
338
  """
339
- # Skicka asynkront
340
  threading.Thread(
341
  target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
342
  daemon=True
343
  ).start()
344
  except Exception as e:
345
  print(f"Kunde inte skicka feedback till Slack: {e}")
346
-
347
  return
348
 
349
  # --- Rapportering ---
350
  def read_logs():
351
- """Läs alla loggposter från loggfilen."""
352
  logs = []
353
  try:
354
  if os.path.exists(log_file_path):
@@ -360,9 +371,9 @@ def read_logs():
360
  log_entry = json.loads(line.strip())
361
  logs.append(log_entry)
362
  except json.JSONDecodeError as e:
363
- print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
364
  continue
365
- print(f"Läste {len(logs)} av {line_count} loggposter")
366
  else:
367
  print(f"Loggfil saknas: {log_file_path}")
368
  except Exception as e:
@@ -370,9 +381,8 @@ def read_logs():
370
  return logs
371
 
372
  def get_latest_conversations(logs, limit=50):
373
- """Hämta de senaste frågorna och svaren."""
374
  conversations = []
375
- for log in reversed(logs):
376
  if 'user_message' in log and 'bot_reply' in log:
377
  conversations.append({
378
  'user_message': log['user_message'],
@@ -383,178 +393,104 @@ def get_latest_conversations(logs, limit=50):
383
  break
384
  return conversations
385
 
386
- def get_feedback_stats(logs):
387
- """Sammanfatta feedback (tumme upp/ned)."""
388
- feedback_count = {"up": 0, "down": 0}
389
- negative_feedback_examples = []
390
-
391
- for log in logs:
392
- if 'feedback' in log:
393
- feedback = log.get('feedback')
394
- if feedback in feedback_count:
395
- feedback_count[feedback] += 1
396
-
397
- # Samla exempel på negativ feedback
398
- if feedback == "down" and 'user_message' in log and len(negative_feedback_examples) < 10:
399
- negative_feedback_examples.append({
400
- 'user_message': log.get('user_message', 'Okänd fråga'),
401
- 'bot_reply': log.get('bot_reply', 'Okänt svar')
402
- })
403
-
404
- return feedback_count, negative_feedback_examples
405
-
406
  def generate_monthly_stats(days=30):
407
- """Genererar omfattande statistik över botanvändning för den senaste månaden."""
408
  print(f"Genererar statistik för de senaste {days} dagarna...")
409
-
410
- # Hämta loggar
411
  logs = read_logs()
412
-
413
- if not logs:
414
- return {"error": "Inga loggar hittades för den angivna perioden"}
415
-
416
- # Filtrera på datumintervall
417
  now = datetime.now()
418
  cutoff_date = now - timedelta(days=days)
419
- filtered_logs = []
 
 
 
 
 
420
 
421
- for log in logs:
422
- if 'timestamp' in log:
423
- try:
424
- log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
425
- if log_date >= cutoff_date:
426
- filtered_logs.append(log)
427
- except:
428
- pass # Hoppa över poster med ogiltigt datum
429
-
430
- logs = filtered_logs
431
-
432
- # Basstatistik
433
  total_conversations = sum(1 for log in logs if 'user_message' in log)
434
- unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
435
- unique_users = len(set(log.get('user_id', 'unknown') for log in logs if 'user_id' in log))
436
 
437
- # Feedback-statistik
438
  feedback_logs = [log for log in logs if 'feedback' in log]
439
  positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
440
  negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
441
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
442
 
443
- # Svarstidsstatistik
444
- response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
445
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
446
-
447
- # Plattformsstatistik
448
- platforms = {}
449
- browsers = {}
450
- operating_systems = {}
451
- for log in logs:
452
- if 'platform' in log:
453
- platforms[log['platform']] = platforms.get(log['platform'], 0) + 1
454
- if 'browser' in log:
455
- browsers[log['browser']] = browsers.get(log['browser'], 0) + 1
456
- if 'os' in log:
457
- operating_systems[log['os']] = operating_systems.get(log['os'], 0) + 1
458
-
459
- # Skapa rapport
460
  report = {
461
- "period": f"Senaste {days} dagarna",
462
- "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
463
  "basic_stats": {
464
  "total_conversations": total_conversations,
465
  "unique_sessions": unique_sessions,
466
- "unique_users": unique_users,
467
- "messages_per_user": round(total_conversations / unique_users, 2) if unique_users else 0
468
  },
469
  "feedback": {
470
- "positive": positive_feedback,
471
- "negative": negative_feedback,
472
  "ratio_percent": round(feedback_ratio, 1)
473
  },
474
- "performance": {
475
- "avg_response_time": round(avg_response_time, 2)
476
- },
477
- "platform_distribution": platforms,
478
- "browser_distribution": browsers,
479
- "os_distribution": operating_systems
480
  }
481
-
482
  return report
483
 
484
  def simple_status_report():
485
- """Skickar en förenklad statusrapport till Slack."""
486
  print("Genererar statusrapport för Slack...")
487
-
488
  try:
489
- # Generera statistik
490
- stats = generate_monthly_stats(days=7) # Senaste veckan
491
-
492
- # Skapa innehåll för Slack
493
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
494
- subject = f"ChargeNode AI Bot - Status {now}"
495
 
496
  if 'error' in stats:
497
  content = f"*Fel vid generering av statistik:* {stats['error']}"
498
  return send_to_slack(subject, content, "#ff0000")
499
 
500
- # Formatera statistik
501
  basic = stats["basic_stats"]
502
  feedback = stats["feedback"]
503
  perf = stats["performance"]
504
 
505
  content = f"""
506
- *ChargeNode AI Bot - Statusrapport {now}*
507
 
508
- *Basstatistik* (senaste 7 dagarna)
509
  - Totalt antal konversationer: {basic['total_conversations']}
510
  - Unika sessioner: {basic['unique_sessions']}
511
- - Unika användare: {basic['unique_users']}
512
- - Genomsnittlig svarstid: {perf['avg_response_time']} sekunder
513
 
514
  *Feedback*
515
  - 👍 Tumme upp: {feedback['positive']}
516
  - 👎 Tumme ned: {feedback['negative']}
517
  - Nöjdhet: {feedback['ratio_percent']}%
518
  """
519
-
520
- # Lägg till de senaste konversationerna
521
- logs = read_logs()
522
- conversations = get_latest_conversations(logs, 3)
523
-
524
  if conversations:
525
- content += "\n*Senaste konversationer*\n"
526
  for conv in conversations:
527
  content += f"""
528
  > *Tid:* {conv['timestamp']}
529
  > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
530
  > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
531
  """
532
-
533
- # Skicka till Slack
534
  return send_to_slack(subject, content, "#2a9d8f")
535
-
536
  except Exception as e:
537
- print(f"Fel vid generering av statusrapport: {e}")
538
-
539
- # Skicka felmeddelande till Slack
540
- error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
541
- error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
542
  return send_to_slack(error_subject, error_content, "#ff0000")
543
 
 
544
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
545
  """Skickar en supportförfrågan till Slack."""
546
  try:
547
- # Formatera chat-historiken
548
  chat_content = ""
549
- for msg in chat_history:
550
- if msg['role'] == 'user':
551
- chat_content += f">*Användare:* {msg['content']}\n\n"
552
- elif msg['role'] == 'assistant':
553
- chat_content += f">*Bot:* {msg['content'][:300]}{'...' if len(msg['content']) > 300 else ''}\n\n"
554
-
555
- # Skapa innehåll
556
- subject = f"Support förfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
557
-
558
  content = f"""
559
  *Användarinformation*
560
  - *Områdeskod:* {områdeskod or 'Ej angiven'}
@@ -565,9 +501,7 @@ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
565
  *Chatthistorik:*
566
  {chat_content}
567
  """
568
-
569
- # Skicka till Slack
570
- return send_to_slack(subject, content, "#e76f51")
571
  except Exception as e:
572
  print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
573
  return False
@@ -575,79 +509,240 @@ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
575
  # --- Schemaläggning av rapporter ---
576
  def run_scheduler():
577
  """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
578
- # Använd den förenklade funktionen för rapportering
579
- schedule.every().day.at("08:00").do(simple_status_report)
580
- schedule.every().day.at("12:00").do(simple_status_report)
581
- schedule.every().day.at("17:00").do(simple_status_report)
582
-
583
- # Veckorapport måndagar
584
- schedule.every().monday.at("09:00").do(lambda: send_to_slack(
585
- "Veckostatistik",
586
- f"*ChargeNode AI Bot - Veckostatistik*\n\n{json.dumps(generate_monthly_stats(7), indent=2)}",
587
- "#3498db"
588
  ))
589
 
 
590
  while True:
591
  schedule.run_pending()
592
- time.sleep(60) # Kontrollera varje minut
593
 
594
- # Starta schemaläggaren i en separat tråd
 
595
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
596
  scheduler_thread.start()
597
 
598
- # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
599
- try:
600
- print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
601
- # Anropa inte direkt här - sker i schemaläggaren
602
- except Exception as e:
603
- print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
604
-
605
  # --- Gradio UI ---
606
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
607
 
608
  custom_css = """
609
- body {background-color: #f7f7f7; font-family: Arial, sans-serif; margin: 0; padding: 0;}
610
- h1 {font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin-bottom: 0.5em;}
611
- .gradio-container {max-width: 400px; margin: 0; padding: 10px; position: fixed; bottom: 20px; right: 20px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); border-radius: 10px; background-color: #fff;}
612
- #chatbot_conversation { max-height: 300px; overflow-y: auto; }
613
- .gr-button {background-color: #2a9d8f; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; margin: 5px;}
614
- .gr-button:hover {background-color: #264653;}
615
- .support-btn {background-color: #000000; color: #ffffff; margin-top: 5px; margin-bottom: 10px;}
616
- .support-btn:hover {background-color: #333333;}
617
- .flex-row {display: flex; flex-direction: row; gap: 5px;}
618
- .gr-form {padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px;}
619
- .chat-preview {max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 8px; margin-top: 10px; font-size: 12px; background-color: #f9f9f9;}
620
- .success-message {font-size: 16px; font-weight: normal; margin-bottom: 15px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  /* Dölj Gradio-footer */
622
- footer {display: none !important;}
623
- .footer {display: none !important;}
624
- .gr-footer {display: none !important;}
625
- .gradio-footer {display: none !important;}
626
- .gradio-container .footer {display: none !important;}
627
- .gradio-container .gr-footer {display: none !important;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  """
629
 
630
  with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
631
  gr.Markdown("Ställ din fråga om ChargeNodes produkter och tjänster nedan. Om du inte gillar botten, så ring oss gärna på 010 – 205 10 55")
632
 
633
- # Chat interface
634
- with gr.Group(visible=True) as chat_interface:
635
- chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
636
  chatbot.like(vote, None, None)
637
 
638
- with gr.Row():
639
- msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
 
640
 
641
- with gr.Row():
642
- with gr.Column(scale=1):
643
- clear = gr.Button("Rensa")
644
- with gr.Column(scale=1):
645
- support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
646
 
647
- # Support form interface (initially hidden)
648
- with gr.Group(visible=False) as support_interface:
649
  gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
650
-
651
  with gr.Group(elem_classes="gr-form"):
652
  områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
653
  uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
@@ -656,107 +751,90 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
656
  gr.Markdown("### Chat som skickas till support:")
657
  chat_preview = gr.Markdown(elem_classes="chat-preview")
658
 
659
- with gr.Row():
660
  back_btn = gr.Button("Tillbaka")
661
  send_support_btn = gr.Button("Skicka")
662
 
663
- # Success message (initially hidden)
664
- with gr.Group(visible=False) as success_interface:
665
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
666
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
667
 
668
  def respond(message, chat_history, request: gr.Request):
669
  global last_log
670
- start = time.time()
671
- response = generate_answer(message)
672
- elapsed = round(time.time() - start, 2)
 
 
 
673
 
674
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
675
- session_id = str(uuid.uuid4())
676
 
677
- # Använd session_id från tidigare logg om det finns
678
- if last_log and 'session_id' in last_log:
679
- session_id = last_log.get('session_id')
680
-
681
- user_id = request.client.host if request else "okänd"
682
-
683
- ua_str = request.headers.get("user-agent", "")
684
- ref = request.headers.get("referer", "")
685
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
 
 
 
 
 
 
686
  ua = parse_ua(ua_str)
687
- browser = f"{ua.browser.family} {ua.browser.version_string}"
688
- osys = f"{ua.os.family} {ua.os.version_string}"
689
 
690
- platform = "webb"
691
- if "chargenode.eu" in ref:
692
- platform = "chargenode.eu"
693
- elif "localhost" in ref:
694
- platform = "test"
695
- elif "app" in ref:
696
- platform = "app"
697
 
698
  log_data = {
699
- "timestamp": timestamp,
700
- "user_id": user_id,
701
- "session_id": session_id,
702
- "user_message": message,
703
- "bot_reply": response,
704
- "response_time": elapsed,
705
- "ip": ip,
706
- "browser": browser,
707
- "os": osys,
708
- "platform": platform
709
  }
710
-
711
- # Använd den förbättrade loggfunktionen
712
  safe_append_to_log(log_data)
713
- last_log = log_data
714
 
715
- # Skicka varje konversation direkt till Slack
716
  try:
717
- # Konversationsinnehåll
718
- conversation_content = f"""
719
  *Ny konversation {timestamp}*
720
-
721
  *Användare:* {message}
722
-
723
- *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
724
-
725
- *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
726
  """
727
- # Skicka asynkront för att inte blockera svarstiden
728
- threading.Thread(
729
- target=lambda: send_to_slack(f"Ny konversation", conversation_content),
730
- daemon=True
731
- ).start()
732
  except Exception as e:
733
  print(f"Kunde inte skicka konversation till Slack: {e}")
734
 
735
  chat_history.append({"role": "user", "content": message})
736
- chat_history.append({"role": "assistant", "content": response})
737
- return "", chat_history
738
-
739
- def format_chat_preview(chat_history):
740
- if not chat_history:
741
- return "Ingen chatthistorik att visa."
742
 
 
 
743
  preview = ""
744
- for msg in chat_history:
745
- sender = "Användare" if msg["role"] == "user" else "Bot"
746
- content = msg["content"]
747
- if len(content) > 100: # Truncate long messages
748
- content = content[:100] + "..."
749
- preview += f"**{sender}:** {content}\n\n"
750
-
751
  return preview
752
 
753
- def show_support_form(chat_history):
754
- preview = format_chat_preview(chat_history)
755
  return {
756
  chat_interface: gr.Group(visible=False),
757
  support_interface: gr.Group(visible=True),
758
  success_interface: gr.Group(visible=False),
759
- chat_preview: preview
760
  }
761
 
762
  def back_to_chat():
@@ -766,85 +844,43 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
766
  success_interface: gr.Group(visible=False)
767
  }
768
 
769
- def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
770
- """Hanterar formulärinskickningen med bättre felhantering."""
771
- print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
772
-
773
- # Validera input med tydligare loggning
774
  validation_errors = []
775
-
776
- if områdeskod and not områdeskod.isdigit():
777
- print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
778
- validation_errors.append("Områdeskod måste vara numerisk.")
779
- else:
780
- print(f"Validerar områdeskod: '{områdeskod}' (ok)")
781
-
782
- if uttagsnummer and not uttagsnummer.isdigit():
783
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
784
- validation_errors.append("Uttagsnummer måste vara numerisk.")
785
- else:
786
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
787
-
788
- if not email:
789
- print("Validerar email: (saknas)")
790
- validation_errors.append("En giltig e-postadress krävs.")
791
- elif '@' not in email or '.' not in email.split('@')[1]:
792
- print(f"Validerar email: '{email}' (felaktigt format)")
793
  validation_errors.append("En giltig e-postadress krävs.")
794
- else:
795
- print(f"Validerar email: '{email}' (ok)")
796
-
797
- # Om det finns valideringsfel
798
  if validation_errors:
799
- print(f"Valideringsfel: {validation_errors}")
800
- return {
801
  chat_interface: gr.Group(visible=False),
802
- support_interface: gr.Group(visible=True),
803
  success_interface: gr.Group(visible=False),
804
- chat_preview: "\n".join(["**Fel:**"] + validation_errors)
805
  }
806
 
807
- # Om formuläret klarade valideringen, försök skicka till Slack
808
- try:
809
- print("Försöker skicka supportförfrågan till Slack...")
810
-
811
- # Skapa en förenklad chathistorik för loggning
812
- chat_summary = []
813
- for msg in chat_history:
814
- if 'role' in msg and 'content' in msg:
815
- chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
816
- print(f"Chatthistorik att skicka: {chat_summary}")
817
-
818
- # Skicka till Slack
819
- success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
820
-
821
- if success:
822
- print("Support-förfrågan skickad till Slack framgångsrikt")
823
- return {
824
- chat_interface: gr.Group(visible=False),
825
- support_interface: gr.Group(visible=False),
826
- success_interface: gr.Group(visible=True)
827
- }
828
- else:
829
- print("Support-förfrågan till Slack misslyckades")
830
- return {
831
- chat_interface: gr.Group(visible=False),
832
- support_interface: gr.Group(visible=True),
833
- success_interface: gr.Group(visible=False),
834
- chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
835
- }
836
- except Exception as e:
837
- print(f"Oväntat fel vid hantering av support-formulär: {e}")
838
  return {
839
  chat_interface: gr.Group(visible=False),
840
- support_interface: gr.Group(visible=True),
 
 
 
 
 
 
 
841
  success_interface: gr.Group(visible=False),
842
- chat_preview: f"**Ett fel uppstod: {str(e)}**"
843
  }
844
-
845
- msg.submit(respond, [msg, chatbot], [msg, chatbot])
846
- clear.click(lambda: None, None, chatbot, queue=False)
847
- support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
 
 
848
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
849
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
850
  send_support_btn.click(
@@ -859,4 +895,5 @@ initialize_embeddings()
859
  print("Embedding-modell och index redo!")
860
 
861
  if __name__ == "__main__":
862
- app.launch(share=True)
 
 
74
  def safe_append_to_log(log_entry):
75
  """Säker metod för att lägga till loggdata utan att förlora historisk information."""
76
  try:
 
77
  with open(log_file_path, "a", encoding="utf-8") as log_file:
78
  log_json = json.dumps(log_entry)
79
  log_file.write(log_json + "\n")
80
+ log_file.flush()
 
81
  print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
82
  return True
 
83
  except Exception as e:
84
  print(f"Fel vid loggning: {e}")
 
 
85
  try:
86
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
 
 
87
  with open(log_file_path, "a", encoding="utf-8") as log_file:
88
  log_json = json.dumps(log_entry)
89
  log_file.write(log_json + "\n")
 
90
  print("Loggpost tillagd efter återhämtning")
91
  return True
 
92
  except Exception as retry_error:
93
  print(f"Kritiskt fel vid loggning: {retry_error}")
94
  return False
 
106
  with open(file, "r", encoding="utf-8") as f:
107
  content = f.read()
108
  elif file.endswith(".docx"):
109
+ from docx import Document
110
  content = "\n".join([p.text for p in Document(file).paragraphs])
111
  elif file.endswith(".pdf"):
112
+ import PyPDF2
113
  with open(file, "rb") as f:
114
  reader = PyPDF2.PdfReader(f)
115
  content = "\n".join([p.extract_text() or "" for p in reader.pages])
116
  elif file.endswith(".csv"):
117
  content = pd.read_csv(file).to_string()
118
  elif file.endswith((".xls", ".xlsx")):
119
+ if file == "FAQ stadat.xlsx": # Specific handling for this file
120
  df = pd.read_excel(file)
121
  rows = []
122
  for index, row in df.iterrows():
 
123
  row_text = f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}"
124
+ if 'kategori' in df.columns and pd.notna(row['kategori']):
 
 
125
  row_text += f"\nKategori: {row['kategori']}"
126
+ elif 'Kategori' in df.columns and pd.notna(row['Kategori']):
127
  row_text += f"\nKategori: {row['Kategori']}"
 
128
  rows.append(row_text)
129
  content = "\n\n".join(rows)
130
  else:
 
136
 
137
  def load_prompt():
138
  """Läser in system-prompts från prompt.txt med bättre felhantering."""
139
+ default_prompt = "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
140
  try:
141
  with open("prompt.txt", "r", encoding="utf-8") as f:
142
  prompt_content = f.read().strip()
143
  if not prompt_content:
144
  print("Varning: prompt.txt är tom, använder standardprompt")
145
+ return default_prompt
146
  return prompt_content
147
  except FileNotFoundError:
148
  print("Varning: prompt.txt hittades inte, använder standardprompt")
149
+ return default_prompt
150
  except Exception as e:
151
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
152
+ return default_prompt
153
 
154
  # Förbered textsegment
155
  def prepare_chunks(text_data):
156
  """Delar upp texten i mindre segment för embedding och sökning."""
157
+ chunks_list, sources_list = [], [] # Renamed to avoid conflict with global 'chunks'
158
  for source, text in text_data.items():
159
  paragraphs = [p for p in text.split("\n") if p.strip()]
160
+ current_chunk = ""
161
  for para in paragraphs:
162
+ if len(current_chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
163
+ current_chunk += (" " + para if current_chunk else para)
164
  else:
165
+ if current_chunk.strip():
166
+ chunks_list.append(current_chunk.strip())
167
+ sources_list.append(source)
168
+ current_chunk = para
169
+ if current_chunk.strip(): # Add the last chunk
170
+ chunks_list.append(current_chunk.strip())
171
+ sources_list.append(source)
172
+ return chunks_list, sources_list
173
+
174
 
175
  def initialize_embeddings():
176
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
 
178
 
179
  if embedder is None:
180
  print("Initierar SentenceTransformer och FAISS-index...")
 
181
  print("Laddar textdata...")
182
  text_data = {"local_files": load_local_files()}
183
  print("Förbereder textsegment...")
184
+ chunks, chunk_sources = prepare_chunks(text_data) # Assign to global variables
185
  print(f"{len(chunks)} segment laddade")
186
 
187
+ if not chunks:
188
+ print("Varning: Inga textsegment kunde laddas. RAG kommer inte fungera.")
189
+ # Initialize with a dummy entry to prevent errors if FAISS needs at least one vector
190
+ chunks = ["Det finns ingen information tillgänglig just nu."]
191
+ chunk_sources = ["system_placeholder"]
192
+
193
  print("Skapar embeddings...")
194
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
195
+ # Ensure chunks is not empty before encoding
196
+ if chunks:
197
+ embeddings_np = embedder.encode(chunks, convert_to_numpy=True)
198
+ if embeddings_np.ndim == 1: # Handle case of single chunk
199
+ embeddings_np = embeddings_np.reshape(1, -1)
200
+
201
+ # Normalize embeddings
202
+ norms = np.linalg.norm(embeddings_np, axis=1, keepdims=True)
203
+ # Avoid division by zero if a norm is zero
204
+ norms[norms == 0] = 1e-10
205
+ embeddings = embeddings_np / norms
206
+
207
+ index = faiss.IndexFlatIP(embeddings.shape[1])
208
+ index.add(embeddings)
209
+ print("FAISS-index klart")
210
+ else: # Should not happen due to dummy entry, but as a safeguard
211
+ print("Kritiskt fel: Inga embeddings kunde skapas då inga textsegment fanns.")
212
+ # Set up dummy index to prevent crashes later, though RAG will be ineffective
213
+ embedder = SentenceTransformer('all-MiniLM-L6-v2') # ensure embedder is not None
214
+ dummy_embedding = embedder.encode(["dummy"], convert_to_numpy=True)
215
+ index = faiss.IndexFlatIP(dummy_embedding.shape[1])
216
+ index.add(dummy_embedding)
217
+ embeddings = dummy_embedding # So it's not None
218
+
219
 
220
  def retrieve_context(query, k=RETRIEVAL_K):
221
  """Hämtar relevant kontext för frågor."""
222
+ initialize_embeddings() # Ensure models are loaded
 
223
 
224
+ if embedder is None or index is None or not chunks:
225
+ print("Varning: Embeddings eller index är inte korrekt initierat. Kan inte hämta kontext.")
226
+ return "", []
227
+
228
  query_embedding = embedder.encode([query], convert_to_numpy=True)
229
+ if query_embedding.ndim == 1: # Handle case of single query
230
+ query_embedding = query_embedding.reshape(1, -1)
231
+
232
+ # Normalize query embedding
233
+ query_norm = np.linalg.norm(query_embedding)
234
+ if query_norm == 0: query_norm = 1e-10 # Avoid division by zero
235
+ query_embedding /= query_norm
236
+
237
+ # Ensure k is not greater than the number of items in the index
238
+ effective_k = min(k, index.ntotal)
239
+ if effective_k == 0:
240
+ return "", []
241
+
242
+ D, I = index.search(query_embedding, effective_k)
243
+ retrieved_chunks, sources_set = [], set()
244
  for idx in I[0]:
245
+ if 0 <= idx < len(chunks): # Check index bounds
246
+ retrieved_chunks.append(chunks[idx])
247
+ sources_set.add(chunk_sources[idx]) # Ensure chunk_sources has corresponding entries
248
+ return " ".join(retrieved_chunks), list(sources_set)
249
 
250
  # Ladda prompt template
251
  prompt_template = load_prompt()
252
 
253
  def generate_answer(query):
254
+ """Genererar svar baserat på fråga och retrieval-baserad kontext med Claude."""
 
255
  context, sources = retrieve_context(query)
256
 
257
  if not context.strip():
258
+ # Default if RAG fails or returns nothing
259
+ # Try to answer without context or provide a specific message
260
+ system_prompt_no_context = "Du är ChargeNode's AI-assistent. Försök svara på användarens fråga generellt om ChargeNode även om specifik kontext saknas. Om du inte kan, be användaren kontakta support."
261
+ user_message_no_context = f"Min fråga är: {query}"
262
+ try:
263
+ response = anthropic_client.messages.create(
264
+ model="claude-3-haiku-20240307", # eller claude-3-opus-20240229 / claude-3-sonnet-20240229
265
+ max_tokens=500,
266
+ temperature=0.3,
267
+ system=system_prompt_no_context,
268
+ messages=[{"role": "user", "content": user_message_no_context}]
269
+ )
270
+ answer = response.content[0].text
271
+ return answer + "\n\nJag kunde inte hitta specifik information om detta i mina källor, så detta är ett mer generellt AI-genererat svar. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
272
+
273
+ except Exception as e:
274
+ return f"Jag hittar ingen relevant information i mina källor och ett tekniskt fel uppstod: {str(e)}.\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
275
+
276
  system_prompt = prompt_template
277
+ user_message = f"""Använd endast följande kontext för att svara på frågan. Svara inte på frågor som inte är relaterade till ChargeNode eller elbilsladdning. Om informationen inte finns i kontexten, säg att du inte vet och hänvisa till support.
 
 
278
 
279
+ Relevant kontext:
280
+ <kontext>
281
  {context}
282
+ </kontext>
283
 
284
  Min fråga är: {query}"""
285
 
286
  try:
 
287
  response = anthropic_client.messages.create(
288
+ model="claude-3-haiku-20240307", # eller claude-3-opus-20240229 / claude-3-sonnet-20240229
289
  max_tokens=500,
290
  temperature=0.3,
291
  system=system_prompt,
292
+ messages=[{"role": "user", "content": user_message}]
 
 
293
  )
294
  answer = response.content[0].text
295
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
296
  except Exception as e:
297
+ return f"Tekniskt fel vid svarshantering: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
298
 
299
  # --- Slack Integration ---
300
  def send_to_slack(subject, content, color="#2a9d8f"):
 
305
  return False
306
 
307
  try:
 
308
  payload = {
309
  "blocks": [
310
+ {"type": "header", "text": {"type": "plain_text", "text": subject}},
311
+ {"type": "section", "text": {"type": "mrkdwn", "text": content}}
 
 
 
 
 
 
 
 
 
 
 
 
312
  ]
313
  }
314
+ response = requests.post(webhook_url, json=payload, headers={"Content-Type": "application/json"}, timeout=10)
 
 
 
 
 
315
 
316
  if response.status_code == 200:
317
  print(f"Slack-meddelande skickat: {subject}")
 
325
 
326
  # --- Feedback & Like-funktion ---
327
  def vote(data: gr.LikeData):
328
+ """Hanterar feedback från Gradio's inbyggda like-funktion."""
 
 
 
 
329
  feedback_type = "up" if data.liked else "down"
330
  global last_log
331
  log_entry = {
 
333
  "feedback": feedback_type,
334
  "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
335
  }
336
+ if last_log: # Ensure last_log is not None
 
337
  log_entry.update({
338
  "session_id": last_log.get("session_id"),
339
  "user_message": last_log.get("user_message"),
340
  })
341
 
 
342
  safe_append_to_log(log_entry)
343
 
 
344
  try:
345
+ if feedback_type == "down" and last_log: # Only send negative feedback and if context exists
346
  feedback_message = f"""
347
  *⚠️ Negativ feedback registrerad*
348
 
349
  *Fråga:* {last_log.get('user_message', 'Okänd fråga')}
350
 
351
+ *Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:500]}{'...' if len(log_entry.get('bot_reply', '')) > 500 else ''}
352
  """
 
353
  threading.Thread(
354
  target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
355
  daemon=True
356
  ).start()
357
  except Exception as e:
358
  print(f"Kunde inte skicka feedback till Slack: {e}")
 
359
  return
360
 
361
  # --- Rapportering ---
362
  def read_logs():
 
363
  logs = []
364
  try:
365
  if os.path.exists(log_file_path):
 
371
  log_entry = json.loads(line.strip())
372
  logs.append(log_entry)
373
  except json.JSONDecodeError as e:
374
+ print(f"Varning: Kunde inte tolka rad {line_count} i loggfilen: {e}")
375
  continue
376
+ print(f"Läste {len(logs)} av {line_count} loggposter från {log_file_path}")
377
  else:
378
  print(f"Loggfil saknas: {log_file_path}")
379
  except Exception as e:
 
381
  return logs
382
 
383
  def get_latest_conversations(logs, limit=50):
 
384
  conversations = []
385
+ for log in reversed(logs): # Iterate from newest to oldest
386
  if 'user_message' in log and 'bot_reply' in log:
387
  conversations.append({
388
  'user_message': log['user_message'],
 
393
  break
394
  return conversations
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  def generate_monthly_stats(days=30):
 
397
  print(f"Genererar statistik för de senaste {days} dagarna...")
 
 
398
  logs = read_logs()
399
+ if not logs: return {"error": "Inga loggar hittades."}
400
+
 
 
 
401
  now = datetime.now()
402
  cutoff_date = now - timedelta(days=days)
403
+ filtered_logs = [
404
+ log for log in logs if 'timestamp' in log and
405
+ datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S") >= cutoff_date
406
+ rescue [] # Handle strptime errors gracefully if any
407
+ ]
408
+ if not filtered_logs: return {"error": f"Inga loggar hittades för de senaste {days} dagarna."}
409
 
410
+ logs = filtered_logs # Use filtered logs from now on
411
+
 
 
 
 
 
 
 
 
 
 
412
  total_conversations = sum(1 for log in logs if 'user_message' in log)
413
+ unique_session_ids = set(log.get('session_id') for log in logs if 'session_id' in log)
414
+ unique_sessions = len(unique_session_ids)
415
 
 
416
  feedback_logs = [log for log in logs if 'feedback' in log]
417
  positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
418
  negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
419
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
420
 
421
+ response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log and isinstance(log.get('response_time'), (int, float))]
 
422
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
423
+
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  report = {
425
+ "period": f"Senaste {days} dagarna (till {now.strftime('%Y-%m-%d %H:%M:%S')})",
 
426
  "basic_stats": {
427
  "total_conversations": total_conversations,
428
  "unique_sessions": unique_sessions,
 
 
429
  },
430
  "feedback": {
431
+ "positive": positive_feedback, "negative": negative_feedback,
 
432
  "ratio_percent": round(feedback_ratio, 1)
433
  },
434
+ "performance": {"avg_response_time_seconds": round(avg_response_time, 2)}
 
 
 
 
 
435
  }
 
436
  return report
437
 
438
  def simple_status_report():
 
439
  print("Genererar statusrapport för Slack...")
 
440
  try:
441
+ stats = generate_monthly_stats(days=1) # Daily stats
442
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
443
+ subject = f"ChargeNode AI Bot - Daglig Status {now_str}"
 
 
 
444
 
445
  if 'error' in stats:
446
  content = f"*Fel vid generering av statistik:* {stats['error']}"
447
  return send_to_slack(subject, content, "#ff0000")
448
 
 
449
  basic = stats["basic_stats"]
450
  feedback = stats["feedback"]
451
  perf = stats["performance"]
452
 
453
  content = f"""
454
+ *ChargeNode AI Bot - Daglig Statusrapport ({stats['period']})*
455
 
456
+ *Statistik (senaste dygnet)*
457
  - Totalt antal konversationer: {basic['total_conversations']}
458
  - Unika sessioner: {basic['unique_sessions']}
459
+ - Genomsnittlig svarstid: {perf['avg_response_time_seconds']} sekunder
 
460
 
461
  *Feedback*
462
  - 👍 Tumme upp: {feedback['positive']}
463
  - 👎 Tumme ned: {feedback['negative']}
464
  - Nöjdhet: {feedback['ratio_percent']}%
465
  """
466
+ logs_for_preview = read_logs() # Read all logs for recent conversations
467
+ conversations = get_latest_conversations(logs_for_preview, 3)
 
 
 
468
  if conversations:
469
+ content += "\n*Några av de senaste konversationerna*\n"
470
  for conv in conversations:
471
  content += f"""
472
  > *Tid:* {conv['timestamp']}
473
  > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
474
  > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
475
  """
 
 
476
  return send_to_slack(subject, content, "#2a9d8f")
 
477
  except Exception as e:
478
+ print(f"Allvarligt fel vid generering av statusrapport: {e}")
479
+ error_subject = f"ChargeNode AI Bot - KRITISKT FEL vid statusrapport"
480
+ error_content = f"*Allvarligt fel vid generering av statusrapport:* ```{str(e)}```"
 
 
481
  return send_to_slack(error_subject, error_content, "#ff0000")
482
 
483
+
484
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
485
  """Skickar en supportförfrågan till Slack."""
486
  try:
 
487
  chat_content = ""
488
+ for msg in chat_history: # chat_history is already in messages format
489
+ role = msg.get('role', 'unknown').capitalize()
490
+ content = msg.get('content', '')
491
+ chat_content += f">{role}: {content[:300]}{'...' if len(content) > 300 else ''}\n\n"
492
+
493
+ subject = f"Supportförfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
 
 
 
494
  content = f"""
495
  *Användarinformation*
496
  - *Områdeskod:* {områdeskod or 'Ej angiven'}
 
501
  *Chatthistorik:*
502
  {chat_content}
503
  """
504
+ return send_to_slack(subject, content, "#e76f51") # Orange color for support
 
 
505
  except Exception as e:
506
  print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
507
  return False
 
509
  # --- Schemaläggning av rapporter ---
510
  def run_scheduler():
511
  """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
512
+ schedule.every().day.at("08:00", "Europe/Stockholm").do(simple_status_report)
513
+ schedule.every().day.at("12:00", "Europe/Stockholm").do(simple_status_report)
514
+ schedule.every().day.at("17:00", "Europe/Stockholm").do(simple_status_report)
515
+
516
+ schedule.every().monday.at("09:00", "Europe/Stockholm").do(lambda: send_to_slack(
517
+ "ChargeNode AI - Veckostatistik",
518
+ f"*ChargeNode AI Bot - Veckostatistik*\n\n```json\n{json.dumps(generate_monthly_stats(7), indent=2)}\n```",
519
+ "#3498db" # Blue color for weekly stats
 
 
520
  ))
521
 
522
+ print("Schemaläggare startad. Väntar på schemalagda jobb...")
523
  while True:
524
  schedule.run_pending()
525
+ time.sleep(60)
526
 
527
+ # Starta schemaläggaren endast om det inte är en Hugging Face Space med auto-restart
528
+ # eller om en specifik miljövariabel är satt. För nu, starta den alltid.
529
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
530
  scheduler_thread.start()
531
 
 
 
 
 
 
 
 
532
  # --- Gradio UI ---
533
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
534
 
535
  custom_css = """
536
+ body, html {
537
+ background-color: #f7f7f7;
538
+ font-family: Arial, sans-serif;
539
+ margin: 0;
540
+ padding: 0;
541
+ height: 100%;
542
+ }
543
+
544
+ /* --- Default styles (Desktop - Floating Widget) --- */
545
+ .gradio-container {
546
+ max-width: 450px;
547
+ margin: 0;
548
+ padding: 0;
549
+ position: fixed;
550
+ bottom: 20px;
551
+ right: 20px;
552
+ box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.15);
553
+ border-radius: 12px;
554
+ background-color: #fff;
555
+ display: flex;
556
+ flex-direction: column;
557
+ height: clamp(70vh, 550px, 85vh);
558
+ overflow: hidden;
559
+ }
560
+
561
+ #chat_interface_main_group, #support_interface_group, #success_interface_group {
562
+ display: flex;
563
+ flex-direction: column;
564
+ flex-grow: 1;
565
+ padding: 15px;
566
+ overflow-y: hidden;
567
+ }
568
+ #support_interface_group, #success_interface_group {
569
+ justify-content: center; /* Centrera innehåll i support/success-vyerna */
570
+ }
571
+
572
+
573
+ #chatbot_conversation {
574
+ flex-grow: 1;
575
+ overflow-y: auto;
576
+ min-height: 100px;
577
+ margin-bottom: 15px;
578
+ border: 1px solid #e0e0e0;
579
+ border-radius: 8px;
580
+ padding: 10px;
581
+ background-color: #ffffff; /* Ensure background for chat messages */
582
+ }
583
+
584
+ .gr-textbox textarea {
585
+ border-radius: 6px;
586
+ padding: 10px;
587
+ border: 1px solid #ccc;
588
+ box-sizing: border-box; /* Include padding and border in the element's total width and height */
589
+ width: 100%; /* Make textarea take full width of its container */
590
+ }
591
+
592
+ .flex-row {
593
+ display: flex;
594
+ flex-direction: row;
595
+ gap: 10px;
596
+ align-items: center;
597
+ margin-top: 10px; /* Space above button row */
598
+ }
599
+
600
+ .gr-button {
601
+ background-color: #2a9d8f;
602
+ color: #fff;
603
+ border: none;
604
+ border-radius: 4px;
605
+ padding: 10px 16px;
606
+ margin: 0;
607
+ flex-grow: 1;
608
+ text-align: center;
609
+ cursor: pointer;
610
+ }
611
+
612
+ .gr-button:hover {
613
+ background-color: #264653;
614
+ }
615
+
616
+ .support-btn {
617
+ background-color: #000000;
618
+ color: #ffffff;
619
+ }
620
+ .support-btn:hover {
621
+ background-color: #333333;
622
+ }
623
+
624
+ #support_interface_group .gr-form {
625
+ padding: 10px;
626
+ border: 1px solid #eee;
627
+ border-radius: 4px;
628
+ margin-bottom: 10px;
629
+ background-color: #f9f9f9;
630
+ }
631
+ #support_interface_group .gr-form label { /* Style labels in support form */
632
+ margin-bottom: 5px;
633
+ display: block;
634
+ font-weight: bold;
635
+ font-size: 0.9em;
636
+ }
637
+
638
+
639
+ .chat-preview {
640
+ max-height: 150px;
641
+ overflow-y: auto;
642
+ border: 1px solid #eee;
643
+ padding: 8px;
644
+ margin-top: 10px;
645
+ font-size: 0.9em;
646
+ background-color: #f0f0f0; /* Slightly different background for preview */
647
+ border-radius: 4px;
648
+ }
649
+
650
+ .success-message {
651
+ font-size: 1.1em;
652
+ font-weight: normal;
653
+ text-align: center;
654
+ padding: 20px;
655
+ }
656
+ .success_interface_group .gr-button { /* Button in success message */
657
+ flex-grow: 0; /* Don't let it grow unnecessarily */
658
+ align-self: center; /* Center button */
659
+ }
660
+
661
+
662
  /* Dölj Gradio-footer */
663
+ footer, .footer, .gr-footer, .gradio-footer {
664
+ display: none !important;
665
+ }
666
+ .gradio-container > .footer {
667
+ display: none !important;
668
+ }
669
+
670
+
671
+ /* --- Mobilanpassade stilar --- */
672
+ @media (max-width: 768px) {
673
+ body, html {
674
+ overflow: hidden;
675
+ }
676
+
677
+ .gradio-container {
678
+ width: 100%;
679
+ max-width: 100%;
680
+ height: 100vh;
681
+ height: 100dvh;
682
+ position: static;
683
+ border-radius: 0;
684
+ box-shadow: none;
685
+ bottom: auto;
686
+ right: auto;
687
+ }
688
+
689
+ #chat_interface_main_group, #support_interface_group, #success_interface_group {
690
+ padding: 10px;
691
+ height: 100%; /* Ensure groups fill the container height */
692
+ box-sizing: border-box; /* Padding included in height */
693
+ }
694
+
695
+ #chatbot_conversation {
696
+ margin-bottom: 10px;
697
+ }
698
+
699
+ .flex-row { /* For buttons */
700
+ flex-direction: column;
701
+ gap: 8px;
702
+ width: 100%; /* Make the column take full width */
703
+ }
704
+
705
+ .flex-row .gr-button {
706
+ width: 100%;
707
+ flex-grow: 0;
708
+ }
709
+
710
+ /* For message input row on mobile if it also needs stacking (not typical) */
711
+ /* Example: if msg textbox was also in a .flex-row */
712
+ /* #chat_interface_main_group > .gr-row:has(> .gr-textbox) { flex-direction: column; } */
713
+
714
+
715
+ .gradio-app {
716
+ height: 100%;
717
+ min-height: 100%;
718
+ }
719
+
720
+ /* Adjust main markdown text for mobile if needed */
721
+ .gradio-container > .gr-markdown { /* Targets the first markdown element if it's a direct child */
722
+ padding-left: 10px;
723
+ padding-right: 10px;
724
+ font-size: 0.95em;
725
+ }
726
+ }
727
  """
728
 
729
  with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
730
  gr.Markdown("Ställ din fråga om ChargeNodes produkter och tjänster nedan. Om du inte gillar botten, så ring oss gärna på 010 – 205 10 55")
731
 
732
+ with gr.Group(visible=True, elem_id="chat_interface_main_group") as chat_interface:
733
+ chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation", bubble_full_width=False)
 
734
  chatbot.like(vote, None, None)
735
 
736
+ with gr.Row(): # Row for message input
737
+ msg = gr.Textbox(label=None, placeholder="Ange din fråga...", scale=1, show_label=False)
738
+ # Removed scale to let CSS handle width with .gr-textbox textarea { width: 100%; }
739
 
740
+ with gr.Row(elem_classes="flex-row"): # Row for buttons
741
+ clear = gr.Button("Rensa")
742
+ support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
 
 
743
 
744
+ with gr.Group(visible=False, elem_id="support_interface_group") as support_interface:
 
745
  gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
 
746
  with gr.Group(elem_classes="gr-form"):
747
  områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
748
  uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
 
751
  gr.Markdown("### Chat som skickas till support:")
752
  chat_preview = gr.Markdown(elem_classes="chat-preview")
753
 
754
+ with gr.Row(elem_classes="flex-row"): # Buttons in support form
755
  back_btn = gr.Button("Tillbaka")
756
  send_support_btn = gr.Button("Skicka")
757
 
758
+ with gr.Group(visible=False, elem_id="success_interface_group") as success_interface:
 
759
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
760
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
761
 
762
  def respond(message, chat_history, request: gr.Request):
763
  global last_log
764
+ if not message.strip(): # Do not respond to empty messages
765
+ return "", chat_history
766
+
767
+ start_time = time.time() # Renamed for clarity
768
+ response_text = generate_answer(message) # Renamed for clarity
769
+ elapsed_time = round(time.time() - start_time, 2) # Renamed for clarity
770
 
771
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
772
 
773
+ # Session ID logic: try to reuse from last_log if it exists and is recent enough (e.g., within 30 mins)
774
+ current_session_id = str(uuid.uuid4()) # Default new session_id
775
+ if last_log and 'session_id' in last_log and 'timestamp' in last_log:
776
+ try:
777
+ last_log_time = datetime.strptime(last_log['timestamp'], "%Y-%m-%d %H:%M:%S")
778
+ if (datetime.now() - last_log_time) < timedelta(minutes=30):
779
+ current_session_id = last_log['session_id']
780
+ except ValueError: # If timestamp format is unexpected
781
+ pass # Keep new session_id
782
+
783
+ user_id = request.client.host if request else "unknown_client"
784
+ ua_str = request.headers.get("user-agent", "") if request else ""
785
+ ref = request.headers.get("referer", "") if request else ""
786
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0] if request else user_id.split(",")[0]
787
+
788
  ua = parse_ua(ua_str)
789
+ browser = f"{ua.browser.family} {ua.browser.version_string}" if ua.browser.family else "Unknown Browser"
790
+ osys = f"{ua.os.family} {ua.os.version_string}" if ua.os.family else "Unknown OS"
791
 
792
+ platform = "web"
793
+ if "chargenode.eu" in ref: platform = "chargenode.eu"
794
+ elif "localhost" in ref: platform = "test"
795
+ elif "app" in ref: platform = "app" # Assuming 'app' in referer indicates your app environment
 
 
 
796
 
797
  log_data = {
798
+ "timestamp": timestamp, "user_id": user_id, "session_id": current_session_id,
799
+ "user_message": message, "bot_reply": response_text, "response_time": elapsed_time,
800
+ "ip": ip, "browser": browser, "os": osys, "platform": platform
 
 
 
 
 
 
 
801
  }
 
 
802
  safe_append_to_log(log_data)
803
+ last_log = log_data # Update last_log with the current log
804
 
805
+ # Slack notification for new conversation (asynchronously)
806
  try:
807
+ convo_subject = f"Ny konversation ({platform})"
808
+ convo_content = f"""
809
  *Ny konversation {timestamp}*
 
810
  *Användare:* {message}
811
+ *Bot:* {response_text[:300]}{'...' if len(response_text) > 300 else ''}
812
+ *Info:* SessID: ...{current_session_id[-8:]} | {browser} {osys} | IP: {ip} | Tid: {elapsed_time}s
 
 
813
  """
814
+ threading.Thread(target=lambda: send_to_slack(convo_subject, convo_content), daemon=True).start()
 
 
 
 
815
  except Exception as e:
816
  print(f"Kunde inte skicka konversation till Slack: {e}")
817
 
818
  chat_history.append({"role": "user", "content": message})
819
+ chat_history.append({"role": "assistant", "content": response_text})
820
+ return "", chat_history # Clear input field, update chat history
 
 
 
 
821
 
822
+ def format_chat_preview(chat_history_messages): # Parameter renamed for clarity
823
+ if not chat_history_messages: return "Ingen chatthistorik att visa."
824
  preview = ""
825
+ for msg in chat_history_messages:
826
+ sender = "Användare" if msg.get("role") == "user" else "Bot"
827
+ content = msg.get("content", "")
828
+ preview += f"**{sender}:** {content[:100]}{'...' if len(content) > 100 else ''}\n\n"
 
 
 
829
  return preview
830
 
831
+ def show_support_form(chat_history_messages):
832
+ preview_md = format_chat_preview(chat_history_messages)
833
  return {
834
  chat_interface: gr.Group(visible=False),
835
  support_interface: gr.Group(visible=True),
836
  success_interface: gr.Group(visible=False),
837
+ chat_preview: preview_md # Update the markdown component directly
838
  }
839
 
840
  def back_to_chat():
 
844
  success_interface: gr.Group(visible=False)
845
  }
846
 
847
+ def submit_support_form(area_code, outlet_number, user_email, chat_history_messages):
 
 
 
 
848
  validation_errors = []
849
+ if area_code and not area_code.isdigit(): validation_errors.append("Områdeskod måste vara numerisk.")
850
+ if outlet_number and not outlet_number.isdigit(): validation_errors.append("Uttagsnummer måste vara numerisk.")
851
+ if not user_email or '@' not in user_email or '.' not in user_email.split('@')[-1]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  validation_errors.append("En giltig e-postadress krävs.")
853
+
 
 
 
854
  if validation_errors:
855
+ error_md = "**Valideringsfel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
856
+ return { # Return updates for all relevant components
857
  chat_interface: gr.Group(visible=False),
858
+ support_interface: gr.Group(visible=True), # Keep support form visible
859
  success_interface: gr.Group(visible=False),
860
+ chat_preview: error_md # Show errors in the preview area
861
  }
862
 
863
+ slack_sent = send_support_to_slack(area_code, outlet_number, user_email, chat_history_messages)
864
+ if slack_sent:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  return {
866
  chat_interface: gr.Group(visible=False),
867
+ support_interface: gr.Group(visible=False),
868
+ success_interface: gr.Group(visible=True) # Show success
869
+ }
870
+ else:
871
+ error_md = "**Ett tekniskt fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare eller kontakta support direkt.**"
872
+ return {
873
+ chat_interface: gr.Group(visible=False),
874
+ support_interface: gr.Group(visible=True), # Keep support form visible
875
  success_interface: gr.Group(visible=False),
876
+ chat_preview: error_md # Show submission error
877
  }
878
+
879
+ # Event handlers
880
+ msg.submit(respond, [msg, chatbot], [msg, chatbot], api_name="respond") # Added api_name
881
+ clear.click(lambda: (None, initial_chat[:]), None, [msg, chatbot], queue=False) # Reset chat to initial state
882
+
883
+ support_btn.click(show_support_form, [chatbot], [chat_interface, support_interface, success_interface, chat_preview])
884
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
885
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
886
  send_support_btn.click(
 
895
  print("Embedding-modell och index redo!")
896
 
897
  if __name__ == "__main__":
898
+ app.launch(share=IS_HUGGINGFACE) # Share only if IS_HUGGINGFACE is True, otherwise local
899
+ # For local development, you might want app.launch() or app.launch(share=False)