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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -281
app.py CHANGED
@@ -3,6 +3,7 @@ import json
3
  import time
4
  import requests
5
  from anthropic import Anthropic
 
6
  import gradio as gr
7
  import pandas as pd
8
  from huggingface_hub import CommitScheduler
@@ -14,20 +15,21 @@ import threading
14
  from sentence_transformers import SentenceTransformer
15
  import numpy as np
16
  import faiss
17
- import re
18
 
19
  # --- Konfiguration ---
20
  CHARGENODE_URL = "https://www.chargenode.eu"
21
- MAX_CHUNK_SIZE = 2000
22
- CHUNK_OVERLAP = 200
23
- RETRIEVAL_K = 5
24
-
25
- # Uppdaterad modell till Sonnet 4
26
- MODEL_NAME = "claude-sonnet-4-20250514"
27
 
28
  # Kontrollera om vi kör i Hugging Face-miljön
29
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
30
 
 
 
 
 
 
 
31
  # Lägg till Anthropic API-nyckel och klient
32
  ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
33
  if not ANTHROPIC_API_KEY:
@@ -41,7 +43,7 @@ log_file_path = os.path.join(log_folder, "conversation_log_v2.txt")
41
  # Skapa en tom loggfil om den inte finns
42
  if not os.path.exists(log_file_path):
43
  with open(log_file_path, "w", encoding="utf-8") as f:
44
- f.write("")
45
  print(f"Skapade tom loggfil: {log_file_path}")
46
 
47
  hf_token = os.environ.get("HF_TOKEN")
@@ -67,7 +69,6 @@ embeddings = None
67
  index = None
68
  chunks = []
69
  chunk_sources = []
70
- faq_dict = {} # Dictionary för direktmatchning av vanliga frågor
71
 
72
  # --- Förbättrad loggfunktion ---
73
  def safe_append_to_log(log_entry):
@@ -105,7 +106,7 @@ def safe_append_to_log(log_entry):
105
  def load_local_files():
106
  """Laddar alla lokala filer och returnerar som en sammanhängande text."""
107
  uploaded_text = ""
108
- allowed = [".txt", ".csv", ".xls", ".xlsx"] # Tog bort .docx och .pdf
109
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
110
  for file in os.listdir("."):
111
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
@@ -113,6 +114,14 @@ def load_local_files():
113
  if file.endswith(".txt"):
114
  with open(file, "r", encoding="utf-8") as f:
115
  content = f.read()
 
 
 
 
 
 
 
 
116
  elif file.endswith(".csv"):
117
  content = pd.read_csv(file).to_string()
118
  elif file.endswith((".xls", ".xlsx")):
@@ -154,104 +163,29 @@ def load_prompt():
154
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
155
  return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
156
 
157
- # --- Förbättrad chunking ---
158
  def prepare_chunks(text_data):
159
  """Delar upp texten i mindre segment för embedding och sökning."""
160
- chunks_list, sources_list = [], []
161
- global faq_dict
162
-
163
  for source, text in text_data.items():
164
- # Split text into paragraph-sized chunks
165
  paragraphs = [p for p in text.split("\n") if p.strip()]
166
-
167
- # Process FAQ-specific content better
168
- i = 0
169
- current_file_chunks = []
170
- current_file_sources = []
171
- while i < len(paragraphs):
172
- # Start a new chunk
173
- current_chunk = ""
174
- start_idx = i
175
-
176
- # Check for FAQ format
177
- if i < len(paragraphs) and paragraphs[i].startswith("Fråga:"):
178
- question = paragraphs[i][7:].strip() # Extract the question text
179
- current_chunk = paragraphs[i]
180
- i += 1
181
-
182
- # Add content until we reach the next question or MAX_CHUNK_SIZE
183
- while i < len(paragraphs) and not paragraphs[i].startswith("Fråga:"):
184
- # Add this paragraph if it doesn't exceed chunk size
185
- if len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
186
- current_chunk += "\n" + paragraphs[i]
187
- else:
188
- # If we're already processing a FAQ answer, don't break mid-answer
189
- if "Svar:" in current_chunk:
190
- # We prefer to keep whole answers together, so let's break only if answer is too long
191
- if len(current_chunk) > MAX_CHUNK_SIZE * 1.5: # Allow some overflow
192
- break
193
- else:
194
- current_chunk += "\n" + paragraphs[i]
195
- else:
196
- break
197
- i += 1
198
-
199
- # Store FAQ pairs in the dictionary for direct lookup
200
- if "Svar:" in current_chunk:
201
- answer_start = current_chunk.find("Svar:")
202
- answer_text = current_chunk[answer_start + 5:].strip()
203
-
204
- # Add the original question to the dictionary
205
- faq_dict[question.lower()] = answer_text
206
  else:
