diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -41,152 +41,265 @@ if not os.path.exists(log_file_path): print(f"Skapade tom loggfil: {log_file_path}") hf_token = os.environ.get("HF_TOKEN") -if not hf_token: - raise ValueError("HF_TOKEN saknas") +# Validera HF_TOKEN endast om vi är i Hugging Face-miljön +if IS_HUGGINGFACE and not hf_token: + raise ValueError("HF_TOKEN saknas (krävs i Hugging Face-miljön)") -# 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 -) +# Konfigurera CommitScheduler endast om vi är i Hugging Face-miljön +if IS_HUGGINGFACE and hf_token: + 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 + ) +else: + scheduler = None # Ingen schemaläggare om inte på Hugging Face + print("Kör lokalt eller HF_TOKEN saknas, CommitScheduler är inte aktiv.") # --- 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() +last_log_lock = threading.Lock() # Lock för att skydda last_log # --- 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.""" + """Säker metod för att lägga till loggdata med rotation och sanering.""" try: - # Kontrollera att loggmappen finns + # Säkerställ att loggmappen finns os.makedirs(os.path.dirname(log_file_path), exist_ok=True) - - # Kontrollera loggfilens storlek + + # Kontrollera loggfilens storlek (valfritt, bra för långvarig körning) 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}") + if os.path.exists(log_file_path): + log_size = os.path.getsize(log_file_path) + # Om loggfilen är större än t.ex. 10MB, rotera den + if log_size > 10 * 1024 * 1024: # 10 MB + timestamp_suffix = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = f"{log_file_path}.{timestamp_suffix}.bak" + os.rename(log_file_path, backup_path) + print(f"Loggfil roterad: {log_file_path} -> {backup_path}") + # Skapa en ny tom fil + with open(log_file_path, "w", encoding="utf-8") as f: + f.write("") except FileNotFoundError: - # Filen finns inte än, inget att rotera - pass - - # Sanitera loggdata för att undvika potentiella injektioner + pass # Filen finns inte, inget att rotera + except Exception as e: + print(f"Fel vid loggrotation: {e}") # Fortsätt även om rotation misslyckas + + # Enkel sanering av loggdata 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 + # Ta bort null-bytes, begränsa längd (skydd mot oväntat stora strängar) + sanitized_entry[k] = v.replace('\0', '')[:20000] # Begränsa till 20k tecken else: sanitized_entry[k] = v - - # Öppna filen i append-läge + + # Skriv till loggfilen with open(log_file_path, "a", encoding="utf-8") as log_file: - log_json = json.dumps(sanitized_entry) + log_json = json.dumps(sanitized_entry, ensure_ascii=False) # Hantera svenska tecken 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')}") + log_file.flush() # Säkerställ att data skrivs direkt + + # Minska mängden output till konsolen + # 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) + print(f"Allvarligt fel vid loggning: {e}") + # Försök logga felet till en separat felfil eller standard error 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 + with open(os.path.join(log_folder, "logging_errors.txt"), "a", encoding="utf-8") as error_file: + error_file.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loggningsfel: {e}\nOriginalpost (delvis): {str(log_entry)[:500]}\n") + except Exception as ef: + print(f"Kunde inte ens logga loggningsfelet: {ef}") + 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: + # Förenklad exkluderingslista + excluded_prefixes = ["requirements", "app", "conversation_log", "secrets"] + current_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.' + + print(f"Söker efter filer i: {current_dir}") + items_in_dir = os.listdir(current_dir) + print(f"Hittade {len(items_in_dir)} objekt: {items_in_dir}") + + files_processed = 0 + for item in items_in_dir: + item_path = os.path.join(current_dir, item) + if not os.path.isfile(item_path): + continue + + is_allowed = item.lower().endswith(tuple(allowed)) + is_excluded = any(item.lower().startswith(prefix) for prefix in excluded_prefixes) + + if is_allowed and not is_excluded: + print(f"Läser fil: {item}") + files_processed += 1 try: - if file.endswith(".txt"): - with open(file, "r", encoding="utf-8") as f: + content = "" + if item.endswith(".txt"): + with open(item_path, "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}" + elif item.endswith(".docx"): + try: + from docx import Document + content = "\n".join([p.text for p in Document(item_path).paragraphs]) + except ImportError: + print(f"Varning: 'python-docx' behövs för {item}. Installera med 'pip install python-docx'.") + elif item.endswith(".pdf"): + try: + import PyPDF2 + with open(item_path, "rb") as f: + reader = PyPDF2.PdfReader(f) + if reader.is_encrypted: + print(f"Varning: PDF-filen {item} är krypterad och kan inte läsas.") + else: + temp_content = [] + for page in reader.pages: + text = page.extract_text() + if text: + temp_content.append(text) + content = "\n".join(temp_content) + except ImportError: + print(f"Varning: 'PyPDF2' behövs f��r {item}. Installera med 'pip install pypdf2'.") + except Exception as pdf_error: + print(f"Fel vid läsning av PDF {item}: {str(pdf_error)}") + elif item.endswith(".csv"): + try: + content = pd.read_csv(item_path, encoding='utf-8').to_string() # Prova utf-8 först + except UnicodeDecodeError: + try: + content = pd.read_csv(item_path, encoding='latin1').to_string() # Fallback + except Exception as csv_err: + print(f"Fel vid läsning av CSV {item}: {csv_err}") + except pd.errors.EmptyDataError: + print(f"Varning: CSV-filen {item} är tom.") + except Exception as csv_err: + print(f"Fel vid läsning av CSV {item}: {csv_err}") + elif item.endswith((".xls", ".xlsx")): + try: + # Kräver openpyxl för xlsx + if item.lower() == "faq stadat.xlsx": + df = pd.read_excel(item_path) + rows = [] + if 'Fråga' in df.columns and 'Svar' in df.columns: + for index, row in df.iterrows(): + # Säkerställ att värden är strängar + q = str(row['Fråga']) if pd.notna(row['Fråga']) else "" + a = str(row['Svar']) if pd.notna(row['Svar']) else "" + if q or a: # Lägg bara till om det finns innehåll + rows.append(f"Fråga: {q}\nSvar: {a}") + content = "\n\n".join(rows) + else: + print(f"Varning: Kolumner 'Fråga'/'Svar' saknas i {item}. Läser som vanlig Excel.") + content = pd.read_excel(item_path).to_string() + else: + content = pd.read_excel(item_path).to_string() + except ImportError: + print(f"Varning: 'openpyxl' kan behövas för {item}. Installera med 'pip install openpyxl'.") + except Exception as excel_err: + print(f"Fel vid läsning av Excel {item}: {excel_err}") + + if content and isinstance(content, str): + uploaded_text += f"\n\n--- FIL: {item} ---\n{content}" + elif not content: + print(f"Info: Ingen text extraherad från {item}") + except Exception as e: - print(f"Fel vid läsning av {file}: {str(e)}") + print(f"Allmänt fel vid bearbetning av fil {item}: {str(e)}") + + if files_processed == 0: + print("Varning: Inga filer hittades att ladda (matchade tillåtna typer och inte exkluderade).") + else: + print(f"Bearbetade {files_processed} filer.") + return uploaded_text.strip() def load_prompt(): + prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)) if '__file__' in locals() else '.', "prompt.txt") try: - with open("prompt.txt", "r", encoding="utf-8") as f: + with open(prompt_path, "r", encoding="utf-8") as f: return f.read().strip() + except FileNotFoundError: + print(f"Info: prompt.txt hittades inte på sökvägen: {prompt_path}. Använder tom system prompt.") + return "Du är en hjälpsam AI-assistent." # Fallback-prompt except Exception as e: - print(f"Fel vid prompt.txt: {e}") - return "" + print(f"Fel vid läsning av prompt.txt ({prompt_path}): {e}") + return "Du är en hjälpsam AI-assistent." prompt_template = load_prompt() # Förbered textsegment def prepare_chunks(text_data): - chunks, sources = [], [] + global chunks, chunk_sources + local_chunks, local_sources = [], [] # Använd lokala listor under bearbetning for source, text in text_data.items(): - paragraphs = [p for p in text.split("\n") if p.strip()] - chunk = "" + if not isinstance(text, str) or not text.strip(): + print(f"Varning: Ogiltig eller tom textdata för källa '{source}'. Hoppar över.") + continue + + # Försök splitta på tydliga avgränsare först + paragraphs = [] + potential_paragraphs = text.split('\n\n') # Dubbel radbrytning + for pp in potential_paragraphs: + # Dela ytterligare på enkel radbrytning om stycket är långt + if len(pp) > MAX_CHUNK_SIZE * 1.5: # Om stycket är mycket längre än max chunk + paragraphs.extend(p for p in pp.split('\n') if p.strip()) + elif pp.strip(): + paragraphs.append(pp.strip()) + + if not paragraphs: # Om ingen splittning fungerade, splitta på enkel radbrytning + paragraphs = [p for p in text.split("\n") if p.strip()] + + if not paragraphs: # Om texten inte har radbrytningar, behandla som en enda paragraf + paragraphs = [text.strip()] + + current_chunk = "" for para in paragraphs: - if len(chunk) + len(para) + 1 <= MAX_CHUNK_SIZE: - chunk += " " + para + para = para.strip() + if not para: continue + + # Om paragrafen i sig är för lång + if len(para) > MAX_CHUNK_SIZE: + # Lägg till föregående chunk om den finns + if current_chunk: + local_chunks.append(current_chunk) + local_sources.append(source) + current_chunk = "" + # Dela upp den långa paragrafen + start = 0 + while start < len(para): + end = start + MAX_CHUNK_SIZE + local_chunks.append(para[start:end]) + local_sources.append(source) + start = end + # current_chunk förblir tom efter detta + # Om paragrafen passar i nuvarande chunk + elif len(current_chunk) + len(para) + 1 <= MAX_CHUNK_SIZE: + current_chunk += (" " + para if current_chunk else para) + # Om paragrafen inte passar, spara nuvarande chunk och starta en ny 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 + if current_chunk: + local_chunks.append(current_chunk) + local_sources.append(source) + current_chunk = para # Starta ny chunk med denna paragraf + + # Lägg till den sista chunken om den inte är tom + if current_chunk: + local_chunks.append(current_chunk) + local_sources.append(source) + + # Uppdatera globala variabler + chunks = local_chunks + chunk_sources = local_sources + return chunks, chunk_sources # Lazy-laddning av SentenceTransformer embedder = None @@ -199,101 +312,123 @@ 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...") + + with embedding_lock: # Förhindra race conditions + if embedder is not None: + print("Embeddings redan initierade.") + return # Redan klart + + print("Initierar SentenceTransformer och FAISS-index...") + try: + # Ladda textdata + print("Laddar textdata...") + text_data = {"local_files": load_local_files()} + if not text_data["local_files"]: + print("Varning: Ingen lokal textdata hittades att indexera.") + # Sätt upp tomma men giltiga strukturer + embedder = SentenceTransformer('all-MiniLM-L6-v2') # Ladda modellen ändå + dimension = embedder.get_sentence_embedding_dimension() + index = faiss.IndexFlatIP(dimension) + chunks = [] + chunk_sources = [] + embeddings = np.array([], dtype=np.float32).reshape(0, dimension) + print("Tomt FAISS-index skapat.") + return + + # Förbered textsegment + print("Förbereder textsegment...") + chunks, chunk_sources = prepare_chunks(text_data) + print(f"{len(chunks)} segment förberedda") + + if not chunks: + print("Varning: Inga textsegment skapades från filerna.") 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) + dimension = embedder.get_sentence_embedding_dimension() + index = faiss.IndexFlatIP(dimension) + embeddings = np.array([], dtype=np.float32).reshape(0, dimension) + print("Tomt FAISS-index skapat.") + return + + # Skapa embeddings + print("Skapar embeddings (detta kan ta tid)...") + embedder = SentenceTransformer('all-MiniLM-L6-v2') + embeddings = embedder.encode(chunks, convert_to_numpy=True, show_progress_bar=True) + + if embeddings.size == 0: + print("Varning: Inga embeddings kunde skapas.") + dimension = embedder.get_sentence_embedding_dimension() + index = faiss.IndexFlatIP(dimension) + embeddings = np.array([], dtype=np.float32).reshape(0, dimension) + print("Tomt FAISS-index skapat.") + return + + # Normalisera embeddings + norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + norms[norms == 0] = 1e-10 # Undvik division med noll + embeddings /= norms + embeddings = embeddings.astype(np.float32) # Konvertera till float32 för FAISS + + # Skapa FAISS-index + dimension = embeddings.shape[1] + index = faiss.IndexFlatIP(dimension) + index.add(embeddings) + print(f"FAISS-index skapat med {index.ntotal} vektorer (dimension: {dimension}).") + + except Exception as e: + print(f"Allvarligt fel vid initiering av embeddings: {e}") + # Försök återställa till ett tomt men funktionellt tillstånd + embedder = None # Markera som oinitierad igen + embeddings = None + index = None + chunks = [] + chunk_sources = [] + # Rensa eventuella felaktiga filer? + raise RuntimeError(f"Kunde inte initiera embeddings: {e}") # Kasta om felet så att appen inte startar felaktigt + def retrieve_context(query, k=RETRIEVAL_K): """Hämtar relevant kontext för frågor.""" - # Säkerställ att modeller är laddade - initialize_embeddings() - + initialize_embeddings() # Säkerställ att allt är laddat + + # Kontrollera om indexet är giltigt + if index is None or embedder is None or index.ntotal == 0: + print("Varning: Embeddings eller index är inte redo. Kan inte hämta kontext.") + return "Ingen kontext tillgänglig (index ej redo).", ["error"] + 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 + return "Kunde inte bearbeta frågan.", ["error"] + + # Normalisera query_embedding + norm = np.linalg.norm(query_embedding) + if norm == 0: norm = 1e-10 + query_embedding /= norm + query_embedding = query_embedding.astype(np.float32) + + # Säkerställ att k inte är större än antalet vektorer i indexet 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") - + if actual_k == 0: return "", [] # Inget att hämta + + # Sök i indexet 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 I.size > 0: + for rank, idx in enumerate(I[0]): + if 0 <= idx < len(chunks): + retrieved.append(f"[Dokument {rank+1}] {chunks[idx]}") # Lägg till ranking/identifierare + if idx < len(chunk_sources): sources.add(chunk_sources[idx]) + else: + print(f"Varning: Ogiltigt index {idx} returnerat från FAISS.") + if not retrieved: - print("Varning: Ingen relevant kontext hittades") - return "Ingen relevant kontext hittades", ["no_context"] - - return " ".join(retrieved), list(sources) + print("Info: Ingen relevant kontext hittades för frågan.") + return "", ["no_context"] # Returnera tom sträng om inget hittades + + # Kombinera hämtade chunks + return "\n---\n".join(retrieved), list(sources) # Separera med tydlig avgränsare + except Exception as e: print(f"Fel vid hämtning av kontext: {e}") return f"Fel vid kontexthämtning: {str(e)[:200]}", ["error"] @@ -301,912 +436,870 @@ def retrieve_context(query, k=RETRIEVAL_K): 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: + + # Anpassa system-prompt baserat på om kontext hittades + system_content = prompt_template # Använd prompt från fil som bas + if "error" in sources: + system_content += "\n(Varning: Ett fel uppstod vid hämtning av lokal kontext.)" + context = "Ingen kontext kunde hämtas på grund av ett tekniskt fel." + elif "no_context" in sources: + system_content += "\n(Info: Ingen specifik lokal kontext hittades för denna fråga.)" + context = "Ingen specifik lokal kontext hittades." + elif not context: + # Detta bör inte hända om retrieve_context fungerar korrekt, men som fallback + system_content += "\n(Info: Ingen lokal kontext hittades.)" + context = "Ingen lokal kontext." + + + # Bygg prompt för användarmeddelandet + user_prompt = f"""Baserat på följande kontext (om någon): +--- KONTEXT START --- {context} -Fråga: {query} -Svar (baserat enbart på den indexerade datan):""" +--- KONTEXT SLUT --- + +Svara på följande fråga: {query}""" + try: response = client.chat.completions.create( - model="gpt-3.5-turbo", + model="gpt-3.5-turbo", # Eller annan modell 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} + {"role": "system", "content": system_content}, + {"role": "user", "content": user_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" + answer = response.choices[0].message.content.strip() + + # Lägg till standard-disclaimer + answer += "\n\n*AI-genererat svar.* För personlig hjälp, kontakta support@chargenode.eu eller ring 010-205 10 55." + return answer + except Exception as e: - return f"Tekniskt fel: {str(e)}\n\nAI-genererat. Kontakta support@chargenode.eu eller 010-2051055" + print(f"Fel vid anrop till OpenAI API: {e}") + return f"Ett tekniskt fel uppstod när jag försökte generera ett svar ({type(e).__name__}). Försök igen senare eller kontakta support.\n\n*AI-genererat svar.* 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") + print("Slack webhook URL saknas, kan inte skicka meddelande.") return False - + try: - # Formatera meddelandet för Slack + # Begränsa längden på subject och content för att undvika Slack API-fel + safe_subject = subject[:150] + safe_content = content[:2900] # Slack har gräns runt 3000 för block text + if len(content) > 2900: + safe_content += "\n... (meddelandet trunkerat)" + payload = { - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": subject - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": content + "attachments": [{ + "color": color, + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": safe_subject, + "emoji": True + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": safe_content + } } - } - ] + ] + }] } - + response = requests.post( webhook_url, json=payload, - headers={"Content-Type": "application/json"} + headers={"Content-Type": "application/json"}, + timeout=15 # Öka timeout lite ) - + if response.status_code == 200: - print(f"Slack-meddelande skickat: {subject}") + # Minska output + # print(f"Slack-meddelande skickat: {safe_subject}") return True else: print(f"Slack-anrop misslyckades: {response.status_code}, {response.text}") + # Försök logga felet mer detaljerat om möjligt + try: + error_details = response.json() + print(f"Slack error details: {error_details}") + except: + pass return False + except requests.exceptions.RequestException as e: + print(f"Nätverksfel vid sändning till Slack: {type(e).__name__}: {e}") + return False except Exception as e: - print(f"Fel vid sändning till Slack: {type(e).__name__}: {e}") + print(f"Allmänt 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. - """ + """Hanterar feedback från Gradio's inbyggda like-funktion.""" 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 = "" - + + bot_reply_liked = "" + if isinstance(data.value, str): + bot_reply_liked = data.value + elif isinstance(data.value, dict) and "value" in data.value: + bot_reply_liked = data.value["value"] + else: + print(f"Varning: Kunde inte extrahera bot_reply från LikeData: {data.value}") + bot_reply_liked = "[Kunde inte extrahera svar]" + log_entry = { "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "event_type": "feedback", "feedback": feedback_type, - "bot_reply": bot_reply + "bot_reply_liked": bot_reply_liked } - - # Använd lock för att säkert komma åt last_log + + # Hämta data från den senaste konversationen SÄKERT + user_message = "[Okänd fråga]" + session_id = "unknown" + associated_bot_reply = "[Okänt svar]" + user_id = "unknown" + ip_address = "unknown" + platform = "unknown" + 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* + if last_log and last_log.get("event_type") == "conversation": + user_message = last_log.get("user_message", user_message) + session_id = last_log.get("session_id", session_id) + associated_bot_reply = last_log.get("bot_reply", associated_bot_reply) # Hela svaret + user_id = last_log.get("user_id", user_id) + ip_address = last_log.get("ip", ip_address) + platform = last_log.get("platform", platform) + else: + print("Varning: Kunde inte koppla feedback till föregående konversation (last_log saknas eller fel typ).") + + # Lägg till den hämtade informationen i loggposten + log_entry.update({ + "session_id": session_id, + "user_message": user_message, + "associated_bot_reply": associated_bot_reply, + "user_id": user_id, + "ip": ip_address, + "platform": platform + }) -*Fråga:* {user_message} + safe_append_to_log(log_entry) -*Svar:* {log_entry.get('bot_reply', 'Okänt svar')[:300]}{'...' if len(log_entry.get('bot_reply', '')) > 300 else ''} + # Skicka ENDAST negativ feedback till Slack för att minska brus + if feedback_type == "down": + try: + feedback_subject = f"👎 Negativ Feedback ({platform})" + feedback_message = f""" +*Användarens fråga:* {user_message[:500]}{'...' if len(user_message) > 500 else ''} +*Botens svar (som fick negativ feedback):* {bot_reply_liked[:500]}{'...' if len(bot_reply_liked) > 500 else ''} +--- +*Session:* `{session_id[:8]}` | *IP:* `{ip_address}` | *Tid:* {log_entry['timestamp']} """ # Skicka asynkront threading.Thread( - target=lambda: send_to_slack("Negativ feedback", feedback_message, "#ff0000"), + target=send_to_slack, + args=(feedback_subject, feedback_message.strip(), "#ff0000"), # Röd färg daemon=True ).start() - except Exception as e: - print(f"Kunde inte skicka feedback till Slack: {e}") - + except Exception as e: + print(f"Kunde inte starta tråd för att skicka negativ feedback till Slack: {e}") + # Gradio förväntar sig ingen retur från like-funktionen return # --- Rapportering --- def read_logs(): """Läs alla loggposter från loggfilen.""" logs = [] + if not os.path.exists(log_file_path): + print(f"Loggfil saknas: {log_file_path}") + return 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}") + # Använd r+b för att kunna hantera potentiella encoding-fel mer robust + with open(log_file_path, "r", encoding="utf-8", errors='replace') as file: + line_count = 0 + malformed_lines = 0 + for line in file: + line_count += 1 + line = line.strip() + if not line: continue + try: + log_entry = json.loads(line) + logs.append(log_entry) + except json.JSONDecodeError as e: + malformed_lines += 1 + # Logga bara var 100:e fel för att undvika spam + if malformed_lines % 100 == 1: + print(f"Varning: Kunde inte tolka JSON på rad {line_count}: {e}. Radinnehåll (början): '{line[:100]}...' (Detta meddelande visas var 100:e gång)") + continue + if malformed_lines > 0: + print(f"Totalt antal felaktigt formaterade JSON-rader: {malformed_lines}") + # print(f"Läste {len(logs)} giltiga loggposter från {line_count} rader.") except Exception as e: - print(f"Fel vid läsning av loggfil: {e}") + print(f"Allvarligt fel vid läsning av loggfil {log_file_path}: {e}") return logs def get_latest_conversations(logs, limit=50): - """Hämta de senaste frågorna och svaren.""" + """Hämta de senaste frågorna och svaren från konversationsloggar.""" conversations = [] + count = 0 for log in reversed(logs): - if 'user_message' in log and 'bot_reply' in log: + if log.get('event_type') == 'conversation' and '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: + count += 1 + if count >= limit: break return conversations def get_feedback_stats(logs): - """Sammanfatta feedback (tumme upp/ned).""" + """Sammanfatta feedback (tumme upp/ned) från feedbackloggar.""" feedback_count = {"up": 0, "down": 0} negative_feedback_examples = [] - + for log in logs: - if 'feedback' in log: + if log.get('event_type') == 'feedback' and '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') - }) - + if feedback == "up": + feedback_count["up"] += 1 + elif feedback == "down": + feedback_count["down"] += 1 + if len(negative_feedback_examples) < 10: # Begränsa antal exempel + negative_feedback_examples.append({ + 'user_message': log.get('user_message', '[Okänd fråga]'), + 'bot_reply_liked': log.get('bot_reply_liked', '[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.""" +def generate_periodic_stats(days=30): + """Genererar statistik över botanvändning för den angivna perioden.""" 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 + all_logs = read_logs() + if not all_logs: + print("Inga loggar hittades.") + return {"error": "Inga loggdata tillgängliga."} + now = datetime.now() cutoff_date = now - timedelta(days=days) filtered_logs = [] - - for log in logs: - if 'timestamp' in log: + invalid_date_count = 0 + + for log in all_logs: + ts = log.get('timestamp') + if ts: try: - log_date = datetime.strptime(log['timestamp'], "%Y-%m-%d %H:%M:%S") + log_date = datetime.strptime(ts, "%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] + except (ValueError, TypeError): + invalid_date_count += 1 + else: + invalid_date_count += 1 + + if invalid_date_count > 0: + print(f"Varning: {invalid_date_count} loggposter ignorerades p.g.a. saknad/ogiltig tidsstämpel.") + + if not filtered_logs: + return {"error": f"Inga loggar hittades för de senaste {days} dagarna."} + + conv_logs = [log for log in filtered_logs if log.get('event_type') == 'conversation'] + feedback_logs = [log for log in filtered_logs if log.get('event_type') == 'feedback'] + + total_conversations = len(conv_logs) + unique_sessions = len(set(log.get('session_id') for log in conv_logs if log.get('session_id'))) + unique_users = len(set(log.get('user_id') for log in conv_logs if log.get('user_id'))) # Baserat på IP - uppskattning + + feedback_stats, neg_examples = get_feedback_stats(feedback_logs) + positive_feedback = feedback_stats.get("up", 0) + negative_feedback = feedback_stats.get("down", 0) + total_feedback = positive_feedback + negative_feedback + feedback_ratio = (positive_feedback / total_feedback * 100) if total_feedback > 0 else 0 + + response_times = [log.get('response_time', 0) for log in conv_logs if isinstance(log.get('response_time'), (int, float))] 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 + for log in conv_logs: + p = log.get('platform', 'Okänd') + b = log.get('browser', 'Okänd') + o = log.get('os', 'Okänd') + platforms[p] = platforms.get(p, 0) + 1 + browsers[b] = browsers.get(b, 0) + 1 + operating_systems[o] = operating_systems.get(o, 0) + 1 + report = { - "period": f"Senaste {days} dagarna", - "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "period": f"Senaste {days} dagarna ({cutoff_date.strftime('%Y-%m-%d')} - {now.strftime('%Y-%m-%d')})", + "generated_at": 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 + "unique_users (estimated)": unique_users, + "messages_per_session": round(total_conversations / unique_sessions, 2) if unique_sessions else 0 }, "feedback": { + "total_feedback_entries": total_feedback, "positive": positive_feedback, "negative": negative_feedback, - "ratio_percent": round(feedback_ratio, 1) + "satisfaction_rate_percent": round(feedback_ratio, 1) }, "performance": { - "avg_response_time": round(avg_response_time, 2) + "avg_response_time_seconds": round(avg_response_time, 2) }, - "platform_distribution": platforms, - "browser_distribution": browsers, - "os_distribution": operating_systems + "top_platforms": dict(sorted(platforms.items(), key=lambda item: item[1], reverse=True)[:5]), + "top_browsers": dict(sorted(browsers.items(), key=lambda item: item[1], reverse=True)[:5]), + "top_os": dict(sorted(operating_systems.items(), key=lambda item: item[1], reverse=True)[:5]), + "negative_feedback_examples": neg_examples } - 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}* +def format_report_for_slack(stats_dict): + """Formaterar statistikrapporten för läsbarhet i Slack.""" + if 'error' in stats_dict: + return f"*Fel vid generering av statistik:* {stats_dict['error']}" -*Basstatistik* (senaste 7 dagarna) -- Totalt antal konversationer: {basic['total_conversations']} + basic = stats_dict["basic_stats"] + feedback = stats_dict["feedback"] + perf = stats_dict["performance"] + platforms = stats_dict["top_platforms"] + browsers = stats_dict["top_browsers"] + osys = stats_dict["top_os"] + neg_examples = stats_dict["negative_feedback_examples"] + + def format_dict(d): return "\n".join([f"- {k}: {v}" for k, v in d.items()]) if d else "_Ingen data_" + def format_neg_examples(examples): + if not examples: return "_Inga exempel_" + return "\n".join([f"- Fråga: `{ex['user_message'][:60]}...` -> Svar: `{ex['bot_reply_liked'][:60]}...`" for ex in examples]) + + content = f""" +*Period:* {stats_dict.get('period', 'Okänd')} (Genererad: {stats_dict.get('generated_at', 'Okänd')}) + +*📊 Användning* +- Konversationer: {basic['total_conversations']} - Unika sessioner: {basic['unique_sessions']} -- Unika användare: {basic['unique_users']} -- Genomsnittlig svarstid: {perf['avg_response_time']} sekunder +- Uppskattade unika användare: {basic['unique_users (estimated)']} +- Meddelanden/session: {basic['messages_per_session']} -*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 ''} +*👍👎 Feedback* +- Totalt feedback: {feedback['total_feedback_entries']} ({feedback['positive']} 👍 / {feedback['negative']} 👎) +- Nöjdhet: {feedback['satisfaction_rate_percent']}% + +*⚡ Prestanda* +- Genomsnittlig svarstid: {perf['avg_response_time_seconds']} sek + +*💻 Teknik (Topp 5)* +*Plattformar:* +{format_dict(platforms)} +*Webbläsare:* +{format_dict(browsers)} +*Operativsystem:* +{format_dict(osys)} + +*📉 Exempel negativ feedback (senaste):* +{format_neg_examples(neg_examples)} """ - - # Skicka till Slack - return send_to_slack(subject, content, "#2a9d8f") - + return content.strip() + +def send_status_report(days=7, report_type="Daglig"): + """Genererar och skickar en statusrapport till Slack.""" + print(f"Genererar {report_type.lower()} statusrapport...") + report_title = f"ChargeNode AI Bot - {report_type} Statusrapport" + try: + stats = generate_periodic_stats(days=days) + slack_content = format_report_for_slack(stats) + color = "#3498db" if "error" not in stats else "#ff0000" + send_to_slack(report_title, slack_content, color) 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") + print(f"Allvarligt fel vid generering/sändning av {report_type.lower()} rapport: {e}") + try: + send_to_slack(f"🚨 Fel: {report_title}", f"Kunde inte generera/skicka rapport.\n*Fel:* `{type(e).__name__}: {e}`", "#ff0000") + except Exception as slack_err: + print(f"Kunde inte skicka felmeddelande till Slack: {slack_err}") +# --- Supportformulär till Slack --- def send_support_to_slack(områdeskod, uttagsnummer, email, chat_history): """Skickar en supportförfrågan till Slack.""" + if not email: + print("Fel: Email saknas för supportförfrågan.") + return False + 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')}" - + history_limit = 15 # Inkludera lite mer historik + start_index = max(0, len(chat_history) - history_limit) + for i, msg in enumerate(chat_history[start_index:]): + if isinstance(msg, dict) and 'role' in msg and 'content' in msg: + role = "👤 Användare" if msg.get('role') == 'user' else "🤖 Bot" + content = msg.get('content', '') + max_len = 400 + display_content = (content[:max_len] + '...') if len(content) > max_len else content + chat_content += f"*{role} ({i+1}):* {display_content}\n\n" # Lägg till numrering + else: + chat_content += f"*Okänt format i historik:* `{str(msg)[:100]}`\n\n" + + subject = f"📩 Supportförfrågan via Chatbot ({email})" + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 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} +*Ny supportförfrågan* ({timestamp}) + +*Kontakt:* {email} +*Områdeskod:* `{områdeskod or 'Ej angiven'}` +*Uttagsnummer:* `{uttagsnummer or 'Ej angiven'}` + +--- +*Chatthistorik (senaste {min(history_limit, len(chat_history))} meddelandena):* +{chat_content or "_Ingen historik tillgänglig_"} """ - - # Skicka till Slack - return send_to_slack(subject, content, "#e76f51") + success = send_to_slack(subject, content.strip(), "#f4a261") # Mer orange färg + if not success: print("Misslyckades med att skicka supportförfrågan till Slack API.") + return success except Exception as e: - print(f"Fel vid sändning av support till Slack: {type(e).__name__}: {e}") + print(f"Allvarligt fel vid sändning av support till Slack: {type(e).__name__}: {e}") return False # --- Schemaläggning av rapporter --- +scheduler_running = True # Flagga för att kunna stoppa tråden snyggt 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") + """Kör schemaläggaren i en separat tråd.""" + global scheduler_running + print("Startar schemaläggningstråd för rapporter...") -# Starta schemaläggaren i en separat tråd -scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) -scheduler_thread.start() + # Dagliga rapporter kl 08:00 + schedule.every().day.at("08:00").do(lambda: send_status_report(days=1, report_type="Daglig")) + # Veckorapport måndagar kl 09:00 + schedule.every().monday.at("09:00").do(lambda: send_status_report(days=7, report_type="Veckovis")) + # Månadsrapport första dagen i månaden kl 10:00 + schedule.every().day.at("10:00").do(monthly_report_if_first_day) -# Registrera en atexit-funktion för att städa upp vid avslut + print("Schema konfigurerat:") + for job in schedule.get_jobs(): print(f"- {job}") + while scheduler_running: + try: + schedule.run_pending() + # Kontrollera flaggan oftare för snabbare avslut + for _ in range(60): + if not scheduler_running: break + time.sleep(1) + except Exception as e: + print(f"Fel i schemaläggningsloopen: {e}") + time.sleep(300) # Vänta vid fel + print("Schemaläggningstråd avslutas.") + +def monthly_report_if_first_day(): + """Kör månadsrapport endast om det är den första dagen i månaden.""" + if datetime.now().day == 1: + print("Första dagen i månaden, kör månadsrapport...") + send_status_report(days=30, report_type="Månadsvis") + +# Starta schemaläggaren endast om Slack Webhook finns +scheduler_thread = None +if os.environ.get("SLACK_WEBHOOK_URL"): + scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) + scheduler_thread.start() +else: + print("Ingen SLACK_WEBHOOK_URL hittades, schemalagda rapporter kommer inte att köras.") + +# Registrera atexit-funktion för att städa upp def cleanup(): """Städa upp resurser vid avslut.""" + global scheduler_running, scheduler_thread print("Städar upp resurser...") - # Stäng scheduler om möjligt - schedule.clear() - + + # Stoppa schemaläggningstråden + if scheduler_thread and scheduler_thread.is_alive(): + print("Försöker stoppa schemaläggningstråden...") + scheduler_running = False + scheduler_thread.join(timeout=5) # Vänta max 5 sekunder + if scheduler_thread.is_alive(): + print("Varning: Schemaläggningstråden avslutades inte inom tidsgränsen.") + else: + print("Schemaläggningstråden stoppad.") + # Stäng CommitScheduler om den är aktiv - if 'scheduler' in globals() and scheduler: + if scheduler and hasattr(scheduler, 'stop'): # Använd global variabel direkt try: - scheduler.stop() - print("CommitScheduler stoppad") + # Kör push i en separat tråd vid exit för att undvika blockering + print("Schemalägger en sista push av loggar...") + # scheduler.push_to_hub() # Kan blockera, kör i tråd istället + threading.Thread(target=scheduler.push_to_hub, daemon=False).start() # daemon=False för att försöka slutföra + # Ge tråden lite tid, men inte för mycket + time.sleep(5) + print("CommitScheduler push initierad (körs i bakgrunden).") except Exception as e: - print(f"Kunde inte stoppa CommitScheduler: {e}") - - # Stäng eventuella öppna filer + print(f"Fel vid sista push med CommitScheduler: {e}") + + # Stäng eventuellt öppna filer (försiktighetsåtgärd) 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 + if isinstance(obj, io.IOBase) and hasattr(obj, 'closed') and not obj.closed: + try: obj.close() + except Exception: pass # Ignorera fel vid stängning + print("Försökt stänga öppna filobjekt.") except Exception as e: - print(f"Fel vid stängning av filer: {e}") + print(f"Fel vid generell filstängning: {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}") +# Kör en inledande statusrapport (valfritt) +def initial_startup_report(): + time.sleep(15) # Vänta lite längre + print("Skickar en inledande statusrapport (senaste 24h)...") + send_status_report(days=1, report_type="Uppstart") + +if os.environ.get("SLACK_WEBHOOK_URL"): + threading.Thread(target=initial_startup_report, daemon=True).start() + -# Definiera respond och chat-relaterade funktioner före Gradio UI +# Definiera respond och UI-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() + + if not message or not isinstance(message, str) or message.isspace(): + return "", chat_history # Returnera ingen ändring om meddelandet är tomt + + message = message.strip()[:1500] # Begränsa längd och ta bort ytterkanter + + start_time = time.time() try: - response = generate_answer(message) + bot_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) + print(f"Fel vid generate_answer för meddelande '{message[:50]}...': {e}") + bot_response = f"Tyvärr uppstod ett internt fel när jag behandlade din fråga. Vänligen försök igen eller kontakta support. (Fel: {type(e).__name__})" + response_time = round(time.time() - start_time, 2) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Hämta session_id säkert 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 last_log and last_log.get("event_type") == "conversation" and last_log.get("session_id"): + session_id = last_log.get("session_id") + + # Hämta metadata från request säkert + user_id, ip_address, user_agent_str, referer, browser_info, os_info = "okänd", "okänd", "", "", "Okänd", "Okänd" 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" + user_id = request.client.host if request.client else "okänd_klient" + headers = request.headers + ip_candidates = [headers.get("x-forwarded-for"), headers.get("x-real-ip"), user_id] + ip_address = next((ip.split(',')[0].strip() for ip in ip_candidates if ip), user_id) + user_agent_str = headers.get("user-agent", "") + referer = headers.get("referer", "") + if user_agent_str: + ua = parse_ua(user_agent_str) + browser_info = f"{ua.browser.family} {ua.browser.version_string}" if ua.browser else "Okänd" + os_info = f"{ua.os.family} {ua.os.version_string}" if ua.os else "Okänd" + except Exception as req_err: + print(f"Fel vid hämtning av request metadata: {req_err}") # 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}") + if referer: + if "chargenode.eu" in referer: platform = "chargenode.eu" + elif any(sub in referer for sub in ["localhost", "127.0.0.1"]): platform = "test" + elif "app" in referer: platform = "app" # Kan behöva justeras + else: platform = "direkt/okänd" - # Skapa loggdata + # Skapa loggposten 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 + "timestamp": timestamp, "event_type": "conversation", "user_id": user_id, + "session_id": session_id, "ip": ip_address, "platform": platform, + "browser": browser_info, "os": os_info, "user_message": message, + "bot_reply": bot_response, "response_time": response_time, + # "referer": referer, "user_agent": user_agent_str # Valfritt att logga } - # 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 + with last_log_lock: # Uppdatera last_log säkert + last_log = log_data - # Skicka varje konversation direkt till Slack + # Skicka konversation till Slack asynkront try: - # Konversationsinnehåll - conversation_content = f""" -*Ny konversation {timestamp}* - + slack_conv_subject = f"💬 Ny konversation ({platform})" + slack_conv_content = f""" *Användare:* {message} - -*Bot:* {response[:300]}{'...' if len(response) > 300 else ''} - -*Sessionsinfo:* {session_id[:8]}... | {browser} | {platform} +*Bot:* {bot_response[:350]}{'...' if len(bot_response) > 350 else ''} +--- +*Tid:* {timestamp} | *Session:* `{session_id[:8]}` | *IP:* `{ip_address}` | `{browser_info}` | `{os_info}` """ - # Skicka asynkront för att inte blockera svarstiden - threading.Thread( - target=lambda: send_to_slack(f"Ny konversation", conversation_content), - daemon=True - ).start() + threading.Thread(target=send_to_slack, args=(slack_conv_subject, slack_conv_content.strip(), "#ADD8E6"), daemon=True).start() except Exception as e: - print(f"Kunde inte skicka konversation till Slack: {e}") + print(f"Kunde inte starta tråd för Slack-konversation: {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 + # Uppdatera Gradio chat history + chat_history.append({"role": "user", "content": message}) + chat_history.append({"role": "assistant", "content": bot_response}) + + return "", chat_history # Rensa input, returnera uppdaterad historik def format_chat_preview(chat_history): - if not chat_history: - return "Ingen chatthistorik att visa." - + 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 + history_limit = 15 + start_index = max(0, len(chat_history) - history_limit) + for msg in chat_history[start_index:]: + if isinstance(msg, dict) and 'role' in msg and 'content' in msg: + sender = "Du" if msg["role"] == "user" else "Bot" + content = msg["content"] + preview += f"**{sender}:** {content[:150]}{'...' if len(content) > 150 else ''}\n\n" + else: + preview += f"**Okänt format:** `{str(msg)[:50]}`\n\n" + return preview.strip() def show_support_form(chat_history): - preview = format_chat_preview(chat_history) + preview_md = 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 + chat_interface: gr.update(visible=False), + support_interface: gr.update(visible=True), + success_interface: gr.update(visible=False), + chat_preview: gr.update(value=preview_md) } def back_to_chat(): return { - chat_interface: gr.Group(visible=True), - support_interface: gr.Group(visible=False), - success_interface: gr.Group(visible=False) + chat_interface: gr.update(visible=True), + support_interface: gr.update(visible=False), + success_interface: gr.update(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 + """Validerar och skickar supportformuläret.""" + print(f"Bearbetar support-förfrågan: Områdeskod='{områdeskod}', Uttag='{uttagsnummer}', Email='{email}'") 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 områdeskod and not områdeskod.isdigit(): validation_errors.append("Områdeskod måste vara numerisk.") + if uttagsnummer and not uttagsnummer.isdigit(): validation_errors.append("Uttagsnummer måste vara numerisk.") + if not email: validation_errors.append("E-postadress är obligatorisk.") + elif '@' not in email or '.' not in email.split('@')[-1]: validation_errors.append("Ange en giltig e-postadress.") + if validation_errors: print(f"Valideringsfel: {validation_errors}") + error_message = "**Valideringsfel:**\n" + "\n".join([f"- {err}" for err in 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) + chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), + success_interface: gr.update(visible=False), chat_preview: gr.update(value=error_message) } - - # 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 + print("Validering OK. Skickar 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") + print("Support-förfrågan skickad.") return { - chat_interface: gr.Group(visible=False), - support_interface: gr.Group(visible=False), - success_interface: gr.Group(visible=True) + chat_interface: gr.update(visible=False), support_interface: gr.update(visible=False), + success_interface: gr.update(visible=True), chat_preview: gr.update(value="") } else: - print("Support-förfrågan till Slack misslyckades") + print("Misslyckades att skicka till Slack API.") + error_message = "**Ett tekniskt fel uppstod.** Försök igen senare eller kontakta support@chargenode.eu." 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.**" + chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), + success_interface: gr.update(visible=False), chat_preview: gr.update(value=error_message) } except Exception as e: - print(f"Oväntat fel vid hantering av support-formulär: {e}") + print(f"Oväntat fel vid submit_support_form: {e}") + error_message = f"**Ett oväntat fel uppstod:** {str(e)}. Försök igen." 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)}**" + chat_interface: gr.update(visible=False), support_interface: gr.update(visible=True), + success_interface: gr.update(visible=False), chat_preview: gr.update(value=error_message) } # --- Gradio UI --- -initial_chat = [{"role": "assistant", "content": "Detta är ChargeNode's AI bot. Hur kan jag hjälpa dig idag?"}] +initial_chat = [{"role": "assistant", "content": "Hej! Jag är ChargeNodes AI-assistent. Ställ din fråga nedan."}] 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;} +body { font-family: sans-serif; margin: 0; padding: 0; background-color: #f8f9fa; } +.gradio-container { max-width: 420px !important; margin: 10px auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 10px; background-color: #ffffff; overflow: hidden; border: 1px solid #dee2e6; position: fixed; bottom: 15px; right: 15px; } +h1 { font-family: 'Segoe UI', sans-serif; color: #1e88e5; text-align: center; margin: 0.8em 0 0.6em 0; font-size: 1.4em; font-weight: 600; } +#chatbot_conversation .message-wrap { padding: 12px; } +#chatbot_conversation { min-height: 280px; max-height: 350px; overflow-y: auto; border-bottom: 1px solid #e9ecef; background-color: #f8f9fa;} +.message { padding: 8px 12px; margin-bottom: 6px; border-radius: 15px; max-width: 88%; word-wrap: break-word; line-height: 1.4; font-size: 0.95em;} +.message.user { background-color: #e3f2fd; border-radius: 15px 15px 5px 15px; margin-left: auto; } +.message.bot { background-color: #e9ecef; border-radius: 15px 15px 15px 5px; margin-right: auto; } +.support-form-container, .success-interface { padding: 15px 20px; } +.support-form-container .gr-form { margin-top: 10px; padding: 15px; border: 1px solid #ced4da; border-radius: 8px; background-color: #f1f3f5; } +.gr-button { background-color: #1e88e5; color: white; border: none; border-radius: 5px; padding: 9px 16px; margin: 4px; cursor: pointer; transition: background-color 0.2s ease; font-weight: 500; font-size: 0.9em; } +.gr-button:hover { background-color: #1565c0; } +.support-btn { background-color: #6c757d; } .support-btn:hover { background-color: #5a6268; } /* Grå supportknapp */ +.clear-btn { background-color: #dc3545; } .clear-btn:hover { background-color: #c82333; } /* Röd rensa-knapp */ +.flex-row { display: flex; flex-direction: row; gap: 8px; align-items: center; } +.chat-preview { max-height: 140px; overflow-y: auto; border: 1px solid #ced4da; padding: 10px; margin-top: 12px; font-size: 0.85em; background-color: #ffffff; border-radius: 4px; line-height: 1.3; } +.success-message { font-size: 1.05em; font-weight: 500; color: #28a745; margin-bottom: 15px; text-align: center; } +footer { display: none !important; } +.gradio-container footer, .gradio-container .gr-footer { display: none !important; visibility: hidden !important; } +.gr-textbox textarea { border-radius: 5px; border: 1px solid #ced4da; padding: 10px; font-size: 0.95em;} +.block.padded { padding: 10px 15px !important; } +.block.gap { gap: 8px !important; } """ -# VIKTIGT: Alla komponenter och eventkopplingar definieras inuti Blocks-kontexten +# --- Gradio App Definition --- 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 + gr.Markdown("# ChargeNode AI Assistent") # Tydligare rubrik + + # --- 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...") - + chatbot = gr.Chatbot( + value=initial_chat, + elem_id="chatbot_conversation", + label="Chatt", show_label=False, + bubble_full_width=False + ) + chatbot.like(vote, inputs=None, outputs=None) # Koppla like direkt + + with gr.Row(equal_height=False): # Ta bort equal_height + msg = gr.Textbox( + label="Meddelande", placeholder="Skriv din fråga här...", + show_label=False, scale=4, lines=1, max_lines=3 # Begränsa rader + ) + # submit_btn = gr.Button("Skicka", scale=1, min_width=80) # Synlig skicka-knapp + 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 och förhindra auto-scroll + clear_btn = gr.Button("Rensa chatt", elem_classes="clear-btn") + support_btn = gr.Button("Kontakta support", elem_classes="support-btn") + + # --- Support Form Interface --- + with gr.Group(visible=False, elem_classes="support-form-container") as support_interface: + gr.Markdown("### Kontakta Support") + gr.Markdown("Fyll i dina uppgifter så återkommer vi via email. Din chatthistorik inkluderas.") + with gr.Column(elem_classes="gr-form"): + email = gr.Textbox(label="Din E-postadress", placeholder="din.email@example.com", info="Obligatorisk.") + områdeskod = gr.Textbox(label="Områdeskod (frivilligt)", placeholder="t.ex. 12345") + uttagsnummer = gr.Textbox(label="Uttagsnummer (frivilligt)", placeholder="t.ex. 1") + gr.Markdown("#### Chatthistorik (senaste)") + chat_preview = gr.Markdown(value="_Laddar..._", elem_classes="chat-preview") + with gr.Row(): + back_btn = gr.Button("Avbryt") + send_support_btn = gr.Button("Skicka till Support") + + # --- Success Message Interface --- + with gr.Group(visible=False) as success_interface: + gr.Markdown("✅ Tack! Din förfrågan har skickats.", elem_classes="success-message") + back_to_chat_btn = gr.Button("Tillbaka till chatten") + + + # --- JavaScript för Scrollning (UPPDATERAD) --- js_code = """ - // Förhindra Gradio's automatiska scrollning till botten - (function() { - // Spara originella scrollTo och scrollIntoView funktioner - const originalScrollTo = window.scrollTo; - const originalElementScrollIntoView = Element.prototype.scrollIntoView; - const originalScrollIntoView = HTMLElement.prototype.scrollIntoView; - - // Övervaka och förhindra automatisk scrollning till botten - window.scrollTo = function(x, y) { - console.log('scrollTo intercepted:', x, y); - // Tillåt bara scrollning som inte är till botten av sidan - if (typeof y !== 'number' || y < document.body.scrollHeight - window.innerHeight - 100) { - originalScrollTo.apply(this, arguments); - } else { - console.log('Prevented automatic scroll to bottom'); - } - }; - - // Övervaka scrollIntoView anrop - Element.prototype.scrollIntoView = function(options) { - console.log('Element scrollIntoView intercepted', this, options); - // Kontrollera om elementet är ett botmeddelande eller en del av chattcontainern - if (this.classList && ( - this.classList.contains('message') || - this.classList.contains('bot') || - this.closest('.message-wrap') - )) { - console.log('Handling chat element scrollIntoView'); - // Anropa vår egen scrollningsfunktion istället - handleChatScroll(); - return; + // Function to scroll the chat so the top of the latest bot message is visible + function scrollToTopOfLatestBotMessage() { + const selectorOptions = [ + '.gradio-container .message-wrap .message.bot:last-child', // Primary target based on CSS + '.gradio-container [data-testid="bot"]:last-child', // Hugging Face specific selector + '.gradio-container #chatbot_conversation .message:is(.bot, [data-testid="bot"]):last-of-type' // More specific last bot message + ]; + + let latestBotMessage = null; + for (const selector of selectorOptions) { + latestBotMessage = document.querySelector(selector); + if (latestBotMessage) break; + } + + if (latestBotMessage) { + try { + latestBotMessage.scrollIntoView({ block: 'start', behavior: 'smooth' }); + } catch (e) { + console.error("Error during scrollIntoView:", e); + const chatContainer = latestBotMessage.closest('#chatbot_conversation .message-wrap'); + if (chatContainer) { + try { + const scrollPosition = latestBotMessage.offsetTop - chatContainer.offsetTop; + chatContainer.scrollTop = scrollPosition; + } catch (e2) { console.error("Manual scroll fallback failed:", e2); } + } } - // Annars, låt originella funktionen köra - originalElementScrollIntoView.apply(this, arguments); - }; - - HTMLElement.prototype.scrollIntoView = Element.prototype.scrollIntoView; - - console.log('Scroll interception set up'); - })(); - - // Huvudfunktion för att hantera chattscrollning - function handleChatScroll() { - console.log('handleChatScroll called'); - - // Hitta chattcontainern - const chatContainers = [ - document.querySelector('#chatbot_conversation .message-wrap'), - document.querySelector('.gradio-container .chat'), - document.querySelector('.gradio-container [id^="component-"] .chat'), - document.querySelector('.gradio-container .message-wrap') - ].filter(Boolean); - - if (chatContainers.length === 0) { - console.log('No chat container found'); - return; + } else { + // console.log("Scroll: Could not find the latest bot message element."); } - - const chatContainer = chatContainers[0]; - console.log('Found chat container:', chatContainer); - - // Hitta alla botmeddelanden - const botMessages = [ - ...Array.from(document.querySelectorAll('.message.bot')), - ...Array.from(document.querySelectorAll('[data-testid="bot"]')), - ...Array.from(document.querySelectorAll('.message:not(.user)')) - ].filter(Boolean); - - if (botMessages.length === 0) { - console.log('No bot messages found'); + } + + // Use MutationObserver for robust detection of new messages + function setupObserver() { + const chatRoot = document.querySelector('#chatbot_conversation'); // Target the chatbot element directly + if (!chatRoot) { + // console.log("Scroll: Chatbot container not found for observer, retrying..."); + setTimeout(setupObserver, 300); return; } - - // Hitta det senaste botmeddelandet - const latestBotMessage = botMessages[botMessages.length - 1]; - console.log('Latest bot message:', latestBotMessage); - - // Beräkna position för att visa botmeddelandet i toppen - try { - const containerRect = chatContainer.getBoundingClientRect(); - const messageRect = latestBotMessage.getBoundingClientRect(); - const relativeTop = messageRect.top - containerRect.top; - - // Scrolla så att botmeddelandet är i toppen - chatContainer.scrollTop = chatContainer.scrollTop + relativeTop - 10; // 10px marginal - console.log('Scrolled chat container to show bot message at top'); - - // Förhindra ytterligare scrollning under en kort period - chatContainer.style.overflowY = 'hidden'; - setTimeout(() => { - chatContainer.style.overflowY = ''; - }, 500); - } catch (e) { - console.error('Error during manual scrolling:', e); - } - } - // Lyssna på DOM-ändringar för att upptäcka nya meddelanden - document.addEventListener('DOMContentLoaded', function() { - console.log('DOM loaded, setting up observers'); - - // Använd MutationObserver för att upptäcka nya meddelanden - const observer = new MutationObserver(function(mutations) { - let newBotMessage = false; - - mutations.forEach(function(mutation) { - if (mutation.type === 'childList' && mutation.addedNodes.length) { - mutation.addedNodes.forEach(function(node) { - if (node.nodeType === 1) { // Element node - // Kontrollera om det är ett botmeddelande - if (node.classList && ( - node.classList.contains('bot') || - node.classList.contains('message') - )) { - newBotMessage = true; - } - // Eller om det innehåller ett botmeddelande - else if (node.querySelector && ( - node.querySelector('.bot') || - node.querySelector('.message:not(.user)') - )) { - newBotMessage = true; - } + const observer = new MutationObserver((mutationsList) => { + let botMessageAdded = false; + for (const mutation of mutationsList) { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE && (node.matches('.message.bot') || node.matches('[data-testid="bot"]'))) { + botMessageAdded = true; + } else if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && (node.querySelector('.message.bot') || node.querySelector('[data-testid="bot"]'))) { + // Sometimes the message is wrapped in another div + botMessageAdded = true; } }); } - }); - - if (newBotMessage) { - console.log('New bot message detected'); - // Använd flera timeouts för att säkerställa att scrollningen fungerar - // efter att alla DOM-uppdateringar är klara - setTimeout(handleChatScroll, 100); - setTimeout(handleChatScroll, 300); - setTimeout(handleChatScroll, 500); - setTimeout(handleChatScroll, 1000); + if (botMessageAdded) break; // Optimization + } + + if (botMessageAdded) { + // console.log("Scroll: MutationObserver detected new bot message."); + // Use a slightly longer delay to ensure rendering is complete + setTimeout(scrollToTopOfLatestBotMessage, 200); } }); - - // Starta övervakning av hela dokumentet - observer.observe(document.body, { - childList: true, - subtree: true - }); - - console.log('MutationObserver started'); - - // Lyssna även på klickhändelser för att hantera scrollning efter användarinteraktion - document.addEventListener('click', function(event) { - // Fördröj för att låta eventuella UI-uppdateringar slutföras - setTimeout(handleChatScroll, 300); - }); - }); - - // Lägg till CSS för att förbättra scrollningsbeteendet - const style = document.createElement('style'); - style.textContent = ` - #chatbot_conversation .message-wrap { - scroll-behavior: smooth !important; - overflow-anchor: none !important; /* Förhindra automatisk ankring */ - } - .message.bot { - scroll-margin-top: 10px !important; - } - `; - document.head.appendChild(style); + + observer.observe(chatRoot, { childList: true, subtree: true }); + // console.log("Scroll: MutationObserver is now observing the chatbot container."); + } + + // Start the observer setup when the DOM is ready or if it's already ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupObserver); + } else { + setupObserver(); + } """ 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] - ) + + # --- Event Listeners (INUTI gr.Blocks) --- + msg.submit(fn=respond, inputs=[msg, chatbot], outputs=[msg, chatbot], api_name="send_message") + # Om du använder en synlig knapp: + # submit_btn.click(fn=respond, inputs=[msg, chatbot], outputs=[msg, chatbot], api_name="send_message") + + clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg], queue=False) # Rensa historik OCH input + + support_btn.click(fn=show_support_form, inputs=[chatbot], outputs=[chat_interface, support_interface, success_interface, chat_preview]) + back_btn.click(fn=back_to_chat, inputs=None, outputs=[chat_interface, support_interface, success_interface]) + back_to_chat_btn.click(fn=back_to_chat, inputs=None, outputs=[chat_interface, support_interface, success_interface]) + send_support_btn.click(fn=submit_support_form, inputs=[områdeskod, uttagsnummer, email, chatbot], outputs=[chat_interface, support_interface, success_interface, chat_preview]) + +# --- Kör appen --- if __name__ == "__main__": - app.launch(share=True) + print("Förbereder att starta Gradio-appen...") + try: + initialize_embeddings() # Ladda embeddings innan appen startas + print("Embeddings initialiserade.") + # Gör share konfigurerbart, default till False för lokal körning + should_share = os.environ.get("GRADIO_SHARE", "false").lower() == "true" + print(f"Startar Gradio {'med' if should_share else 'utan'} delning...") + app.launch(share=should_share) + except RuntimeError as e: + print(f"Kunde inte starta appen på grund av tidigare fel: {e}") + except Exception as e: + print(f"Ett oväntat fel uppstod vid start av Gradio: {e}") \ No newline at end of file