import gradio as gr import faiss import json import numpy as np import openai import os import spacy from sentence_transformers import CrossEncoder from dotenv import load_dotenv # Ladda spaCy-modellen (använd "en_core_web_sm" eller byt ut mot "sv_core_news_sm" om du vill) try: nlp = spacy.load("en_core_web_sm") except OSError: from spacy.cli import download download("en_core_web_sm") nlp = spacy.load("en_core_web_sm") # === Ladda miljövariabler och API-nyckel === load_dotenv() openai.api_key = os.getenv("OPENAI_API_KEY") # === Ladda FAISS-index och metadata === index = faiss.read_index("faiss.index") with open("faiss_metadata.json", "r", encoding="utf-8") as f: metadata = json.load(f) # === Ladda CrossEncoder för re-ranking === reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2") # === Funktion för att hämta embedding via OpenAI === def get_embedding(text): response = openai.embeddings.create( model="text-embedding-ada-002", input=[text] ) data = response.dict()["data"] return np.array(data[0]["embedding"], dtype=np.float32) # === LLM-baserad nyckelordsutvinning (few-shot) med explicit BECCS-exempel === def extract_keywords_llm(text): prompt = ( "Här är några exempel:\n" "Exempel 1:\n" "Fråga: \"Hur fungerar BECCS enligt Stockholm Exergi?\"\n" "Förväntad nyckelordslista: [\"beccs\", \"bio energy\", \"carbon capture\", \"lagring\"]\n\n" "Exempel 2:\n" "Fråga: \"Hur ändrar jag postadress på en kund?\"\n" "Förväntad nyckelordslista: [\"postadress\", \"kund\", \"adressändring\"]\n\n" "Extrahera de viktigaste nyckelorden från frågan nedan, inkludera även akronymer och facktermer. " "Returnera svaret som en JSON-lista.\n" f"Fråga: \"{text}\"" ) response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], max_tokens=100, temperature=0.0, ) answer_text = response.choices[0].message.content.strip() try: keywords = json.loads(answer_text) if isinstance(keywords, list): return [kw.strip().lower() for kw in keywords if isinstance(kw, str) and kw.strip()] else: return [kw.strip().lower() for kw in answer_text.split(",") if kw.strip()] except Exception as e: return [kw.strip().lower() for kw in answer_text.split(",") if kw.strip()] # === spaCy-baserad nyckelordsutvinning === def extract_keywords_spacy(text): doc = nlp(text) keywords = set() for ent in doc.ents: keywords.add(ent.text.lower()) for token in doc: if token.pos_ in ["NOUN", "PROPN", "ADJ"]: keywords.add(token.text.lower()) return list(keywords) # === Kombinerad nyckelordsutvinning === def extract_keywords_combined(text): llm_keywords = extract_keywords_llm(text) spacy_keywords = extract_keywords_spacy(text) combined = set(llm_keywords) | set(spacy_keywords) return list(combined) # === Hämta topp-k kandidater via FAISS === def retrieve_top_k(question, k=20): embedding = get_embedding(question) embedding = np.array([embedding]) distances, indices = index.search(embedding, k) return [metadata[i] for i in indices[0] if i < len(metadata)] # === Re-ranking av kandidater med dynamisk nyckelordsbonus === def re_rank_candidates(question, candidates): keywords = extract_keywords_combined(question) pair_list = [(question, c["text"]) for c in candidates] scores = reranker.predict(pair_list) modified_scores = [] for cand, score in zip(candidates, scores): text_lower = cand["text"].lower() bonus = 0.0 for kw in keywords: # Om "beccs" finns, ge högre bonus (t.ex. 0.3 per träff) if kw == "beccs" and kw in text_lower: bonus += 0.3 elif kw in text_lower: bonus += 0.1 modified_scores.append(score + bonus) reranked = sorted(zip(candidates, modified_scores), key=lambda x: x[1], reverse=True) return [cand for cand, _ in reranked[:3]] # === Bygg prompt med inbäddad kontext från utvalda artiklar === def build_prompt(question, docs): context = "" for doc in docs: doc_id = doc.get("id", "Okänt-ID") rubrik = doc.get("rubrik", "") content = doc.get("text", "") context += f"\n[Artikel: {doc_id} - {rubrik}]\n{content}\n" lower_q = question.lower() additional_instruction = "" if any(term in lower_q for term in ["steg för steg", "detaljerad", "guide"]): additional_instruction = "Ge en detaljerad steg-för-steg-guide baserad på informationen ovan." prompt = ( "Du är en hjälpsam supportagent hos Stockholm Exergi. " "Svara endast om du hittar tydlig och direkt information i texterna nedan. " "Om informationen inte finns, skriv exakt: 'Ingen information finns.'\n\n" "Viktig instruktion: I ditt svar, ange alltid Offentligt artikelnummer " "för de artiklar du använde.\n\n" f"{context}\n" f"Fråga: {question}\n" f"{additional_instruction}\n" "Svar:" ) return prompt # === Chat-funktion med cache och hantering av personlig info === def chat_rag(question, history, user_profile, cache): lower_q = question.lower().strip() normalized_q = question.strip().lower() # Hantera personlig information if "mitt namn är" in lower_q: try: name = question.split("mitt namn är", 1)[1].strip().split()[0] except IndexError: name = "Okänt" user_profile["name"] = name answer = f"Ok, jag har sparat att ditt namn är {name}." history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": answer}) return "", history, user_profile, cache elif "vad heter jag" in lower_q: name = user_profile.get("name") if name: answer = f"Du heter {name}." else: answer = "Jag har inte fått veta ditt namn än. Skriv 'mitt namn är ...' för att uppdatera." history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": answer}) return "", history, user_profile, cache # Cache för detaljerade frågor qualifies_for_cache = any(term in lower_q for term in ["steg för steg", "detaljerad", "guide"]) if qualifies_for_cache and normalized_q in cache: cached_answer = cache[normalized_q] history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": cached_answer}) return "", history, user_profile, cache # Om frågan uttryckligen ber om en steg-för-steg-guide och det finns ett tidigare retrieval-svar, använd det if qualifies_for_cache and "kan du ge mig svaret steg för steg" in lower_q: if history and history[-1]["role"] == "assistant": prev_answer = history[-1]["content"] new_prompt = ( "Utgå från detta tidigare svar:\n\n" f"{prev_answer}\n\n" "Och ge mig en detaljerad steg-för-steg-guide baserad på informationen ovan." ) response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": new_prompt}], max_tokens=150, temperature=0.0 ) answer = response.choices[0].message.content history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": answer}) cache[normalized_q] = answer return "", history, user_profile, cache # Annars: retrieval och re-ranking top_docs = retrieve_top_k(question, k=20) best_docs = re_rank_candidates(question, top_docs) prompt = build_prompt(question, best_docs) response = openai.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], max_tokens=300, temperature=0.0 ) answer = response.choices[0].message.content used_ids = [doc.get("id", "Okänt-ID") for doc in best_docs] unique_ids = set(used_ids) if unique_ids: references = ", ".join(unique_ids) answer += f"\n\n[Information hämtad från Offentligt artikelnummer(n): {references}]" history.append({"role": "user", "content": question}) history.append({"role": "assistant", "content": answer}) if qualifies_for_cache: cache[normalized_q] = answer return "", history, user_profile, cache # --- Inbyggt lösenordsskydd --- # Definiera ditt lösenord PASSWORD = "agrikaremexergi" def login(input_password): if input_password == PASSWORD: return gr.update(visible=True), "Inloggning lyckades!" else: return gr.update(visible=False), "Fel lösenord, försök igen." # --- Gradio UI --- with gr.Blocks() as demo: # Inloggningspanel (visas först) with gr.Column(visible=True) as login_panel: gr.Markdown("## Logga in") password_input = gr.Textbox(label="Ange lösenord", type="password") login_button = gr.Button("Logga in") login_message = gr.Markdown("") # Huvudapp-panel (gömd tills rätt lösenord anges) with gr.Column(visible=False) as main_app_panel: gr.Markdown("## 💬 OpenAI RAG-Chat med FAISS + Re-Ranking") chatbot = gr.Chatbot(label="RAG-Chat", type="messages") msg = gr.Textbox(label="Ställ en fråga...") send = gr.Button("Skicka") state = gr.State([]) # Chatthistorik profile = gr.State({}) # Användarprofil cache_state = gr.State({}) # Cache för återkommande frågor send.click( fn=chat_rag, inputs=[msg, state, profile, cache_state], outputs=[msg, chatbot, profile, cache_state] ) # Koppla login-knappen till inloggningsfunktionen login_button.click( fn=login, inputs=[password_input], outputs=[main_app_panel, login_message] ) if __name__ == "__main__": demo.launch()