207
- # Handle non-FAQ text using sliding window
208
- while i < len(paragraphs) and len(current_chunk) + len(paragraphs[i]) + 1 <= MAX_CHUNK_SIZE:
209
- if current_chunk:
210
- current_chunk += " " + paragraphs[i]
211
- else:
212
- current_chunk = paragraphs[i]
213
- i += 1
214
-
215
- # Save the chunk if it has content
216
- if current_chunk.strip():
217
- current_file_chunks.append(current_chunk.strip())
218
- current_file_sources.append(source)
219
-
220
- # If we've added a chunk but haven't advanced, we need to move forward
221
- if i == start_idx:
222
- i += 1
223
-
224
- # Create overlapping chunks for better context preservation for THIS source
225
- overlap_chunks_for_file = []
226
- overlap_sources_for_file = []
227
-
228
- for j in range(len(current_file_chunks)):
229
- overlap_chunks_for_file.append(current_file_chunks[j])
230
- overlap_sources_for_file.append(current_file_sources[j])
231
-
232
- if j < len(current_file_chunks) - 1:
233
- # Calculate available space in the current chunk
234
- space_left = MAX_CHUNK_SIZE - len(current_file_chunks[j])
235
-
236
- # If there's enough space, add part of the next chunk
237
- if space_left >= CHUNK_OVERLAP:
238
- # Ensure we don't duplicate if chunks are already naturally overlapping significantly
239
- if not current_file_chunks[j].endswith(current_file_chunks[j+1][:CHUNK_OVERLAP]):
240
- overlap_text = current_file_chunks[j] + " " + current_file_chunks[j+1][:CHUNK_OVERLAP]
241
- if len(overlap_text) <= MAX_CHUNK_SIZE: # Ensure overlap doesn't exceed max size
242
- overlap_chunks_for_file.append(overlap_text)
243
- overlap_sources_for_file.append(current_file_sources[j])
244
-
245
- chunks_list.extend(overlap_chunks_for_file)
246
- sources_list.extend(overlap_sources_for_file)
247
-
248
- print(f"Genererade {len(chunks_list)} chunks med {len(faq_dict)} FAQ-par")
249
- return chunks_list, sources_list
250
-
251
 
252
  def initialize_embeddings():
253
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
254
- global embedder, embeddings, index, chunks, chunk_sources, faq_dict
255
 
256
  if embedder is None:
257
  print("Initierar SentenceTransformer och FAISS-index...")
@@ -262,143 +196,65 @@ def initialize_embeddings():
262
  chunks, chunk_sources = prepare_chunks(text_data)
263
  print(f"{len(chunks)} segment laddade")
264
 
265
- if not chunks:
266
- print("Varning: Inga chunks genererades. Kontrollera textkällor och chunking-logik.")
267
- # Sätt upp tomma men giltiga strukturer för att undvika fel senare
268
- embedder = SentenceTransformer('all-MiniLM-L6-v2')
269
- embeddings = np.array([]).reshape(0, embedder.get_sentence_embedding_dimension())
270
- index = faiss.IndexFlatIP(embedder.get_sentence_embedding_dimension())
271
- print("FAISS-index initialiserat tomt då inga chunks fanns.")
272
- return
273
-
274
  print("Skapar embeddings...")
275
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
276
  embeddings = embedder.encode(chunks, convert_to_numpy=True)
277
-
278
- # Normalisera embeddings för IndexFlatIP (dot product)
279
- if embeddings.ndim == 2 and embeddings.shape[0] > 0:
280
- embeddings_norm = np.linalg.norm(embeddings, axis=1, keepdims=True)
281
- # Undvik division med noll om någon norm är noll
282
- embeddings_norm[embeddings_norm == 0] = 1e-10
283
- embeddings = embeddings / embeddings_norm
284
-
285
- index = faiss.IndexFlatIP(embeddings.shape[1])
286
- index.add(embeddings)
287
- print("FAISS-index klart")
288
- else:
289
- print("Varning: Inga embeddings genererades, FAISS-index kan vara tomt eller ogiltigt.")
290
- # Fallback: skapa ett tomt index om embeddings är tomma
291
- dimension = embedder.get_sentence_embedding_dimension() if embedder else 384
292
- index = faiss.IndexFlatIP(dimension)
293
- print("FAISS-index initialiserat tomt.")
294
-
295
- print(f"FAQ Dictionary innehåller {len(faq_dict)} nycklar")
296
-
297
- def check_direct_match(query):
298
- """Kontrollerar om frågan matchar någon av våra fördefinierade FAQ-svar."""
299
- query_lower = query.lower().strip('?').strip()
300
-
301
- # Check if query directly matches a FAQ
302
- if query_lower in faq_dict:
303
- return faq_dict[query_lower]
304
-
305
- # Check for close matches using pattern matching
306
- for key, value in faq_dict.items():
307
- # Check if key and query share important terms
308
- query_terms = set(re.findall(r'\w+', query_lower))
309
- key_terms = set(re.findall(r'\w+', key))
310
- if len(query_terms.intersection(key_terms)) >= 2: # At least 2 words in common
311
- return value
312
-
313
- return None
314
 
315
  def retrieve_context(query, k=RETRIEVAL_K):
