Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -39,22 +39,29 @@ if not os.path.exists(log_file_path):
|
|
| 39 |
print(f"Skapade tom loggfil: {log_file_path}")
|
| 40 |
|
| 41 |
hf_token = os.environ.get("HF_TOKEN")
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
# --- Globala variabler ---
|
| 57 |
last_log = None # Sparar loggdata från senaste svar för feedback
|
|
|
|
|
|
|
| 58 |
|
| 59 |
# --- Förbättrad loggfunktion ---
|
| 60 |
def safe_append_to_log(log_entry):
|
|
@@ -62,28 +69,28 @@ def safe_append_to_log(log_entry):
|
|
| 62 |
try:
|
| 63 |
# Öppna filen i append-läge
|
| 64 |
with open(log_file_path, "a", encoding="utf-8") as log_file:
|
| 65 |
-
log_json = json.dumps(log_entry)
|
| 66 |
log_file.write(log_json + "\n")
|
| 67 |
log_file.flush() # Säkerställ att data skrivs till disk omedelbart
|
| 68 |
-
|
| 69 |
-
print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}")
|
| 70 |
return True
|
| 71 |
-
|
| 72 |
except Exception as e:
|
| 73 |
print(f"Fel vid loggning: {e}")
|
| 74 |
-
|
| 75 |
# Försök skapa mappen om den inte finns
|
| 76 |
try:
|
| 77 |
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
|
| 78 |
-
|
| 79 |
# Försök igen
|
| 80 |
with open(log_file_path, "a", encoding="utf-8") as log_file:
|
| 81 |
-
log_json = json.dumps(log_entry)
|
| 82 |
log_file.write(log_json + "\n")
|
| 83 |
-
|
| 84 |
print("Loggpost tillagd efter återhämtning")
|
| 85 |
return True
|
| 86 |
-
|
| 87 |
except Exception as retry_error:
|
| 88 |
print(f"Kritiskt fel vid loggning: {retry_error}")
|
| 89 |
return False
|
|
@@ -92,67 +99,135 @@ def safe_append_to_log(log_entry):
|
|
| 92 |
def load_local_files():
|
| 93 |
uploaded_text = ""
|
| 94 |
allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
try:
|
| 99 |
-
if
|
| 100 |
-
with open(
|
| 101 |
content = f.read()
|
| 102 |
-
elif
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
except Exception as e:
|
| 123 |
-
print(f"
|
| 124 |
return uploaded_text.strip()
|
| 125 |
|
| 126 |
def load_prompt():
|
| 127 |
try:
|
| 128 |
-
|
|
|
|
| 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 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
for para in paragraphs:
|
| 143 |
-
if len(
|
| 144 |
-
|
| 145 |
else:
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
sources.append(source)
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
| 152 |
sources.append(source)
|
| 153 |
-
return chunks, sources
|
| 154 |
|
| 155 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
embedder = None
|
| 157 |
embeddings = None
|
| 158 |
index = None
|
|
@@ -160,108 +235,215 @@ index = None
|
|
| 160 |
def initialize_embeddings():
|
| 161 |
"""Initierar SentenceTransformer och FAISS-index vid första anrop."""
|
| 162 |
global embedder, embeddings, index, chunks, chunk_sources
|
| 163 |
-
|
|
|
|
| 164 |
if embedder is None:
|
| 165 |
print("Initierar SentenceTransformer och FAISS-index...")
|
| 166 |
# Ladda och förbered lokal data
|
| 167 |
print("Laddar textdata...")
|
| 168 |
text_data = {"local_files": load_local_files()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
print("Förbereder textsegment...")
|
| 170 |
-
chunks, chunk_sources = prepare_chunks(text_data)
|
| 171 |
-
print(f"{len(chunks)} segment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
print("Skapar embeddings...")
|
| 174 |
embedder = SentenceTransformer('all-MiniLM-L6-v2')
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
def retrieve_context(query, k=RETRIEVAL_K):
|
| 182 |
"""Hämtar relevant kontext för frågor."""
|
| 183 |
-
# Säkerställ att
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
def generate_answer(query):
|
| 197 |
"""Genererar svar baserat på fråga och kontextinformation."""
|
|
|
|
| 198 |
context, sources = retrieve_context(query)
|
|
|
|
|
|
|
|
|
|
| 199 |
if not context.strip():
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
prompt = f"""{prompt_template}
|
| 202 |
-
|
| 203 |
Relevant kontext:
|
| 204 |
-
{context}
|
|
|
|
| 205 |
Fråga: {query}
|
| 206 |
-
|
|
|
|
|
|
|
| 207 |
try:
|
|
|
|
| 208 |
response = client.chat.completions.create(
|
| 209 |
model="gpt-3.5-turbo",
|
| 210 |
messages=[
|
| 211 |
-
{"role": "system", "content": "Du är en
|
| 212 |
{"role": "user", "content": prompt}
|
| 213 |
],
|
| 214 |
temperature=0.2,
|
| 215 |
max_tokens=500
|
| 216 |
)
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
except Exception as e:
|
| 220 |
-
|
|
|
|
|
|
|
| 221 |
|
| 222 |
# --- Slack Integration ---
|
| 223 |
def send_to_slack(subject, content, color="#2a9d8f"):
|
| 224 |
"""Basfunktion för att skicka meddelanden till Slack."""
|
| 225 |
webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
|
| 226 |
if not webhook_url:
|
| 227 |
-
print("Slack webhook URL saknas")
|
| 228 |
return False
|
| 229 |
-
|
| 230 |
try:
|
| 231 |
# Formatera meddelandet för Slack
|
| 232 |
payload = {
|
| 233 |
-
"
|
| 234 |
-
{
|
| 235 |
-
"type": "header",
|
| 236 |
-
"text": {
|
| 237 |
-
"type": "plain_text",
|
| 238 |
-
"text": subject
|
| 239 |
-
}
|
| 240 |
-
},
|
| 241 |
{
|
| 242 |
-
"
|
| 243 |
-
"
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
}
|
| 248 |
]
|
| 249 |
}
|
| 250 |
-
|
| 251 |
response = requests.post(
|
| 252 |
webhook_url,
|
| 253 |
json=payload,
|
| 254 |
-
headers={"Content-Type": "application/json"}
|
|
|
|
| 255 |
)
|
| 256 |
-
|
| 257 |
if response.status_code == 200:
|
| 258 |
-
print(f"Slack-meddelande skickat: {subject}")
|
| 259 |
return True
|
| 260 |
else:
|
| 261 |
print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
|
| 262 |
return False
|
|
|
|
|
|
|
|
|
|
| 263 |
except Exception as e:
|
| 264 |
-
print(f"
|
| 265 |
return False
|
| 266 |
|
| 267 |
# --- Feedback & Like-funktion ---
|
|
@@ -273,69 +455,106 @@ def vote(data: gr.LikeData):
|
|
| 273 |
"""
|
| 274 |
feedback_type = "up" if data.liked else "down"
|
| 275 |
global last_log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
log_entry = {
|
| 277 |
-
"timestamp":
|
|
|
|
| 278 |
"feedback": feedback_type,
|
| 279 |
-
"
|
| 280 |
}
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
| 283 |
log_entry.update({
|
| 284 |
"session_id": last_log.get("session_id"),
|
| 285 |
"user_message": last_log.get("user_message"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
})
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
# Använd den förbättrade loggfunktionen
|
| 289 |
safe_append_to_log(log_entry)
|
| 290 |
-
|
| 291 |
# Skicka feedback till Slack
|
| 292 |
try:
|
| 293 |
-
if feedback_type ==
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
*
|
|
|
|
| 298 |
|
| 299 |
-
*
|
| 300 |
"""
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
|
|
|
| 306 |
except Exception as e:
|
| 307 |
print(f"Kunde inte skicka feedback till Slack: {e}")
|
| 308 |
-
|
|
|
|
| 309 |
return
|
| 310 |
|
| 311 |
# --- Rapportering ---
|
| 312 |
def read_logs():
|
| 313 |
"""Läs alla loggposter från loggfilen."""
|
| 314 |
logs = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
try:
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
except Exception as e:
|
| 331 |
-
print(f"
|
| 332 |
return logs
|
| 333 |
|
| 334 |
def get_latest_conversations(logs, limit=50):
|
| 335 |
-
"""Hämta de senaste frågorna och svaren."""
|
| 336 |
conversations = []
|
|
|
|
| 337 |
for log in reversed(logs):
|
| 338 |
-
|
|
|
|
| 339 |
conversations.append({
|
| 340 |
'user_message': log['user_message'],
|
| 341 |
'bot_reply': log['bot_reply'],
|
|
@@ -346,504 +565,670 @@ def get_latest_conversations(logs, limit=50):
|
|
| 346 |
return conversations
|
| 347 |
|
| 348 |
def get_feedback_stats(logs):
|
| 349 |
-
"""Sammanfatta feedback (tumme upp/ned)."""
|
| 350 |
-
feedback_count = {"up": 0, "down": 0}
|
| 351 |
-
negative_feedback_examples = []
|
| 352 |
-
|
| 353 |
for log in logs:
|
| 354 |
-
|
|
|
|
| 355 |
feedback = log.get('feedback')
|
| 356 |
-
if feedback
|
| 357 |
-
feedback_count[
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
negative_feedback_examples
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
return feedback_count, negative_feedback_examples
|
| 367 |
|
| 368 |
-
def
|
| 369 |
-
"""Genererar
|
| 370 |
print(f"Genererar statistik för de senaste {days} dagarna...")
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
# Filtrera på datumintervall
|
| 379 |
now = datetime.now()
|
| 380 |
cutoff_date = now - timedelta(days=days)
|
| 381 |
filtered_logs = []
|
| 382 |
-
|
| 383 |
-
|
|
|
|
| 384 |
if 'timestamp' in log:
|
| 385 |
try:
|
|
|
|
| 386 |
log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S")
|
| 387 |
if log_date >= cutoff_date:
|
| 388 |
filtered_logs.append(log)
|
| 389 |
-
except:
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
avg_response_time = sum(response_times) / len(response_times) if response_times else 0
|
| 408 |
-
|
| 409 |
-
#
|
| 410 |
platforms = {}
|
| 411 |
browsers = {}
|
| 412 |
operating_systems = {}
|
| 413 |
-
for log in
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
# Skapa rapport
|
| 422 |
report = {
|
| 423 |
-
"period": f"Senaste {days} dagarna",
|
| 424 |
-
"generated_at":
|
| 425 |
"basic_stats": {
|
| 426 |
"total_conversations": total_conversations,
|
| 427 |
"unique_sessions": unique_sessions,
|
| 428 |
-
"unique_users": unique_users,
|
| 429 |
-
"
|
| 430 |
},
|
| 431 |
"feedback": {
|
|
|
|
| 432 |
"positive": positive_feedback,
|
| 433 |
"negative": negative_feedback,
|
| 434 |
-
"
|
| 435 |
},
|
| 436 |
"performance": {
|
| 437 |
-
"
|
| 438 |
},
|
| 439 |
-
"
|
| 440 |
-
"
|
| 441 |
-
"
|
|
|
|
| 442 |
}
|
| 443 |
-
|
| 444 |
return report
|
| 445 |
|
| 446 |
-
def simple_status_report():
|
| 447 |
-
"""Skickar en förenklad statusrapport till Slack."""
|
| 448 |
-
print("Genererar statusrapport för Slack...")
|
| 449 |
-
|
| 450 |
-
try:
|
| 451 |
-
# Generera statistik
|
| 452 |
-
stats = generate_monthly_stats(days=7) # Senaste veckan
|
| 453 |
-
|
| 454 |
-
# Skapa innehåll för Slack
|
| 455 |
-
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 456 |
-
subject = f"ChargeNode AI Bot - Status {now}"
|
| 457 |
-
|
| 458 |
-
if 'error' in stats:
|
| 459 |
-
content = f"*Fel vid generering av statistik:* {stats['error']}"
|
| 460 |
-
return send_to_slack(subject, content, "#ff0000")
|
| 461 |
-
|
| 462 |
-
# Formatera statistik
|
| 463 |
-
basic = stats["basic_stats"]
|
| 464 |
-
feedback = stats["feedback"]
|
| 465 |
-
perf = stats["performance"]
|
| 466 |
-
|
| 467 |
-
content = f"""
|
| 468 |
-
*ChargeNode AI Bot - Statusrapport {now}*
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
- Unika sessioner: {basic['unique_sessions']}
|
| 473 |
-
-
|
| 474 |
-
-
|
| 475 |
|
| 476 |
-
|
| 477 |
-
-
|
| 478 |
-
-
|
| 479 |
-
- Nöjdhet: {feedback['
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
|
|
|
| 493 |
"""
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
except Exception as e:
|
| 499 |
-
print(f"
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
| 505 |
|
|
|
|
| 506 |
def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history):
|
| 507 |
"""Skickar en supportförfrågan till Slack."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
try:
|
| 509 |
# Formatera chat-historiken
|
| 510 |
chat_content = ""
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
# Skapa innehåll
|
| 518 |
-
subject = f"
|
| 519 |
-
|
|
|
|
| 520 |
content = f"""
|
| 521 |
-
*
|
| 522 |
-
|
| 523 |
-
|
| 524 |
- *Email:* {email}
|
| 525 |
-
- *
|
|
|
|
| 526 |
|
| 527 |
-
|
| 528 |
-
{chat_content}
|
| 529 |
"""
|
| 530 |
-
|
| 531 |
# Skicka till Slack
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
except Exception as e:
|
| 534 |
-
print(f"
|
| 535 |
return False
|
| 536 |
|
| 537 |
-
|
|
|
|
| 538 |
def run_scheduler():
|
| 539 |
-
"""Kör schemaläggaren i en separat tråd
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
schedule.every().day.at("
|
| 544 |
-
|
| 545 |
-
# Veckorapport på måndagar
|
| 546 |
-
schedule.every().monday.at("09:00").do(lambda:
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
while True:
|
| 553 |
-
|
|
|
|
|
|
|
|
|
|
| 554 |
time.sleep(60) # Kontrollera varje minut
|
| 555 |
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
# Kör en statusrapport vid uppstart för att verifiera att allt fungerar
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
#
|
| 564 |
-
|
| 565 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
# --- Gradio UI ---
|
| 568 |
-
initial_chat = [{"role": "assistant", "content": "
|
| 569 |
|
|
|
|
| 570 |
custom_css = """
|
| 571 |
-
body {
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
#chatbot_conversation {
|
| 575 |
-
|
| 576 |
-
.message.
|
| 577 |
-
.
|
| 578 |
-
.
|
| 579 |
-
.
|
| 580 |
-
.gr-
|
| 581 |
-
.
|
| 582 |
-
.
|
| 583 |
-
.
|
| 584 |
-
.
|
| 585 |
-
.
|
| 586 |
-
.
|
| 587 |
-
|
| 588 |
-
footer {display: none !important;}
|
| 589 |
-
.footer {display: none !important;}
|
| 590 |
-
|
| 591 |
-
.
|
| 592 |
-
.
|
| 593 |
-
.
|
| 594 |
"""
|
| 595 |
|
| 596 |
-
|
| 597 |
-
gr.Markdown("Ställ din fråga om ChargeNodes produkter och tjänster nedan. Om du inte gillar botten, så ring oss gärna på 010 – 205 10 55")
|
| 598 |
-
|
| 599 |
-
# Chat interface
|
| 600 |
-
with gr.Group(visible=True) as chat_interface:
|
| 601 |
-
chatbot = gr.Chatbot(value=initial_chat, type="messages", elem_id="chatbot_conversation")
|
| 602 |
-
chatbot.like(vote, None, None)
|
| 603 |
-
|
| 604 |
-
with gr.Row():
|
| 605 |
-
msg = gr.Textbox(label="Meddelande", placeholder="Ange din fråga...")
|
| 606 |
-
|
| 607 |
-
with gr.Row():
|
| 608 |
-
with gr.Column(scale=1):
|
| 609 |
-
clear = gr.Button("Rensa")
|
| 610 |
-
with gr.Column(scale=1):
|
| 611 |
-
support_btn = gr.Button("Behöver du mer hjälp?", elem_classes="support-btn")
|
| 612 |
-
|
| 613 |
-
# Lägg till anpassad JavaScript för att styra scrollning
|
| 614 |
-
js_code = """
|
| 615 |
-
function scrollToTop() {
|
| 616 |
-
const chatContainer = document.querySelector('.gradio-container .message-wrap');
|
| 617 |
-
if (chatContainer) {
|
| 618 |
-
chatContainer.scrollTop = 0;
|
| 619 |
-
}
|
| 620 |
-
}
|
| 621 |
-
|
| 622 |
-
// Kör funktionen när nya meddelanden läggs till
|
| 623 |
-
document.addEventListener('DOMNodeInserted', function(event) {
|
| 624 |
-
if (event.target.classList && event.target.classList.contains('bot')) {
|
| 625 |
-
setTimeout(scrollToTop, 100);
|
| 626 |
-
}
|
| 627 |
-
});
|
| 628 |
-
"""
|
| 629 |
-
app.load(js=js_code)
|
| 630 |
-
|
| 631 |
-
# Support form interface (initially hidden)
|
| 632 |
-
with gr.Group(visible=False, elem_classes="support-form-container") as support_interface:
|
| 633 |
-
gr.Markdown("### Vänligen fyll i din områdeskod, uttagsnummer och din email adress")
|
| 634 |
-
|
| 635 |
-
with gr.Group(elem_classes="gr-form"):
|
| 636 |
-
områdeskod = gr.Textbox(label="Områdeskod", placeholder="Områdeskod (valfritt)", info="Numeriskt värde")
|
| 637 |
-
uttagsnummer = gr.Textbox(label="Uttagsnummer", placeholder="Uttagsnummer (valfritt)", info="Numeriskt värde")
|
| 638 |
-
email = gr.Textbox(label="Din email adress", placeholder="din@email.se", info="Email adress krävs")
|
| 639 |
-
|
| 640 |
-
gr.Markdown("### Chat som skickas till support:")
|
| 641 |
-
chat_preview = gr.Markdown(elem_classes="chat-preview")
|
| 642 |
-
|
| 643 |
-
with gr.Row():
|
| 644 |
-
back_btn = gr.Button("Tillbaka")
|
| 645 |
-
send_support_btn = gr.Button("Skicka")
|
| 646 |
-
|
| 647 |
-
# Success message (initially hidden)
|
| 648 |
-
with gr.Group(visible=False) as success_interface:
|
| 649 |
-
gr.Markdown("Tack för att du kontaktar support@chargenode.eu. Vi återkommer inom kort", elem_classes="success-message")
|
| 650 |
-
back_to_chat_btn = gr.Button("Tillbaka till chatten")
|
| 651 |
-
|
| 652 |
-
# Detta bör vara slutet av app.py-filen från funktionen "respond" och nedåt
|
| 653 |
|
| 654 |
def respond(message, chat_history, request: gr.Request):
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
|
| 660 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 661 |
-
session_id = str(uuid.uuid4())
|
| 662 |
-
|
| 663 |
-
# Använd session_id från tidigare logg om det finns
|
| 664 |
-
if last_log and 'session_id' in last_log:
|
| 665 |
-
session_id = last_log.get('session_id')
|
| 666 |
-
|
| 667 |
-
user_id = request.client.host if request else "okänd"
|
| 668 |
-
|
| 669 |
-
ua_str = request.headers.get("user-agent", "")
|
| 670 |
-
ref = request.headers.get("referer", "")
|
| 671 |
-
ip = request.headers.get("x-forwarded-for", user_id).split(",")[0]
|
| 672 |
-
ua = parse_ua(ua_str)
|
| 673 |
-
browser = f"{ua.browser.family} {ua.browser.version_string}"
|
| 674 |
-
osys = f"{ua.os.family} {ua.os.version_string}"
|
| 675 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
platform = "webb"
|
| 677 |
-
if
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
|
|
|
| 684 |
log_data = {
|
| 685 |
"timestamp": timestamp,
|
|
|
|
| 686 |
"user_id": user_id,
|
| 687 |
"session_id": session_id,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
"user_message": message,
|
| 689 |
-
"bot_reply":
|
| 690 |
-
"response_time":
|
| 691 |
-
|
| 692 |
-
"
|
| 693 |
-
"
|
| 694 |
-
"platform": platform
|
| 695 |
}
|
| 696 |
|
| 697 |
-
# Använd den förbättrade loggfunktionen
|
| 698 |
safe_append_to_log(log_data)
|
| 699 |
-
last_log = log_data
|
| 700 |
|
| 701 |
-
# Skicka
|
| 702 |
try:
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
*Ny konversation {timestamp}*
|
| 706 |
-
|
| 707 |
*Användare:* {message}
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
*Sessionsinfo:* {session_id[:8]}... | {browser} | {platform}
|
| 712 |
"""
|
| 713 |
-
# Skicka asynkront för att inte blockera svarstiden
|
| 714 |
threading.Thread(
|
| 715 |
-
target=
|
|
|
|
| 716 |
daemon=True
|
| 717 |
).start()
|
| 718 |
except Exception as e:
|
| 719 |
-
print(f"Kunde inte skicka konversation till Slack: {e}")
|
|
|
|
| 720 |
|
|
|
|
| 721 |
chat_history.append({"role": "user", "content": message})
|
| 722 |
-
chat_history.append({"role": "assistant", "content":
|
| 723 |
-
|
| 724 |
-
#
|
| 725 |
-
# Vi förlitar oss istället på JavaScript-koden som redan laddats via app.load()
|
| 726 |
-
|
| 727 |
return "", chat_history
|
| 728 |
|
| 729 |
def format_chat_preview(chat_history):
|
|
|
|
| 730 |
if not chat_history:
|
| 731 |
-
return "
|
| 732 |
-
|
| 733 |
preview = ""
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
|
| 743 |
def show_support_form(chat_history):
|
| 744 |
-
|
| 745 |
-
|
|
|
|
| 746 |
return {
|
| 747 |
-
chat_interface: gr.
|
| 748 |
-
support_interface: gr.
|
| 749 |
-
success_interface: gr.
|
| 750 |
-
chat_preview:
|
| 751 |
}
|
| 752 |
|
| 753 |
def back_to_chat():
|
|
|
|
| 754 |
return {
|
| 755 |
-
chat_interface: gr.
|
| 756 |
-
support_interface: gr.
|
| 757 |
-
success_interface: gr.
|
| 758 |
}
|
| 759 |
|
| 760 |
def submit_support_form(områdeskod, uttagsnummer, email, chat_history):
|
| 761 |
-
"""
|
| 762 |
-
print(f"Support-förfrågan:
|
| 763 |
-
|
| 764 |
-
# Validera input med tydligare loggning
|
| 765 |
validation_errors = []
|
| 766 |
-
|
| 767 |
if områdeskod and not områdeskod.isdigit():
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
else:
|
| 771 |
-
print(f"Validerar områdeskod: '{områdeskod}' (ok)")
|
| 772 |
-
|
| 773 |
if uttagsnummer and not uttagsnummer.isdigit():
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
else:
|
| 777 |
-
print(f"Validerar uttagsnummer: '{uttagsnummer}' (ok)")
|
| 778 |
-
|
| 779 |
if not email:
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
validation_errors.append("En giltig e-postadress krävs.")
|
| 785 |
-
else:
|
| 786 |
-
print(f"Validerar email: '{email}' (ok)")
|
| 787 |
-
|
| 788 |
-
# Om det finns valideringsfel
|
| 789 |
if validation_errors:
|
| 790 |
-
print(f"Valideringsfel: {validation_errors}")
|
|
|
|
|
|
|
| 791 |
return {
|
| 792 |
-
chat_interface: gr.
|
| 793 |
-
support_interface: gr.
|
| 794 |
-
success_interface: gr.
|
| 795 |
-
chat_preview:
|
| 796 |
}
|
| 797 |
-
|
| 798 |
-
# Om
|
| 799 |
try:
|
| 800 |
-
print("Försöker skicka supportförfrågan till Slack...")
|
| 801 |
-
|
| 802 |
-
# Skapa en förenklad chathistorik för loggning
|
| 803 |
-
chat_summary = []
|
| 804 |
-
for msg in chat_history:
|
| 805 |
-
if 'role' in msg and 'content' in msg:
|
| 806 |
-
chat_summary.append(f"{msg['role']}: {msg['content'][:30]}...")
|
| 807 |
-
print(f"Chatthistorik att skicka: {chat_summary}")
|
| 808 |
-
|
| 809 |
-
# Skicka till Slack
|
| 810 |
success = send_support_to_slack(områdeskod, uttagsnummer, email, chat_history)
|
| 811 |
-
|
| 812 |
if success:
|
| 813 |
-
print("Support-förfrågan skickad till Slack
|
|
|
|
| 814 |
return {
|
| 815 |
-
chat_interface: gr.
|
| 816 |
-
support_interface: gr.
|
| 817 |
-
success_interface: gr.
|
|
|
|
| 818 |
}
|
| 819 |
else:
|
| 820 |
-
print("
|
|
|
|
|
|
|
| 821 |
return {
|
| 822 |
-
chat_interface: gr.
|
| 823 |
-
support_interface: gr.
|
| 824 |
-
success_interface: gr.
|
| 825 |
-
chat_preview:
|
| 826 |
}
|
| 827 |
except Exception as e:
|
| 828 |
print(f"Oväntat fel vid hantering av support-formulär: {e}")
|
|
|
|
| 829 |
return {
|
| 830 |
-
chat_interface: gr.
|
| 831 |
-
support_interface: gr.
|
| 832 |
-
success_interface: gr.
|
| 833 |
-
chat_preview:
|
| 834 |
}
|
| 835 |
|
| 836 |
-
#
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
|
|
|
|
| 848 |
if __name__ == "__main__":
|
| 849 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
print(f"Skapade tom loggfil: {log_file_path}")
|
| 40 |
|
| 41 |
hf_token = os.environ.get("HF_TOKEN")
|
| 42 |
+
# Validera HF_TOKEN endast om vi är i Hugging Face-miljön
|
| 43 |
+
if IS_HUGGINGFACE and not hf_token:
|
| 44 |
+
raise ValueError("HF_TOKEN saknas (krävs i Hugging Face-miljön)")
|
| 45 |
|
| 46 |
+
# Konfigurera CommitScheduler endast om vi är i Hugging Face-miljön
|
| 47 |
+
if IS_HUGGINGFACE and hf_token:
|
| 48 |
+
scheduler = CommitScheduler(
|
| 49 |
+
repo_id="ChargeNodeEurope/logfiles",
|
| 50 |
+
repo_type="dataset",
|
| 51 |
+
folder_path=log_folder,
|
| 52 |
+
path_in_repo="logs_v2",
|
| 53 |
+
every=300, # Vänta 5 minuter
|
| 54 |
+
token=hf_token
|
| 55 |
+
)
|
| 56 |
+
else:
|
| 57 |
+
scheduler = None # Ingen schemaläggare om inte på Hugging Face
|
| 58 |
+
print("Kör lokalt eller HF_TOKEN saknas, CommitScheduler är inte aktiv.")
|
| 59 |
|
| 60 |
|
| 61 |
# --- Globala variabler ---
|
| 62 |
last_log = None # Sparar loggdata från senaste svar för feedback
|
| 63 |
+
chunks = [] # Definiera globalt så de är tillgängliga
|
| 64 |
+
chunk_sources = [] # Definiera globalt så de är tillgängliga
|
| 65 |
|
| 66 |
# --- Förbättrad loggfunktion ---
|
| 67 |
def safe_append_to_log(log_entry):
|
|
|
|
| 69 |
try:
|
| 70 |
# Öppna filen i append-läge
|
| 71 |
with open(log_file_path, "a", encoding="utf-8") as log_file:
|
| 72 |
+
log_json = json.dumps(log_entry, ensure_ascii=False) # ensure_ascii=False för svenska tecken
|
| 73 |
log_file.write(log_json + "\n")
|
| 74 |
log_file.flush() # Säkerställ att data skrivs till disk omedelbart
|
| 75 |
+
|
| 76 |
+
# print(f"Loggpost tillagd: {log_entry.get('timestamp', 'okänd tid')}") # Kan bli för mycket output
|
| 77 |
return True
|
| 78 |
+
|
| 79 |
except Exception as e:
|
| 80 |
print(f"Fel vid loggning: {e}")
|
| 81 |
+
|
| 82 |
# Försök skapa mappen om den inte finns
|
| 83 |
try:
|
| 84 |
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
|
| 85 |
+
|
| 86 |
# Försök igen
|
| 87 |
with open(log_file_path, "a", encoding="utf-8") as log_file:
|
| 88 |
+
log_json = json.dumps(log_entry, ensure_ascii=False)
|
| 89 |
log_file.write(log_json + "\n")
|
| 90 |
+
|
| 91 |
print("Loggpost tillagd efter återhämtning")
|
| 92 |
return True
|
| 93 |
+
|
| 94 |
except Exception as retry_error:
|
| 95 |
print(f"Kritiskt fel vid loggning: {retry_error}")
|
| 96 |
return False
|
|
|
|
| 99 |
def load_local_files():
|
| 100 |
uploaded_text = ""
|
| 101 |
allowed = [".txt", ".docx", ".pdf", ".csv", ".xls", ".xlsx"]
|
| 102 |
+
# Förenklad exkluderingslista för att undvika problem med exakta filnamn
|
| 103 |
+
excluded_prefixes = ["requirements", "app", "conversation_log", "secrets"]
|
| 104 |
+
current_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.' # Hämta aktuell mapp
|
| 105 |
+
|
| 106 |
+
for item in os.listdir(current_dir):
|
| 107 |
+
# Kontrollera om det är en fil och inte en katalog
|
| 108 |
+
item_path = os.path.join(current_dir, item)
|
| 109 |
+
if not os.path.isfile(item_path):
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
if item.lower().endswith(tuple(allowed)) and not any(item.lower().startswith(prefix) for prefix in excluded_prefixes):
|
| 113 |
+
print(f"Läser fil: {item}")
|
| 114 |
try:
|
| 115 |
+
if item.endswith(".txt"):
|
| 116 |
+
with open(item_path, "r", encoding="utf-8") as f:
|
| 117 |
content = f.read()
|
| 118 |
+
elif item.endswith(".docx"):
|
| 119 |
+
try:
|
| 120 |
+
from docx import Document # Import sker vid behov
|
| 121 |
+
content = "\n".join([p.text for p in Document(item_path).paragraphs])
|
| 122 |
+
except ImportError:
|
| 123 |
+
print("Varning: 'python-docx' behövs för att läsa .docx-filer. Installera med 'pip install python-docx'.")
|
| 124 |
+
content = ""
|
| 125 |
+
elif item.endswith(".pdf"):
|
| 126 |
+
try:
|
| 127 |
+
import PyPDF2 # Import sker vid behov
|
| 128 |
+
content = ""
|
| 129 |
+
with open(item_path, "rb") as f:
|
| 130 |
+
# Korrigerad PdfReader anrop
|
| 131 |
+
reader = PyPDF2.PdfReader(f)
|
| 132 |
+
for page in reader.pages:
|
| 133 |
+
text = page.extract_text()
|
| 134 |
+
if text: # Lägg bara till om text extraherades
|
| 135 |
+
content += text + "\n"
|
| 136 |
+
except ImportError:
|
| 137 |
+
print("Varning: 'PyPDF2' behövs för att läsa .pdf-filer. Installera med 'pip install pypdf2'.")
|
| 138 |
+
content = ""
|
| 139 |
+
except Exception as pdf_error: # Fånga specifika PDF-läsningsfel
|
| 140 |
+
print(f"Fel vid läsning av PDF {item}: {str(pdf_error)}")
|
| 141 |
+
content = ""
|
| 142 |
+
elif item.endswith(".csv"):
|
| 143 |
+
try:
|
| 144 |
+
content = pd.read_csv(item_path).to_string()
|
| 145 |
+
except pd.errors.EmptyDataError:
|
| 146 |
+
print(f"Varning: CSV-filen {item} är tom.")
|
| 147 |
+
content = ""
|
| 148 |
+
except Exception as csv_err:
|
| 149 |
+
print(f"Fel vid läsning av CSV {item}: {csv_err}")
|
| 150 |
+
content = ""
|
| 151 |
+
elif item.endswith((".xls", ".xlsx")):
|
| 152 |
+
try:
|
| 153 |
+
if item == "FAQ stadat.xlsx": # Hårdkodat filnamn - kan vara skört
|
| 154 |
+
df = pd.read_excel(item_path)
|
| 155 |
+
rows = []
|
| 156 |
+
# Säkerställ att kolumnerna finns
|
| 157 |
+
if 'Fråga' in df.columns and 'Svar' in df.columns:
|
| 158 |
+
for index, row in df.iterrows():
|
| 159 |
+
rows.append(f"Fråga: {row['Fråga']}\nSvar: {row['Svar']}")
|
| 160 |
+
content = "\n\n".join(rows)
|
| 161 |
+
else:
|
| 162 |
+
print(f"Varning: Kolumner 'Fråga' eller 'Svar' saknas i {item}. Läser som vanlig Excel.")
|
| 163 |
+
content = pd.read_excel(item_path).to_string()
|
| 164 |
+
else:
|
| 165 |
+
content = pd.read_excel(item_path).to_string()
|
| 166 |
+
except Exception as excel_err:
|
| 167 |
+
print(f"Fel vid läsning av Excel {item}: {excel_err}")
|
| 168 |
+
content = ""
|
| 169 |
+
|
| 170 |
+
if content: # Lägg bara till om innehåll finns
|
| 171 |
+
uploaded_text += f"\n\nFIL: {item}\n{content}"
|
| 172 |
+
|
| 173 |
except Exception as e:
|
| 174 |
+
print(f"Allmänt fel vid läsning av {item}: {str(e)}")
|
| 175 |
return uploaded_text.strip()
|
| 176 |
|
| 177 |
def load_prompt():
|
| 178 |
try:
|
| 179 |
+
prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.', "prompt.txt")
|
| 180 |
+
with open(prompt_path, "r", encoding="utf-8") as f:
|
| 181 |
return f.read().strip()
|
| 182 |
+
except FileNotFoundError:
|
| 183 |
+
print(f"Varning: prompt.txt hittades inte. Använder tom prompt.")
|
| 184 |
+
return ""
|
| 185 |
except Exception as e:
|
| 186 |
+
print(f"Fel vid läsning av prompt.txt: {e}")
|
| 187 |
return ""
|
| 188 |
|
| 189 |
prompt_template = load_prompt()
|
| 190 |
|
| 191 |
# Förbered textsegment
|
| 192 |
def prepare_chunks(text_data):
|
| 193 |
+
global chunks, chunk_sources # Uppdatera globala variabler
|
| 194 |
chunks, sources = [], []
|
| 195 |
for source, text in text_data.items():
|
| 196 |
+
if not isinstance(text, str): # Säkerställ att text är en sträng
|
| 197 |
+
print(f"Varning: Ogiltig textdata för källa '{source}'. Hoppar över.")
|
| 198 |
+
continue
|
| 199 |
+
# Dela på dubbla radbrytningar först, sedan enkla
|
| 200 |
+
paragraphs = [p.strip() for p in text.replace('\n\n', '---PARAGRAPH---').replace('\n', ' ').split('---PARAGRAPH---') if p.strip()]
|
| 201 |
+
current_chunk = ""
|
| 202 |
for para in paragraphs:
|
| 203 |
+
if len(current_chunk) + len(para) + 1 <= MAX_CHUNK_SIZE:
|
| 204 |
+
current_chunk += (" " + para if current_chunk else para)
|
| 205 |
else:
|
| 206 |
+
# Lägg till föregående chunk om den inte är tom
|
| 207 |
+
if current_chunk:
|
| 208 |
+
chunks.append(current_chunk)
|
| 209 |
+
sources.append(source)
|
| 210 |
+
# Starta en ny chunk (se till att den inte överskrider maxstorleken direkt)
|
| 211 |
+
if len(para) <= MAX_CHUNK_SIZE:
|
| 212 |
+
current_chunk = para
|
| 213 |
+
else:
|
| 214 |
+
# Om en enskild paragraf är för lång, dela upp den
|
| 215 |
+
# (Enkel strategi: dela vid MAX_CHUNK_SIZE)
|
| 216 |
+
chunks.append(para[:MAX_CHUNK_SIZE])
|
| 217 |
sources.append(source)
|
| 218 |
+
# Hantera resten av paragrafen i nästa iteration (om det behövs mer logik)
|
| 219 |
+
current_chunk = para[MAX_CHUNK_SIZE:] # Eller hantera detta mer robust
|
| 220 |
+
|
| 221 |
+
# Lägg till den sista chunken om den inte är tom
|
| 222 |
+
if current_chunk:
|
| 223 |
+
chunks.append(current_chunk)
|
| 224 |
sources.append(source)
|
|
|
|
| 225 |
|
| 226 |
+
# Uppdatera de globala variablerna
|
| 227 |
+
chunk_sources = sources
|
| 228 |
+
return chunks, sources # Returnera också för säkerhets skull
|
| 229 |
+
|
| 230 |
+
# Lazy-laddning av SentenceTransformer
|
| 231 |
embedder = None
|
| 232 |
embeddings = None
|
| 233 |
index = None
|
|
|
|
| 235 |
def initialize_embeddings():
|
| 236 |
"""Initierar SentenceTransformer och FAISS-index vid första anrop."""
|
| 237 |
global embedder, embeddings, index, chunks, chunk_sources
|
| 238 |
+
|
| 239 |
+
# Kör endast om embedder inte redan är initierad
|
| 240 |
if embedder is None:
|
| 241 |
print("Initierar SentenceTransformer och FAISS-index...")
|
| 242 |
# Ladda och förbered lokal data
|
| 243 |
print("Laddar textdata...")
|
| 244 |
text_data = {"local_files": load_local_files()}
|
| 245 |
+
if not text_data["local_files"]:
|
| 246 |
+
print("Varning: Ingen lokal textdata hittades att indexera.")
|
| 247 |
+
# Sätt tomma listor för att undvika fel senare
|
| 248 |
+
chunks = []
|
| 249 |
+
chunk_sources = []
|
| 250 |
+
embeddings = np.array([]) # Tom numpy array
|
| 251 |
+
# Skapa ett tomt index om inga chunks finns
|
| 252 |
+
embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda modellen ändå
|
| 253 |
+
# FAISS kräver minst en vektor, eller dimensionen
|
| 254 |
+
dimension = embedder.get_sentence_embedding_dimension()
|
| 255 |
+
index = faiss.IndexFlatIP(dimension)
|
| 256 |
+
print("FAISS-index skapat (tomt).")
|
| 257 |
+
return # Avsluta tidigt
|
| 258 |
+
|
| 259 |
print("Förbereder textsegment...")
|
| 260 |
+
chunks, chunk_sources = prepare_chunks(text_data) # Detta uppdaterar globala chunks/chunk_sources
|
| 261 |
+
print(f"{len(chunks)} segment förberedda")
|
| 262 |
+
|
| 263 |
+
if not chunks:
|
| 264 |
+
print("Varning: Inga textsegment skapades från filerna.")
|
| 265 |
+
embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda modellen ändå
|
| 266 |
+
dimension = embedder.get_sentence_embedding_dimension()
|
| 267 |
+
index = faiss.IndexFlatIP(dimension)
|
| 268 |
+
embeddings = np.array([])
|
| 269 |
+
print("FAISS-index skapat (tomt).")
|
| 270 |
+
return
|
| 271 |
|
| 272 |
print("Skapar embeddings...")
|
| 273 |
embedder = SentenceTransformer('all-MiniLM-L6-v2')
|
| 274 |
+
try:
|
| 275 |
+
embeddings = embedder.encode(chunks, convert_to_numpy=True, show_progress_bar=True)
|
| 276 |
+
# Normalisera embeddings
|
| 277 |
+
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
| 278 |
+
# Undvik division med noll om någon norm är noll
|
| 279 |
+
norms[norms == 0] = 1e-10
|
| 280 |
+
embeddings /= norms
|
| 281 |
+
|
| 282 |
+
if embeddings.shape[0] > 0:
|
| 283 |
+
dimension = embeddings.shape[1]
|
| 284 |
+
index = faiss.IndexFlatIP(dimension)
|
| 285 |
+
index.add(embeddings.astype(np.float32)) # FAISS föredrar float32
|
| 286 |
+
print(f"FAISS-index skapat med {index.ntotal} vektorer.")
|
| 287 |
+
else:
|
| 288 |
+
print("Inga embeddings skapades, skapar tomt FAISS-index.")
|
| 289 |
+
dimension = embedder.get_sentence_embedding_dimension()
|
| 290 |
+
index = faiss.IndexFlatIP(dimension)
|
| 291 |
+
|
| 292 |
+
except Exception as e:
|
| 293 |
+
print(f"Fel vid skapande av embeddings eller FAISS-index: {e}")
|
| 294 |
+
# Försök återställa till ett tomt men funktionellt tillstånd
|
| 295 |
+
embedder = SentenceTransformer('all-MiniLM-L6-v2')
|
| 296 |
+
dimension = embedder.get_sentence_embedding_dimension()
|
| 297 |
+
index = faiss.IndexFlatIP(dimension)
|
| 298 |
+
embeddings = np.array([])
|
| 299 |
+
chunks = []
|
| 300 |
+
chunk_sources = []
|
| 301 |
|
| 302 |
def retrieve_context(query, k=RETRIEVAL_K):
|
| 303 |
"""Hämtar relevant kontext för frågor."""
|
| 304 |
+
initialize_embeddings() # Säkerställ att allt är laddat
|
| 305 |
+
|
| 306 |
+
# Kontrollera om indexet är tomt eller om nödvändiga objekt saknas
|
| 307 |
+
if index is None or index.ntotal == 0 or embedder is None or not chunks:
|
| 308 |
+
print("Varning: Embeddings eller index är inte redo. Kan inte hämta kontext.")
|
| 309 |
+
return "", []
|
| 310 |
+
|
| 311 |
+
try:
|
| 312 |
+
query_embedding = embedder.encode([query], convert_to_numpy=True)
|
| 313 |
+
# Normalisera query_embedding
|
| 314 |
+
norm = np.linalg.norm(query_embedding)
|
| 315 |
+
if norm == 0: norm = 1e-10 # Undvik division med noll
|
| 316 |
+
query_embedding /= norm
|
| 317 |
+
|
| 318 |
+
# Säkerställ att k inte är större än antalet vektorer i indexet
|
| 319 |
+
actual_k = min(k, index.ntotal)
|
| 320 |
+
if actual_k == 0:
|
| 321 |
+
return "", []
|
| 322 |
+
|
| 323 |
+
# Sök i indexet
|
| 324 |
+
D, I = index.search(query_embedding.astype(np.float32), actual_k)
|
| 325 |
+
|
| 326 |
+
retrieved, sources = [], set()
|
| 327 |
+
if I.size > 0: # Kontrollera att sökningen gav resultat
|
| 328 |
+
for idx in I[0]:
|
| 329 |
+
# Säkerställ att indexet är giltigt
|
| 330 |
+
if 0 <= idx < len(chunks):
|
| 331 |
+
retrieved.append(chunks[idx])
|
| 332 |
+
# Säkerställ att chunk_sources har samma längd
|
| 333 |
+
if idx < len(chunk_sources):
|
| 334 |
+
sources.add(chunk_sources[idx])
|
| 335 |
+
else:
|
| 336 |
+
print(f"Varning: Ogiltigt index {idx} returnerat från FAISS.")
|
| 337 |
+
|
| 338 |
+
return " ".join(retrieved), list(sources)
|
| 339 |
+
except Exception as e:
|
| 340 |
+
print(f"Fel vid hämtning av kontext: {e}")
|
| 341 |
+
return "", []
|
| 342 |
+
|
| 343 |
|
| 344 |
def generate_answer(query):
|
| 345 |
"""Genererar svar baserat på fråga och kontextinformation."""
|
| 346 |
+
start_time = time.time()
|
| 347 |
context, sources = retrieve_context(query)
|
| 348 |
+
retrieval_time = time.time() - start_time
|
| 349 |
+
|
| 350 |
+
# Om ingen kontext hittades, ge ett standardmeddelande
|
| 351 |
if not context.strip():
|
| 352 |
+
print("Ingen relevant kontext hittades.")
|
| 353 |
+
# Överväg att låta GPT försöka svara ändå, eller returnera direkt
|
| 354 |
+
# return "Jag kunde inte hitta någon specifik information om det i mina källor. Kan du omformulera frågan?\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
|
| 355 |
+
|
| 356 |
+
# Bygg prompten även om kontext är tom (GPT kan ha allmän kunskap)
|
| 357 |
prompt = f"""{prompt_template}
|
| 358 |
+
|
| 359 |
Relevant kontext:
|
| 360 |
+
{context if context.strip() else "Ingen specifik kontext hittades i de lokala dokumenten."}
|
| 361 |
+
|
| 362 |
Fråga: {query}
|
| 363 |
+
|
| 364 |
+
Svar (baserat på tillgänglig information):"""
|
| 365 |
+
|
| 366 |
try:
|
| 367 |
+
start_time_gpt = time.time()
|
| 368 |
response = client.chat.completions.create(
|
| 369 |
model="gpt-3.5-turbo",
|
| 370 |
messages=[
|
| 371 |
+
{"role": "system", "content": "Du är en hjälpsam assistent för ChargeNode. Basera ditt svar primärt på den 'Relevanta kontexten' om den finns. Om ingen kontext finns, eller om frågan inte täcks av kontexten, använd din allmänna kunskap men ange att svaret är generellt."},
|
| 372 |
{"role": "user", "content": prompt}
|
| 373 |
],
|
| 374 |
temperature=0.2,
|
| 375 |
max_tokens=500
|
| 376 |
)
|
| 377 |
+
gpt_time = time.time() - start_time_gpt
|
| 378 |
+
answer = response.choices[0].message.content.strip()
|
| 379 |
+
|
| 380 |
+
# Lägg till standard-disclaimer
|
| 381 |
+
answer += "\n\nAI-genererat. Otillräcklig hjälp? Kontakta support@chargenode.eu eller 010-2051055"
|
| 382 |
+
|
| 383 |
+
print(f"Svar genererat. Retrieval: {retrieval_time:.2f}s, GPT: {gpt_time:.2f}s")
|
| 384 |
+
return answer
|
| 385 |
+
|
| 386 |
except Exception as e:
|
| 387 |
+
print(f"Fel vid anrop till OpenAI API: {e}")
|
| 388 |
+
return f"Ett tekniskt fel uppstod när jag försökte generera ett svar ({type(e).__name__}). Försök igen senare.\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055"
|
| 389 |
+
|
| 390 |
|
| 391 |
# --- Slack Integration ---
|
| 392 |
def send_to_slack(subject, content, color="#2a9d8f"):
|
| 393 |
"""Basfunktion för att skicka meddelanden till Slack."""
|
| 394 |
webhook_url = os.environ.get("SLACK_WEBHOOK_URL")
|
| 395 |
if not webhook_url:
|
| 396 |
+
print("Slack webhook URL saknas, kan inte skicka meddelande.")
|
| 397 |
return False
|
| 398 |
+
|
| 399 |
try:
|
| 400 |
# Formatera meddelandet för Slack
|
| 401 |
payload = {
|
| 402 |
+
"attachments": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
{
|
| 404 |
+
"color": color,
|
| 405 |
+
"blocks": [
|
| 406 |
+
{
|
| 407 |
+
"type": "header",
|
| 408 |
+
"text": {
|
| 409 |
+
"type": "plain_text",
|
| 410 |
+
"text": subject,
|
| 411 |
+
"emoji": True
|
| 412 |
+
}
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"type": "divider"
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"type": "section",
|
| 419 |
+
"text": {
|
| 420 |
+
"type": "mrkdwn",
|
| 421 |
+
"text": content
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
]
|
| 425 |
}
|
| 426 |
]
|
| 427 |
}
|
| 428 |
+
|
| 429 |
response = requests.post(
|
| 430 |
webhook_url,
|
| 431 |
json=payload,
|
| 432 |
+
headers={"Content-Type": "application/json"},
|
| 433 |
+
timeout=10 # Lägg till timeout
|
| 434 |
)
|
| 435 |
+
|
| 436 |
if response.status_code == 200:
|
| 437 |
+
# print(f"Slack-meddelande skickat: {subject}") # Kan bli för mycket output
|
| 438 |
return True
|
| 439 |
else:
|
| 440 |
print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}")
|
| 441 |
return False
|
| 442 |
+
except requests.exceptions.RequestException as e:
|
| 443 |
+
print(f"Nätverksfel vid sändning till Slack: {type(e).__name__}: {e}")
|
| 444 |
+
return False
|
| 445 |
except Exception as e:
|
| 446 |
+
print(f"Allmänt fel vid sändning till Slack: {type(e).__name__}: {e}")
|
| 447 |
return False
|
| 448 |
|
| 449 |
# --- Feedback & Like-funktion ---
|
|
|
|
| 455 |
"""
|
| 456 |
feedback_type = "up" if data.liked else "down"
|
| 457 |
global last_log
|
| 458 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 459 |
+
|
| 460 |
+
# Försök extrahera bot-svaret från LikeData
|
| 461 |
+
bot_reply = ""
|
| 462 |
+
if isinstance(data.value, str):
|
| 463 |
+
bot_reply = data.value
|
| 464 |
+
elif isinstance(data.value, dict) and "value" in data.value:
|
| 465 |
+
bot_reply = data.value["value"]
|
| 466 |
+
else:
|
| 467 |
+
print(f"Varning: Kunde inte extrahera bot_reply från LikeData: {data.value}")
|
| 468 |
+
bot_reply = "[Kunde inte extrahera svar]"
|
| 469 |
+
|
| 470 |
+
|
| 471 |
log_entry = {
|
| 472 |
+
"timestamp": timestamp,
|
| 473 |
+
"event_type": "feedback", # Tydliggör typ av loggpost
|
| 474 |
"feedback": feedback_type,
|
| 475 |
+
"bot_reply_liked": bot_reply # Namnge tydligare vad det är
|
| 476 |
}
|
| 477 |
+
|
| 478 |
+
# Om global logdata finns från senaste *konversationen*, lägg till metadata.
|
| 479 |
+
# Detta antar att 'like' sker direkt efter ett svar.
|
| 480 |
+
if last_log and last_log.get("event_type") == "conversation":
|
| 481 |
log_entry.update({
|
| 482 |
"session_id": last_log.get("session_id"),
|
| 483 |
"user_message": last_log.get("user_message"),
|
| 484 |
+
"associated_bot_reply": last_log.get("bot_reply"), # Spara hela ursprungliga svaret
|
| 485 |
+
"user_id": last_log.get("user_id"),
|
| 486 |
+
"ip": last_log.get("ip"),
|
| 487 |
+
"platform": last_log.get("platform")
|
| 488 |
})
|
| 489 |
+
else:
|
| 490 |
+
print("Varning: Kunde inte koppla feedback till föregående konversation (last_log saknas eller är av fel typ).")
|
| 491 |
+
# Lägg till fallback-värden om möjligt
|
| 492 |
+
log_entry["session_id"] = "unknown"
|
| 493 |
+
log_entry["user_message"] = "[Okänd fråga]"
|
| 494 |
+
|
| 495 |
+
|
| 496 |
# Använd den förbättrade loggfunktionen
|
| 497 |
safe_append_to_log(log_entry)
|
| 498 |
+
|
| 499 |
# Skicka feedback till Slack
|
| 500 |
try:
|
| 501 |
+
feedback_subject = f"Feedback: {'👍 Positiv' if feedback_type == 'up' else '👎 Negativ'}"
|
| 502 |
+
feedback_message = f"""
|
| 503 |
+
*Feedback mottagen ({timestamp})*
|
| 504 |
+
|
| 505 |
+
*Typ:* {"👍 Tumme upp" if feedback_type == 'up' else '👎 Tumme ned'}
|
| 506 |
|
| 507 |
+
*Associerad fråga:* {log_entry.get('user_message', '[Okänd fråga]')[:500]}...
|
| 508 |
+
*Botens svar (som fick feedback):* {log_entry.get('bot_reply_liked', '[Okänt svar]')[:500]}...
|
| 509 |
|
| 510 |
+
*Session:* {log_entry.get('session_id', 'okänd')} | *Plattform:* {log_entry.get('platform', 'okänd')}
|
| 511 |
"""
|
| 512 |
+
# Skicka asynkront för att inte blockera
|
| 513 |
+
threading.Thread(
|
| 514 |
+
target=send_to_slack,
|
| 515 |
+
args=(feedback_subject, feedback_message, "#2a9d8f" if feedback_type == 'up' else "#ff0000"),
|
| 516 |
+
daemon=True
|
| 517 |
+
).start()
|
| 518 |
except Exception as e:
|
| 519 |
print(f"Kunde inte skicka feedback till Slack: {e}")
|
| 520 |
+
|
| 521 |
+
# Gradio förväntar sig ingen retur från like-funktionen
|
| 522 |
return
|
| 523 |
|
| 524 |
# --- Rapportering ---
|
| 525 |
def read_logs():
|
| 526 |
"""Läs alla loggposter från loggfilen."""
|
| 527 |
logs = []
|
| 528 |
+
if not os.path.exists(log_file_path):
|
| 529 |
+
print(f"Loggfil saknas: {log_file_path}")
|
| 530 |
+
return logs
|
| 531 |
+
|
| 532 |
try:
|
| 533 |
+
with open(log_file_path, "r", encoding="utf-8") as file:
|
| 534 |
+
line_count = 0
|
| 535 |
+
for line in file:
|
| 536 |
+
line_count += 1
|
| 537 |
+
line = line.strip()
|
| 538 |
+
if not line: # Hoppa över tomma rader
|
| 539 |
+
continue
|
| 540 |
+
try:
|
| 541 |
+
log_entry = json.loads(line)
|
| 542 |
+
logs.append(log_entry)
|
| 543 |
+
except json.JSONDecodeError as e:
|
| 544 |
+
print(f"Varning: Kunde inte tolka JSON på rad {line_count}: {e}. Radinnehåll: '{line[:100]}...'")
|
| 545 |
+
continue
|
| 546 |
+
print(f"Läste {len(logs)} giltiga loggposter från {line_count} rader.")
|
| 547 |
except Exception as e:
|
| 548 |
+
print(f"Allvarligt fel vid läsning av loggfil {log_file_path}: {e}")
|
| 549 |
return logs
|
| 550 |
|
| 551 |
def get_latest_conversations(logs, limit=50):
|
| 552 |
+
"""Hämta de senaste frågorna och svaren från konversationsloggar."""
|
| 553 |
conversations = []
|
| 554 |
+
# Iterera baklänges för att få de senaste först
|
| 555 |
for log in reversed(logs):
|
| 556 |
+
# Filtrera på loggar av typen 'conversation'
|
| 557 |
+
if log.get('event_type') == 'conversation' and 'user_message' in log and 'bot_reply' in log:
|
| 558 |
conversations.append({
|
| 559 |
'user_message': log['user_message'],
|
| 560 |
'bot_reply': log['bot_reply'],
|
|
|
|
| 565 |
return conversations
|
| 566 |
|
| 567 |
def get_feedback_stats(logs):
|
| 568 |
+
"""Sammanfatta feedback (tumme upp/ned) från feedbackloggar."""
|
| 569 |
+
feedback_count = {"up": 0, "down": 0, "unknown": 0}
|
| 570 |
+
negative_feedback_examples = [] # Hämta exempel på negativ feedback
|
| 571 |
+
|
| 572 |
for log in logs:
|
| 573 |
+
# Filtrera på loggar av typen 'feedback'
|
| 574 |
+
if log.get('event_type') == 'feedback' and 'feedback' in log:
|
| 575 |
feedback = log.get('feedback')
|
| 576 |
+
if feedback == "up":
|
| 577 |
+
feedback_count["up"] += 1
|
| 578 |
+
elif feedback == "down":
|
| 579 |
+
feedback_count["down"] += 1
|
| 580 |
+
# Samla exempel på 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 på 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 så 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
|