k96beni commited on
Commit
bf2831c
·
verified ·
1 Parent(s): db39613

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +409 -601
app.py CHANGED
@@ -74,21 +74,30 @@ 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
  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,25 +115,29 @@ def load_local_files():
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,41 +149,39 @@ def load_local_files():
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,123 +189,73 @@ def initialize_embeddings():
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,13 +266,31 @@ 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,7 +304,11 @@ def send_to_slack(subject, content, color="#2a9d8f"):
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,33 +316,39 @@ def vote(data: gr.LikeData):
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,9 +360,9 @@ def read_logs():
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,8 +370,9 @@ def read_logs():
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,117 +383,178 @@ def get_latest_conversations(logs, limit=50):
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
 
404
- filtered_logs = [] # Korrekt lista för filtrerade loggar
405
- for log_entry in logs: # Byt namn på loopvariabel för tydlighet
406
- if 'timestamp' in log_entry and isinstance(log_entry['timestamp'], str):
407
  try:
408
- log_date = datetime.strptime(log_entry['timestamp'], "%Y-%m-%d %H:%M:%S")
409
  if log_date >= cutoff_date:
410
- filtered_logs.append(log_entry)
411
- except ValueError:
412
- # Ignorera loggposter med felaktigt formaterad tidsstämpel
413
- # print(f"Varning: Felaktigt tidsstämpelformat för logg: {log_entry.get('session_id', 'N/A')}, timestamp: {log_entry['timestamp']}")
414
- pass
415
- # Loggposter utan tidsstämpel eller med fel typ ignoreras också implicit
416
-
417
- if not filtered_logs:
418
- return {"error": f"Inga giltiga loggar hittades för de senaste {days} dagarna."}
419
 
420
- logs = filtered_logs # Använd de korrekt filtrerade loggarna
421
-
 
422
  total_conversations = sum(1 for log in logs if 'user_message' in log)
423
- # Använd get() med default None för session_id för att undvika KeyError om nyckeln saknas
424
- unique_session_ids = set(log.get('session_id') for log in logs)
425
- # Ta bort None från set om det finns med, eftersom det inte representerar en giltig session
426
- unique_session_ids.discard(None)
427
- unique_sessions = len(unique_session_ids)
428
 
 
429
  feedback_logs = [log for log in logs if 'feedback' in log]
430
  positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
431
  negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
432
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
433
 
434
- response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log and isinstance(log.get('response_time'), (int, float))]
 
435
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
436
-
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  report = {
438
- "period": f"Senaste {days} dagarna (till {now.strftime('%Y-%m-%d %H:%M:%S')})",
 
439
  "basic_stats": {
440
  "total_conversations": total_conversations,
441
  "unique_sessions": unique_sessions,
 
 
442
  },
443
  "feedback": {
444
- "positive": positive_feedback, "negative": negative_feedback,
 
445
  "ratio_percent": round(feedback_ratio, 1)
446
  },
447
- "performance": {"avg_response_time_seconds": round(avg_response_time, 2)}
 
 
 
 
 
448
  }
 
449
  return report
450
 
451
  def simple_status_report():
 
452
  print("Genererar statusrapport för Slack...")
 
453
  try:
454
- stats = generate_monthly_stats(days=1) # Daily stats
455
- now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
456
- subject = f"ChargeNode AI Bot - Daglig Status {now_str}"
 
 
 
457
 
458
  if 'error' in stats:
459
  content = f"*Fel vid generering av statistik:* {stats['error']}"
460
  return send_to_slack(subject, content, "#ff0000")
461
 
 
462
  basic = stats["basic_stats"]
463
  feedback = stats["feedback"]
464
  perf = stats["performance"]
465
 
466
  content = f"""
467
- *ChargeNode AI Bot - Daglig Statusrapport ({stats['period']})*
468
 
469
- *Statistik (senaste dygnet)*
470
  - Totalt antal konversationer: {basic['total_conversations']}
471
  - Unika sessioner: {basic['unique_sessions']}
472
- - Genomsnittlig svarstid: {perf['avg_response_time_seconds']} sekunder
 
473
 
474
  *Feedback*
475
  - 👍 Tumme upp: {feedback['positive']}
476
  - 👎 Tumme ned: {feedback['negative']}
477
  - Nöjdhet: {feedback['ratio_percent']}%
478
  """