316
- """Hämtar relevant kontext för frågor med direkt matchning för vanliga frågor."""
317
  # Säkerställ att modeller är laddade
318
  initialize_embeddings()
319
 
320
- # Först, kolla efter direktmatchningar för vanliga frågor
321
- direct_match = check_direct_match(query)
322
- if direct_match:
323
- print(f"Direkt matchning hittad för frågan: {query}")
324
- return f"Fråga: {query}\nSvar: {direct_match}", ["direct_match"]
325
-
326
- # Om ingen direktmatchning, använd vanlig embedding-sökning
327
- if embedder is None or index is None or index.ntotal == 0:
328
- print("Varning: Embedder eller FAISS-index är inte korrekt initierat eller är tomt. Returnerar tom kontext.")
329
- return "", []
330
-
331
  query_embedding = embedder.encode([query], convert_to_numpy=True)
332
- # Normalisera query_embedding samma sätt som indexets embeddings
333
- query_embedding_norm = np.linalg.norm(query_embedding)
334
- if query_embedding_norm == 0: query_embedding_norm = 1e-10
335
- query_embedding = query_embedding / query_embedding_norm
336
-
337
  D, I = index.search(query_embedding, k)
338
- retrieved, sources_set = [], set()
339
  for idx in I[0]:
340
- if 0 <= idx < len(chunks):
341
  retrieved.append(chunks[idx])
342
- sources_set.add(chunk_sources[idx])
343
- return " ".join(retrieved), list(sources_set)
344
 
345
  # Ladda prompt template
346
  prompt_template = load_prompt()
347
 
348
- def format_chat_history_for_claude(chat_history):
349
- """Formaterar chatthistoriken för Claude API med endast de senaste meddelandena för att undvika token-gränser."""
350
- # Ta endast de senaste 10 meddelandena för att hålla kontexten hanterbar
351
- recent_history = chat_history[-10:] if len(chat_history) > 10 else chat_history
352
-
353
- messages = []
354
- for msg in recent_history:
355
- if msg["role"] in ["user", "assistant"]:
356
- messages.append({
357
- "role": msg["role"],
358
- "content": msg["content"]
359
- })
360
-
361
- return messages
362
-
363
- def generate_answer(query, chat_history=None):
364
- """Genererar svar baserat på fråga, chatthistorik och retrieval-baserad kontext med Claude Sonnet 4."""
365
  # Hämta relevant kontext via RAG istället för hela databasen
366
  context, sources = retrieve_context(query)
367
 
368
  if not context.strip():
369
- print("Ingen RAG-kontext hittades. Försöker svara utan.")
370
-
371
- # System-prompts
372
- system_prompt = prompt_template
373
-
374
- # Förbered meddelanden för Claude API
375
- messages = []
376
 
377
- # Lägg till chatthistorik om den finns och är meningsfull
378
- if chat_history and len(chat_history) > 1:
379
- formatted_history = format_chat_history_for_claude(chat_history[:-1])
380
- messages.extend(formatted_history)
381
 
382
- # Skapa användarmeddelandet med kontext och aktuell fråga
383
- user_message_content = f"Relevant kontext för frågan:\n{context}\n\nMin fråga är: {query}"
384
- if not context.strip():
385
- user_message_content = f"Min fråga är: {query}"
386
-
387
- messages.append({"role": "user", "content": user_message_content})
 
388
 
389
  try:
390
- # Använd Claude Sonnet 4 med RAG-baserad kontext och chatthistorik
391
  response = anthropic_client.messages.create(
392
- model=MODEL_NAME,
393
- max_tokens=1024,
394
  temperature=0.3,
395
  system=system_prompt,
396
- messages=messages
 
 
397
  )
398
  answer = response.content[0].text
399
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
400
  except Exception as e:
401
- print(f"Fel vid API-anrop: {str(e)}")
402
  return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
403
 
404
  # --- Slack Integration ---
@@ -450,7 +306,7 @@ def send_to_slack(subject, content, color="#2a9d8f"):
450
  def vote(data: gr.LikeData):
451
  """
452
  Hanterar feedback från Gradio's inbyggda like-funktion.
453
- data.liked är True om upvote, annars False.
454
  data.value innehåller information om meddelandet.
455
  """
456
  feedback_type = "up" if data.liked else "down"
@@ -472,7 +328,7 @@ def vote(data: gr.LikeData):
472
 
473
  # Skicka feedback till Slack
474
  try:
