k96beni commited on
Commit
fb65c42
·
verified ·
1 Parent(s): 22b43ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +901 -516
app.py CHANGED
@@ -39,22 +39,29 @@ if not os.path.exists(log_file_path):
39
  print(f"Skapade tom loggfil: {log_file_path}")
40
 
41
  hf_token = os.environ.get("HF_TOKEN")
42
- if not hf_token:
43
- raise ValueError("HF_TOKEN saknas")
 
44
 
45
- # Minsta möjliga konfiguration som bör fungera
46
- scheduler = CommitScheduler(
47
- repo_id="ChargeNodeEurope/logfiles",
48
- repo_type="dataset",
49
- folder_path=log_folder,
50
- path_in_repo="logs_v2",
51
- every=300, # Vänta 5 minuter
52
- token=hf_token
53
- )
 
 
 
 
54
 
55
 
56
  # --- Globala variabler ---
57
  last_log = None # Sparar loggdata från senaste svar för feedback
 
 
58
 
59
  # --- Förbättrad loggfunktion ---
60
  def safe_append_to_log(log_entry):
@@ -62,28 +69,28 @@ def safe_append_to_log(log_entry):
62
  try:
63
  # Öppna filen i append-läge
64
  with open(log_file_path, "a", encoding="utf-8") as log_file:
65
- log_json = json.dumps(log_entry)
66
  log_file.write(log_json + "\n")
67
  log_file.flush() # Säkerställ att data skrivs till disk omedelbart
68
-
69
- print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
70
  return True
71
-
72
  except Exception as e:
73
  print(f"Fel vid loggning: {e}")
74
-
75
  # Försök skapa mappen om den inte finns
76
  try:
77
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
78
-
79
  # Försök igen
80
  with open(log_file_path, "a", encoding="utf-8") as log_file:
81
- log_json = json.dumps(log_entry)
82
  log_file.write(log_json + "\n")
83
-
84
  print("Loggpost tillagd efter återhämtning")
85
  return True
86
-
87
  except Exception as retry_error:
88
  print(f"Kritiskt fel vid loggning: {retry_error}")
89
  return False
@@ -92,67 +99,135 @@ def safe_append_to_log(log_entry):
92
  def load_local_files():
93
  uploaded_text = ""
94
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
95
- excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets"]
96
- for file in os.listdir("."):
97
- if file.lower().endswith(tuple(allowed)) and file not in excluded:
 
 
 
 
 
 
 
 
 
98
  try:
99
- if file.endswith(".txt"):
100
- with open(file, "r", encoding="utf-8") as f:
101
  content = f.read()
102
- elif file.endswith(".docx"):
103
- from docx import Document # Import sker vid behov
104
- content = "\n".join([p.text for p in Document(file).paragraphs])
105
- elif file.endswith(".pdf"):
106
- import PyPDF2 # Import sker vid behov
107
- with open(file, "rb") as f:
108
- reader = PyPDF2.PdfReader(f)
109
- content = "\n".join([p.extract_text() or "" for p in reader.pages])
110
- elif file.endswith(".csv"):
111
- content = pd.read_csv(file).to_string()
112
- elif file.endswith((".xls", ".xlsx")):
113
- if file == "FAQ stadat.xlsx":
114
- df = pd.read_excel(file)
115
- rows = []
116
- for index, row in df.iterrows():
117
- rows.append(f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}")
118
- content = "\n\n".join(rows)
119
- else:
120
- content = pd.read_excel(file).to_string()
121
- uploaded_text += f"\n\nFIL: {file}\n{content}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  except Exception as e:
123
- print(f"Fel vid läsning av {file}: {str(e)}")
124
  return uploaded_text.strip()
125
 
126
  def load_prompt():
127
  try:
128
- with open("prompt.txt", "r", encoding="utf-8") as f:
 
129
  return f.read().strip()
 
 
 
130
  except Exception as e:
131
- print(f"Fel vid prompt.txt: {e}")
132
  return ""
133
 
134
  prompt_template = load_prompt()
135
 
136
  # Förbered textsegment
137
  def prepare_chunks(text_data):
 
138
  chunks, sources = [], []
139
  for source, text in text_data.items():
140
- paragraphs = [p for p in text.split("\n") if p.strip()]
141
- chunk = ""
 
 
 
 
142
  for para in paragraphs:
143
- if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
144
- chunk += " " + para
145
  else:
146
- if chunk.strip():
147
- chunks.append(chunk.strip())
 
 
 
 
 
 
 
 
 
148
  sources.append(source)
149
- chunk = para
150
- if chunk.strip():
151
- chunks.append(chunk.strip())
 
 
 
152
  sources.append(source)
153
- return chunks, sources
154
 
155
- # Lazy-laddning av SentenceTransformer
 
 
 
 
156
  embedder = None
157
  embeddings = None
158
  index = None
@@ -160,108 +235,215 @@ index = None
160
  def initialize_embeddings():
161
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
162
  global embedder, embeddings, index, chunks, chunk_sources
163
-
 
164
  if embedder is None:
165
  print("Initierar SentenceTransformer och FAISS-index...")
166
  # Ladda och förbered lokal data
167
  print("Laddar textdata...")
