Chatbot_4o_mini / app.py
k96beni's picture
Update app.py
d89236a verified
raw
history blame
45.4 kB
import os
import json
import time
import requests
from openai import OpenAI
import gradio as gr
from sentence_transformers import SentenceTransformer
import numpy as np
import faiss
import pandas as pd
from huggingface_hub import CommitScheduler
from datetime import datetime, timedelta
import uuid
from user_agents import parse as parse_ua
import schedule
import threading
import io
import atexit
# --- Konfiguration ---
CHARGENODE_URL = "https://www.chargenode.eu"
MAX_CHUNK_SIZE = 1024
RETRIEVAL_K = 3
# Kontrollera om vi kör i Hugging Face-miljön
IS_HUGGINGFACE = os.environ.get("SPACE_ID") is not None
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY saknas")
client = OpenAI(api_key=OPENAI_API_KEY)
log_folder = "logs"
os.makedirs(log_folder, exist_ok=True)
log_file_path = os.path.join(log_folder, "conversation_log_v2.txt")
# Skapa en tom loggfil om den inte finns
if not os.path.exists(log_file_path):
with open(log_file_path, "w", encoding="utf-8") as f:
f.write("") # Skapa en tom fil
print(f"Skapade tom loggfil: {log_file_path}")
hf_token = os.environ.get("HF_TOKEN")
if not hf_token:
raise ValueError("HF_TOKEN saknas")
# Minsta möjliga konfiguration som bör fungera
scheduler = CommitScheduler(
repo_id="ChargeNodeEurope/logfiles",
repo_type="dataset",
folder_path=log_folder,
path_in_repo="logs_v2",
every=300, # Vänta 5 minuter
token=hf_token
)
# --- Globala variabler ---
last_log = None # Sparar loggdata från senaste svar för feedback
# Lägg till en lock för att skydda åtkomst till last_log
last_log_lock = threading.Lock()
# --- Förbättrad loggfunktion ---
def safe_append_to_log(log_entry):
"""Säker metod för att lägga till loggdata utan att förlora historisk information."""
try:
# Kontrollera att loggmappen finns
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Kontrollera loggfilens storlek
try:
log_size = os.path.getsize(log_file_path)
# Om loggfilen är större än 10MB, rotera den
if log_size > 10 * 1024 * 1024: # 10MB
backup_path = f"{log_file_path}.bak"
if os.path.exists(backup_path):
os.remove(backup_path)
os.rename(log_file_path, backup_path)
print(f"Loggfil roterad: {log_file_path} -> {backup_path}")
except FileNotFoundError:
# Filen finns inte än, inget att rotera
pass
# Sanitera loggdata för att undvika potentiella injektioner
sanitized_entry = {}
for k, v in log_entry.items():
if isinstance(v, str):
# Ta bort null-bytes och begränsa längden
sanitized_entry[k] = v.replace('\0', '')[:10000] # Begränsa till 10000 tecken
else:
sanitized_entry[k] = v
# Öppna filen i append-läge
with open(log_file_path, "a", encoding="utf-8") as log_file:
log_json = json.dumps(sanitized_entry)
log_file.write(log_json + "\n")
log_file.flush() # Säkerställ att data skrivs till disk omedelbart
print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
return True
except Exception as e:
print(f"Fel vid loggning: {e}")
# Försök skapa mappen om den inte finns (detta bör redan ha gjorts ovan)
try:
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
# Försök igen med minimal loggdata för att undvika fel
minimal_entry = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"error": f"Fel vid loggning: {str(e)[:200]}",
"recovery": True
}
with open(log_file_path, "a", encoding="utf-8") as log_file:
log_json = json.dumps(minimal_entry)
log_file.write(log_json + "\n")
print("Minimal loggpost tillagd efter återhämtning")
return True
except Exception as retry_error:
print(f"Kritiskt fel vid loggning: {retry_error}")
return False
# --- Laddar textkällor ---
def load_local_files():
uploaded_text = ""
allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
excluded = ["requirements.txt", "app.py", "conversation_log.txt", "conversation_log_v2.txt", "secrets"]
for file in os.listdir("."):
if file.lower().endswith(tuple(allowed)) and file not in excluded:
try:
if file.endswith(".txt"):
with open(file, "r", encoding="utf-8") as f:
content = f.read()
elif file.endswith(".docx"):
from docx import Document # Import sker vid behov
content = "\n".join([p.text for p in Document(file).paragraphs])
elif file.endswith(".pdf"):
import PyPDF2 # Import sker vid behov
with open(file, "rb") as f:
reader = PyPDF2.PdfReader(f)
content = "\n".join([p.extract_text() or "" for p in reader.pages])
elif file.endswith(".csv"):
content = pd.read_csv(file).to_string()
elif file.endswith((".xls", ".xlsx")):
if file == "FAQ stadat.xlsx":
df = pd.read_excel(file)
rows = []
for index, row in df.iterrows():
rows.append(f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}")
content = "\n\n".join(rows)
else:
content = pd.read_excel(file).to_string()
uploaded_text += f"\n\nFIL: {file}\n{content}"
except Exception as e:
print(f"Fel vid läsning av {file}: {str(e)}")
return uploaded_text.strip()
def load_prompt():
try:
with open("prompt.txt", "r", encoding="utf-8") as f:
return f.read().strip()
except Exception as e:
print(f"Fel vid prompt.txt: {e}")
return ""
prompt_template = load_prompt()
# Förbered textsegment
def prepare_chunks(text_data):
chunks, sources = [], []
for source, text in text_data.items():
paragraphs = [p for p in text.split("\n") if p.strip()]
chunk = ""
for para in paragraphs:
if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
chunk += " " + para
else:
if chunk.strip():
chunks.append(chunk.strip())
sources.append(source)
chunk = para
if chunk.strip():
chunks.append(chunk.strip())
sources.append(source)
return chunks, sources
# Lazy-laddning av SentenceTransformer
embedder = None
embeddings = None
index = None
chunks = []
chunk_sources = []
embedding_lock = threading.Lock()
def initialize_embeddings():
"""Initierar SentenceTransformer och FAISS-index vid första anrop."""
global embedder, embeddings, index, chunks, chunk_sources
# Använd en lock för att förhindra att flera trådar initierar samtidigt
with embedding_lock:
if embedder is None:
try:
print("Initierar SentenceTransformer och FAISS-index...")
# Ladda och förbered lokal data
print("Laddar textdata...")
text_data = {"local_files": load_local_files()}
print("Förbereder textsegment...")
chunks, chunk_sources = prepare_chunks(text_data)
print(f"{len(chunks)} segment laddade")
if not chunks:
print("Varning: Inga textsegment hittades. Kontrollera textdata.")
# Skapa tomma listor för att undvika fel
chunks = [""]
chunk_sources = ["empty"]
print("Skapar embeddings...")
embedder = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = embedder.encode(chunks, convert_to_numpy=True)
# Kontrollera att embeddings inte är tomma
if embeddings.size > 0:
embeddings /= np.linalg.norm(embeddings, axis=1, keepdims=True)
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings)
print("FAISS-index klart")
else:
print("Varning: Tomma embeddings. Skapar ett tomt index.")
# Skapa ett tomt index med rätt dimensioner
index = faiss.IndexFlatIP(384) # Standard dimension för all-MiniLM-L6-v2
except Exception as e:
print(f"Fel vid initiering av embeddings: {e}")
# Sätt upp grundläggande värden för att undvika fel
if embedder is None:
print("Försöker återhämta från fel...")
try:
embedder = SentenceTransformer('all-MiniLM-L6-v2')
if not chunks:
chunks = ["Fel vid laddning av data"]
chunk_sources = ["error"]
embeddings = embedder.encode(chunks, convert_to_numpy=True)
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings)
print("Återhämtning lyckades")
except Exception as recovery_error:
print(f"Kunde inte återhämta: {recovery_error}")
# Sätt upp dummy-värden som sista utväg
embedder = None
embeddings = np.zeros((1, 384))
index = faiss.IndexFlatIP(384)
def retrieve_context(query, k=RETRIEVAL_K):
"""Hämtar relevant kontext för frågor."""
# Säkerställ att modeller är laddade
initialize_embeddings()
try:
if embedder is None:
print("Varning: Embedder är fortfarande None efter initiering")
return "Kunde inte ladda kontext", ["error"]
query_embedding = embedder.encode([query], convert_to_numpy=True)
# Kontrollera att embedding inte är tom
if query_embedding.size == 0:
print("Varning: Tom query embedding")
return "Kunde inte bearbeta frågan", ["error"]
query_embedding /= np.linalg.norm(query_embedding)
# Kontrollera att index finns
if index is None:
print("Varning: FAISS-index är None")
return "Kunde inte söka i kontext", ["error"]
# Säkerställ att k inte är större än antalet element i index
actual_k = min(k, index.ntotal)
if actual_k < k:
print(f"Varning: Justerade k från {k} till {actual_k} baserat på index.ntotal")
D, I = index.search(query_embedding, actual_k)
retrieved, sources = [], set()
for idx in I[0]:
if 0 <= idx < len(chunks):
retrieved.append(chunks[idx])
sources.add(chunk_sources[idx])
if not retrieved:
print("Varning: Ingen relevant kontext hittades")
return "Ingen relevant kontext hittades", ["no_context"]
return " ".join(retrieved), list(sources)
except Exception as e:
print(f"Fel vid hämtning av kontext: {e}")
return f"Fel vid kontexthämtning: {str(e)[:200]}", ["error"]
def generate_answer(query):
"""Genererar svar baserat på fråga och kontextinformation."""
context, sources = retrieve_context(query)
if not context.strip():
return "Jag hittar ingen relevant information i mina källor.\n\nDetta är ett AI genererat svar."
prompt = f"""{prompt_template}
Relevant kontext:
{context}
Fråga: {query}
Svar (baserat enbart på den indexerade datan):"""
try:
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Du är en expert på ChargeNodes produkter och tjänster. Svara enbart baserat på den information som finns i den indexerade datan."},
{"role": "user", "content": prompt}
],
temperature=0.2,
max_tokens=500
)
answer = response.choices[0].message.content
return answer + "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
except Exception as e:
return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
# --- Slack Integration ---
def send_to_slack(subject, content, color="#2a9d8f"):
"""Basfunktion för att skicka meddelanden till Slack."""
webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
if not webhook_url:
print("Slack webhook URL saknas")
return False
try:
# Formatera meddelandet för Slack
payload = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": subject
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": content
}
}
]
}
response = requests.post(
webhook_url,
json=payload,
headers={"Content-Type": "application/json"}
)
if response.status_code == 200:
print(f"Slack-meddelande skickat: {subject}")
return True
else:
print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
return False
except Exception as e:
print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}")
return False
# --- Feedback & Like-funktion ---
def vote(data: gr.LikeData):
"""
Hanterar feedback från Gradio's inbyggda like-funktion.
data.liked är True om uppvote, annars False.
data.value innehåller information om meddelandet.
"""
feedback_type = "up" if data.liked else "down"
global last_log, last_log_lock
# Skapa en kopia av data.value för att undvika potentiella race conditions
bot_reply = data.value if not isinstance(data.value, dict) else data.value.get("value", "")
if bot_reply is None:
bot_reply = ""
log_entry = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"feedback": feedback_type,
"bot_reply": bot_reply
}
# Använd lock för att säkert komma åt last_log
with last_log_lock:
# Om global logdata finns, lägg till ytterligare metadata.
if last_log:
log_entry.update({
"session_id": last_log.get("session_id", "unknown"),
"user_message": last_log.get("user_message", "Okänd fråga"),
})
# Använd den förbättrade loggfunktionen
safe_append_to_log(log_entry)
# Skicka feedback till Slack
try:
if feedback_type == "down": # Skicka bara negativ feedback
# Hämta user_message säkert
user_message = "Okänd fråga"
with last_log_lock:
if last_log:
user_message = last_log.get("user_message", "Okänd fråga")
feedback_message = f"""
*⚠️ Negativ feedback registrerad*
*Fråga:* {user_message}
*Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''}
"""
# Skicka asynkront
threading.Thread(
target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"),
daemon=True
).start()
except Exception as e:
print(f"Kunde inte skicka feedback till Slack: {e}")
return
# --- Rapportering ---
def read_logs():
"""Läs alla loggposter från loggfilen."""
logs = []
try:
if os.path.exists(log_file_path):
with open(log_file_path, "r", encoding="utf-8") as file:
line_count = 0
for line in file:
line_count += 1
try:
log_entry = json.loads(line.strip())
logs.append(log_entry)
except json.JSONDecodeError as e:
print(f"Varning: Kunde inte tolka rad {line_count}: {e}")
continue
print(f"Läste {len(logs)} av {line_count} loggposter")
else:
print(f"Loggfil saknas: {log_file_path}")
except Exception as e:
print(f"Fel vid läsning av loggfil: {e}")
return logs
def get_latest_conversations(logs, limit=50):
"""Hämta de senaste frågorna och svaren."""
conversations = []
for log in reversed(logs):
if 'user_message' in log and 'bot_reply' in log:
conversations.append({
'user_message': log['user_message'],
'bot_reply': log['bot_reply'],
'timestamp': log.get('timestamp', '')
})
if len(conversations) >= limit:
break
return conversations
def get_feedback_stats(logs):
"""Sammanfatta feedback (tumme upp/ned)."""
feedback_count = {"up": 0, "down": 0}
negative_feedback_examples = []
for log in logs:
if 'feedback' in log:
feedback = log.get('feedback')
if feedback in feedback_count:
feedback_count[feedback] += 1
# Samla exempel på negativ feedback
if feedback == "down" and 'user_message' in log and len(negative_feedback_examples) < 10:
negative_feedback_examples.append({
'user_message': log.get('user_message', 'Okänd fråga'),
'bot_reply': log.get('bot_reply', 'Okänt svar')
})
return feedback_count, negative_feedback_examples
def generate_monthly_stats(days=30):
"""Genererar omfattande statistik över botanvändning för den senaste månaden."""
print(f"Genererar statistik för de senaste {days} dagarna...")
# Hämta loggar
logs = read_logs()
if not logs:
return {"error": "Inga loggar hittades för den angivna perioden"}
# Filtrera på datumintervall
now = datetime.now()
cutoff_date = now - timedelta(days=days)
filtered_logs = []
for log in logs:
if 'timestamp' in log:
try:
log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
if log_date >= cutoff_date:
filtered_logs.append(log)
except:
pass # Hoppa över poster med ogiltigt datum
logs = filtered_logs
# Basstatistik
total_conversations = sum(1 for log in logs if 'user_message' in log)
unique_sessions = len(set(log.get('session_id', 'unknown') for log in logs if 'session_id' in log))
unique_users = len(set(log.get('user_id', 'unknown') for log in logs if 'user_id' in log))
# Feedback-statistik
feedback_logs = [log for log in logs if 'feedback' in log]
positive_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'up')
negative_feedback = sum(1 for log in feedback_logs if log.get('feedback') == 'down')
feedback_ratio = (positive_feedback / len(feedback_logs) * 100) if feedback_logs else 0
# Svarstidsstatistik
response_times = [log.get('response_time', 0) for log in logs if 'response_time' in log]
avg_response_time = sum(response_times) / len(response_times) if response_times else 0
# Plattformsstatistik
platforms = {}
browsers = {}
operating_systems = {}
for log in logs:
if 'platform' in log:
platforms[log['platform']] = platforms.get(log['platform'], 0) + 1
if 'browser' in log:
browsers[log['browser']] = browsers.get(log['browser'], 0) + 1
if 'os' in log:
operating_systems[log['os']] = operating_systems.get(log['os'], 0) + 1
# Skapa rapport
report = {
"period": f"Senaste {days} dagarna",
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"basic_stats": {
"total_conversations": total_conversations,
"unique_sessions": unique_sessions,
"unique_users": unique_users,
"messages_per_user": round(total_conversations / unique_users, 2) if unique_users else 0
},
"feedback": {
"positive": positive_feedback,
"negative": negative_feedback,
"ratio_percent": round(feedback_ratio, 1)
},
"performance": {
"avg_response_time": round(avg_response_time, 2)
},
"platform_distribution": platforms,
"browser_distribution": browsers,
"os_distribution": operating_systems
}
return report
def simple_status_report():
"""Skickar en förenklad statusrapport till Slack."""
print("Genererar statusrapport för Slack...")
try:
# Generera statistik
stats = generate_monthly_stats(days=7) # Senaste veckan
# Skapa innehåll för Slack
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
subject = f"ChargeNode AI Bot - Status {now}"
if 'error' in stats:
content = f"*Fel vid generering av statistik:* {stats['error']}"
return send_to_slack(subject, content, "#ff0000")
# Formatera statistik
basic = stats["basic_stats"]
feedback = stats["feedback"]
perf = stats["performance"]
content = f"""
*ChargeNode AI Bot - Statusrapport {now}*
*Basstatistik* (senaste 7 dagarna)
- Totalt antal konversationer: {basic['total_conversations']}
- Unika sessioner: {basic['unique_sessions']}
- Unika användare: {basic['unique_users']}
- Genomsnittlig svarstid: {perf['avg_response_time']} sekunder
*Feedback*
- 👍 Tumme upp: {feedback['positive']}
- 👎 Tumme ned: {feedback['negative']}
- Nöjdhet: {feedback['ratio_percent']}%
"""
# Lägg till de senaste konversationerna
logs = read_logs()
conversations = get_latest_conversations(logs, 3)
if conversations:
content += "\n*Senaste konversationer*\n"
for conv in conversations:
content += f"""
> *Tid:* {conv['timestamp']}
> *Fråga:* {conv['user_message'][:100]}{'...' if len(conv['user_message']) > 100 else ''}
> *Svar:* {conv['bot_reply'][:100]}{'...' if len(conv['bot_reply']) > 100 else ''}
"""
# Skicka till Slack
return send_to_slack(subject, content, "#2a9d8f")
except Exception as e:
print(f"Fel vid generering av statusrapport: {e}")
# Skicka felmeddelande till Slack
error_subject = f"ChargeNode AI Bot - Fel vid statusrapport"
error_content = f"*Fel vid generering av statusrapport:* {str(e)}"
return send_to_slack(error_subject, error_content, "#ff0000")
def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
"""Skickar en supportförfrågan till Slack."""
try:
# Formatera chat-historiken
chat_content = ""
for msg in chat_history:
if msg['role'] == 'user':
chat_content += f">*Användare:* {msg['content']}\n\n"
elif msg['role'] == 'assistant':
chat_content += f">*Bot:* {msg['content'][:300]}{'...' if len(msg['content']) > 300 else ''}\n\n"
# Skapa innehåll
subject = f"Support förfrågan - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
content = f"""
*Användarinformation*
- *Områdeskod:* {områdeskod or 'Ej angiven'}
- *Uttagsnummer:* {uttagsnummer or 'Ej angiven'}
- *Email:* {email}
- *Tidpunkt:* {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
*Chatthistorik:*
{chat_content}
"""
# Skicka till Slack
return send_to_slack(subject, content, "#e76f51")
except Exception as e:
print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}")
return False
# --- Schemaläggning av rapporter ---
def run_scheduler():
"""Kör schemaläggaren i en separat tråd med förenklad statusrapportering."""
# Flagga för att kontrollera om tråden ska fortsätta köra
running = True
try:
# Använd den förenklade funktionen för rapportering
schedule.every().day.at("08:00").do(simple_status_report)
schedule.every().day.at("12:00").do(simple_status_report)
schedule.every().day.at("17:00").do(simple_status_report)
# Veckorapport på måndagar
schedule.every().monday.at("09:00").do(lambda: send_to_slack(
"Veckostatistik",
f"*ChargeNode AI Bot - Veckostatistik*\n\n{json.dumps(generate_monthly_stats(7), indent=2)}",
"#3498db"
))
while running:
try:
schedule.run_pending()
time.sleep(60) # Kontrollera varje minut
except Exception as e:
print(f"Fel i schemaläggaren: {e}")
time.sleep(300) # Vänta 5 minuter vid fel
except Exception as e:
print(f"Kritiskt fel i schemaläggaren: {e}")
finally:
print("Schemaläggaren avslutad")
# Starta schemaläggaren i en separat tråd
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)
scheduler_thread.start()
# Registrera en atexit-funktion för att städa upp vid avslut
def cleanup():
"""Städa upp resurser vid avslut."""
print("Städar upp resurser...")
# Stäng scheduler om möjligt
schedule.clear()
# Stäng CommitScheduler om den är aktiv
if 'scheduler' in globals() and scheduler:
try:
scheduler.stop()
print("CommitScheduler stoppad")
except Exception as e:
print(f"Kunde inte stoppa CommitScheduler: {e}")
# Stäng eventuella öppna filer
try:
# Försök att stänga alla öppna filer
import gc
for obj in gc.get_objects():
if isinstance(obj, io.IOBase) and not obj.closed:
try:
obj.close()
except:
pass
except Exception as e:
print(f"Fel vid stängning av filer: {e}")
atexit.register(cleanup)
# Kör en statusrapport vid uppstart för att verifiera att allt fungerar
try:
print("Skickar en inledande statusrapport för att verifiera Slack-integrationen...")
# Anropa inte direkt här - sker i schemaläggaren
except Exception as e:
print(f"Information: Statusrapport kommer att skickas enligt schema: {e}")
# Definiera respond och chat-relaterade funktioner före Gradio UI
def respond(message, chat_history, request: gr.Request):
global last_log, last_log_lock
# Validera indata
if not message or not isinstance(message, str):
print(f"Varning: Ogiltigt meddelande: {type(message)}")
message = str(message) if message is not None else ""
# Begränsa meddelandets längd för att förhindra överbelastning
if len(message) > 1000:
message = message[:1000] + "..."
print("Meddelande trunkerat på grund av längd")
start = time.time()
try:
response = generate_answer(message)
except Exception as e:
print(f"Fel vid generering av svar: {e}")
response = f"Tyvärr uppstod ett tekniskt fel. Vänligen försök igen eller kontakta support@chargenode.eu. Felkod: {str(e)[:50]}"
elapsed = round(time.time() - start, 2)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
session_id = str(uuid.uuid4())
# Använd session_id från tidigare logg om det finns
with last_log_lock:
if last_log and 'session_id' in last_log:
session_id = last_log.get('session_id')
# Säker hantering av request-objektet
user_id = "okänd"
ua_str = ""
ref = ""
ip = ""
if request:
try:
user_id = request.client.host if hasattr(request.client, 'host') else "okänd"
ua_str = request.headers.get("user-agent", "") if hasattr(request, 'headers') else ""
ref = request.headers.get("referer", "") if hasattr(request, 'headers') else ""
ip = request.headers.get("x-forwarded-for", user_id).split(",")[0] if hasattr(request, 'headers') else user_id
except Exception as e:
print(f"Fel vid läsning av request-data: {e}")
# Säker parsing av user agent
try:
ua = parse_ua(ua_str)
browser = f"{ua.browser.family} {ua.browser.version_string}"
osys = f"{ua.os.family} {ua.os.version_string}"
except Exception as e:
print(f"Fel vid parsing av user agent: {e}")
browser = "okänd"
osys = "okänd"
# Bestäm plattform
platform = "webb"
try:
if ref:
if "chargenode.eu" in ref:
platform = "chargenode.eu"
elif "localhost" in ref:
platform = "test"
elif "app" in ref:
platform = "app"
except Exception as e:
print(f"Fel vid bestämning av plattform: {e}")
# Skapa loggdata
log_data = {
"timestamp": timestamp,
"user_id": user_id,
"session_id": session_id,
"user_message": message,
"bot_reply": response,
"response_time": elapsed,
"ip": ip,
"browser": browser,
"os": osys,
"platform": platform
}
# Använd den förbättrade loggfunktionen
safe_append_to_log(log_data)
# Uppdatera last_log säkert
with last_log_lock:
last_log = log_data.copy() # Använd en kopia för att undvika race conditions
# Skicka varje konversation direkt till Slack
try:
# Konversationsinnehåll
conversation_content = f"""
*Ny konversation {timestamp}*
*Användare:* {message}
*Bot:* {response[:300]}{'...' if len(response) > 300 else ''}
*Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
"""
# Skicka asynkront för att inte blockera svarstiden
threading.Thread(
target=lambda: send_to_slack(f"Ny konversation", conversation_content),
daemon=True
).start()
except Exception as e:
print(f"Kunde inte skicka konversation till Slack: {e}")
# Uppdatera chatthistorik
try:
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": response})
except Exception as e:
print(f"Fel vid uppdatering av chatthistorik: {e}")
# Försök återställa chatthistoriken om något går fel
if not chat_history:
chat_history = []
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": response})
return "", chat_history
def format_chat_preview(chat_history):
if not chat_history:
return "Ingen chatthistorik att visa."
preview = ""
for msg in chat_history:
sender = "Användare" if msg["role"] == "user" else "Bot"
content = msg["content"]
if len(content) > 100: # Truncate long messages
content = content[:100] + "..."
preview += f"**{sender}:** {content}\n\n"
return preview
def show_support_form(chat_history):
preview = format_chat_preview(chat_history)
return {
chat_interface: gr.Group(visible=False),
support_interface: gr.Group(visible=True),
success_interface: gr.Group(visible=False),
chat_preview: preview
}
def back_to_chat():
return {
chat_interface: gr.Group(visible=True),
support_interface: gr.Group(visible=False),
success_interface: gr.Group(visible=False)
}
def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
"""Hanterar formulärinskickningen med bättre felhantering."""
print(f"Support-förfrågan: områdeskod={områdeskod}, uttagsnummer={uttagsnummer}, email={email}")
# Validera input med tydligare loggning
validation_errors = []
if områdeskod and not områdeskod.isdigit():
print(f"Validerar områdeskod: '{områdeskod}' (felaktig)")
validation_errors.append("Områdeskod måste vara numerisk.")
else:
print(f"Validerar områdeskod: '{områdeskod}' (ok)")
if uttagsnummer and not uttagsnummer.isdigit():
print(f"Validerar uttagsnummer: '{uttagsnummer}' (felaktig)")
validation_errors.append("Uttagsnummer måste vara numerisk.")
else:
print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
if not email:
print("Validerar email: (saknas)")
validation_errors.append("En giltig e-postadress krävs.")
elif '@' not in email or '.' not in email.split('@')[1]:
print(f"Validerar email: '{email}' (felaktigt format)")
validation_errors.append("En giltig e-postadress krävs.")
else:
print(f"Validerar email: '{email}' (ok)")
# Om det finns valideringsfel
if validation_errors:
print(f"Valideringsfel: {validation_errors}")
return {
chat_interface: gr.Group(visible=False),
support_interface: gr.Group(visible=True),
success_interface: gr.Group(visible=False),
chat_preview: "\n".join(["**Fel:**"] + validation_errors)
}
# Om formuläret klarade valideringen, försök skicka till Slack
try:
print("Försöker skicka supportförfrågan till Slack...")
# Skapa en förenklad chathistorik för loggning
chat_summary = []
for msg in chat_history:
if 'role' in msg and 'content' in msg:
chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
print(f"Chatthistorik att skicka: {chat_summary}")
# Skicka till Slack
success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
if success:
print("Support-förfrågan skickad till Slack framgångsrikt")
return {
chat_interface: gr.Group(visible=False),
support_interface: gr.Group(visible=False),
success_interface: gr.Group(visible=True)
}
else:
print("Support-förfrågan till Slack misslyckades")
return {
chat_interface: gr.Group(visible=False),
support_interface: gr.Group(visible=True),
success_interface: gr.Group(visible=False),
chat_preview: "**Ett fel uppstod när meddelandet skulle skickas. Vänligen försök igen senare.**"
}
except Exception as e:
print(f"Oväntat fel vid hantering av support-formulär: {e}")
return {
chat_interface: gr.Group(visible=False),
support_interface: gr.Group(visible=True),
success_interface: gr.Group(visible=False),
chat_preview: f"**Ett fel uppstod: {str(e)}**"
}
# --- Gradio UI ---
initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}]
custom_css = """
body {background-color: #f7f7f7; font-family: Arial, sans-serif; margin: 0; padding: 0;}
h1 {font-family: Helvetica, sans-serif; color: #2a9d8f; text-align: center; margin-bottom: 0.5em;}
.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;}
#chatbot_conversation { max-height: 300px; overflow-y: auto; }
.message-wrap { scroll-behavior: smooth; }
.message.bot:last-child { scroll-margin-top: 100px; }
.support-form-container { margin-top: 40px; }
.support-form-container .gr-form { margin-top: 15px; }
.gr-button {background-color: #2a9d8f; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; margin: 5px;}
.gr-button:hover {background-color: #264653;}
.support-btn {background-color: #000000; color: #ffffff; margin-top: 5px; margin-bottom: 10px;}
.support-btn:hover {background-color: #333333;}
.flex-row {display: flex; flex-direction: row; gap: 5px;}
.gr-form {padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px;}
.chat-preview {max-height: 150px; overflow-y: auto; border: 1px solid #eee; padding: 8px; margin-top: 10px; font-size: 12px; background-color: #f9f9f9;}
.success-message {font-size: 16px; font-weight: normal; margin-bottom: 15px;}
/* Dölj Gradio-footer */
footer {display: none !important;}
.footer {display: none !important;}
.gr-footer {display: none !important;}
.gradio-footer {display: none !important;}
.gradio-container .footer {display: none !important;}
.gradio-container .gr-footer {display: none !important;}
"""
# VIKTIGT: Alla komponenter och eventkopplingar definieras inuti Blocks-kontexten
with gr.Blocks(css=custom_css, title="ChargeNode Kundtjänst") as app:
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")
# Chat interface
with gr.Group(visible=True) as chat_interface:
chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
chatbot.like(vote, None, None)
with gr.Row():
msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
with gr.Row():
with gr.Column(scale=1):
clear = gr.Button("Rensa")
with gr.Column(scale=1):
support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
# Lägg till anpassad JavaScript för att styra scrollning
js_code = """
// Mer robust funktion för att scrolla till senaste botmeddelandet
function scrollToLatestBotMessage() {
console.log("Försöker scrolla till senaste botmeddelandet");
// Försök hitta det senaste botmeddelandet med olika selektorer (för kompatibilitet med Hugging Face)
let latestBotMessage = document.querySelector('.gradio-container .message.bot:last-child');
// Alternativ selektor för Hugging Face
if (!latestBotMessage) {
latestBotMessage = document.querySelector('.gradio-container [data-testid="bot"]:last-child');
}
// Ytterligare alternativ selektor
if (!latestBotMessage) {
latestBotMessage = document.querySelector('.gradio-container .message:last-child');
}
if (latestBotMessage) {
console.log("Hittade senaste botmeddelandet, scrollar till det");
// Scrolla så att det senaste botmeddelandet är synligt i toppen av chattrutan
try {
latestBotMessage.scrollIntoView({block: 'start', behavior: 'smooth'});
console.log("scrollIntoView utförd");
} catch (e) {
console.error("Fel vid scrollning:", e);
// Fallback: Använd manuell scrollning om scrollIntoView misslyckas
try {
const chatContainer = latestBotMessage.closest('.chat');
if (chatContainer) {
const topPos = latestBotMessage.offsetTop;
chatContainer.scrollTop = topPos;
console.log("Manuell scrollning utförd");
}
} catch (e2) {
console.error("Fallback-scrollning misslyckades:", e2);
}
}
} else {
console.log("Kunde inte hitta senaste botmeddelandet");
}
}
// Kör funktionen när nya meddelanden läggs till
document.addEventListener('DOMNodeInserted', function(event) {
// Kontrollera om det infogade elementet är ett botmeddelande
if (event.target.classList &&
(event.target.classList.contains('bot') ||
event.target.getAttribute('data-testid') === 'bot')) {
console.log("Bot-meddelande detekterat, schemalägg scrollning");
// Använd flera timeouts med olika fördröjningar för att säkerställa att scrollningen fungerar
setTimeout(scrollToLatestBotMessage, 100);
setTimeout(scrollToLatestBotMessage, 300);
setTimeout(scrollToLatestBotMessage, 500);
}
});
// Lägg även till en MutationObserver som ett alternativt sätt att upptäcka nya meddelanden
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM laddad, sätter upp MutationObserver");
// Hitta chatcontainern
const chatContainer = document.querySelector('.gradio-container');
if (chatContainer) {
// Skapa en observer som övervakar ändringar i DOM
const observer = new MutationObserver(function(mutations) {
let shouldScroll = false;
mutations.forEach(function(mutation) {
// Kontrollera om nya noder har lagts till
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(function(node) {
// Kontrollera om den tillagda noden är ett botmeddelande
if (node.classList &&
(node.classList.contains('bot') ||
node.getAttribute('data-testid') === 'bot')) {
shouldScroll = true;
}
});
}
});
if (shouldScroll) {
console.log("MutationObserver detekterade nytt botmeddelande");
setTimeout(scrollToLatestBotMessage, 200);
}
});
// Starta övervakning av hela chatcontainern för ändringar i DOM
observer.observe(chatContainer, {
childList: true,
subtree: true
});
console.log("MutationObserver startad");
}
});
"""
app.load(js=js_code)
# Support form interface (initially hidden)
with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
with gr.Group(elem_classes="gr-form"):
områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
gr.Markdown("### Chat som skickas till support:")
chat_preview = gr.Markdown(elem_classes="chat-preview")
with gr.Row():
back_btn = gr.Button("Tillbaka")
send_support_btn = gr.Button("Skicka")
# Success message (initially hidden)
with gr.Group(visible=False) as success_interface:
gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
back_to_chat_btn = gr.Button("Tillbaka till chatten")
# VIKTIGT: Händelsehanterare definieras INOM gr.Blocks-kontexten, efter att komponenterna definierats
msg.submit(respond, [msg, chatbot], [msg, chatbot])
clear.click(lambda: None, None, chatbot, queue=False)
support_btn.click(show_support_form, chatbot, [chat_interface, support_interface, success_interface, chat_preview])
back_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
back_to_chat_btn.click(back_to_chat, None, [chat_interface, support_interface, success_interface])
send_support_btn.click(
submit_support_form,
[områdeskod, uttagsnummer, email, chatbot],
[chat_interface, support_interface, success_interface, chat_preview]
)
if __name__ == "__main__":
app.launch(share=True)