475
- if feedback_type == "down" and last_log:
476
  feedback_message = f"""
477
  *⚠️ Negativ feedback registrerad*
478
 
@@ -572,9 +428,7 @@ def generate_monthly_stats(days=30):
572
  pass # Hoppa över poster med ogiltigt datum
573
 
574
  logs = filtered_logs
575
- if not logs:
576
- return {"error": f"Inga loggar hittades för de senaste {days} dagarna"}
577
-
578
  # Basstatistik
579
  total_conversations = sum(1 for log in logs if 'user_message' in log)
580
  unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
@@ -587,7 +441,7 @@ def generate_monthly_stats(days=30):
587
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
588
 
589
  # Svarstidsstatistik
590
- response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log and isinstance(log.get('response_time'), (int, float))]
591
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
592
 
593
  # Plattformsstatistik
@@ -636,8 +490,8 @@ def simple_status_report():
636
  stats = generate_monthly_stats(days=7) # Senaste veckan
637
 
638
  # Skapa innehåll för Slack
639
- now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
640
- subject = f"ChargeNode AI Bot - Status {now_str}"
641
 
642
  if 'error' in stats:
643
  content = f"*Fel vid generering av statistik:* {stats['error']}"
@@ -649,7 +503,7 @@ def simple_status_report():
649
  perf = stats["performance"]
650
 
651
  content = f"""
652
- *ChargeNode AI Bot - Statusrapport {now_str}*
653
 
654
  *Basstatistik* (senaste 7 dagarna)
655
  - Totalt antal konversationer: {basic['total_conversations']}
@@ -664,8 +518,8 @@ def simple_status_report():
664
  """
665
 
666
  # Lägg till de senaste konversationerna
667
- all_logs = read_logs()
668
- conversations = get_latest_conversations(all_logs, 3)
669
 
670
  if conversations:
671
  content += "\n*Senaste konversationer*\n"
@@ -687,12 +541,12 @@ def simple_status_report():
687
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
688
  return send_to_slack(error_subject, error_content, "#ff0000")
689
 
690
- def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history_list):
691
  """Skickar en supportförfrågan till Slack."""
692
  try:
693
  # Formatera chat-historiken
694
  chat_content = ""
695
- for msg in chat_history_list:
696
  if msg['role'] == 'user':
697
  chat_content += f">*Användare:* {msg['content']}\n\n"
698
  elif msg['role'] == 'assistant':
@@ -741,6 +595,13 @@ def run_scheduler():
741
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
742
  scheduler_thread.start()
743
 
 
 
 
 
 
 
 
744
  # --- Gradio UI ---
745
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
746
 
@@ -804,16 +665,11 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
804
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
805
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
806
 
807
- def respond(message, chat_history_list, request: gr.Request):
808
  global last_log
809
- start_time = time.time()
810
-
811
- # Lägg till användarens nuvarande meddelande i historiken FÖRE anrop till generate_answer
812
- chat_history_list.append({"role": "user", "content": message})
813
-
814
- # Skicka den uppdaterade chatthistoriken till generate_answer
815
- response_text = generate_answer(message, chat_history_list)
816
- elapsed = round(time.time() - start_time, 2)
817
 
818
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
819
  session_id = str(uuid.uuid4())
@@ -826,7 +682,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
826
 
827
  ua_str = request.headers.get("user-agent", "")
828
  ref = request.headers.get("referer", "")
829
- ip = request.headers.get("x-forwarded-for", user_id).split(",")[0].strip()
830
  ua = parse_ua(ua_str)
831
  browser = f"{ua.browser.family} {ua.browser.version_string}"
832
  osys = f"{ua.os.family} {ua.os.version_string}"
@@ -834,7 +690,7 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
834
  platform = "webb"
835
  if "chargenode.eu" in ref:
836
  platform = "chargenode.eu"
837
- elif "localhost" in ref or "127.0.0.1" in ref:
838
  platform = "test"
839
  elif "app" in ref:
840
  platform = "app"
@@ -844,29 +700,31 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
844
  "user_id": user_id,
845
  "session_id": session_id,
846
  "user_message": message,
847
- "bot_reply": response_text,
848
  "response_time": elapsed,
849
  "ip": ip,
850
  "browser": browser,
851
  "os": osys,
852
- "platform": platform,
853
- "chat_history_length": len(chat_history_list)
854
  }
855
 
 
856
  safe_append_to_log(log_data)
857
  last_log = log_data
858
 
859
  # Skicka varje konversation direkt till Slack
860
  try:
 
861
  conversation_content = f"""
862
  *Ny konversation {timestamp}*
863
 
864
  *Användare:* {message}
865
 
866
- *Bot:* {response_text[:300]}{'...' if len(response_text) > 300 else ''}
867
 
