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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +505 -895
app.py CHANGED
@@ -39,29 +39,22 @@ 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
- # 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,28 +62,28 @@ 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,133 +92,65 @@ def safe_append_to_log(log_entry):
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
@@ -235,215 +160,107 @@ 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,106 +272,69 @@ def vote(data: gr.LikeData):
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,670 +345,500 @@ def get_latest_conversations(logs, limit=50):
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 på 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
 
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
  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
  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
 
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
  def retrieve_context(query, k=RETRIEVAL_K):
181
  """Hämtar relevant kontext för frågor."""
182
+ # Säkerställ att modeller är laddade
183
+ initialize_embeddings()
184
+
185
+ query_embedding = embedder.encode([query], convert_to_numpy=True)
186
+ query_embedding /= np.linalg.norm(query_embedding)
187
+ D, I = index.search(query_embedding, k)
188
+ retrieved, sources = [], set()
189
+ for idx in I[0]:
190
+ if idx < len(chunks):
191
+ retrieved.append(chunks[idx])
192
+ sources.add(chunk_sources[idx])
193
+ return " ".join(retrieved), list(sources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  def generate_answer(query):
196
  """Genererar svar baserat på fråga och kontextinformation."""
 
197
  context, sources = retrieve_context(query)
 
 
 
198
  if not context.strip():
199
+ return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
 
 
 
 
200
  prompt = f"""{prompt_template}
201
+
202
  Relevant kontext:
203
+ {context}
 
204
  Fråga: {query}
205
+ Svar (baserat enbart på den indexerade datan):"""
 
 
206
  try:
 
207
  response = client.chat.completions.create(
208
  model="gpt-3.5-turbo",
209
  messages=[
210
+ {"role": "system", "content": "Du är en expert ChargeNodes produkter och tjänster. Svara enbart baserat på den information som finns i den indexerade datan."},
211
  {"role": "user", "content": prompt}
212
  ],
213
  temperature=0.2,
214
  max_tokens=500
215
  )
216
+ answer = response.choices[0].message.content
217
+ return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
 
 
 
 
 
 
 
218
  except Exception as e:
219
+ return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
 
 
220
 
221
  # --- Slack Integration ---
222
  def send_to_slack(subject, content, color="#2a9d8f"):
223
  """Basfunktion för att skicka meddelanden till Slack."""
224
  webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
225
  if not webhook_url:
226
+ print("Slack webhook URL saknas")
227
  return False
228
+
229
  try:
230
  # Formatera meddelandet för Slack
231
  payload = {
232
+ "blocks": [
233
+ {
234
+ "type": "header",
235
+ "text": {
236
+ "type": "plain_text",
237
+ "text": subject
238
+ }
239
+ },
240
  {
241
+ "type": "section",
242
+ "text": {
243
+ "type": "mrkdwn",
244
+ "text": content
245
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
  ]
248
  }
249
+
250
  response = requests.post(
251
  webhook_url,
252
  json=payload,
253
+ headers={"Content-Type": "application/json"}
 
254
  )
255
+
256
  if response.status_code == 200:
257
+ print(f"Slack-meddelande skickat: {subject}")
258
  return True
259
  else:
260
  print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
261
  return False
 
 
 
262
  except Exception as e:
263
+ print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
264
  return False
265
 
266
  # --- Feedback & Like-funktion ---
 
272
  """
273
  feedback_type = "up" if data.liked else "down"
274
  global last_log
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  log_entry = {
276
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
 
277
  "feedback": feedback_type,
278
+ "bot_reply": data.value if not isinstance(data.value, dict) else data.value.get("value")
279
  }
280
+ # Om global logdata finns, lägg till ytterligare metadata.
281
+ if last_log:
 
 
282
  log_entry.update({
283
  "session_id": last_log.get("session_id"),
284
  "user_message": last_log.get("user_message"),
 
 
 
 
285
  })
286
+
 
 
 
 
 
 
287
  # Använd den förbättrade loggfunktionen
288
  safe_append_to_log(log_entry)
289
+
290
  # Skicka feedback till Slack
291
  try:
292
+ if feedback_type == "down": # Skicka bara negativ feedback
293
+ feedback_message = f"""
294
+ *⚠️ Negativ feedback registrerad*
 
 
295
 
296
+ *Fråga:* {last_log.get('user_message', 'Okänd fråga')}
 
297
 
298
+ *Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''}
299
  """
300
+ # Skicka asynkront
301
+ threading.Thread(
302
+ target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
303
+ daemon=True
304
+ ).start()
 
305
  except Exception as e:
306
  print(f"Kunde inte skicka feedback till Slack: {e}")
307
+
 
308
  return
309
 
310
  # --- Rapportering ---
311
  def read_logs():
312
  """Läs alla loggposter från loggfilen."""
313
  logs = []
 
 
 
 
314
  try:
315
+ if os.path.exists(log_file_path):
316
+ with open(log_file_path, "r", encoding="utf-8") as file:
317
+ line_count = 0
318
+ for line in file:
319
+ line_count += 1
320
+ try:
321
+ log_entry = json.loads(line.strip())
322
+ logs.append(log_entry)
323
+ except json.JSONDecodeError as e:
324
+ print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
325
+ continue
326
+ print(f"Läste {len(logs)} av {line_count} loggposter")
327
+ else:
328
+ print(f"Loggfil saknas: {log_file_path}")
329
  except Exception as e:
330
+ print(f"Fel vid läsning av loggfil: {e}")
331
  return logs
332
 
333
  def get_latest_conversations(logs, limit=50):
334
+ """Hämta de senaste frågorna och svaren."""
335
  conversations = []
 
336
  for log in reversed(logs):
337
+ if 'user_message' in log and 'bot_reply' in log:
 
338
  conversations.append({
339
  'user_message': log['user_message'],
340
  'bot_reply': log['bot_reply'],
 
345
  return conversations
346
 
347
  def get_feedback_stats(logs):
348
+ """Sammanfatta feedback (tumme upp/ned)."""
349
+ feedback_count = {"up": 0, "down": 0}
350
+ negative_feedback_examples = []
351
+
352
  for log in logs:
353
+ if 'feedback' in log:
 
354
  feedback = log.get('feedback')
355
+ if feedback in feedback_count:
356
+ feedback_count[feedback] += 1
357
+
358
+ # Samla exempel på negativ feedback
359
+ if feedback == "down" and 'user_message' in log and len(negative_feedback_examples) < 10:
360
+ negative_feedback_examples.append({
361
+ 'user_message': log.get('user_message', 'Okänd fråga'),
362
+ 'bot_reply': log.get('bot_reply', 'Okänt svar')
363
+ })
364
+
 
 
 
 
365
  return feedback_count, negative_feedback_examples
366
 
367
+ def generate_monthly_stats(days=30):
368
+ """Genererar omfattande statistik över botanvändning för den senaste månaden."""
369
  print(f"Genererar statistik för de senaste {days} dagarna...")
370
+
371
+ # Hämta loggar
372
+ logs = read_logs()
373
+
374
+ if not logs:
375
+ return {"error": "Inga loggar hittades för den angivna perioden"}
376
+
377
+ # Filtrera på datumintervall
378
  now = datetime.now()
379
  cutoff_date = now - timedelta(days=days)
380
  filtered_logs = []
381
+
382
+ for log in logs:
 
383
  if 'timestamp' in log:
384
  try:
 
385
  log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
386
  if log_date >= cutoff_date:
387
  filtered_logs.append(log)
388
+ except:
389
+ pass # Hoppa över poster med ogiltigt datum
390
+
391
+ logs = filtered_logs
392
+
393
+ # Basstatistik
394
+ total_conversations = sum(1 for log in logs if 'user_message' in log)
395
+ unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
396
+ unique_users = len(set(log.get('user_id', 'unknown') for log in logs if 'user_id' in log))
397
+
398
+ # Feedback-statistik
399
+ feedback_logs = [log for log in logs if 'feedback' in log]
400
+ positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
401
+ negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
402
+ feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
403
+
404
+ # Svarstidsstatistik
405
+ response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
407
+
408
+ # Plattformsstatistik
409
  platforms = {}
410
  browsers = {}
411
  operating_systems = {}
412
+ for log in logs:
413
+ if 'platform' in log:
414
+ platforms[log['platform']] = platforms.get(log['platform'], 0) + 1
415
+ if 'browser' in log:
416
+ browsers[log['browser']] = browsers.get(log['browser'], 0) + 1
417
+ if 'os' in log:
418
+ operating_systems[log['os']] = operating_systems.get(log['os'], 0) + 1
419
+
420
  # Skapa rapport
421
  report = {
422
+ "period": f"Senaste {days} dagarna",
423
+ "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
424
  "basic_stats": {
425
  "total_conversations": total_conversations,
426
  "unique_sessions": unique_sessions,
427
+ "unique_users": unique_users,
428
+ "messages_per_user": round(total_conversations / unique_users, 2) if unique_users else 0
429
  },
430
  "feedback": {
 
431
  "positive": positive_feedback,
432
  "negative": negative_feedback,
433
+ "ratio_percent": round(feedback_ratio, 1)
434
  },
435
  "performance": {
436
+ "avg_response_time": round(avg_response_time, 2)
437
  },
438
+ "platform_distribution": platforms,
439
+ "browser_distribution": browsers,
440
+ "os_distribution": operating_systems
 
441
  }
442
+
443
  return report
444
 
445
+ def simple_status_report():
446
+ """Skickar en förenklad statusrapport till Slack."""
447
+ print("Genererar statusrapport för Slack...")
448
+
449
+ try:
450
+ # Generera statistik
451
+ stats = generate_monthly_stats(days=7) # Senaste veckan
452
+
453
+ # Skapa innehåll för Slack
454
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
455
+ subject = f"ChargeNode AI Bot - Status {now}"
456
+
457
+ if 'error' in stats:
458
+ content = f"*Fel vid generering av statistik:* {stats['error']}"
459
+ return send_to_slack(subject, content, "#ff0000")
460
+
461
+ # Formatera statistik
462
+ basic = stats["basic_stats"]
463
+ feedback = stats["feedback"]
464
+ perf = stats["performance"]
465
+
466
+ content = f"""
467
+ *ChargeNode AI Bot - Statusrapport {now}*
468
 
469
+ *Basstatistik* (senaste 7 dagarna)
470
+ - Totalt antal konversationer: {basic['total_conversations']}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  - Unika sessioner: {basic['unique_sessions']}
472
+ - Unika användare: {basic['unique_users']}
473
+ - Genomsnittlig svarstid: {perf['avg_response_time']} sekunder
 
 
 
 
 
474
 
475
+ *Feedback*
476
+ - 👍 Tumme upp: {feedback['positive']}
477
+ - 👎 Tumme ned: {feedback['negative']}
478
+ - Nöjdhet: {feedback['ratio_percent']}%
 
 
 
 
 
 
 
 
 
479
  """
480
+
481
+ # Lägg till de senaste konversationerna
482
+ logs = read_logs()
483
+ conversations = get_latest_conversations(logs, 3)
484
+
485
+ if conversations:
486
+ content += "\n*Senaste konversationer*\n"
487
+ for conv in conversations:
488
+ content += f"""
489
+ > *Tid:* {conv['timestamp']}
490
+ > *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
491
+ > *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
492
+ """
493
+
494
+ # Skicka till Slack
495
+ return send_to_slack(subject, content, "#2a9d8f")
496
+
497
  except Exception as e:
498
+ print(f"Fel vid generering av statusrapport: {e}")
499
+
500
+ # Skicka felmeddelande till Slack
501
+ error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
502
+ error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
503
+ return send_to_slack(error_subject, error_content, "#ff0000")
 
 
 
504
 
 
505
  def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
506
  """Skickar en supportförfrågan till Slack."""
 
 
 
 
507
  try:
508
  # Formatera chat-historiken
509
  chat_content = ""
510
+ for msg in chat_history:
511
+ if msg['role'] == 'user':
512
+ chat_content += f">*Användare:* {msg['content']}\n\n"
513
+ elif msg['role'] == 'assistant':
514
+ chat_content += f">*Bot:* {msg['content'][:300]}{'...' if len(msg['content']) > 300 else ''}\n\n"
515
+
 
 
 
 
 
 
 
 
 
 
516
  # Skapa innehåll
517
+ subject = f"Support förfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
518
+
 
519
  content = f"""
520
+ *Användarinformation*
521
+ - *Områdeskod:* {områdeskod or 'Ej angiven'}
522
+ - *Uttagsnummer:* {uttagsnummer or 'Ej angiven'}
523
  - *Email:* {email}
524
+ - *Tidpunkt:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
 
525
 
526
+ *Chatthistorik:*
527
+ {chat_content}
528
  """
529
+
530
  # Skicka till Slack
531
+ return send_to_slack(subject, content, "#e76f51")
 
 
 
 
532
  except Exception as e:
533
+ print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
534
  return False
535
 
 
536
  # --- Schemaläggning av rapporter ---
537
  def run_scheduler():
538
+ """Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
539
+ # Använd den förenklade funktionen för rapportering
540
+ schedule.every().day.at("08:00").do(simple_status_report)
541
+ schedule.every().day.at("12:00").do(simple_status_report)
542
+ schedule.every().day.at("17:00").do(simple_status_report)
543
+
544
+ # Veckorapport på måndagar
545
+ schedule.every().monday.at("09:00").do(lambda: send_to_slack(
546
+ "Veckostatistik",
547
+ f"*ChargeNode AI Bot - Veckostatistik*\n\n{json.dumps(generate_monthly_stats(7), indent=2)}",
548
+ "#3498db"
549
+ ))
550
+
 
 
 
 
 
551
  while True:
552
+ schedule.run_pending()
 
 
 
553
  time.sleep(60) # Kontrollera varje minut
554
 
555
+ # Starta schemaläggaren i en separat tråd
556
+ scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
557
+ scheduler_thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
  # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
560
+ try:
561
+ print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
562
+ # Anropa inte direkt här - sker i schemaläggaren
563
+ except Exception as e:
564
+ print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
 
566
+ # Definiera respond och chat-relaterade funktioner före Gradio UI
567
  def respond(message, chat_history, request: gr.Request):
568
+ global last_log
569
+ start = time.time()
570
+ response = generate_answer(message)
571
+ elapsed = round(time.time() - start, 2)
 
 
 
 
 
 
572
 
573
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
574
+ session_id = str(uuid.uuid4())
575
+
576
+ # Använd session_id från tidigare logg om det finns
577
+ if last_log and 'session_id' in last_log:
578
+ session_id = last_log.get('session_id')
579
+
580
+ user_id = request.client.host if request else "okänd"
581
+
582
+ ua_str = request.headers.get("user-agent", "")
583
+ ref = request.headers.get("referer", "")
584
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
585
+ ua = parse_ua(ua_str)
586
+ browser = f"{ua.browser.family} {ua.browser.version_string}"
587
+ osys = f"{ua.os.family} {ua.os.version_string}"
588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  platform = "webb"
590
+ if "chargenode.eu" in ref:
591
+ platform = "chargenode.eu"
592
+ elif "localhost" in ref:
593
+ platform = "test"
594
+ elif "app" in ref:
595
+ platform = "app"
 
 
 
 
596
 
 
597
  log_data = {
598
  "timestamp": timestamp,
 
599
  "user_id": user_id,
600
  "session_id": session_id,
 
 
 
 
601
  "user_message": message,
602
+ "bot_reply": response,
603
+ "response_time": elapsed,
604
+ "ip": ip,
605
+ "browser": browser,
606
+ "os": osys,
607
+ "platform": platform
608
  }
609
 
610
+ # Använd den förbättrade loggfunktionen
611
  safe_append_to_log(log_data)
612
+ last_log = log_data
613
 
614
+ # Skicka varje konversation direkt till Slack
615
  try:
616
+ # Konversationsinnehåll
617
+ conversation_content = f"""
618
+ *Ny konversation {timestamp}*
619
+
620
  *Användare:* {message}
621
+
622
+ *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
623
+
624
+ *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
625
  """
626
+ # Skicka asynkront för att inte blockera svarstiden
627
  threading.Thread(
628
+ target=lambda: send_to_slack(f"Ny konversation", conversation_content),
 
629
  daemon=True
630
  ).start()
631
  except Exception as e:
632
+ print(f"Kunde inte skicka konversation till Slack: {e}")
 
633
 
 
634
  chat_history.append({"role": "user", "content": message})
635
+ chat_history.append({"role": "assistant", "content": response})
636
+
 
637
  return "", chat_history
638
 
639
  def format_chat_preview(chat_history):
 
640
  if not chat_history:
641
+ return "Ingen chatthistorik att visa."
642
+
643
  preview = ""
644
+ for msg in chat_history:
645
+ sender = "Användare" if msg["role"] == "user" else "Bot"
646
+ content = msg["content"]
647
+ if len(content) > 100: # Truncate long messages
648
+ content = content[:100] + "..."
649
+ preview += f"**{sender}:** {content}\n\n"
650
+
651
+ return preview
 
 
 
 
 
 
 
652
 
653
  def show_support_form(chat_history):
654
+ preview = format_chat_preview(chat_history)
 
 
655
  return {
656
+ chat_interface: gr.Group(visible=False),
657
+ support_interface: gr.Group(visible=True),
658
+ success_interface: gr.Group(visible=False),
659
+ chat_preview: preview
660
  }
661
 
662
  def back_to_chat():
 
663
  return {
664
+ chat_interface: gr.Group(visible=True),
665
+ support_interface: gr.Group(visible=False),
666
+ success_interface: gr.Group(visible=False)
667
  }
668
 
669
  def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
670
+ """Hanterar formulärinskickningen med bättre felhantering."""
671
+ print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
672
+
673
+ # Validera input med tydligare loggning
674
  validation_errors = []
675
+
676
  if områdeskod and not områdeskod.isdigit():
677
+ print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
678
+ validation_errors.append("Områdeskod måste vara numerisk.")
679
+ else:
680
+ print(f"Validerar områdeskod: '{områdeskod}' (ok)")
681
+
682
  if uttagsnummer and not uttagsnummer.isdigit():
683
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
684
+ validation_errors.append("Uttagsnummer måste vara numerisk.")
685
+ else:
686
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
687
+
688
  if not email:
689
+ print("Validerar email: (saknas)")
690
+ validation_errors.append("En giltig e-postadress krävs.")
691
+ elif '@' not in email or '.' not in email.split('@')[1]:
692
+ print(f"Validerar email: '{email}' (felaktigt format)")
693
+ validation_errors.append("En giltig e-postadress krävs.")
694
+ else:
695
+ print(f"Validerar email: '{email}' (ok)")
696
+
697
+ # Om det finns valideringsfel
698
  if validation_errors:
699
+ print(f"Valideringsfel: {validation_errors}")
 
 
700
  return {
701
+ chat_interface: gr.Group(visible=False),
702
+ support_interface: gr.Group(visible=True),
703
+ success_interface: gr.Group(visible=False),
704
+ chat_preview: "\n".join(["**Fel:**"] + validation_errors)
705
  }
706
+
707
+ # Om formuläret klarade valideringen, försök skicka till Slack
708
  try:
709
+ print("Försöker skicka supportförfrågan till Slack...")
710
+
711
+ # Skapa en förenklad chathistorik för loggning
712
+ chat_summary = []
713
+ for msg in chat_history:
714
+ if 'role' in msg and 'content' in msg:
715
+ chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
716
+ print(f"Chatthistorik att skicka: {chat_summary}")
717
+
718
+ # Skicka till Slack
719
  success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
720
+
721
  if success:
722
+ print("Support-förfrågan skickad till Slack framgångsrikt")
 
723
  return {
724
+ chat_interface: gr.Group(visible=False),
725
+ support_interface: gr.Group(visible=False),
726
+ success_interface: gr.Group(visible=True)
 
727
  }
728
  else:
729
+ print("Support-förfrågan till Slack misslyckades")
 
 
730
  return {
731
+ chat_interface: gr.Group(visible=False),
732
+ support_interface: gr.Group(visible=True),
733
+ success_interface: gr.Group(visible=False),
734
+ chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
735
  }
736
  except Exception as e:
737
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
 
738
  return {
739
+ chat_interface: gr.Group(visible=False),
740
+ support_interface: gr.Group(visible=True),
741
+ success_interface: gr.Group(visible=False),
742
+ chat_preview: f"**Ett fel uppstod: {str(e)}**"
743
  }
744
 
745
+ # --- Gradio UI ---
746
+ initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
 
747
 
748
+ custom_css = """
749
+ body {background-color: #f7f7f7; font-family: Arial, sans-serif; margin: 0; padding: 0;}
750
+ h1 {font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin-bottom: 0.5em;}
751
+ .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;}
752
+ #chatbot_conversation { max-height: 300px; overflow-y: auto; }
753
+ .message-wrap { scroll-behavior: smooth; }
754
+ .message.bot:last-child { scroll-margin-top: 100px; }
755
+ .support-form-container { margin-top: 40px; }
756
+ .support-form-container .gr-form { margin-top: 15px; }
757
+ .gr-button {background-color: #2a9d8f; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; margin: 5px;}
758
+ .gr-button:hover {background-color: #264653;}
759
+ .support-btn {background-color: #000000; color: #ffffff; margin-top: 5px; margin-bottom: 10px;}
760
+ .support-btn:hover {background-color: #333333;}
761
+ .flex-row {display: flex; flex-direction: row; gap: 5px;}
762
+ .gr-form {padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px;}
763
+ .chat-preview {max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 8px; margin-top: 10px; font-size: 12px; background-color: #f9f9f9;}
764
+ .success-message {font-size: 16px; font-weight: normal; margin-bottom: 15px;}
765
+ /* Dölj Gradio-footer */
766
+ footer {display: none !important;}
767
+ .footer {display: none !important;}
768
+ .gr-footer {display: none !important;}
769
+ .gradio-footer {display: none !important;}
770
+ .gradio-container .footer {display: none !important;}
771
+ .gradio-container .gr-footer {display: none !important;}
772
+ """
773
 
774
+ # VIKTIGT: Alla komponenter och eventkopplingar definieras inuti Blocks-kontexten
775
+ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
776
+ 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")
777
+
778
+ # Chat interface
779
+ with gr.Group(visible=True) as chat_interface:
780
+ chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
781
+ chatbot.like(vote, None, None)
782
+
783
  with gr.Row():
784
+ msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
785
+
 
 
 
 
 
 
 
786
  with gr.Row():
787
+ with gr.Column(scale=1):
788
+ clear = gr.Button("Rensa")
789
+ with gr.Column(scale=1):
790
+ support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
791
+
792
+ # Lägg till anpassad JavaScript för att styra scrollning
793
+ js_code = """
794
+ function scrollToTop() {
795
+ const chatContainer = document.querySelector('.gradio-container .message-wrap');
796
+ if (chatContainer) {
797
+ chatContainer.scrollTop = 0;
798
+ }
799
+ }
800
 
801
+ // Kör funktionen när nya meddelanden läggs till
802
+ document.addEventListener('DOMNodeInserted', function(event) {
803
+ if (event.target.classList && event.target.classList.contains('bot')) {
804
+ setTimeout(scrollToTop, 100);
805
+ }
806
+ });
807
+ """
808
+ app.load(js=js_code)
809
+
810
+ # Support form interface (initially hidden)
811
  with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
812
+ gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
813
+
814
+ with gr.Group(elem_classes="gr-form"):
815
+ områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
816
+ uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
817
+ email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
818
+
819
+ gr.Markdown("### Chat som skickas till support:")
820
+ chat_preview = gr.Markdown(elem_classes="chat-preview")
821
+
 
822
  with gr.Row():
823
+ back_btn = gr.Button("Tillbaka")
824
+ send_support_btn = gr.Button("Skicka")
825
+
826
+ # Success message (initially hidden)
827
  with gr.Group(visible=False) as success_interface:
828
+ gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
829
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
830
+
831
+ # VIKTIGT: Händelsehanterare definieras INOM gr.Blocks-kontexten, efter att komponenterna definierats
832
+ msg.submit(respond, [msg, chatbot], [msg, chatbot])
833
+ clear.click(lambda: None, None, chatbot, queue=False)
834
+ support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
835
+ back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
836
+ back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  send_support_btn.click(
838
+ submit_support_form,
839
+ [områdeskod, uttagsnummer, email, chatbot],
840
+ [chat_interface, support_interface, success_interface, chat_preview]
841
  )
842
 
 
 
843
  if __name__ == "__main__":
844
+ app.launch(share=True)