479
- logs_for_preview = read_logs() # Read all logs for recent conversations
480
- conversations = get_latest_conversations(logs_for_preview, 3)
 
 
 
481
  if conversations:
482
- content += "\n*Några av de senaste konversationerna*\n"
483
  for conv in conversations:
484
  content += f"""
485
  > *Tid:* {conv['timestamp']}
486
  > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
487
  > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
488
  """
 
 
489
  return send_to_slack(subject, content, "#2a9d8f")
 
490
  except Exception as e:
491
- print(f"Allvarligt fel vid generering av statusrapport: {e}")
492
- error_subject = f"ChargeNode AI Bot - KRITISKT FEL vid statusrapport"
493
- error_content = f"*Allvarligt fel vid generering av statusrapport:* ```{str(e)}```"
 
 
494
  return send_to_slack(error_subject, error_content, "#ff0000")
495
 
496
-
497
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
498
  """Skickar en supportförfrågan till Slack."""
499
  try:
 
500
  chat_content = ""
501
- for msg in chat_history: # chat_history is already in messages format
502
- role = msg.get('role', 'unknown').capitalize()
503
- content = msg.get('content', '')
504
- chat_content += f">{role}: {content[:300]}{'...' if len(content) > 300 else ''}\n\n"
505
-
506
- subject = f"Supportförfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
 
 
 
507
  content = f"""
508
  *Användarinformation*
509
  - *Områdeskod:* {områdeskod or 'Ej angiven'}
@@ -514,7 +565,9 @@ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
514
  *Chatthistorik:*
515
  {chat_content}
516
  """
517
- return send_to_slack(subject, content, "#e76f51") # Orange color for support
 
 
518
  except Exception as e:
519
  print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
520
  return False
@@ -522,522 +575,282 @@ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
522
  # --- Schemaläggning av rapporter ---
523
  def run_scheduler():
524
  """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
525
- schedule.every().day.at("08:00", "Europe/Stockholm").do(simple_status_report)
526
- schedule.every().day.at("12:00", "Europe/Stockholm").do(simple_status_report)
527
- schedule.every().day.at("17:00", "Europe/Stockholm").do(simple_status_report)
528
-
529
- schedule.every().monday.at("09:00", "Europe/Stockholm").do(lambda: send_to_slack(
530
- "ChargeNode AI - Veckostatistik",
531
- f"*ChargeNode AI Bot - Veckostatistik*\n\n```json\n{json.dumps(generate_monthly_stats(7), indent=2)}\n```",
532
- "#3498db" # Blue color for weekly stats
 
 
533
  ))
534
 
535
- print("Schemaläggare startad. Väntar på schemalagda jobb...")
536
  while True:
537
  schedule.run_pending()
538
- time.sleep(60)
539
 
540
- # Starta schemaläggaren endast om det inte är en Hugging Face Space med auto-restart
541
- # eller om en specifik miljövariabel är satt. För nu, starta den alltid.
542
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
543
  scheduler_thread.start()
544
 
 
 
 
 
 
 
 
545
  # --- Gradio UI ---
546
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
547
 
548
- # FÖRBÄTTRAD CSS MED KORREKT VISIBILITY OCH MOBILSUPPORT
549
  custom_css = """