868
- *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform} | Chat längd: {len(chat_history_list)} meddelanden
869
  """
 
870
  threading.Thread(
871
  target=lambda: send_to_slack(f"Ny konversation", conversation_content),
872
  daemon=True
@@ -874,26 +732,26 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
874
  except Exception as e:
875
  print(f"Kunde inte skicka konversation till Slack: {e}")
876
 
877
- # Användarens meddelande är redan tillagt, lägg bara till assistentens svar.
878
- chat_history_list.append({"role": "assistant", "content": response_text})
879
- return "", chat_history_list
880
 
881
- def format_chat_preview(chat_history_list):
882
- if not chat_history_list:
883
  return "Ingen chatthistorik att visa."
884
 
885
  preview = ""
886
- for msg_item in chat_history_list:
887
- sender = "Användare" if msg_item["role"] == "user" else "Bot"
888
- content = msg_item["content"]
889
  if len(content) > 100: # Truncate long messages
890
  content = content[:100] + "..."
891
  preview += f"**{sender}:** {content}\n\n"
892
 
893
  return preview
894
 
895
- def show_support_form(chat_history_list):
896
- preview = format_chat_preview(chat_history_list)
897
  return {
898
  chat_interface: gr.Group(visible=False),
899
  support_interface: gr.Group(visible=True),
@@ -908,82 +766,84 @@ with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
908
  success_interface: gr.Group(visible=False)
909
  }
910
 
911
- def submit_support_form(omr_kod, uttags_nr, email_addr, chat_history_list):
912
  """Hanterar formulärinskickningen med bättre felhantering."""
913
- print(f"Support-förfrågan: områdeskod={omr_kod}, uttagsnummer={uttags_nr}, email={email_addr}")
914
 
 
915
  validation_errors = []
916
 
917
- if omr_kod and not omr_kod.isdigit():
918
- print(f"Validerar områdeskod: '{omr_kod}' (felaktig)")
919
  validation_errors.append("Områdeskod måste vara numerisk.")
920
  else:
921
- print(f"Validerar områdeskod: '{omr_kod}' (ok)")
922
 
923
- if uttags_nr and not uttags_nr.isdigit():
924
- print(f"Validerar uttagsnummer: '{uttags_nr}' (felaktig)")
925
  validation_errors.append("Uttagsnummer måste vara numerisk.")
926
  else:
927
- print(f"Validerar uttagsnummer: '{uttags_nr}' (ok)")
928
 
929
- if not email_addr:
930
  print("Validerar email: (saknas)")
931
  validation_errors.append("En giltig e-postadress krävs.")
932
- elif '@' not in email_addr or '.' not in email_addr.split('@')[-1]:
933
- print(f"Validerar email: '{email_addr}' (felaktigt format)")
934
  validation_errors.append("En giltig e-postadress krävs.")
935
  else:
936
- print(f"Validerar email: '{email_addr}' (ok)")
937
 
 
938
  if validation_errors:
939
  print(f"Valideringsfel: {validation_errors}")
940
- error_message_md = "**Fel:**\n" + "\n".join(f"- {err}" for err in validation_errors)
941
  return {
942
- chat_interface: gr.update(visible=False),
943
- support_interface: gr.update(visible=True),
944
- success_interface: gr.update(visible=False),
945
- chat_preview: gr.update(value=error_message_md)
946
  }
947
 
 
948
  try:
949
  print("Försöker skicka supportförfrågan till Slack...")
950
 
 
951
  chat_summary = []
952
- for msg_item in chat_history_list:
953
- if 'role' in msg_item and 'content' in msg_item:
954
- chat_summary.append(f"{msg_item['role']}: {msg_item['content'][:30]}...")
955
  print(f"Chatthistorik att skicka: {chat_summary}")
956
 
957
- success = send_support_to_slack(omr_kod, uttags_nr, email_addr, chat_history_list)
 
958
 
959
  if success:
960
  print("Support-förfrågan skickad till Slack framgångsrikt")
961
  return {
962
- chat_interface: gr.update(visible=False),
963
- support_interface: gr.update(visible=False),
964
- success_interface: gr.update(visible=True)
965
  }
966
  else:
967
  print("Support-förfrågan till Slack misslyckades")
968
- error_message_md = "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
969
  return {
970
- chat_interface: gr.update(visible=False),
971
- support_interface: gr.update(visible=True),
972
- success_interface: gr.update(visible=False),
973
- chat_preview: gr.update(value=error_message_md)
974
  }
975
  except Exception as e:
976
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
977
- error_message_md = f"**Ett oväntat fel uppstod: {str(e)}**"
978
  return {
979
- chat_interface: gr.update(visible=False),
980
- support_interface: gr.update(visible=True),
981
- success_interface: gr.update(visible=False),
982
- chat_preview: gr.update(value=error_message_md)
983
  }
984
 
985
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
986
- clear.click(lambda: initial_chat, None, chatbot, queue=False)
987
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
988
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
989
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
@@ -999,4 +859,4 @@ initialize_embeddings()
999
  print("Embedding-modell och index redo!")
1000
 
1001
  if __name__ == "__main__":
1002
- app.launch(share=IS_HUGGINGFACE)
 
3
  import time
4
  import requests
5
  from anthropic import Anthropic
6
+ from openai import OpenAI
7
  import gradio as gr
8
  import pandas as pd
9
  from huggingface_hub import CommitScheduler
 
15
  from sentence_transformers import SentenceTransformer
16
  import numpy as np
17
  import faiss
 
18
 
19
  # --- Konfiguration ---
20
  CHARGENODE_URL = "https://www.chargenode.eu"
21
+ MAX_CHUNK_SIZE = 1024 # Storlek på chunker för indexering
22
+ RETRIEVAL_K = 8 # Antal chunker att hämta vid varje sökning
 
 
 
 
23
 
24
  # Kontrollera om vi kör i Hugging Face-miljön
25
  IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
26
 
27
+ # OpenAI-klient behålls för bakåtkompatibilitet
28
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
29
+ if not OPENAI_API_KEY:
30
+ raise ValueError("OPENAI_API_KEY saknas")
31
+ client = OpenAI(api_key=OPENAI_API_KEY)
32
+
33
  # Lägg till Anthropic API-nyckel och klient
34
  ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
35
  if not ANTHROPIC_API_KEY:
 
43
  # Skapa en tom loggfil om den inte finns
44
  if not os.path.exists(log_file_path):
45
  with open(log_file_path, "w", encoding="utf-8") as f:
46
+ f.write("") # Skapa en tom fil
47
  print(f"Skapade tom loggfil: {log_file_path}")
48
 
49
  hf_token = os.environ.get("HF_TOKEN")
 
69
  index = None
70
  chunks = []
71
  chunk_sources = []
 
72
 
73
  # --- Förbättrad loggfunktion ---
74
  def safe_append_to_log(log_entry):
 
106
  def load_local_files():
107
  """Laddar alla lokala filer och returnerar som en sammanhängande text."""
108
  uploaded_text = ""
109
+ allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
110
  excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets", "prompt.txt"]
111
  for file in os.listdir("."):
112
  if file.lower().endswith(tuple(allowed)) and file not in excluded:
 
114
  if file.endswith(".txt"):
115
  with open(file, "r", encoding="utf-8") as f:
116
  content = f.read()
117
+ elif file.endswith(".docx"):
118
+ from docx import Document # Import sker vid behov
119
+ content = "\n".join([p.text for p in Document(file).paragraphs])
120
+ elif file.endswith(".pdf"):
121
+ import PyPDF2 # Import sker vid behov
122
+ with open(file, "rb") as f:
123
+ reader = PyPDF2.PdfReader(f)
124
+ content = "\n".join([p.extract_text() or "" for p in reader.pages])
125
  elif file.endswith(".csv"):
126
  content = pd.read_csv(file).to_string()
127
  elif file.endswith((".xls", ".xlsx")):
 
163
  print(f"Fel vid inläsning av prompt.txt: {e}, använder standardprompt")
164
  return "Du är ChargeNode's AI-assistent. Svara på frågor om ChargeNode's produkter och tjänster baserat på den tillhandahållna informationen."
165
 
166
+ # Förbered textsegment
167
  def prepare_chunks(text_data):
168
  """Delar upp texten i mindre segment för embedding och sökning."""
169
+ chunks, sources = [], []
 
 
170
  for source, text in text_data.items():
 
171
  paragraphs = [p for p in text.split("\n") if p.strip()]
172
+ chunk = ""
173
+ for para in paragraphs:
174
+ if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
175
+ chunk += " " + para
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  else:
177
+ if chunk.strip():
178
+ chunks.append(chunk.strip())
179
+ sources.append(source)
180
+ chunk = para
181
+ if chunk.strip():
182
+ chunks.append(chunk.strip())
183
+ sources.append(source)
184
+ return chunks, sources
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  def initialize_embeddings():
187
  """Initierar SentenceTransformer och FAISS-index vid första anrop."""
188
+ global embedder, embeddings, index, chunks, chunk_sources
189
 
190
  if embedder is None:
191
  print("Initierar SentenceTransformer och FAISS-index...")
 
196
  chunks, chunk_sources = prepare_chunks(text_data)
197
  print(f"{len(chunks)} segment laddade")
198
 
 
 
 
 
 
 
 
 
 
199
  print("Skapar embeddings...")
200
  embedder = SentenceTransformer('all-MiniLM-L6-v2')
201
  embeddings = embedder.encode(chunks, convert_to_numpy=True)
202
+ embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
203
+ index = faiss.IndexFlatIP(embeddings.shape[1])
204
+ index.add(embeddings)
205
+ print("FAISS-index klart")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  def retrieve_context(query, k=RETRIEVAL_K):
208
+ """Hämtar relevant kontext för frågor."""
209
  # Säkerställ att modeller är laddade
210
  initialize_embeddings()
211
 
 
 
 
 
 
 
 
 
 
 
 
212
  query_embedding = embedder.encode([query], convert_to_numpy=True)
213
+ query_embedding /= np.linalg.norm(query_embedding)
 
 
 
 
214
  D, I = index.search(query_embedding, k)
215
+ retrieved, sources = [], set()
216
  for idx in I[0]:
217
+ if idx < len(chunks):
218
  retrieved.append(chunks[idx])
219
+ sources.add(chunk_sources[idx])
220
+ return " ".join(retrieved), list(sources)
221
 
222
  # Ladda prompt template
223
  prompt_template = load_prompt()
224
 
225
+ def generate_answer(query):
226
+ """Genererar svar baserat fråga och retrieval-baserad kontext med Claude Haiku."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  # Hämta relevant kontext via RAG istället för hela databasen
228
  context, sources = retrieve_context(query)