168
  text_data = {"local_files": load_local_files()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  print("Förbereder textsegment...")
170
- chunks, chunk_sources = prepare_chunks(text_data)
171
- print(f"{len(chunks)} segment laddade")
 
 
 
 
 
 
 
 
 
172
 
173
  print("Skapar embeddings...")
174
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
175
- embeddings = embedder.encode(chunks, convert_to_numpy=True)
176
- embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
177
- index = faiss.IndexFlatIP(embeddings.shape[1])
178
- index.add(embeddings)
179
- print("FAISS-index klart")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  def retrieve_context(query, k=RETRIEVAL_K):
182
  """Hämtar relevant kontext för frågor."""
183
- # Säkerställ att modeller är laddade
184
- initialize_embeddings()
185
-
186
- query_embedding = embedder.encode([query], convert_to_numpy=True)
187
- query_embedding /= np.linalg.norm(query_embedding)
188
- D, I = index.search(query_embedding, k)
189
- retrieved, sources = [], set()
190
- for idx in I[0]:
191
- if idx < len(chunks):
192
- retrieved.append(chunks[idx])
193
- sources.add(chunk_sources[idx])
194
- return " ".join(retrieved), list(sources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  def generate_answer(query):
197
  """Genererar svar baserat på fråga och kontextinformation."""
 
198
  context, sources = retrieve_context(query)
 
 
 
199
  if not context.strip():
200
- return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
 
 
 
 
201
  prompt = f"""{prompt_template}
202
-
203
  Relevant kontext:
204
- {context}
 
205
  Fråga: {query}
206
- Svar (baserat enbart på den indexerade datan):"""
 
 
207
  try:
 
208
  response = client.chat.completions.create(
209
  model="gpt-3.5-turbo",
210
  messages=[
211
- {"role": "system", "content": "Du är en expert ChargeNodes produkter och tjänster. Svara enbart baserat på den information som finns i den indexerade datan."},
212
  {"role": "user", "content": prompt}
213
  ],
214
  temperature=0.2,
215
  max_tokens=500
216
  )
217
- answer = response.choices[0].message.content
218
- return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
 
 
 
 
 
 
 
219
  except Exception as e:
220
- return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
 
 
221
 
222
  # --- Slack Integration ---
223
  def send_to_slack(subject, content, color="#2a9d8f"):
224
  """Basfunktion för att skicka meddelanden till Slack."""
225
  webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
226
  if not webhook_url:
227
- print("Slack webhook URL saknas")
228
  return False
229
-
230
  try:
231
  # Formatera meddelandet för Slack
232
  payload = {
233
- "blocks": [
234
- {
235
- "type": "header",
236
- "text": {
237
- "type": "plain_text",
238
- "text": subject
239
- }
240
- },
241
  {
242
- "type": "section",
243
- "text": {
244
- "type": "mrkdwn",
245
- "text": content
246
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
  ]
249
  }
250
-
251
  response = requests.post(
252
  webhook_url,
253
  json=payload,
254
- headers={"Content-Type": "application/json"}
 
255
  )
256
-
257
  if response.status_code == 200:
258
- print(f"Slack-meddelande skickat: {subject}")
259
  return True
260
  else:
261
  print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
262
  return False
 
 
 
263
  except Exception as e:
264
- print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
265
  return False
266
 
267
  # --- Feedback & Like-funktion ---
@@ -273,69 +455,106 @@ def vote(data: gr.LikeData):
273
  """
274
  feedback_type = "up" if data.liked else "down"
275
  global last_log
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  log_entry = {
277
- "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
 
278
  "feedback": feedback_type,
279
- "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
280
  }
281
- # Om global logdata finns, lägg till ytterligare metadata.
282
- if last_log:
 
 
283
  log_entry.update({
284
  "session_id": last_log.get("session_id"),
285
  "user_message": last_log.get("user_message"),
 
 
 
 
286
  })
287
-
 
 
 
 
 
 
288
  # Använd den förbättrade loggfunktionen
289
  safe_append_to_log(log_entry)
290
-
291
  # Skicka feedback till Slack
292
  try:
293
- if feedback_type == "down": # Skicka bara negativ feedback
294
- feedback_message = f"""
295
- *⚠️ Negativ feedback registrerad*
 
 
296
 
297
- *Fråga:* {last_log.get('user_message', 'Okänd fråga')}
 
298
 
299
- *Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''}
300
  """
301
- # Skicka asynkront
302
- threading.Thread(
303
- target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
304
- daemon=True
305
- ).start()
 
306
  except Exception as e:
307
  print(f"Kunde inte skicka feedback till Slack: {e}")
308
-
 
309
  return
310
 
311
  # --- Rapportering ---
312
  def read_logs():
313
  """Läs alla loggposter från loggfilen."""
314
  logs = []
 
 
 
 
315
  try:
316
- if os.path.exists(log_file_path):
317
- with open(log_file_path, "r", encoding="utf-8") as file:
318
- line_count = 0
319
- for line in file:
320
- line_count += 1
321
- try:
322
- log_entry = json.loads(line.strip())
323
- logs.append(log_entry)
324
- except json.JSONDecodeError as e:
325
- print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
326
- continue
327
- print(f"Läste {len(logs)} av {line_count} loggposter")
328
- else:
329
- print(f"Loggfil saknas: {log_file_path}")
330
  except Exception as e:
331
- print(f"Fel vid läsning av loggfil: {e}")
332
  return logs
333
 
334
  def get_latest_conversations(logs, limit=50):
335
- """Hämta de senaste frågorna och svaren."""
336
  conversations = []
 
337
  for log in reversed(logs):
338
- if 'user_message' in log and 'bot_reply' in log:
 
339
  conversations.append({
340
  'user_message': log['user_message'],
341
  'bot_reply': log['bot_reply'],
@@ -346,504 +565,670 @@ def get_latest_conversations(logs, limit=50):
346
  return conversations
347
 
348
  def get_feedback_stats(logs):
349
- """Sammanfatta feedback (tumme upp/ned)."""
350
- feedback_count = {"up": 0, "down": 0}
351
- negative_feedback_examples = []
352
-
353
  for log in logs:
354
- if 'feedback' in log:
 
355
  feedback = log.get('feedback')
356
- if feedback in feedback_count:
357
- feedback_count[feedback] += 1
358
-
359
- # Samla exempel på negativ feedback
360
- if feedback == "down" and 'user_message' in log and len(negative_feedback_examples) < 10:
361
- negative_feedback_examples.append({
362
- 'user_message': log.get('user_message', 'Okänd fråga'),
363
- 'bot_reply': log.get('bot_reply', 'Okänt svar')
364
- })
365
-
 
 
 
 
366
  return feedback_count, negative_feedback_examples
367
 
368
- def generate_monthly_stats(days=30):
369
- """Genererar omfattande statistik över botanvändning för den senaste månaden."""
370
  print(f"Genererar statistik för de senaste {days} dagarna...")
371
-
372
- # Hämta loggar
373
- logs = read_logs()
374
-
375
- if not logs:
376
- return {"error": "Inga loggar hittades för den angivna perioden"}
377
-
378
- # Filtrera på datumintervall
379
  now = datetime.now()
380
  cutoff_date = now - timedelta(days=days)
381
  filtered_logs = []
382
-
383
- for log in logs:
 
384
  if 'timestamp' in log:
385
  try:
 
386
  log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
387
  if log_date >= cutoff_date:
388
  filtered_logs.append(log)
389
- except:
390
- pass # Hoppa över poster med ogiltigt datum
391
-
392
- logs = filtered_logs
393
-
394
- # Basstatistik
395
- total_conversations = sum(1 for log in logs if 'user_message' in log)
396
- unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
397
- unique_users = len(set(log.get('user_id', 'unknown') for log in logs if 'user_id' in log))
398
-
399
- # Feedback-statistik
400
- feedback_logs = [log for log in logs if 'feedback' in log]
401
- positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
402
- negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
403
- feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
404
-
405
- # Svarstidsstatistik
406
- response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
408
-
409
- # Plattformsstatistik
410
  platforms = {}
411
  browsers = {}
412
  operating_systems = {}
413
- for log in logs:
414
- if 'platform' in log:
415
- platforms[log['platform']] = platforms.get(log['platform'], 0) + 1
416
- if 'browser' in log:
417
- browsers[log['browser']] = browsers.get(log['browser'], 0) + 1
418
- if 'os' in log:
419
- operating_systems[log['os']] = operating_systems.get(log['os'], 0) + 1
420
-
421
  # Skapa rapport
422
  report = {
423
- "period": f"Senaste {days} dagarna",
424
- "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
425
  "basic_stats": {
426
  "total_conversations": total_conversations,
427
  "unique_sessions": unique_sessions,
428
- "unique_users": unique_users,
429
- "messages_per_user": round(total_conversations / unique_users, 2) if unique_users else 0
430
  },
431
  "feedback": {
 
432
  "positive": positive_feedback,
433
  "negative": negative_feedback,
434
- "ratio_percent": round(feedback_ratio, 1)
435
  },
436
  "performance": {
437
- "avg_response_time": round(avg_response_time, 2)
438
  },
439
- "platform_distribution": platforms,
440
- "browser_distribution": browsers,
441
- "os_distribution": operating_systems
 
442
  }
443
-
444
  return report
445
 
446
- def simple_status_report():
447
- """Skickar en förenklad statusrapport till Slack."""
448
- print("Genererar statusrapport för Slack...")
449
-
450
- try:
451
- # Generera statistik
452
- stats = generate_monthly_stats(days=7) # Senaste veckan
453
-
454
- # Skapa innehåll för Slack
455
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
456
- subject = f"ChargeNode AI Bot - Status {now}"
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
- # Formatera statistik
463
- basic = stats["basic_stats"]
464
- feedback = stats["feedback"]
465
- perf = stats["performance"]
466
-
467
- content = f"""
468
- *ChargeNode AI Bot - Statusrapport {now}*
469
 
470
- *Basstatistik* (senaste 7 dagarna)
471
- - Totalt antal konversationer: {basic['total_conversations']}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  - Unika sessioner: {basic['unique_sessions']}
473
- - Unika användare: {basic['unique_users']}
474
- - Genomsnittlig svarstid: {perf['avg_response_time']} sekunder
475
 
476
- *Feedback*
477
- - 👍 Tumme upp: {feedback['positive']}
478
- - 👎 Tumme ned: {feedback['negative']}
479
- - Nöjdhet: {feedback['ratio_percent']}%
480
- """
481
-
482
- # Lägg till de senaste konversationerna
483
- logs = read_logs()
484
- conversations = get_latest_conversations(logs, 3)
485
-
486
- if conversations:
487
- content += "\n*Senaste konversationer*\n"
488
- for conv in conversations:
489
- content += f"""
490
- > *Tid:* {conv['timestamp']}
491
- > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
492
- > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
 
493
  """
494
-
495
- # Skicka till Slack
496
- return send_to_slack(subject, content, "#2a9d8f")
497
-
 
 
 
 
 
 
 
 
 
 
 
 
498
  except Exception as e:
499
- print(f"Fel vid generering av statusrapport: {e}")
500
-
501
- # Skicka felmeddelande till Slack
502
- error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
503
- error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
504
- return send_to_slack(error_subject, error_content, "#ff0000")
 
 
 
505
 
 
506
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
507
  """Skickar en supportförfrågan till Slack."""
 
 
 
 
508
  try:
509
  # Formatera chat-historiken
510
  chat_content = ""
511
- for msg in chat_history:
512
- if msg['role'] == 'user':
513
- chat_content += f">*Användare:* {msg['content']}\n\n"
514
- elif msg['role'] == 'assistant':
515
- chat_content += f">*Bot:* {msg['content'][:300]}{'...' if len(msg['content']) > 300 else ''}\n\n"
516
-
 
 
 
 
 
 
 
 
 
 
517
  # Skapa innehåll
518
- subject = f"Support förfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
519
-
 
520
  content = f"""
521
- *Användarinformation*
522
- - *Områdeskod:* {områdeskod or 'Ej angiven'}
523
- - *Uttagsnummer:* {uttagsnummer or 'Ej angiven'}
524
  - *Email:* {email}
525
- - *Tidpunkt:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
 
526
 
527
- *Chatthistorik:*
528
- {chat_content}
529
  """
530
-
531
  # Skicka till Slack
532
- return send_to_slack(subject, content, "#e76f51")
 
 
 
 
533
  except Exception as e:
534
- print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
535
  return False
536
 
537
- # --- Schemaläggning av rapporter ---
 
538
  def run_scheduler():
539
- """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
540
- # Använd den förenklade funktionen för rapportering
541
- schedule.every().day.at("08:00").do(simple_status_report)
542
- schedule.every().day.at("12:00").do(simple_status_report)
543
- schedule.every().day.at("17:00").do(simple_status_report)
544
-
545
- # Veckorapport på måndagar
546
- schedule.every().monday.at("09:00").do(lambda: send_to_slack(
547
- "Veckostatistik",
548
- f"*ChargeNode AI Bot - Veckostatistik*\n\n{json.dumps(generate_monthly_stats(7), indent=2)}",
549
- "#3498db"
550
- ))
551
-
 
 
 
 
 
552
  while True:
553
- schedule.run_pending()
 
 
 
554
  time.sleep(60) # Kontrollera varje minut
555
 
556
- # Starta schemaläggaren i en separat tråd
557
- scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
558
- scheduler_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
  # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
561
- try:
562
- print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
563
- # Anropa inte direkt här - sker i schemaläggaren
564
- except Exception as e:
565
- print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
 
 
 
 
 
566
 
567
  # --- Gradio UI ---
568
- initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
569
 
 
570
  custom_css = """
571
- body {background-color: #f7f7f7; font-family: Arial, sans-serif; margin: 0; padding: 0;}
572
- h1 {font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin-bottom: 0.5em;}
573
- .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;}
574
- #chatbot_conversation { max-height: 300px; overflow-y: auto; }
575
- .message-wrap { scroll-behavior: smooth; }
576
- .message.bot:last-child { scroll-margin-top: 100px; }
577
- .support-form-container { margin-top: 40px; }
578
- .support-form-container .gr-form { margin-top: 15px; }
579
- .gr-button {background-color: #2a9d8f; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; margin: 5px;}
580
- .gr-button:hover {background-color: #264653;}
581
- .support-btn {background-color: #000000; color: #ffffff; margin-top: 5px; margin-bottom: 10px;}
582
- .support-btn:hover {background-color: #333333;}
583
- .flex-row {display: flex; flex-direction: row; gap: 5px;}
584
- .gr-form {padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px;}
585
- .chat-preview {max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 8px; margin-top: 10px; font-size: 12px; background-color: #f9f9f9;}
586
- .success-message {font-size: 16px; font-weight: normal; margin-bottom: 15px;}
587
- /* Dölj Gradio-footer */
588
- footer {display: none !important;}
589
- .footer {display: none !important;}
590
- .gr-footer {display: none !important;}
591
- .gradio-footer {display: none !important;}
592
- .gradio-container .footer {display: none !important;}
593
- .gradio-container .gr-footer {display: none !important;}
594
  """
595
 
596
- with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
597
- 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")
598
-
599
- # Chat interface
600
- with gr.Group(visible=True) as chat_interface:
601
- chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
602
- chatbot.like(vote, None, None)
603
-
604
- with gr.Row():
605
- msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
606
-
607
- with gr.Row():
608
- with gr.Column(scale=1):
609
- clear = gr.Button("Rensa")
610
- with gr.Column(scale=1):
611
- support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
612
-
613
- # Lägg till anpassad JavaScript för att styra scrollning
614
- js_code = """
615
- function scrollToTop() {
616
- const chatContainer = document.querySelector('.gradio-container .message-wrap');
617
- if (chatContainer) {
618
- chatContainer.scrollTop = 0;
619
- }
620
- }
621
-
622
- // Kör funktionen när nya meddelanden läggs till
623
- document.addEventListener('DOMNodeInserted', function(event) {
624
- if (event.target.classList && event.target.classList.contains('bot')) {
625
- setTimeout(scrollToTop, 100);
626
- }
627
- });
628
- """
629
- app.load(js=js_code)
630
-
631
- # Support form interface (initially hidden)
632
- with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
633
- gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
634
-
635
- with gr.Group(elem_classes="gr-form"):
636
- områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
637
- uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
638
- email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
639
-
640
- gr.Markdown("### Chat som skickas till support:")
641
- chat_preview = gr.Markdown(elem_classes="chat-preview")
642
-
643
- with gr.Row():
644
- back_btn = gr.Button("Tillbaka")
645
- send_support_btn = gr.Button("Skicka")
646
-
647
- # Success message (initially hidden)
648
- with gr.Group(visible=False) as success_interface:
649
- gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
650
- back_to_chat_btn = gr.Button("Tillbaka till chatten")
651
-
652
- # Detta bör vara slutet av app.py-filen från funktionen "respond" och nedåt
653
 
654
  def respond(message, chat_history, request: gr.Request):
655
- global last_log
656
- start = time.time()
657
- response = generate_answer(message)
658
- elapsed = round(time.time() - start, 2)
 
 
 
 
 
 
659
 
660
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
661
- session_id = str(uuid.uuid4())
662
-
663
- # Använd session_id från tidigare logg om det finns
664
- if last_log and 'session_id' in last_log:
665
- session_id = last_log.get('session_id')
666
-
667
- user_id = request.client.host if request else "okänd"
668
-
669
- ua_str = request.headers.get("user-agent", "")
670
- ref = request.headers.get("referer", "")
671
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
672
- ua = parse_ua(ua_str)
673
- browser = f"{ua.browser.family} {ua.browser.version_string}"
674
- osys = f"{ua.os.family} {ua.os.version_string}"
675
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
  platform = "webb"
677
- if "chargenode.eu" in ref:
678
- platform = "chargenode.eu"
679
- elif "localhost" in ref:
680
- platform = "test"
681
- elif "app" in ref:
682
- platform = "app"
 
 
 
 
683
 
 
684
  log_data = {
685
  "timestamp": timestamp,
 
686
  "user_id": user_id,
687
  "session_id": session_id,
 
 
 
 
688
  "user_message": message,
689
- "bot_reply": response,
690
- "response_time": elapsed,
691
- "ip": ip,
692
- "browser": browser,
693
- "os": osys,
694
- "platform": platform
695
  }
696
 
697
- # Använd den förbättrade loggfunktionen
698
  safe_append_to_log(log_data)
699
- last_log = log_data
700
 
701
- # Skicka varje konversation direkt till Slack
702
  try:
703
- # Konversationsinnehåll
704
- conversation_content = f"""
705
- *Ny konversation {timestamp}*
706
-
707
  *Användare:* {message}
708
-
709
- *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
710
-
711
- *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
712
  """
713
- # Skicka asynkront för att inte blockera svarstiden
714
  threading.Thread(
715
- target=lambda: send_to_slack(f"Ny konversation", conversation_content),
 
716
  daemon=True
717
  ).start()
718
  except Exception as e:
719
- print(f"Kunde inte skicka konversation till Slack: {e}")
 
720
 
 
721
  chat_history.append({"role": "user", "content": message})
722
- chat_history.append({"role": "assistant", "content": response})
723
-
724
- # Borttaget: gr.javascript("setTimeout(function() { scrollToTop(); }, 100);")
725
- # Vi förlitar oss istället på JavaScript-koden som redan laddats via app.load()
726
-
727
  return "", chat_history
728
 
729
  def format_chat_preview(chat_history):
 
730
  if not chat_history:
731
- return "Ingen chatthistorik att visa."
732
-
733
  preview = ""
734
- for msg in chat_history:
735
- sender = "Användare" if msg["role"] == "user" else "Bot"
736
- content = msg["content"]
737
- if len(content) > 100: # Truncate long messages
738
- content = content[:100] + "..."
739
- preview += f"**{sender}:** {content}\n\n"
740
-
741
- return preview
 
 
 
 
 
 
 
742
 
743
  def show_support_form(chat_history):
744
- preview = format_chat_preview(chat_history)
745
- # Borttaget: gr.javascript("setTimeout(function() { window.scrollTo(0, 0); }, 100);")
 
746
  return {
747
- chat_interface: gr.Group(visible=False),
748
- support_interface: gr.Group(visible=True),
749
- success_interface: gr.Group(visible=False),
750
- chat_preview: preview
751
  }
752
 
753
  def back_to_chat():
 
754
  return {
755
- chat_interface: gr.Group(visible=True),
756
- support_interface: gr.Group(visible=False),
757
- success_interface: gr.Group(visible=False)
758
  }
759
 
760
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
761
- """Hanterar formulärinskickningen med bättre felhantering."""
762
- print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
763
-
764
- # Validera input med tydligare loggning
765
  validation_errors = []
766
-
767
  if områdeskod and not områdeskod.isdigit():
768
- print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
769
- validation_errors.append("Områdeskod måste vara numerisk.")
770
- else:
771
- print(f"Validerar områdeskod: '{områdeskod}' (ok)")
772
-
773
  if uttagsnummer and not uttagsnummer.isdigit():
774
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
775
- validation_errors.append("Uttagsnummer måste vara numerisk.")
776
- else:
777
- print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
778
-
779
  if not email:
780
- print("Validerar email: (saknas)")
781
- validation_errors.append("En giltig e-postadress krävs.")
782
- elif '@' not in email or '.' not in email.split('@')[1]:
783
- print(f"Validerar email: '{email}' (felaktigt format)")
784
- validation_errors.append("En giltig e-postadress krävs.")
785
- else:
786
- print(f"Validerar email: '{email}' (ok)")
787
-
788
- # Om det finns valideringsfel
789
  if validation_errors:
790
- print(f"Valideringsfel: {validation_errors}")
 
 
791
  return {
792
- chat_interface: gr.Group(visible=False),
793
- support_interface: gr.Group(visible=True),
794
- success_interface: gr.Group(visible=False),
795
- chat_preview: "\n".join(["**Fel:**"] + validation_errors)
796
  }
797
-
798
- # Om formuläret klarade valideringen, försök skicka till Slack
799
  try:
800
- print("Försöker skicka supportförfrågan till Slack...")
801
-
802
- # Skapa en förenklad chathistorik för loggning
803
- chat_summary = []
804
- for msg in chat_history:
805
- if 'role' in msg and 'content' in msg:
806
- chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
807
- print(f"Chatthistorik att skicka: {chat_summary}")
808
-
809
- # Skicka till Slack
810
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
811
-
812
  if success:
813
- print("Support-förfrågan skickad till Slack framgångsrikt")
 
814
  return {
815
- chat_interface: gr.Group(visible=False),
816
- support_interface: gr.Group(visible=False),
817
- success_interface: gr.Group(visible=True)
 
818
  }
819
  else:
820
- print("Support-förfrågan till Slack misslyckades")
 
 
821
  return {
822
- chat_interface: gr.Group(visible=False),
823
- support_interface: gr.Group(visible=True),
824
- success_interface: gr.Group(visible=False),
825
- chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
826
  }
827
  except Exception as e:
828
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
 
829
  return {
830
- chat_interface: gr.Group(visible=False),
831
- support_interface: gr.Group(visible=True),
832
- success_interface: gr.Group(visible=False),
833
- chat_preview: f"**Ett fel uppstod: {str(e)}**"
834
  }
835
 
836
- # Eventkopplingar
837
- msg.submit(respond, [msg, chatbot], [msg, chatbot])
838
- clear.click(lambda: None, None, chatbot, queue=False)
839
- support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
840
- back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
841
- back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
842
- send_support_btn.click(
843
- submit_support_form,
844
- [områdeskod, uttagsnummer, email, chatbot],
845
- [chat_interface, support_interface, success_interface, chat_preview]
846
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
 
 
848
  if __name__ == "__main__":
849
- app.launch(share=True)
 
 
 
 
 
 
39
  print(f"Skapade tom loggfil: {log_file_path}")
40
 
41
  hf_token = os.environ.get("HF_TOKEN")
42
+ # Validera HF_TOKEN endast om vi är i Hugging Face-miljön
43
+ if IS_HUGGINGFACE and not hf_token:
44
+ raise ValueError("HF_TOKEN saknas (krävs i Hugging Face-miljön)")
45
 
46
+ # Konfigurera CommitScheduler endast om vi är i Hugging Face-miljön
47
+ if IS_HUGGINGFACE and hf_token:
48
+ scheduler = CommitScheduler(
49
+ repo_id="ChargeNodeEurope/logfiles",
50
+ repo_type="dataset",
51
+ folder_path=log_folder,
52
+ path_in_repo="logs_v2",
53
+ every=300, # Vänta 5 minuter
54
+ token=hf_token
55
+ )
56
+ else:
57
+ scheduler = None # Ingen schemaläggare om inte på Hugging Face
58
+ print("Kör lokalt eller HF_TOKEN saknas, CommitScheduler är inte aktiv.")
59
 
60
 
61
  # --- Globala variabler ---
62
  last_log = None # Sparar loggdata från senaste svar för feedback
63
+ chunks = [] # Definiera globalt så de är tillgängliga
64
+ chunk_sources = [] # Definiera globalt så de är tillgängliga
65
 
66
  # --- Förbättrad loggfunktion ---
67
  def safe_append_to_log(log_entry):
 
69
  try:
70
  # Öppna filen i append-läge
71
  with open(log_file_path, "a", encoding="utf-8") as log_file:
72
+ log_json = json.dumps(log_entry, ensure_ascii=False) # ensure_ascii=False för svenska tecken
73
  log_file.write(log_json + "\n")
74
  log_file.flush() # Säkerställ att data skrivs till disk omedelbart
75
+
76
+ # print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}") # Kan bli för mycket output
77
  return True
78
+
79
  except Exception as e:
80
  print(f"Fel vid loggning: {e}")
81
+
82
  # Försök skapa mappen om den inte finns
83
  try:
84
  os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
85
+
86
  # Försök igen
87
  with open(log_file_path, "a", encoding="utf-8") as log_file:
88
+ log_json = json.dumps(log_entry, ensure_ascii=False)
89
  log_file.write(log_json + "\n")
90
+
91
  print("Loggpost tillagd efter återhämtning")
92
  return True
93
+
94
  except Exception as retry_error:
95
  print(f"Kritiskt fel vid loggning: {retry_error}")
96
  return False
 
99
  def load_local_files():
100
  uploaded_text = ""
101
  allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
102
+ # Förenklad exkluderingslista för att undvika problem med exakta filnamn
103
+ excluded_prefixes = ["requirements", "app", "conversation_log", "secrets"]
104
+ current_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.' # Hämta aktuell mapp
105
+
106
+ for item in os.listdir(current_dir):
107
+ # Kontrollera om det är en fil och inte en katalog
108
+ item_path = os.path.join(current_dir, item)
109
+ if not os.path.isfile(item_path):
110
+ continue
111
+
112
+ if item.lower().endswith(tuple(allowed)) and not any(item.lower().startswith(prefix) for prefix in excluded_prefixes):
113
+ print(f"Läser fil: {item}")
114
  try:
115
+ if item.endswith(".txt"):
116
+ with open(item_path, "r", encoding="utf-8") as f:
117
  content = f.read()
118
+ elif item.endswith(".docx"):
119
+ try:
120
+ from docx import Document # Import sker vid behov
121
+ content = "\n".join([p.text for p in Document(item_path).paragraphs])
122
+ except ImportError:
123
+ print("Varning: 'python-docx' behövs för att läsa .docx-filer. Installera med 'pip install python-docx'.")
124
+ content = ""
125
+ elif item.endswith(".pdf"):
126
+ try:
127
+ import PyPDF2 # Import sker vid behov
128
+ content = ""
129
+ with open(item_path, "rb") as f:
130
+ # Korrigerad PdfReader anrop
131
+ reader = PyPDF2.PdfReader(f)
132
+ for page in reader.pages:
133
+ text = page.extract_text()
134
+ if text: # Lägg bara till om text extraherades
135
+ content += text + "\n"
136
+ except ImportError:
137
+ print("Varning: 'PyPDF2' behövs för att läsa .pdf-filer. Installera med 'pip install pypdf2'.")
138
+ content = ""
139
+ except Exception as pdf_error: # Fånga specifika PDF-läsningsfel
140
+ print(f"Fel vid läsning av PDF {item}: {str(pdf_error)}")
141
+ content = ""
142
+ elif item.endswith(".csv"):
143
+ try:
144
+ content = pd.read_csv(item_path).to_string()
145
+ except pd.errors.EmptyDataError:
146
+ print(f"Varning: CSV-filen {item} är tom.")
147
+ content = ""
148
+ except Exception as csv_err:
149
+ print(f"Fel vid läsning av CSV {item}: {csv_err}")
150
+ content = ""
151
+ elif item.endswith((".xls", ".xlsx")):
152
+ try:
153
+ if item == "FAQ stadat.xlsx": # Hårdkodat filnamn - kan vara skört
154
+ df = pd.read_excel(item_path)
155
+ rows = []
156
+ # Säkerställ att kolumnerna finns
157
+ if 'Fråga' in df.columns and 'Svar' in df.columns:
158
+ for index, row in df.iterrows():
159
+ rows.append(f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}")
160
+ content = "\n\n".join(rows)
161
+ else:
162
+ print(f"Varning: Kolumner 'Fråga' eller 'Svar' saknas i {item}. Läser som vanlig Excel.")
163
+ content = pd.read_excel(item_path).to_string()
164
+ else:
165
+ content = pd.read_excel(item_path).to_string()
166
+ except Exception as excel_err:
167
+ print(f"Fel vid läsning av Excel {item}: {excel_err}")
168
+ content = ""
169
+
170
+ if content: # Lägg bara till om innehåll finns
171
+ uploaded_text += f"\n\nFIL: {item}\n{content}"
172
+
173
  except Exception as e:
174
+ print(f"Allmänt fel vid läsning av {item}: {str(e)}")
175
  return uploaded_text.strip()
176
 
177
  def load_prompt():
178
  try:
179
+ prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.', "prompt.txt")
180
+ with open(prompt_path, "r", encoding="utf-8") as f:
181
  return f.read().strip()
182
+ except FileNotFoundError:
183
+ print(f"Varning: prompt.txt hittades inte. Använder tom prompt.")
184
+ return ""
185
  except Exception as e:
186
+ print(f"Fel vid läsning av prompt.txt: {e}")
187
  return ""
188
 
189
  prompt_template = load_prompt()
190
 
191
  # Förbered textsegment
192
  def prepare_chunks(text_data):
193
+ global chunks, chunk_sources # Uppdatera globala variabler
194
  chunks, sources = [], []
195
  for source, text in text_data.items():
196
+ if not isinstance(text, str): # Säkerställ att text är en sträng
197
+ print(f"Varning: Ogiltig textdata för källa '{source}'. Hoppar över.")
198
+ continue
199
+ # Dela på dubbla radbrytningar först, sedan enkla
200
+ paragraphs = [p.strip() for p in text.replace('\n\n', '---PARAGRAPH---').replace('\n', ' ').split('---PARAGRAPH---') if p.strip()]
201
+ current_chunk = ""
202
  for para in paragraphs:
203
+ if len(current_chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
204
+ current_chunk += (" " + para if current_chunk else para)
205
  else:
206
+ # Lägg till föregående chunk om den inte är tom
207
+ if current_chunk:
208
+ chunks.append(current_chunk)
209
+ sources.append(source)
210
+ # Starta en ny chunk (se till att den inte överskrider maxstorleken direkt)
211
+ if len(para) <= MAX_CHUNK_SIZE:
212
+ current_chunk = para
213
+ else:
214
+ # Om en enskild paragraf är för lång, dela upp den
215
+ # (Enkel strategi: dela vid MAX_CHUNK_SIZE)
216
+ chunks.append(para[:MAX_CHUNK_SIZE])
217
  sources.append(source)
218
+ # Hantera resten av paragrafen i nästa iteration (om det behövs mer logik)
219
+ current_chunk = para[MAX_CHUNK_SIZE:] # Eller hantera detta mer robust
220
+
221
+ # Lägg till den sista chunken om den inte är tom
222
+ if current_chunk:
223
+ chunks.append(current_chunk)
224
  sources.append(source)
 
225
 
226
+ # Uppdatera de globala variablerna
227
+ chunk_sources = sources
228
+ return chunks, sources # Returnera också för säkerhets skull
229
+
230
+ # Lazy-laddning av SentenceTransformer
231
  embedder = None
232
  embeddings = None
233
  index = None
 
235
  def initialize_embeddings():
236
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
237
  global embedder, embeddings, index, chunks, chunk_sources
238
+
239
+ # Kör endast om embedder inte redan är initierad
240
  if embedder is None:
241
  print("Initierar SentenceTransformer och FAISS-index...")
242
  # Ladda och förbered lokal data
243
  print("Laddar textdata...")
244
  text_data = {"local_files": load_local_files()}
245
+ if not text_data["local_files"]:
246
+ print("Varning: Ingen lokal textdata hittades att indexera.")
247
+ # Sätt tomma listor för att undvika fel senare
248
+ chunks = []
249
+ chunk_sources = []
250
+ embeddings = np.array([]) # Tom numpy array
251
+ # Skapa ett tomt index om inga chunks finns
252
+ embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda modellen ändå
253
+ # FAISS kräver minst en vektor, eller dimensionen
254
+ dimension = embedder.get_sentence_embedding_dimension()
255
+ index = faiss.IndexFlatIP(dimension)
256
+ print("FAISS-index skapat (tomt).")
257
+ return # Avsluta tidigt
258
+
259
  print("Förbereder textsegment...")
260
+ chunks, chunk_sources = prepare_chunks(text_data) # Detta uppdaterar globala chunks/chunk_sources
261
+ print(f"{len(chunks)} segment förberedda")
262
+
263
+ if not chunks:
264
+ print("Varning: Inga textsegment skapades från filerna.")
265
+ embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda modellen ändå
266
+ dimension = embedder.get_sentence_embedding_dimension()
267
+ index = faiss.IndexFlatIP(dimension)
268
+ embeddings = np.array([])
269
+ print("FAISS-index skapat (tomt).")
270
+ return
271
 
272
  print("Skapar embeddings...")
273
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
274
+ try:
275
+ embeddings = embedder.encode(chunks, convert_to_numpy=True, show_progress_bar=True)
276
+ # Normalisera embeddings
277
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
278
+ # Undvik division med noll om någon norm är noll
279
+ norms[norms == 0] = 1e-10
280
+ embeddings /= norms
281
+
282
+ if embeddings.shape[0] > 0:
283
+ dimension = embeddings.shape[1]
284
+ index = faiss.IndexFlatIP(dimension)
285
+ index.add(embeddings.astype(np.float32)) # FAISS föredrar float32
286
+ print(f"FAISS-index skapat med {index.ntotal} vektorer.")
287
+ else:
288
+ print("Inga embeddings skapades, skapar tomt FAISS-index.")
289
+ dimension = embedder.get_sentence_embedding_dimension()
290
+ index = faiss.IndexFlatIP(dimension)
291
+
292
+ except Exception as e:
293
+ print(f"Fel vid skapande av embeddings eller FAISS-index: {e}")
294
+ # Försök återställa till ett tomt men funktionellt tillstånd
295
+ embedder = SentenceTransformer('all-MiniLM-L6-v2')
296
+ dimension = embedder.get_sentence_embedding_dimension()
297
+ index = faiss.IndexFlatIP(dimension)
298
+ embeddings = np.array([])
299
+ chunks = []
300
+ chunk_sources = []
301
 
302
  def retrieve_context(query, k=RETRIEVAL_K):
303
  """Hämtar relevant kontext för frågor."""
304
+ initialize_embeddings() # Säkerställ att allt är laddat
305
+
306
+ # Kontrollera om indexet är tomt eller om nödvändiga objekt saknas
307
+ if index is None or index.ntotal == 0 or embedder is None or not chunks:
308
+ print("Varning: Embeddings eller index är inte redo. Kan inte hämta kontext.")
309
+ return "", []
310
+
311
+ try:
312
+ query_embedding = embedder.encode([query], convert_to_numpy=True)
313
+ # Normalisera query_embedding
314
+ norm = np.linalg.norm(query_embedding)
315
+ if norm == 0: norm = 1e-10 # Undvik division med noll
316
+ query_embedding /= norm
317
+
318
+ # Säkerställ att k inte är större än antalet vektorer i indexet
319
+ actual_k = min(k, index.ntotal)
320
+ if actual_k == 0:
321
+ return "", []
322
+
323
+ # Sök i indexet
324
+ D, I = index.search(query_embedding.astype(np.float32), actual_k)
325
+
326
+ retrieved, sources = [], set()
327
+ if I.size > 0: # Kontrollera att sökningen gav resultat
328
+ for idx in I[0]:
329
+ # Säkerställ att indexet är giltigt
330
+ if 0 <= idx < len(chunks):
331
+ retrieved.append(chunks[idx])
332
+ # Säkerställ att chunk_sources har samma längd
333
+ if idx < len(chunk_sources):
334
+ sources.add(chunk_sources[idx])
335
+ else:
336
+ print(f"Varning: Ogiltigt index {idx} returnerat från FAISS.")
337
+
338
+ return " ".join(retrieved), list(sources)
339
+ except Exception as e:
340
+ print(f"Fel vid hämtning av kontext: {e}")
341
+ return "", []
342
+
343
 
344
  def generate_answer(query):
345
  """Genererar svar baserat på fråga och kontextinformation."""
346
+ start_time = time.time()
347
  context, sources = retrieve_context(query)
348
+ retrieval_time = time.time() - start_time
349
+
350
+ # Om ingen kontext hittades, ge ett standardmeddelande
351
  if not context.strip():
352
+ print("Ingen relevant kontext hittades.")
353
+ # Överväg att låta GPT försöka svara ändå, eller returnera direkt
354
+ # return "Jag kunde inte hitta någon specifik information om det i mina källor. Kan du omformulera frågan?\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
355
+
356
+ # Bygg prompten även om kontext är tom (GPT kan ha allmän kunskap)
357
  prompt = f"""{prompt_template}
358
+
359
  Relevant kontext:
360
+ {context if context.strip() else "Ingen specifik kontext hittades i de lokala dokumenten."}
361
+
362
  Fråga: {query}
363
+
364
+ Svar (baserat på tillgänglig information):"""
365
+
366
  try:
367
+ start_time_gpt = time.time()
368
  response = client.chat.completions.create(
369
  model="gpt-3.5-turbo",
370
  messages=[
371
+ {"role": "system", "content": "Du är en hjälpsam assistent för ChargeNode. Basera ditt svar primärt på den 'Relevanta kontexten' om den finns. Om ingen kontext finns, eller om frågan inte täcks av kontexten, använd din allmänna kunskap men ange att svaret är generellt."},
372
  {"role": "user", "content": prompt}
373
  ],
374
  temperature=0.2,
375
  max_tokens=500
376
  )
377
+ gpt_time = time.time() - start_time_gpt
378
+ answer = response.choices[0].message.content.strip()
379
+
380
+ # Lägg till standard-disclaimer
381
+ answer += "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
382
+
383
+ print(f"Svar genererat. Retrieval: {retrieval_time:.2f}s, GPT: {gpt_time:.2f}s")
384
+ return answer
385
+
386
  except Exception as e:
387
+ print(f"Fel vid anrop till OpenAI API: {e}")
388
+ return f"Ett tekniskt fel uppstod när jag försökte generera ett svar ({type(e).__name__}). Försök igen senare.\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
389
+
390
 
391
  # --- Slack Integration ---
392
  def send_to_slack(subject, content, color="#2a9d8f"):
393
  """Basfunktion för att skicka meddelanden till Slack."""
394
  webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
395
  if not webhook_url:
396
+ print("Slack webhook URL saknas, kan inte skicka meddelande.")
397
  return False
398
+
399
  try:
400
  # Formatera meddelandet för Slack
401
  payload = {
402
+ "attachments": [
 
 
 
 
 
 
 
403
  {
404
+ "color": color,
405
+ "blocks": [
406
+ {
407
+ "type": "header",
408
+ "text": {
409
+ "type": "plain_text",
410
+ "text": subject,
411
+ "emoji": True
412
+ }
413
+ },
414
+ {
415
+ "type": "divider"
416
+ },
417
+ {
418
+ "type": "section",
419
+ "text": {
420
+ "type": "mrkdwn",
421
+ "text": content
422
+ }
423
+ }
424
+ ]
425
  }
426
  ]
427
  }
428
+
429
  response = requests.post(
430
  webhook_url,
431
  json=payload,
432
+ headers={"Content-Type": "application/json"},
433
+ timeout=10 # Lägg till timeout
434
  )
435
+
436
  if response.status_code == 200:
437
+ # print(f"Slack-meddelande skickat: {subject}") # Kan bli för mycket output
438
  return True
439
  else:
440
  print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
441
  return False
442
+ except requests.exceptions.RequestException as e:
443
+ print(f"Nätverksfel vid sändning till Slack: {type(e).__name__}: {e}")
444
+ return False
445
  except Exception as e:
446
+ print(f"Allmänt fel vid sändning till Slack: {type(e).__name__}: {e}")
447
  return False
448
 
449
  # --- Feedback & Like-funktion ---
 
455
  """
456
  feedback_type = "up" if data.liked else "down"
457
  global last_log
458
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
459
+
460
+ # Försök extrahera bot-svaret från LikeData
461
+ bot_reply = ""
462
+ if isinstance(data.value, str):
463
+ bot_reply = data.value
464
+ elif isinstance(data.value, dict) and "value" in data.value:
465
+ bot_reply = data.value["value"]
466
+ else:
467
+ print(f"Varning: Kunde inte extrahera bot_reply från LikeData: {data.value}")
468
+ bot_reply = "[Kunde inte extrahera svar]"
469
+
470
+
471
  log_entry = {
472
+ "timestamp": timestamp,
473
+ "event_type": "feedback", # Tydliggör typ av loggpost
474
  "feedback": feedback_type,
475
+ "bot_reply_liked": bot_reply # Namnge tydligare vad det är
476
  }
477
+
478
+ # Om global logdata finns från senaste *konversationen*, lägg till metadata.
479
+ # Detta antar att 'like' sker direkt efter ett svar.
480
+ if last_log and last_log.get("event_type") == "conversation":
481
  log_entry.update({
482
  "session_id": last_log.get("session_id"),
483
  "user_message": last_log.get("user_message"),
484
+ "associated_bot_reply": last_log.get("bot_reply"), # Spara hela ursprungliga svaret
485
+ "user_id": last_log.get("user_id"),
486
+ "ip": last_log.get("ip"),
487
+ "platform": last_log.get("platform")
488
  })
489
+ else:
490
+ print("Varning: Kunde inte koppla feedback till föregående konversation (last_log saknas eller är av fel typ).")
491
+ # Lägg till fallback-värden om möjligt
492
+ log_entry["session_id"] = "unknown"
493
+ log_entry["user_message"] = "[Okänd fråga]"
494
+
495
+
496
  # Använd den förbättrade loggfunktionen
497
  safe_append_to_log(log_entry)
498
+
499
  # Skicka feedback till Slack
500
  try:
501
+ feedback_subject = f"Feedback: {'👍 Positiv' if feedback_type == 'up' else '👎 Negativ'}"
502
+ feedback_message = f"""
503
+ *Feedback mottagen ({timestamp})*
504
+
505
+ *Typ:* {"👍 Tumme upp" if feedback_type == 'up' else '👎 Tumme ned'}
506
 
507
+ *Associerad fråga:* {log_entry.get('user_message', '[Okänd fråga]')[:500]}...
508
+ *Botens svar (som fick feedback):* {log_entry.get('bot_reply_liked', '[Okänt svar]')[:500]}...
509
 
510
+ *Session:* {log_entry.get('session_id', 'okänd')} | *Plattform:* {log_entry.get('platform', 'okänd')}
511
  """
512
+ # Skicka asynkront för att inte blockera
513
+ threading.Thread(
514
+ target=send_to_slack,
515
+ args=(feedback_subject, feedback_message, "#2a9d8f" if feedback_type == 'up' else "#ff0000"),
516
+ daemon=True
517
+ ).start()
518
  except Exception as e:
519
  print(f"Kunde inte skicka feedback till Slack: {e}")
520
+
521
+ # Gradio förväntar sig ingen retur från like-funktionen
522
  return
523
 
524
  # --- Rapportering ---
525
  def read_logs():
526
  """Läs alla loggposter från loggfilen."""
527
  logs = []
528
+ if not os.path.exists(log_file_path):
529
+ print(f"Loggfil saknas: {log_file_path}")
530
+ return logs
531
+
532
  try:
533
+ with open(log_file_path, "r", encoding="utf-8") as file:
534
+ line_count = 0
535
+ for line in file:
536
+ line_count += 1
537
+ line = line.strip()
538
+ if not line: # Hoppa över tomma rader
539
+ continue
540
+ try:
541
+ log_entry = json.loads(line)
542
+ logs.append(log_entry)
543
+ except json.JSONDecodeError as e:
544
+ print(f"Varning: Kunde inte tolka JSON på rad {line_count}: {e}. Radinnehåll: '{line[:100]}...'")
545
+ continue
546
+ print(f"Läste {len(logs)} giltiga loggposter från {line_count} rader.")
547
  except Exception as e:
548
+ print(f"Allvarligt fel vid läsning av loggfil {log_file_path}: {e}")
549
  return logs
550
 
551
  def get_latest_conversations(logs, limit=50):
552
+ """Hämta de senaste frågorna och svaren från konversationsloggar."""
553
  conversations = []
554
+ # Iterera baklänges för att få de senaste först
555
  for log in reversed(logs):
556
+ # Filtrera loggar av typen 'conversation'
557
+ if log.get('event_type') == 'conversation' and 'user_message' in log and 'bot_reply' in log:
558
  conversations.append({
559
  'user_message': log['user_message'],
560
  'bot_reply': log['bot_reply'],
 
565
  return conversations
566
 
567
  def get_feedback_stats(logs):
568
+ """Sammanfatta feedback (tumme upp/ned) från feedbackloggar."""
569
+ feedback_count = {"up": 0, "down": 0, "unknown": 0}
570
+ negative_feedback_examples = [] # Hämta exempel på negativ feedback
571
+
572
  for log in logs:
573
+ # Filtrera på loggar av typen 'feedback'
574
+ if log.get('event_type') == 'feedback' and 'feedback' in log:
575
  feedback = log.get('feedback')
576
+ if feedback == "up":
577
+ feedback_count["up"] += 1
578
+ elif feedback == "down":
579
+ feedback_count["down"] += 1
580
+ # Samla exempel negativ feedback (max 10 st)
581
+ if len(negative_feedback_examples) < 10:
582
+ negative_feedback_examples.append({
583
+ 'user_message': log.get('user_message', '[Okänd fråga]'),
584
+ 'bot_reply_liked': log.get('bot_reply_liked', '[Okänt svar]')
585
+ })
586
+ else:
587
+ feedback_count["unknown"] +=1
588
+
589
+
590
  return feedback_count, negative_feedback_examples
591
 
592
+ def generate_periodic_stats(days=30):
593
+ """Genererar statistik över botanvändning för den angivna perioden."""
594
  print(f"Genererar statistik för de senaste {days} dagarna...")
595
+
596
+ all_logs = read_logs()
597
+ if not all_logs:
598
+ print("Inga loggar hittades.")
599
+ return {"error": "Inga loggdata tillgängliga."}
600
+
601
+ # Filtrera loggar baserat på tidsstämpel
 
602
  now = datetime.now()
603
  cutoff_date = now - timedelta(days=days)
604
  filtered_logs = []
605
+ invalid_date_count = 0
606
+
607
+ for log in all_logs:
608
  if 'timestamp' in log:
609
  try:
610
+ # Försök tolka datumet - var flexibel med format om nödvändigt
611
  log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
612
  if log_date >= cutoff_date:
613
  filtered_logs.append(log)
614
+ except ValueError:
615
+ # Försök med annat vanligt format om det första misslyckas
616
+ try:
617
+ log_date = datetime.fromisoformat(log['timestamp']) # För ISO 8601
618
+ if log_date >= cutoff_date:
619
+ filtered_logs.append(log)
620
+ except ValueError:
621
+ invalid_date_count += 1
622
+ else:
623
+ invalid_date_count += 1 # Räkna även loggar som saknar tidsstämpel
624
+
625
+ if invalid_date_count > 0:
626
+ print(f"Varning: {invalid_date_count} loggposter ignorerades grund av saknad eller ogiltig tidsstämpel.")
627
+
628
+ if not filtered_logs:
629
+ return {"error": f"Inga loggar hittades för de senaste {days} dagarna."}
630
+
631
+ # Separera loggtyper
632
+ conv_logs = [log for log in filtered_logs if log.get('event_type') == 'conversation']
633
+ feedback_logs = [log for log in filtered_logs if log.get('event_type') == 'feedback']
634
+
635
+ # --- Beräkna statistik ---
636
+ total_conversations = len(conv_logs)
637
+ unique_sessions = len(set(log.get('session_id', 'unknown') for log in conv_logs if log.get('session_id')))
638
+ # User ID kan vara mindre tillförlitligt (baserat på IP), men vi inkluderar det
639
+ unique_users = len(set(log.get('user_id', 'unknown') for log in conv_logs if log.get('user_id')))
640
+
641
+ # Feedback
642
+ feedback_stats, neg_examples = get_feedback_stats(feedback_logs) # Använd filtrerade feedbackloggar
643
+ positive_feedback = feedback_stats.get("up", 0)
644
+ negative_feedback = feedback_stats.get("down", 0)
645
+ total_feedback = positive_feedback + negative_feedback
646
+ feedback_ratio = (positive_feedback / total_feedback * 100) if total_feedback > 0 else 0
647
+
648
+ # Svarstid (från konversationsloggar)
649
+ response_times = [log.get('response_time', 0) for log in conv_logs if 'response_time' in log]
650
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
651
+
652
+ # Plattform, Browser, OS (från konversationsloggar)
653
  platforms = {}
654
  browsers = {}
655
  operating_systems = {}
656
+ for log in conv_logs:
657
+ p = log.get('platform', 'Okänd')
658
+ b = log.get('browser', 'Okänd')
659
+ o = log.get('os', 'Okänd')
660
+ platforms[p] = platforms.get(p, 0) + 1
661
+ browsers[b] = browsers.get(b, 0) + 1
662
+ operating_systems[o] = operating_systems.get(o, 0) + 1
663
+
664
  # Skapa rapport
665
  report = {
666
+ "period": f"Senaste {days} dagarna ({cutoff_date.strftime('%Y-%m-%d')} - {now.strftime('%Y-%m-%d')})",
667
+ "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"),
668
  "basic_stats": {
669
  "total_conversations": total_conversations,
670
  "unique_sessions": unique_sessions,
671
+ "unique_users (estimated)": unique_users,
672
+ "messages_per_session": round(total_conversations / unique_sessions, 2) if unique_sessions else 0
673
  },
674
  "feedback": {
675
+ "total_feedback_entries": total_feedback,
676
  "positive": positive_feedback,
677
  "negative": negative_feedback,
678
+ "satisfaction_rate_percent": round(feedback_ratio, 1)
679
  },
680
  "performance": {
681
+ "avg_response_time_seconds": round(avg_response_time, 2)
682
  },
683
+ "top_platforms": dict(sorted(platforms.items(), key=lambda item: item[1], reverse=True)[:5]),
684
+ "top_browsers": dict(sorted(browsers.items(), key=lambda item: item[1], reverse=True)[:5]),
685
+ "top_os": dict(sorted(operating_systems.items(), key=lambda item: item[1], reverse=True)[:5]),
686
+ "negative_feedback_examples": neg_examples # Lägg till exempel
687
  }
688
+
689
  return report
690
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
 
692
+ def format_report_for_slack(stats_dict):
693
+ """Formaterar statistikrapporten för läsbarhet i Slack."""
694
+ if 'error' in stats_dict:
695
+ return f"*Fel vid generering av statistik:* {stats_dict['error']}"
696
+
697
+ basic = stats_dict["basic_stats"]
698
+ feedback = stats_dict["feedback"]
699
+ perf = stats_dict["performance"]
700
+ platforms = stats_dict["top_platforms"]
701
+ browsers = stats_dict["top_browsers"]
702
+ osys = stats_dict["top_os"]
703
+ neg_examples = stats_dict["negative_feedback_examples"]
704
+
705
+ # Formatera listor snyggt
706
+ def format_dict(d):
707
+ return "\n".join([f"- {k}: {v}" for k, v in d.items()]) if d else "Ingen data"
708
+
709
+ # Formatera negativa exempel
710
+ neg_example_str = "\n".join([
711
+ f"- Fråga: {ex['user_message'][:80]}...\n Svar: {ex['bot_reply_liked'][:80]}..."
712
+ for ex in neg_examples
713
+ ]) if neg_examples else "Inga exempel registrerade."
714
+
715
+
716
+ content = f"""
717
+ *Period:* {stats_dict.get('period', 'Okänd')} (Genererad: {stats_dict.get('generated_at', 'Okänd')})
718
+
719
+ *📊 Användning*
720
+ - Konversationer: {basic['total_conversations']}
721
  - Unika sessioner: {basic['unique_sessions']}
722
+ - Uppskattade unika användare: {basic['unique_users (estimated)']}
723
+ - Meddelanden/session: {basic['messages_per_session']}
724
 
725
+ *👍👎 Feedback*
726
+ - Totalt antal feedback: {feedback['total_feedback_entries']}
727
+ - Positiv: {feedback['positive']} | Negativ: {feedback['negative']}
728
+ - Nöjdhet: {feedback['satisfaction_rate_percent']}%
729
+
730
+ *⚡ Prestanda*
731
+ - Genomsnittlig svarstid: {perf['avg_response_time_seconds']} sekunder
732
+
733
+ *💻 Teknik (Topp 5)*
734
+ *Plattformar:*
735
+ {format_dict(platforms)}
736
+ *Webbläsare:*
737
+ {format_dict(browsers)}
738
+ *Operativsystem:*
739
+ {format_dict(osys)}
740
+
741
+ *📉 Exempel negativ feedback (senaste):*
742
+ {neg_example_str}
743
  """
744
+ return content.strip()
745
+
746
+ def send_status_report(days=7, report_type="Daglig"):
747
+ """Genererar och skickar en statusrapport till Slack."""
748
+ print(f"Genererar {report_type.lower()} statusrapport för Slack...")
749
+ report_title = f"ChargeNode AI Bot - {report_type} Statusrapport"
750
+
751
+ try:
752
+ stats = generate_periodic_stats(days=days)
753
+ slack_content = format_report_for_slack(stats)
754
+ color = "#3498db" # Blå för vanlig rapport
755
+ if "error" in stats:
756
+ color = "#ff0000" # Röd vid fel
757
+
758
+ send_to_slack(report_title, slack_content, color)
759
+
760
  except Exception as e:
761
+ print(f"Allvarligt fel vid generering/sändning av {report_type.lower()} statusrapport: {e}")
762
+ # Försök skicka ett felmeddelande till Slack
763
+ try:
764
+ error_subject = f"🚨 Fel: {report_title}"
765
+ error_content = f"Kunde inte generera eller skicka statusrapporten.\n*Felmeddelande:* `{type(e).__name__}: {e}`"
766
+ send_to_slack(error_subject, error_content, "#ff0000")
767
+ except Exception as slack_err:
768
+ print(f"Kunde inte ens skicka felmeddelande till Slack: {slack_err}")
769
+
770
 
771
+ # --- Supportformulär till Slack ---
772
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
773
  """Skickar en supportförfrågan till Slack."""
774
+ if not email: # Grundläggande validering
775
+ print("Fel: Email saknas för supportförfrågan.")
776
+ return False # Returnera False om email saknas
777
+
778
  try:
779
  # Formatera chat-historiken
780
  chat_content = ""
781
+ # Ta med de senaste N meddelandena (t.ex. 10 senaste)
782
+ history_limit = 10
783
+ start_index = max(0, len(chat_history) - history_limit)
784
+ for msg in chat_history[start_index:]:
785
+ # Säkerställ att msg är en dict med 'role' och 'content'
786
+ if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
787
+ role = msg.get('role', 'unknown').capitalize()
788
+ content = msg.get('content', '')
789
+ # Truncate långa meddelanden för Slack
790
+ max_len = 300
791
+ display_content = (content[:max_len] + '...') if len(content) > max_len else content
792
+ chat_content += f"*{role}:* {display_content}\n"
793
+ else:
794
+ chat_content += f"*Okänt format:* `{str(msg)[:100]}`\n"
795
+
796
+
797
  # Skapa innehåll
798
+ subject = f"📩 Supportförfrågan via Chatbot"
799
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
800
+
801
  content = f"""
802
+ *Ny supportförfrågan* ({timestamp})
803
+
804
+ *👤 Användarinformation*
805
  - *Email:* {email}
806
+ - *Områdeskod:* {områdeskod or '_Ej angiven_'}
807
+ - *Uttagsnummer:* {uttagsnummer or '_Ej angiven_'}
808
 
809
+ *💬 Chatthistorik (senaste {history_limit} meddelandena):*
810
+ {chat_content or "_Ingen historik tillgänglig_"}
811
  """
812
+
813
  # Skicka till Slack
814
+ success = send_to_slack(subject, content.strip(), "#e76f51") # Orange färg för support
815
+ if not success:
816
+ print("Misslyckades med att skicka supportförfrågan till Slack via API.")
817
+ return success # Returnera True/False baserat på Slack-sändning
818
+
819
  except Exception as e:
820
+ print(f"Allvarligt fel vid sändning av support till Slack: {type(e).__name__}: {e}")
821
  return False
822
 
823
+
824
+ # --- Schemaläggning av rapporter ---
825
  def run_scheduler():
826
+ """Kör schemaläggaren i en separat tråd."""
827
+ print("Startar schemaläggningstråd för rapporter...")
828
+
829
+ # Dagliga rapporter (skicka för föregående dag)
830
+ schedule.every().day.at("08:00").do(lambda: send_status_report(days=1, report_type="Daglig"))
831
+
832
+ # Veckorapport på måndagar (för föregående 7 dagar)
833
+ schedule.every().monday.at("09:00").do(lambda: send_status_report(days=7, report_type="Veckovis"))
834
+
835
+ # Månadsrapport den första dagen i månaden (för föregående ~30 dagar)
836
+ # schedule.every().month.at("10:00").do(lambda: send_status_report(days=30, report_type="Månadsvis")) # Kan bli mycket data
837
+ # Kör istället första dagen i månaden
838
+ schedule.every().day.at("10:00").do(lambda: monthly_report_if_first_day())
839
+
840
+ print("Schema konfigurerat:")
841
+ for job in schedule.get_jobs():
842
+ print(f"- {job}")
843
+
844
  while True:
845
+ try:
846
+ schedule.run_pending()
847
+ except Exception as e:
848
+ print(f"Fel i schemaläggningsloopen: {e}")
849
  time.sleep(60) # Kontrollera varje minut
850
 
851
+ def monthly_report_if_first_day():
852
+ """Kör månadsrapport endast om det är den första dagen i månaden."""
853
+ if datetime.now().day == 1:
854
+ print("Första dagen i månaden, kör månadsrapport...")
855
+ send_status_report(days=30, report_type="Månadsvis")
856
+ else:
857
+ # print("Inte första dagen i månaden, hoppar över månadsrapport.") # Behövs ej loggas varje dag
858
+ pass
859
+
860
+
861
+ # Starta schemaläggaren endast om Slack Webhook finns
862
+ if os.environ.get("SLACK_WEBHOOK_URL"):
863
+ scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
864
+ scheduler_thread.start()
865
+ else:
866
+ print("Ingen SLACK_WEBHOOK_URL hittades, schemalagda rapporter kommer inte att köras.")
867
 
868
  # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
869
+ # Gör detta i en separat tråd för att inte blockera start
870
+ def initial_startup_report():
871
+ # Vänta några sekunder att appen hinner starta helt
872
+ time.sleep(10)
873
+ print("Skickar en inledande statusrapport (senaste 24h) för att verifiera Slack-integrationen...")
874
+ send_status_report(days=1, report_type="Uppstart")
875
+
876
+ if os.environ.get("SLACK_WEBHOOK_URL"):
877
+ threading.Thread(target=initial_startup_report, daemon=True).start()
878
+
879
 
880
  # --- Gradio UI ---
881
+ initial_chat = [{"role": "assistant", "content": "Hej! Jag är ChargeNodes AI-assistent. Hur kan jag hjälpa dig idag?"}]
882
 
883
+ # Mer robust CSS
884
  custom_css = """
885
+ body { font-family: sans-serif; margin: 0; padding: 0; background-color: #f0f0f0; }
886
+ .gradio-container { max-width: 450px !important; margin: 0 auto; /* Centrera om inte fixerad */ /* position: fixed; bottom: 20px; right: 20px; */ box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-radius: 12px; background-color: #ffffff; overflow: hidden; border: 1px solid #e0e0e0; }
887
+ h1 { font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin: 1em 0 0.5em 0; font-size: 1.5em; }
888
+ #chatbot_conversation .message-wrap { padding: 10px; } /* Lägg till padding inuti chatbot */
889
+ #chatbot_conversation { min-height: 300px; max-height: 400px; overflow-y: auto; border-bottom: 1px solid #e0e0e0; }
890
+ .message.user { background-color: #e0f2f7; border-radius: 10px 10px 0 10px; }
891
+ .message.bot { background-color: #f1f1f1; border-radius: 10px 10px 10px 0; }
892
+ .message { padding: 10px 15px; margin-bottom: 8px; max-width: 85%; word-wrap: break-word; }
893
+ .support-form-container, .success-interface { padding: 20px; }
894
+ .support-form-container .gr-form { margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 8px; background-color: #fafafa; }
895
+ .gr-button { background-color: #2a9d8f; color: white; border: none; border-radius: 5px; padding: 10px 18px; margin: 5px; cursor: pointer; transition: background-color 0.2s ease; font-weight: bold; }
896
+ .gr-button:hover { background-color: #264653; }
897
+ .support-btn { background-color: #f4a261; color: #ffffff; } /* Orange knapp */
898
+ .support-btn:hover { background-color: #e76f51; }
899
+ .flex-row { display: flex; flex-direction: row; gap: 10px; align-items: center; }
900
+ .chat-preview { max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 10px; margin-top: 10px; font-size: 0.9em; background-color: #f9f9f9; border-radius: 4px; line-height: 1.4; }
901
+ .success-message { font-size: 1.1em; font-weight: bold; color: #2a9d8f; margin-bottom: 15px; text-align: center; }
902
+ footer { display: none !important; } /* Dölj Gradio-footer mer aggressivt */
903
+ .gradio-container footer, .gradio-container .gr-footer { display: none !important; visibility: hidden !important; }
904
+ /* Specifik targeting för input/textbox */
905
+ .gr-textbox textarea { border-radius: 5px; border: 1px solid #ccc; padding: 10px; }
906
+ .block.padded { padding: 15px !important; } /* Lägg till padding till block om nödvändigt */
907
+ .block.gap { gap: 10px !important; } /* Justera mellanrum mellan element */
908
  """
909
 
910
+ # --- Funktioner för UI-logik --- (definieras före Blocks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
911
 
912
  def respond(message, chat_history, request: gr.Request):
913
+ """Hanterar användarens meddelande, genererar svar och loggar."""
914
+ global last_log # Använd globala variabeln för att spara loggdata
915
+
916
+ if not message or message.isspace():
917
+ # Returnera ingen ändring om meddelandet är tomt
918
+ return "", chat_history
919
+
920
+ start_time = time.time()
921
+ bot_response = generate_answer(message) # Generera svaret
922
+ response_time = round(time.time() - start_time, 2)
923
 
924
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
 
926
+ # Försök hämta session_id från tidigare loggar i *samma* session
927
+ session_id = str(uuid.uuid4()) # Skapa ny som standard
928
+ if last_log and last_log.get("event_type") == "conversation" and last_log.get("session_id"):
929
+ # Återanvänd session_id om den senaste loggen var en konversation
930
+ session_id = last_log.get("session_id")
931
+
932
+ # Hämta metadata från request
933
+ user_id = "unknown"
934
+ ip_address = "unknown"
935
+ user_agent_str = ""
936
+ referer = ""
937
+ browser_info = "Okänd"
938
+ os_info = "Okänd"
939
+
940
+ if request:
941
+ user_id = request.client.host if request.client else "unknown_client"
942
+ # Försök hämta IP från headers (vanligt bakom proxys)
943
+ headers = request.headers
944
+ ip_candidates = [
945
+ headers.get("x-forwarded-for"),
946
+ headers.get("x-real-ip"),
947
+ user_id # Fallback till client.host
948
+ ]
949
+ for ip in ip_candidates:
950
+ if ip:
951
+ # Ta den första IP:n om listan är kommaseparerad
952
+ ip_address = ip.split(',')[0].strip()
953
+ break
954
+
955
+ user_agent_str = headers.get("user-agent", "")
956
+ referer = headers.get("referer", "")
957
+
958
+ if user_agent_str:
959
+ try:
960
+ ua = parse_ua(user_agent_str)
961
+ browser_info = f"{ua.browser.family} {ua.browser.version_string}" if ua.browser else "Okänd"
962
+ os_info = f"{ua.os.family} {ua.os.version_string}" if ua.os else "Okänd"
963
+ except Exception as ua_err:
964
+ print(f"Fel vid parsning av User Agent: {ua_err}")
965
+ browser_info = "Parse Error"
966
+ os_info = "Parse Error"
967
+
968
+ # Bestäm plattform baserat på referer
969
  platform = "webb"
970
+ if referer:
971
+ if "chargenode.eu" in referer:
972
+ platform = "chargenode.eu"
973
+ elif any(sub in referer for sub in ["localhost", "127.0.0.1"]): # Utöka localhost-check
974
+ platform = "test"
975
+ elif "app" in referer: # Kan vara för generellt
976
+ platform = "app"
977
+ # Lägg till fler regler vid behov
978
+ else:
979
+ platform = "okänd_referer"
980
 
981
+ # Skapa loggposten
982
  log_data = {
983
  "timestamp": timestamp,
984
+ "event_type": "conversation", # Tydliggör typ
985
  "user_id": user_id,
986
  "session_id": session_id,
987
+ "ip": ip_address,
988
+ "platform": platform,
989
+ "browser": browser_info,
990
+ "os": os_info,
991
  "user_message": message,
992
+ "bot_reply": bot_response,
993
+ "response_time": response_time,
994
+ # Lägg eventuellt till referer och user_agent om det är värdefullt
995
+ # "referer": referer,
996
+ # "user_agent": user_agent_str
 
997
  }
998
 
 
999
  safe_append_to_log(log_data)
1000
+ last_log = log_data # Spara som den senaste loggen
1001
 
1002
+ # Skicka konversationen till Slack (asynkront)
1003
  try:
1004
+ slack_conv_subject = f"💬 Ny konversation ({platform})"
1005
+ slack_conv_content = f"""
 
 
1006
  *Användare:* {message}
1007
+ *Bot:* {bot_response[:300]}{'...' if len(bot_response) > 300 else ''}
1008
+ ---
1009
+ *Tid:* {timestamp} | *Session:* `{session_id[:8]}` | *IP:* `{ip_address}` | *Browser:* {browser_info} | *OS:* {os_info}
 
1010
  """
 
1011
  threading.Thread(
1012
+ target=send_to_slack,
1013
+ args=(slack_conv_subject, slack_conv_content.strip(), "#ADD8E6"), # Ljusblå för konversation
1014
  daemon=True
1015
  ).start()
1016
  except Exception as e:
1017
+ print(f"Kunde inte starta tråd för att skicka konversation till Slack: {e}")
1018
+
1019
 
1020
+ # Uppdatera Gradio chat history
1021
  chat_history.append({"role": "user", "content": message})
1022
+ chat_history.append({"role": "assistant", "content": bot_response})
1023
+
1024
+ # Rensa inputfältet (returnera tom sträng för msg) och uppdatera chatbot
 
 
1025
  return "", chat_history
1026
 
1027
  def format_chat_preview(chat_history):
1028
+ """Formaterar chatthistoriken för förhandsgranskning i supportformuläret."""
1029
  if not chat_history:
1030
+ return "_Ingen chatthistorik att visa._"
1031
+
1032
  preview = ""
1033
+ history_limit = 10 # Begränsa förhandsgranskningen
1034
+ start_index = max(0, len(chat_history) - history_limit)
1035
+ for msg in chat_history[start_index:]:
1036
+ if isinstance(msg, dict) and 'role' in msg and 'content' in msg:
1037
+ sender = "Du" if msg["role"] == "user" else "Bot"
1038
+ content = msg["content"]
1039
+ if len(content) > 150: # Korta ner långa meddelanden i förhandsgranskningen
1040
+ content = content[:150] + "..."
1041
+ # Använd Markdown för fet stil
1042
+ preview += f"**{sender}:** {content}\n\n"
1043
+ else:
1044
+ # Hantera oväntat format
1045
+ preview += f"**Okänt format:** `{str(msg)[:50]}`\n\n"
1046
+
1047
+ return preview.strip()
1048
 
1049
  def show_support_form(chat_history):
1050
+ """Visar supportformuläret och döljer chatten."""
1051
+ preview_md = format_chat_preview(chat_history)
1052
+ # Uppdatera synligheten för gränssnitten och innehållet i förhandsgranskningen
1053
  return {
1054
+ chat_interface: gr.update(visible=False),
1055
+ support_interface: gr.update(visible=True),
1056
+ success_interface: gr.update(visible=False),
1057
+ chat_preview: gr.update(value=preview_md) # Uppdatera Markdown-komponenten
1058
  }
1059
 
1060
  def back_to_chat():
1061
+ """Visar chatten och döljer support/success-vyerna."""
1062
  return {
1063
+ chat_interface: gr.update(visible=True),
1064
+ support_interface: gr.update(visible=False),
1065
+ success_interface: gr.update(visible=False)
1066
  }
1067
 
1068
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
1069
+ """Validerar och skickar supportformuläret."""
1070
+ print(f"Support-förfrågan mottagen: Områdeskod='{områdeskod}', Uttagsnummer='{uttagsnummer}', Email='{email}'")
1071
+
 
1072
  validation_errors = []
1073
+ # Validera Områdeskod (om angiven)
1074
  if områdeskod and not områdeskod.isdigit():
1075
+ validation_errors.append("Områdeskod måste vara ett nummer (om angivet).")
1076
+ # Validera Uttagsnummer (om angiven)
 
 
 
1077
  if uttagsnummer and not uttagsnummer.isdigit():
1078
+ validation_errors.append("Uttagsnummer måste vara ett nummer (om angivet).")
1079
+ # Validera Email (obligatorisk)
 
 
 
1080
  if not email:
1081
+ validation_errors.append("E-postadress är obligatorisk.")
1082
+ elif '@' not in email or '.' not in email.split('@')[-1]: # Enkel email-validering
1083
+ validation_errors.append("Ange en giltig e-postadress.")
1084
+
 
 
 
 
 
1085
  if validation_errors:
1086
+ print(f"Valideringsfel i supportformulär: {validation_errors}")
1087
+ error_message = "**Valideringsfel:**\n" + "\n".join([f"- {err}" for err in validation_errors])
1088
+ # Uppdatera UI för att visa felen, behåll supportformuläret synligt
1089
  return {
1090
+ chat_interface: gr.update(visible=False),
1091
+ support_interface: gr.update(visible=True), # Behåll synligt
1092
+ success_interface: gr.update(visible=False),
1093
+ chat_preview: gr.update(value=error_message) # Visa felmeddelande i preview-rutan
1094
  }
1095
+
1096
+ # Om validering OK, försök skicka till Slack
1097
  try:
1098
+ print("Validering OK. Försöker skicka supportförfrågan till Slack...")
 
 
 
 
 
 
 
 
 
1099
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
1100
+
1101
  if success:
1102
+ print("Support-förfrågan skickad till Slack.")
1103
+ # Visa success-meddelandet
1104
  return {
1105
+ chat_interface: gr.update(visible=False),
1106
+ support_interface: gr.update(visible=False),
1107
+ success_interface: gr.update(visible=True), # Visa success
1108
+ chat_preview: gr.update(value="") # Rensa preview
1109
  }
1110
  else:
1111
+ print("Misslyckades med att skicka supportförfrågan till Slack.")
1112
+ error_message = "**Ett tekniskt fel uppstod.** Meddelandet kunde inte skickas till supporten just nu. Vänligen försök igen senare eller kontakta support@chargenode.eu direkt."
1113
+ # Visa fel i supportformuläret
1114
  return {
1115
+ chat_interface: gr.update(visible=False),
1116
+ support_interface: gr.update(visible=True), # Behåll synligt
1117
+ success_interface: gr.update(visible=False),
1118
+ chat_preview: gr.update(value=error_message) # Visa felmeddelande
1119
  }
1120
  except Exception as e:
1121
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
1122
+ error_message = f"**Ett oväntat fel uppstod:** {str(e)}. Försök igen."
1123
  return {
1124
+ chat_interface: gr.update(visible=False),
1125
+ support_interface: gr.update(visible=True), # Behåll synligt
1126
+ success_interface: gr.update(visible=False),
1127
+ chat_preview: gr.update(value=error_message) # Visa felmeddelande
1128
  }
1129
 
1130
+ # --- Bygg Gradio App ---
1131
+ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
1132
+ gr.Markdown("# ChargeNode AI Assistent") # Tydligare rubrik
1133
+
1134
+ # --- Chat Interface ---
1135
+ with gr.Group(visible=True) as chat_interface:
1136
+ chatbot = gr.Chatbot(
1137
+ value=initial_chat,
1138
+ # type="messages", # 'messages' är inte en giltig typ, använd standard
1139
+ elem_id="chatbot_conversation",
1140
+ label="Chatt",
1141
+ show_label=False,
1142
+ bubble_full_width=False # Gör bubblorna smalare
1143
+ )
1144
+ # Koppla like-funktionen direkt efter definitionen
1145
+ chatbot.like(vote, inputs=None, outputs=None) # inputs/outputs=None är standard vid like
1146
+
1147
+ with gr.Row():
1148
+ msg = gr.Textbox(
1149
+ label="Meddelande",
1150
+ placeholder="Skriv din fråga här...",
1151
+ show_label=False,
1152
+ scale=4 # Ge textrutan mer bredd
1153
+ )
1154
+ # Submit-knapp (kan vara implicit via Enter, men bra att ha synlig)
1155
+ # submit_btn = gr.Button("Skicka", scale=1) # Alternativt
1156
+
1157
+ with gr.Row():
1158
+ clear_btn = gr.Button("Rensa chatt")
1159
+ support_btn = gr.Button("Behöver mer hjälp? Kontakta support", elem_classes="support-btn")
1160
+
1161
+ # --- Support Form Interface ---
1162
+ with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
1163
+ gr.Markdown("### Kontakta Support")
1164
+ gr.Markdown("Fyll i dina uppgifter så återkommer vi via email. Din chatthistorik inkluderas.")
1165
+
1166
+ with gr.Column(elem_classes="gr-form"): # Använd Column för bättre layout
1167
+ email = gr.Textbox(label="Din E-postadress", placeholder="din.email@example.com", info="Obligatorisk för att vi ska kunna svara dig.")
1168
+ områdeskod = gr.Textbox(label="Områdeskod (frivilligt)", placeholder="t.ex. 12345", info="Numeriskt värde om du vet det.")
1169
+ uttagsnummer = gr.Textbox(label="Uttagsnummer (frivilligt)", placeholder="t.ex. 1", info="Numeriskt värde om relevant.")
1170
+
1171
+ gr.Markdown("#### Chatthistorik (senaste)")
1172
+ chat_preview = gr.Markdown(value="_Laddar förhandsgranskning..._", elem_classes="chat-preview") # Startvärde
1173
+
1174
+ with gr.Row():
1175
+ back_btn = gr.Button("Avbryt (Tillbaka till chatten)")
1176
+ send_support_btn = gr.Button("Skicka till Support")
1177
+
1178
+ # --- Success Message Interface ---
1179
+ with gr.Group(visible=False) as success_interface:
1180
+ gr.Markdown("✅ Tack! Din förfrågan har skickats till support@chargenode.eu.", elem_classes="success-message")
1181
+ back_to_chat_btn = gr.Button("Tillbaka till chatten")
1182
+
1183
+
1184
+ # --- Event Listeners (VIKTIGT: Måste vara INUTI 'with gr.Blocks') ---
1185
+ # När användaren trycker Enter i textrutan eller klickar på (en osynlig) submit-knapp
1186
+ msg.submit(
1187
+ fn=respond,
1188
+ inputs=[msg, chatbot],
1189
+ outputs=[msg, chatbot], # Rensa msg, uppdatera chatbot
1190
+ api_name="send_message" # För API-anrop om det används
1191
+ )
1192
+ # Om du hade en synlig submit-knapp:
1193
+ # submit_btn.click(fn=respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
1194
+
1195
+ # Rensa chatt-historiken
1196
+ clear_btn.click(lambda: (initial_chat, ""), outputs=[chatbot, msg], queue=False) # Återställ till initial + rensa input
1197
+
1198
+ # Visa supportformulär
1199
+ support_btn.click(
1200
+ fn=show_support_form,
1201
+ inputs=[chatbot], # Skicka med aktuell historik
1202
+ outputs=[chat_interface, support_interface, success_interface, chat_preview] # Uppdatera synlighet + preview
1203
+ )
1204
+
1205
+ # Gå tillbaka från supportformulär till chatt
1206
+ back_btn.click(
1207
+ fn=back_to_chat,
1208
+ inputs=None,
1209
+ outputs=[chat_interface, support_interface, success_interface] # Uppdatera bara synlighet
1210
+ )
1211
+
1212
+ # Gå tillbaka från success-meddelande till chatt
1213
+ back_to_chat_btn.click(
1214
+ fn=back_to_chat,
1215
+ inputs=None,
1216
+ outputs=[chat_interface, support_interface, success_interface] # Uppdatera bara synlighet
1217
+ )
1218
+
1219
+ # Skicka in supportformuläret
1220
+ send_support_btn.click(
1221
+ fn=submit_support_form,
1222
+ inputs=[områdeskod, uttagsnummer, email, chatbot], # Skicka med formulärdata + historik
1223
+ outputs=[chat_interface, support_interface, success_interface, chat_preview] # Uppdatera synlighet + ev. fel i preview
1224
+ )
1225
+
1226
 
1227
+ # --- Kör appen ---
1228
  if __name__ == "__main__":
1229
+ print("Förbereder att starta Gradio-appen...")
1230
+ initialize_embeddings() # Ladda embeddings innan appen startas
1231
+ print("Embeddings initialiserade.")
1232
+ # Kör med share=True endast om det behövs för extern åtkomst
1233
+ app.launch(share=os.environ.get("GRADIO_SHARE", "true").lower() == "true") # Gör share konfigurerbart via env var
1234
+ # app.launch() # För lokal körning utan delning