550
- /* Reset och grundläggande stilar */
551
- * {
552
- box-sizing: border-box;
553
- }
554
-
555
- body, html {
556
- margin: 0;
557
- padding: 0;
558
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
559
- background-color: #f8f9fa;
560
- height: 100%;
561
- overflow: hidden;
562
- }
563
-
564
- /* Huvudcontainer - anpassar sig efter iframe */
565
- .gradio-container {
566
- width: 100% !important;
567
- height: 100vh !important;
568
- max-width: none !important;
569
- margin: 0 !important;
570
- padding: 0 !important;
571
- border-radius: 0 !important;
572
- box-shadow: none !important;
573
- position: relative;
574
- display: flex;
575
- flex-direction: column;
576
- background-color: #ffffff;
577
- }
578
-
579
- /* Huvud-grupper */
580
- #chat_interface_main_group,
581
- #support_interface_group,
582
- #success_interface_group {
583
- display: flex;
584
- flex-direction: column;
585
- height: 100%;
586
- padding: 16px;
587
- overflow: hidden;
588
- }
589
-
590
- /* KORREKT HANTERING AV VISIBILITY */
591
- #chat_interface_main_group.hidden,
592
- #support_interface_group.hidden,
593
- #success_interface_group.hidden {
594
- display: none !important;
595
- visibility: hidden !important;
596
- }
597
-
598
- /* Chatbot-område */
599
- #chatbot_conversation {
600
- flex: 1;
601
- min-height: 300px;
602
- margin-bottom: 16px;
603
- border: 1px solid #e9ecef;
604
- border-radius: 8px;
605
- background-color: #ffffff;
606
- overflow-y: auto;
607
- padding: 12px;
608
- }
609
-
610
- /* Meddelande-input */
611
- .message-input-container {
612
- margin-bottom: 12px;
613
- }
614
-
615
- .message-input-container .gr-textbox {
616
- margin: 0;
617
- }
618
-
619
- .message-input-container .gr-textbox textarea {
620
- border-radius: 8px;
621
- border: 2px solid #e9ecef;
622
- padding: 12px;
623
- font-size: 14px;
624
- resize: none;
625
- transition: border-color 0.2s ease;
626
- }
627
-
628
- .message-input-container .gr-textbox textarea:focus {
629
- border-color: #2a9d8f;
630
- outline: none;
631
- box-shadow: 0 0 0 3px rgba(42, 157, 143, 0.1);
632
- }
633
-
634
- /* Knappar */
635
- .button-row {
636
- display: flex;
637
- gap: 8px;
638
- flex-wrap: wrap;
639
- }
640
-
641
- .gr-button {
642
- flex: 1;
643
- min-width: 120px;
644
- padding: 12px 16px;
645
- border: none;
646
- border-radius: 6px;
647
- font-weight: 500;
648
- font-size: 14px;
649
- cursor: pointer;
650
- transition: all 0.2s ease;
651
- background-color: #2a9d8f;
652
- color: white;
653
- }
654
-
655
- .gr-button:hover {
656
- background-color: #238277;
657
- transform: translateY(-1px);
658
- }
659
-
660
- .gr-button.support-btn {
661
- background-color: #495057;
662
- }
663
-
664
- .gr-button.support-btn:hover {
665
- background-color: #343a40;
666
- }
667
-
668
- /* Support-formulär */
669
- #support_interface_group {
670
- justify-content: flex-start;
671
- padding-top: 20px;
672
- }
673
-
674
- .support-form {
675
- background-color: #f8f9fa;
676
- padding: 20px;
677
- border-radius: 8px;
678
- border: 1px solid #dee2e6;
679
- margin-bottom: 16px;
680
- }
681
-
682
- .support-form .gr-textbox {
683
- margin-bottom: 12px;
684
- }
685
-
686
- .support-form .gr-textbox label {
687
- font-weight: 600;
688
- color: #495057;
689
- margin-bottom: 4px;
690
- display: block;
691
- }
692
-
693
- .support-form .gr-textbox textarea,
694
- .support-form .gr-textbox input {
695
- border: 1px solid #ced4da;
696
- border-radius: 4px;
697
- padding: 8px 12px;
698
- width: 100%;
699
- }
700
-
701
- /* Chat-förhandsvisning */
702
- .chat-preview {
703
- max-height: 200px;
704
- overflow-y: auto;
705
- background-color: #f8f9fa;
706
- border: 1px solid #dee2e6;
707
- border-radius: 4px;
708
- padding: 12px;
709
- font-size: 13px;
710
- line-height: 1.4;
711
- margin-top: 12px;
712
- }
713
-
714
- /* Success-meddelande */
715
- #success_interface_group {
716
- justify-content: center;
717
- align-items: center;
718
- text-align: center;
719
- }
720
-
721
- .success-message {
722
- font-size: 18px;
723
- color: #198754;
724
- margin-bottom: 24px;
725
- padding: 20px;
726
- background-color: #d1e7dd;
727
- border: 1px solid #badbcc;
728
- border-radius: 8px;
729
- }
730
-
731
- /* Dölj Gradio footer */
732
- footer, .footer, .gr-footer, .gradio-footer {
733
- display: none !important;
734
- }
735
-
736
- /* --- MOBILANPASSNINGAR --- */
737
- @media (max-width: 768px) {
738
- .gradio-container {
739
- height: 100vh !important;
740
- height: 100dvh !important;
741
- }
742
-
743
- #chat_interface_main_group,
744
- #support_interface_group,
745
- #success_interface_group {
746
- padding: 12px;
747
- }
748
-
749
- #chatbot_conversation {
750
- min-height: 250px;
751
- margin-bottom: 12px;
752
- padding: 8px;
753
- }
754
-
755
- .message-input-container .gr-textbox textarea {
756
- font-size: 16px; /* Förhindrar zoom på iOS */
757
- padding: 12px;
758
- }
759
-
760
- .button-row {
761
- flex-direction: column;
762
- gap: 8px;
763
- }
764
-
765
- .gr-button {
766
- width: 100%;
767
- min-width: unset;
768
- padding: 14px 16px;
769
- font-size: 16px;
770
- }
771
-
772
- .support-form {
773
- padding: 16px;
774
- margin-bottom: 12px;
775
- }
776
-
777
- .chat-preview {
778
- max-height: 150px;
779
- font-size: 12px;
780
- }
781
-
782
- .success-message {
783
- font-size: 16px;
784
- padding: 16px;
785
- margin-bottom: 20px;
786
- }
787
- }
788
-
789
- /* --- EXTRA SMÅ SKÄRMAR --- */
790
- @media (max-width: 480px) {
791
- #chat_interface_main_group,
792
- #support_interface_group,
793
- #success_interface_group {
794
- padding: 8px;
795
- }
796
-
797
- #chatbot_conversation {
798
- margin-bottom: 8px;
799
- padding: 6px;
800
- }
801
-
802
- .support-form {
803
- padding: 12px;
804
- }
805
- }
806
-
807
- /* IFRAME-SPECIFIKA JUSTERINGAR */
808
- body.iframe-embedded {
809
- overflow: hidden;
810
- }
811
-
812
- body.iframe-embedded .gradio-container {
813
- border: none;
814
- border-radius: 0;
815
- }
816
  """
817
 
818
- with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst", theme=gr.themes.Soft()) as app:
819
- # State för att hantera aktuell vy
820
- current_view = gr.State("chat") # "chat", "support", "success"
821
-
822
- 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**", elem_classes=["header-text"])
823
 
824
- # Chat Interface
825
- with gr.Group(visible=True, elem_id="chat_interface_main_group") as chat_interface:
826
- chatbot = gr.Chatbot(
827
- value=initial_chat,
828
- type="messages",
829
- elem_id="chatbot_conversation",
830
- bubble_full_width=False,
831
- show_copy_button=True
832
- )
833
  chatbot.like(vote, None, None)
834
 
835
- with gr.Group(elem_classes=["message-input-container"]):
836
- msg = gr.Textbox(
837
- label=None,
838
- placeholder="Ange din fråga...",
839
- show_label=False,
840
- lines=2,
841
- max_lines=4
842
- )
843
 
844
- with gr.Group(elem_classes=["button-row"]):
845
- clear = gr.Button("Rensa chat", variant="secondary")
846
- support_btn = gr.Button("Behöver du mer hjälp?", elem_classes=["support-btn"])
847
-
848
- # Support Interface
849
- with gr.Group(visible=False, elem_id="support_interface_group") as support_interface:
 
 
850
  gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
851
 
852
- with gr.Group(elem_classes=["support-form"]):
853
- områdeskod = gr.Textbox(
854
- label="Områdeskod (valfritt)",
855
- placeholder="Områdeskod",
856
- info="Numeriskt värde"
857
- )
858
- uttagsnummer = gr.Textbox(
859
- label="Uttagsnummer (valfritt)",
860
- placeholder="Uttagsnummer",
861
- info="Numeriskt värde"
862
- )
863
- email = gr.Textbox(
864
- label="Din email adress",
865
- placeholder="din@email.se",
866
- info="Email adress krävs"
867
- )
868
 
869
  gr.Markdown("### Chat som skickas till support:")
870
- chat_preview = gr.Markdown("", elem_classes=["chat-preview"])
871
 
872
- with gr.Group(elem_classes=["button-row"]):
873
- back_btn = gr.Button("Tillbaka", variant="secondary")
874
- send_support_btn = gr.Button("Skicka till support", variant="primary")
875
 
876
- # Success Interface
877
- with gr.Group(visible=False, elem_id="success_interface_group") as success_interface:
878
- gr.Markdown("**Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort**", elem_classes=["success-message"])
879
- back_to_chat_btn = gr.Button("Tillbaka till chatten", variant="primary")
880
 
881
  def respond(message, chat_history, request: gr.Request):
882
  global last_log
883
- if not message.strip():
884
- return "", chat_history
885
-
886
- start_time = time.time()
887
- response_text = generate_answer(message)
888
- elapsed_time = round(time.time() - start_time, 2)
889
 
890
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
891
 
892
- current_session_id = str(uuid.uuid4())
893
- if last_log and 'session_id' in last_log and 'timestamp' in last_log:
894
- try:
895
- last_log_time = datetime.strptime(last_log['timestamp'], "%Y-%m-%d %H:%M:%S")
896
- if (datetime.now() - last_log_time) < timedelta(minutes=30):
897
- current_session_id = last_log['session_id']
898
- except ValueError:
899
- pass
900
 
901
- user_id = request.client.host if request else "unknown_client"
902
- ua_str = request.headers.get("user-agent", "") if request else ""
903
- ref = request.headers.get("referer", "") if request else ""
904
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0] if request else user_id.split(",")[0]
905
-
906
  ua = parse_ua(ua_str)
907
- browser = f"{ua.browser.family} {ua.browser.version_string}" if ua.browser.family else "Unknown Browser"
908
- osys = f"{ua.os.family} {ua.os.version_string}" if ua.os.family else "Unknown OS"
909
 
910
- platform = "web"
911
- if "chargenode.eu" in ref: platform = "chargenode.eu"
912
- elif "localhost" in ref: platform = "test"
913
- elif "app" in ref: platform = "app"
 
 
 
914
 
915
  log_data = {
916
- "timestamp": timestamp, "user_id": user_id, "session_id": current_session_id,
917
- "user_message": message, "bot_reply": response_text, "response_time": elapsed_time,
918
- "ip": ip, "browser": browser, "os": osys, "platform": platform
 
 
 
 
 
 
 
919
  }
 
 
920
  safe_append_to_log(log_data)
921
  last_log = log_data
922
 
 
923
  try:
924
- convo_subject = f"Ny konversation ({platform})"
925
- convo_content = f"""
926
  *Ny konversation {timestamp}*
 