229
 
230
  if not context.strip():
231
+ return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
 
 
 
 
 
 
232
 
233
+ # System-prompts och användarfråga
234
+ system_prompt = prompt_template
 
 
235
 
236
+ # Skapa ett renare användarmeddelande med bara den relevanta kontexten
237
+ user_message = f"""Jag har en fråga om ChargeNode.
238
+
239
+ Relevant kontext för frågan:
240
+ {context}
241
+
242
+ Min fråga är: {query}"""
243
 
244
  try:
245
+ # Använd Claude Haiku med RAG-baserad kontext
246
  response = anthropic_client.messages.create(
247
+ model="claude-3-7-sonnet-20250219",
248
+ max_tokens=500,
249
  temperature=0.3,
250
  system=system_prompt,
251
+ messages=[
252
+ {"role": "user", "content": user_message}
253
+ ]
254
  )
255
  answer = response.content[0].text
256
  return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
257
  except Exception as e:
 
258
  return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
259
 
260
  # --- Slack Integration ---
 
306
  def vote(data: gr.LikeData):
307
  """
308
  Hanterar feedback från Gradio's inbyggda like-funktion.
309
+ data.liked är True om uppvote, annars False.
310
  data.value innehåller information om meddelandet.
311
  """
312
  feedback_type = "up" if data.liked else "down"
 
328
 
329
  # Skicka feedback till Slack
330
  try:
331
+ if feedback_type == "down": # Skicka bara negativ feedback
332
  feedback_message = f"""
333
  *⚠️ Negativ feedback registrerad*
334
 
 
428
  pass # Hoppa över poster med ogiltigt datum
429
 
430
  logs = filtered_logs
431
+
 
 
432
  # Basstatistik
433
  total_conversations = sum(1 for log in logs if 'user_message' in log)
434
  unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
 
441
  feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
442
 
443
  # Svarstidsstatistik
444
+ response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
445
  avg_response_time = sum(response_times) / len(response_times) if response_times else 0
446
 
447
  # Plattformsstatistik
 
490
  stats = generate_monthly_stats(days=7) # Senaste veckan
491
 
492
  # Skapa innehåll för Slack
493
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
494
+ subject = f"ChargeNode AI Bot - Status {now}"
495
 
496
  if 'error' in stats:
497
  content = f"*Fel vid generering av statistik:* {stats['error']}"
 
503
  perf = stats["performance"]
504
 
505
  content = f"""
506
+ *ChargeNode AI Bot - Statusrapport {now}*
507
 
508
  *Basstatistik* (senaste 7 dagarna)
509
  - Totalt antal konversationer: {basic['total_conversations']}
 
518
  """
519
 
520
  # Lägg till de senaste konversationerna
521
+ logs = read_logs()
522
+ conversations = get_latest_conversations(logs, 3)
523
 
524
  if conversations:
525
  content += "\n*Senaste konversationer*\n"
 
541
  error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
542
  return send_to_slack(error_subject, error_content, "#ff0000")
543
 
544
+ def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
545
  """Skickar en supportförfrågan till Slack."""
546
  try:
547
  # Formatera chat-historiken
548
  chat_content = ""
549
+ for msg in chat_history:
550
  if msg['role'] == 'user':
551
  chat_content += f">*Användare:* {msg['content']}\n\n"
552
  elif msg['role'] == 'assistant':
 
595
  scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
596
  scheduler_thread.start()
597
 
598
+ # Kör en statusrapport vid uppstart för att verifiera att allt fungerar
599
+ try:
600
+ print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
601
+ # Anropa inte direkt här - sker i schemaläggaren
602
+ except Exception as e:
603
+ print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
604
+
605
  # --- Gradio UI ---
606
  initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
607
 
 
665
  gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
666
  back_to_chat_btn = gr.Button("Tillbaka till chatten")
667
 
668
+ def respond(message, chat_history, request: gr.Request):
669
  global last_log
670
+ start = time.time()
671
+ response = generate_answer(message)
672
+ elapsed = round(time.time() - start, 2)
 
 
 
 
 
673
 
674
  timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
675
  session_id = str(uuid.uuid4())
 
682
 
683
  ua_str = request.headers.get("user-agent", "")
684
  ref = request.headers.get("referer", "")
685
+ ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
686
  ua = parse_ua(ua_str)
687
  browser = f"{ua.browser.family} {ua.browser.version_string}"
688
  osys = f"{ua.os.family} {ua.os.version_string}"
 
690
  platform = "webb"
691
  if "chargenode.eu" in ref:
692
  platform = "chargenode.eu"
693
+ elif "localhost" in ref:
694
  platform = "test"
695
  elif "app" in ref:
696
  platform = "app"
 
700
  "user_id": user_id,
701
  "session_id": session_id,
702
  "user_message": message,
703
+ "bot_reply": response,
704
  "response_time": elapsed,
705
  "ip": ip,
706
  "browser": browser,
707
  "os": osys,
708
+ "platform": platform
 
709
  }
710
 
711
+ # Använd den förbättrade loggfunktionen
712
  safe_append_to_log(log_data)
713
  last_log = log_data
714
 
715
  # Skicka varje konversation direkt till Slack
716
  try:
717
+ # Konversationsinnehåll
718
  conversation_content = f"""