927
  *Användare:* {message}
928
- *Bot:* {response_text[:300]}{'...' if len(response_text) > 300 else ''}
929
- *Info:* SessID: ...{current_session_id[-8:]} | {browser} {osys} | IP: {ip} | Tid: {elapsed_time}s
 
 
930
  """
931
- threading.Thread(target=lambda: send_to_slack(convo_subject, convo_content), daemon=True).start()
 
 
 
 
932
  except Exception as e:
933
  print(f"Kunde inte skicka konversation till Slack: {e}")
934
 
935
  chat_history.append({"role": "user", "content": message})
936
- chat_history.append({"role": "assistant", "content": response_text})
937
  return "", chat_history
938
-
939
- def format_chat_preview(chat_history_messages):
940
- if not chat_history_messages:
941
  return "Ingen chatthistorik att visa."
 
942
  preview = ""
943
- for msg in chat_history_messages:
944
- sender = "**Användare**" if msg.get("role") == "user" else "**Bot**"
945
- content = msg.get("content", "")
946
- preview += f"{sender}: {content[:100]}{'...' if len(content) > 100 else ''}\n\n"
 
 
 
947
  return preview
948
 
949
- def show_support_form(chat_history_messages, current_view_state):
950
- preview_md = format_chat_preview(chat_history_messages)
951
  return {
952
  chat_interface: gr.Group(visible=False),
953
  support_interface: gr.Group(visible=True),
954
  success_interface: gr.Group(visible=False),
955
- chat_preview: preview_md,
956
- current_view: "support"
957
  }
958
 
959
- def back_to_chat(current_view_state):
960
  return {
961
  chat_interface: gr.Group(visible=True),
962
  support_interface: gr.Group(visible=False),
963
- success_interface: gr.Group(visible=False),
964
- current_view: "chat"
965
  }
966
 
967
- def submit_support_form(area_code, outlet_number, user_email, chat_history_messages, current_view_state):
 
 
 
 
968
  validation_errors = []
969
- if area_code and not area_code.isdigit():
 
 
970
  validation_errors.append("Områdeskod måste vara numerisk.")
971
- if outlet_number and not outlet_number.isdigit():
 
 
 
 
972
  validation_errors.append("Uttagsnummer måste vara numerisk.")
973
- if not user_email or '@' not in user_email or '.' not in user_email.split('@')[-1]:
 
 
 
 
974
  validation_errors.append("En giltig e-postadress krävs.")
975
-
 
 
 
 
 
 
976
  if validation_errors:
977
- error_md = "**Valideringsfel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
978
  return {
979
  chat_interface: gr.Group(visible=False),
980
  support_interface: gr.Group(visible=True),
981
  success_interface: gr.Group(visible=False),
982
- chat_preview: error_md,
983
- current_view: "support"
984
  }
985
 
986
- slack_sent = send_support_to_slack(area_code, outlet_number, user_email, chat_history_messages)
987
- if slack_sent:
988
- return {
989
- chat_interface: gr.Group(visible=False),
990
- support_interface: gr.Group(visible=False),
991
- success_interface: gr.Group(visible=True),
992
- current_view: "success"
993
- }
994
- else:
995
- error_md = "**Ett tekniskt fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare eller kontakta support direkt.**"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
996
  return {
997
  chat_interface: gr.Group(visible=False),
998
  support_interface: gr.Group(visible=True),
999
  success_interface: gr.Group(visible=False),
1000
- chat_preview: error_md,
1001
- current_view: "support"
1002
  }
1003
-
1004
- # Event handlers med korrekt state management
1005
- msg.submit(
1006
- respond,
1007
- [msg, chatbot],
1008
- [msg, chatbot],
1009
- api_name="respond"
1010
- )
1011
-
1012
- clear.click(
1013
- lambda: (None, initial_chat[:]),
1014
- None,
1015
- [msg, chatbot],
1016
- queue=False
1017
- )
1018
-
1019
- support_btn.click(
1020
- show_support_form,
1021
- [chatbot, current_view],
1022
- [chat_interface, support_interface, success_interface, chat_preview, current_view]
1023
- )
1024
-
1025
- back_btn.click(
1026
- back_to_chat,
1027
- [current_view],
1028
- [chat_interface, support_interface, success_interface, current_view]
1029
- )
1030
-
1031
- back_to_chat_btn.click(
1032
- back_to_chat,
1033
- [current_view],
1034
- [chat_interface, support_interface, success_interface, current_view]
1035
- )
1036
 
 
 
 
 
 
1037
  send_support_btn.click(
1038
  submit_support_form,
1039
- [områdeskod, uttagsnummer, email, chatbot, current_view],
1040
- [chat_interface, support_interface, success_interface, chat_preview, current_view]
1041
  )
1042
 
1043
  # Initialisera embeddings vid uppstart
@@ -1046,9 +859,4 @@ initialize_embeddings()
1046
  print("Embedding-modell och index redo!")
1047
 
1048
  if __name__ == "__main__":
1049
- app.launch(
1050
- share=IS_HUGGINGFACE,
1051
- server_name="0.0.0.0" if IS_HUGGINGFACE else "127.0.0.1",
1052
- show_error=True,
1053
- show_api=False
1054
- )
 
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
  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
 
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
 
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
  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
 
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
  "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
  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
  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
  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
  *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
  # --- 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")
654
+ email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
 
 
 
 
 
 
 
 
 
 
 
 
655
 
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():
763
  return {
764
  chat_interface: gr.Group(visible=True),
765
  support_interface: gr.Group(visible=False),
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(
851
  submit_support_form,
852
+ [områdeskod, uttagsnummer, email, chatbot],
853
+ [chat_interface, support_interface, success_interface, chat_preview]
854
  )
855
 
856
  # Initialisera embeddings vid uppstart
 
859
  print("Embedding-modell och index redo!")
860
 
861
  if __name__ == "__main__":
862
+ app.launch(share=True)