719
  *Ny konversation {timestamp}*
720
 
721
  *Användare:* {message}
722
 
723
+ *Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
724
 
725
+ *Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
726
  """
727
+ # Skicka asynkront för att inte blockera svarstiden
728
  threading.Thread(
729
  target=lambda: send_to_slack(f"Ny konversation", conversation_content),
730
  daemon=True
 
732
  except Exception as e:
733
  print(f"Kunde inte skicka konversation till Slack: {e}")
734
 
735
+ chat_history.append({"role": "user", "content": message})
736
+ chat_history.append({"role": "assistant", "content": response})
737
+ return "", chat_history
738
 
739
+ def format_chat_preview(chat_history):
740
+ if not chat_history:
741
  return "Ingen chatthistorik att visa."
742
 
743
  preview = ""
744
+ for msg in chat_history:
745
+ sender = "Användare" if msg["role"] == "user" else "Bot"
746
+ content = msg["content"]
747
  if len(content) > 100: # Truncate long messages
748
  content = content[:100] + "..."
749
  preview += f"**{sender}:** {content}\n\n"
750
 
751
  return preview
752
 
753
+ def show_support_form(chat_history):
754
+ preview = format_chat_preview(chat_history)
755
  return {
756
  chat_interface: gr.Group(visible=False),
757
  support_interface: gr.Group(visible=True),
 
766
  success_interface: gr.Group(visible=False)
767
  }
768
 
769
+ def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
770
  """Hanterar formulärinskickningen med bättre felhantering."""
771
+ print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
772
 
773
+ # Validera input med tydligare loggning
774
  validation_errors = []
775
 
776
+ if områdeskod and not områdeskod.isdigit():
777
+ print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
778
  validation_errors.append("Områdeskod måste vara numerisk.")
779
  else:
780
+ print(f"Validerar områdeskod: '{områdeskod}' (ok)")
781
 
782
+ if uttagsnummer and not uttagsnummer.isdigit():
783
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
784
  validation_errors.append("Uttagsnummer måste vara numerisk.")
785
  else:
786
+ print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
787
 
788
+ if not email:
789
  print("Validerar email: (saknas)")
790
  validation_errors.append("En giltig e-postadress krävs.")
791
+ elif '@' not in email or '.' not in email.split('@')[1]:
792
+ print(f"Validerar email: '{email}' (felaktigt format)")
793
  validation_errors.append("En giltig e-postadress krävs.")
794
  else:
795
+ print(f"Validerar email: '{email}' (ok)")
796
 
797
+ # Om det finns valideringsfel
798
  if validation_errors:
799
  print(f"Valideringsfel: {validation_errors}")
 
800
  return {
801
+ chat_interface: gr.Group(visible=False),
802
+ support_interface: gr.Group(visible=True),
803
+ success_interface: gr.Group(visible=False),
804
+ chat_preview: "\n".join(["**Fel:**"] + validation_errors)
805
  }
806
 
807
+ # Om formuläret klarade valideringen, försök skicka till Slack
808
  try:
809
  print("Försöker skicka supportförfrågan till Slack...")
810
 
811
+ # Skapa en förenklad chathistorik för loggning
812
  chat_summary = []
813
+ for msg in chat_history:
814
+ if 'role' in msg and 'content' in msg:
815
+ chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
816
  print(f"Chatthistorik att skicka: {chat_summary}")
817
 
818
+ # Skicka till Slack
819
+ success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
820
 
821
  if success:
822
  print("Support-förfrågan skickad till Slack framgångsrikt")
823
  return {
824
+ chat_interface: gr.Group(visible=False),
825
+ support_interface: gr.Group(visible=False),
826
+ success_interface: gr.Group(visible=True)
827
  }
828
  else:
829
  print("Support-förfrågan till Slack misslyckades")
 
830
  return {
831
+ chat_interface: gr.Group(visible=False),
832
+ support_interface: gr.Group(visible=True),
833
+ success_interface: gr.Group(visible=False),
834
+ chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
835
  }
836
  except Exception as e:
837
  print(f"Oväntat fel vid hantering av support-formulär: {e}")
 
838
  return {
839
+ chat_interface: gr.Group(visible=False),
840
+ support_interface: gr.Group(visible=True),
841
+ success_interface: gr.Group(visible=False),
842
+ chat_preview: f"**Ett fel uppstod: {str(e)}**"
843
  }
844
 
845
  msg.submit(respond, [msg, chatbot], [msg, chatbot])
846
+ clear.click(lambda: None, None, chatbot, queue=False)
847
  support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
848
  back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
849
  back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
 
859
  print("Embedding-modell och index redo!")
860
 
861
  if __name__ == "__main__":
862
+ app.launch(share=True)