Spaces:
Running
Running
Király Zoltán
commited on
Commit
·
138e4b0
1
Parent(s):
79aa6e9
Fix: Clean up requirements.txt to resolve build conflicts
Browse files- appv1.py +64 -96
- backendv1.py +111 -302
appv1.py
CHANGED
|
@@ -1,42 +1,58 @@
|
|
| 1 |
# appv1.py
|
| 2 |
# A RAG rendszer grafikus felhasználói felülete Streamlit segítségével.
|
| 3 |
-
#
|
| 4 |
-
# Igazítva a backendv1.py-hoz.
|
| 5 |
-
# Kiegészítve a legjobb találati pontszám megjelenítésével.
|
| 6 |
|
| 7 |
import streamlit as st
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
)
|
|
|
|
| 25 |
|
| 26 |
# --- Oldal Konfiguráció ---
|
| 27 |
st.set_page_config(page_title="Dunaelektronika AI", layout="wide")
|
| 28 |
st.title("🤖 Dunaelektronika AI Asszisztens")
|
| 29 |
|
| 30 |
|
| 31 |
-
# --- Backend Betöltése (gyorsítótárazva) ---
|
| 32 |
@st.cache_resource
|
| 33 |
def load_backend_components():
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
|
|
|
| 37 |
backend = load_backend_components()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
|
|
|
| 40 |
if "messages" not in st.session_state:
|
| 41 |
st.session_state.messages = []
|
| 42 |
if "last_confidence_score" not in st.session_state:
|
|
@@ -47,15 +63,12 @@ if "page" not in st.session_state:
|
|
| 47 |
# --- Navigáció az Oldalsávon ---
|
| 48 |
with st.sidebar:
|
| 49 |
st.header("Menü")
|
| 50 |
-
if st.button("💬 Chat", use_container_width=True,
|
| 51 |
-
type="primary" if st.session_state.page == "Chat" else "secondary"):
|
| 52 |
st.session_state.page = "Chat"
|
| 53 |
st.rerun()
|
| 54 |
-
if st.button("⚙️ Feedback Adminisztráció", use_container_width=True,
|
| 55 |
-
type="primary" if st.session_state.page == "Admin" else "secondary"):
|
| 56 |
st.session_state.page = "Admin"
|
| 57 |
st.rerun()
|
| 58 |
-
|
| 59 |
st.write("---")
|
| 60 |
|
| 61 |
# ==============================================================================
|
|
@@ -64,17 +77,14 @@ with st.sidebar:
|
|
| 64 |
if st.session_state.page == "Chat":
|
| 65 |
with st.sidebar:
|
| 66 |
st.header("Beállítások")
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
step=0.1)
|
| 70 |
-
fallback_message = st.text_area("Válasz alacsony pontosságnál",
|
| 71 |
-
"A rendelkezésre álló információk alapján sajnos nem tudok egyértelmű választ adni a kérdésre.",
|
| 72 |
-
height=100)
|
| 73 |
CONFIG["GENERATION_TEMPERATURE"] = st.slider("Kreativitás (Temperature)", 0.0, 1.0, 0.6, 0.05)
|
| 74 |
|
| 75 |
st.write("---")
|
| 76 |
st.subheader("Utolsó Válasz Elemzése")
|
| 77 |
score = st.session_state.last_confidence_score
|
|
|
|
| 78 |
if score == "N/A":
|
| 79 |
level, help_text = "N/A", "Tegyen fel egy kérdést a megbízhatóság méréséhez."
|
| 80 |
elif score is None:
|
|
@@ -83,12 +93,9 @@ if st.session_state.page == "Chat":
|
|
| 83 |
level, help_text = "Kurált Válasz", "Ez egy korábban megadott, pontosított válasz."
|
| 84 |
else:
|
| 85 |
help_text = f"Nyers pontszám: {score:.4f}"
|
| 86 |
-
if score > 1.0:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
level = "Közepes"
|
| 90 |
-
else:
|
| 91 |
-
level = "Alacsony"
|
| 92 |
st.metric(label="Keresési Magabiztosság", value=level, help=help_text)
|
| 93 |
|
| 94 |
# Chat Előzmények Megjelenítése
|
|
@@ -96,72 +103,53 @@ if st.session_state.page == "Chat":
|
|
| 96 |
with st.chat_message(message["role"]):
|
| 97 |
st.markdown(message["content"].replace('$', '\\$'))
|
| 98 |
if message["role"] == "assistant":
|
| 99 |
-
# --- HOZZÁADOTT RÉSZ ---
|
| 100 |
-
# A válaszhoz tartozó pontszám megjelenítése, ha létezik.
|
| 101 |
score_value = message.get("score")
|
| 102 |
if score_value is not None:
|
| 103 |
-
if score_value == 10.0
|
| 104 |
-
score_display = "Kurált válasz (legmagasabb)"
|
| 105 |
-
else:
|
| 106 |
-
score_display = f"{score_value:.4f}"
|
| 107 |
st.caption(f"A válasz legjobb score értéke: **{score_display}**")
|
| 108 |
-
|
| 109 |
-
|
| 110 |
if message.get("sources"):
|
| 111 |
with st.expander("Felhasznált források"):
|
| 112 |
for source in message["sources"]:
|
| 113 |
st.caption(f"Forrás: {source.get('url', 'N/A')}")
|
| 114 |
st.markdown(f"> {source.get('content', '')[:250]}...")
|
| 115 |
-
|
| 116 |
feedback_key_prefix = f"feedback_{i}"
|
| 117 |
if not message.get("rated"):
|
| 118 |
st.write("---")
|
| 119 |
cols = st.columns(7)
|
| 120 |
if cols[0].button("👍 Jó", key=f"{feedback_key_prefix}_good"):
|
| 121 |
-
message["rated"] = "good";
|
| 122 |
-
st.toast("Köszönjük a visszajelzést!");
|
| 123 |
-
st.rerun()
|
| 124 |
if cols[1].button("👎 Rossz", key=f"{feedback_key_prefix}_bad"):
|
| 125 |
-
message["rated"] = "bad";
|
| 126 |
-
st.rerun()
|
| 127 |
|
| 128 |
if message.get("rated") == "bad":
|
| 129 |
with st.form(key=f"{feedback_key_prefix}_form"):
|
| 130 |
-
correction_text = st.text_area("Javítás:", key=f"{feedback_key_prefix}_text",
|
| 131 |
-
value=message.get("correction", ""))
|
| 132 |
if st.form_submit_button("Javítás elküldése"):
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
message["original_question"], correction_text)
|
| 136 |
-
st.success("Javításodat rögzítettük!");
|
| 137 |
-
message["rated"] = "corrected";
|
| 138 |
-
st.rerun()
|
| 139 |
|
| 140 |
# Felhasználói Kérdés Feldolgozása
|
| 141 |
if prompt := st.chat_input("Kérdezz valamit a Dunaelektronikáról..."):
|
| 142 |
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 143 |
with st.spinner("Keresek és gondolkodom..."):
|
| 144 |
-
|
| 145 |
-
response_data = process_query(prompt, st.session_state.messages, backend, confidence_threshold,
|
| 146 |
-
fallback_message)
|
| 147 |
|
| 148 |
st.session_state.last_confidence_score = response_data.get("confidence_score")
|
| 149 |
-
|
| 150 |
-
# --- MÓDOSÍTOTT RÉSZ ---
|
| 151 |
-
# A válasz üzenethez hozzáadjuk a 'score' kulcsot is, hogy később meg tudjuk jeleníteni.
|
| 152 |
st.session_state.messages.append({
|
| 153 |
"role": "assistant",
|
| 154 |
"content": response_data.get("answer", "Hiba történt."),
|
| 155 |
"sources": response_data.get("sources", []),
|
| 156 |
"original_question": prompt,
|
| 157 |
"rated": False,
|
| 158 |
-
"score": response_data.get("confidence_score")
|
| 159 |
})
|
| 160 |
-
# --- MÓDOSÍTOTT RÉSZ VÉGE ---
|
| 161 |
st.rerun()
|
| 162 |
|
| 163 |
# ==============================================================================
|
| 164 |
-
# =
|
| 165 |
# ==============================================================================
|
| 166 |
elif st.session_state.page == "Admin":
|
| 167 |
st.header("Rögzített Visszajelzések Kezelése")
|
|
@@ -169,52 +157,32 @@ elif st.session_state.page == "Admin":
|
|
| 169 |
if st.button("Lista frissítése"):
|
| 170 |
st.cache_data.clear()
|
| 171 |
|
| 172 |
-
|
| 173 |
@st.cache_data(ttl=60)
|
| 174 |
def get_cached_feedback():
|
| 175 |
-
# Hívás a backendv1 függvényre
|
| 176 |
return get_all_feedback(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"])
|
| 177 |
|
| 178 |
-
|
| 179 |
feedback_list = get_cached_feedback()
|
| 180 |
|
| 181 |
if not feedback_list:
|
| 182 |
st.warning("Nincsenek rögzített visszajelzések.")
|
| 183 |
else:
|
| 184 |
st.info(f"Összesen {len(feedback_list)} visszajelzés található.")
|
| 185 |
-
|
| 186 |
for item in feedback_list:
|
| 187 |
doc_id = item["_id"]
|
| 188 |
source = item["_source"]
|
| 189 |
-
|
| 190 |
with st.container(border=True):
|
| 191 |
st.markdown(f"**Kérdés:** `{source.get('question_text', 'N/A')}`")
|
| 192 |
-
|
| 193 |
with st.form(key=f"edit_form_{doc_id}"):
|
| 194 |
-
new_comment = st.text_area("Javítás/Megjegyzés:", value=source.get('correction_text', ''),
|
| 195 |
-
key=f"text_{doc_id}", label_visibility="collapsed")
|
| 196 |
-
|
| 197 |
col1, col2 = st.columns([4, 1])
|
| 198 |
-
|
| 199 |
with col1:
|
| 200 |
if st.form_submit_button("💾 Mentés"):
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
st.success("Sikeresen frissítve!")
|
| 205 |
-
st.cache_data.clear()
|
| 206 |
-
st.rerun()
|
| 207 |
-
else:
|
| 208 |
-
st.error("Hiba történt a frissítés során.")
|
| 209 |
-
|
| 210 |
with col2:
|
| 211 |
if st.form_submit_button("🗑️ Törlés"):
|
| 212 |
-
# Hívás a backendv1 függvényre
|
| 213 |
if delete_feedback_by_id(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"], doc_id):
|
| 214 |
-
st.success(f"Sikeresen törölve!")
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
else:
|
| 218 |
-
st.error("Hiba történt a törlés során.")
|
| 219 |
-
|
| 220 |
-
st.caption(f"Elasticsearch ID: {doc_id} | Időbélyeg: {source.get('timestamp', 'N/A')}")
|
|
|
|
| 1 |
# appv1.py
|
| 2 |
# A RAG rendszer grafikus felhasználói felülete Streamlit segítségével.
|
| 3 |
+
# JAVÍTOTT VERZIÓ: A modern, cloud-kompatibilis backendv1.py-hoz igazítva.
|
|
|
|
|
|
|
| 4 |
|
| 5 |
import streamlit as st
|
| 6 |
import sys
|
| 7 |
import os
|
| 8 |
|
| 9 |
+
# --- Backend Importálása ---
|
| 10 |
+
# A backendv1.py-nak ebben a mappában kell lennie.
|
| 11 |
+
try:
|
| 12 |
+
from backendv1 import (
|
| 13 |
+
initialize_backend,
|
| 14 |
+
process_query,
|
| 15 |
+
index_feedback,
|
| 16 |
+
get_all_feedback,
|
| 17 |
+
delete_feedback_by_id,
|
| 18 |
+
update_feedback_comment,
|
| 19 |
+
CONFIG
|
| 20 |
+
)
|
| 21 |
+
except ImportError:
|
| 22 |
+
st.error("Hiba: A 'backendv1.py' fájl nem található. Győződj meg róla, hogy ugyanabban a mappában van, mint ez a script.")
|
| 23 |
+
st.stop()
|
| 24 |
|
| 25 |
# --- Oldal Konfiguráció ---
|
| 26 |
st.set_page_config(page_title="Dunaelektronika AI", layout="wide")
|
| 27 |
st.title("🤖 Dunaelektronika AI Asszisztens")
|
| 28 |
|
| 29 |
|
| 30 |
+
# --- Backend Betöltése (gyorsítótárazva, hogy ne töltődjön be minden interakciónál újra) ---
|
| 31 |
@st.cache_resource
|
| 32 |
def load_backend_components():
|
| 33 |
+
"""
|
| 34 |
+
Betölti a backendet (AI modellek, DB kapcsolat).
|
| 35 |
+
A @st.cache_resource biztosítja, hogy ez a lassú folyamat csak egyszer fusson le.
|
| 36 |
+
"""
|
| 37 |
+
print("Backend komponensek inicializálása...")
|
| 38 |
+
backend_data = initialize_backend()
|
| 39 |
+
if backend_data:
|
| 40 |
+
print("Backend sikeresen betöltve.")
|
| 41 |
+
else:
|
| 42 |
+
print("Hiba a backend betöltésekor.")
|
| 43 |
+
return backend_data
|
| 44 |
|
| 45 |
+
# Backend betöltése és hibakezelés
|
| 46 |
backend = load_backend_components()
|
| 47 |
+
if not backend:
|
| 48 |
+
st.error(
|
| 49 |
+
"A háttérrendszer (AI modellek vagy adatbázis kapcsolat) nem tudott elindulni. "
|
| 50 |
+
"Ellenőrizd a konzol logját a hiba okáért, és hogy a környezeti változók (pl. .env fájl) helyesen vannak-e beállítva."
|
| 51 |
+
)
|
| 52 |
+
st.stop()
|
| 53 |
|
| 54 |
+
|
| 55 |
+
# --- Session State Inicializálása (az adatok tárolására a böngészőben) ---
|
| 56 |
if "messages" not in st.session_state:
|
| 57 |
st.session_state.messages = []
|
| 58 |
if "last_confidence_score" not in st.session_state:
|
|
|
|
| 63 |
# --- Navigáció az Oldalsávon ---
|
| 64 |
with st.sidebar:
|
| 65 |
st.header("Menü")
|
| 66 |
+
if st.button("💬 Chat", use_container_width=True, type="primary" if st.session_state.page == "Chat" else "secondary"):
|
|
|
|
| 67 |
st.session_state.page = "Chat"
|
| 68 |
st.rerun()
|
| 69 |
+
if st.button("⚙️ Feedback Adminisztráció", use_container_width=True, type="primary" if st.session_state.page == "Admin" else "secondary"):
|
|
|
|
| 70 |
st.session_state.page = "Admin"
|
| 71 |
st.rerun()
|
|
|
|
| 72 |
st.write("---")
|
| 73 |
|
| 74 |
# ==============================================================================
|
|
|
|
| 77 |
if st.session_state.page == "Chat":
|
| 78 |
with st.sidebar:
|
| 79 |
st.header("Beállítások")
|
| 80 |
+
confidence_threshold = st.slider("Minimális pontossági küszöb", min_value=-5.0, max_value=5.0, value=0.1, step=0.1)
|
| 81 |
+
fallback_message = st.text_area("Válasz alacsony pontosságnál", "A rendelkezésre álló információk alapján sajnos nem tudok egyértelmű választ adni a kérdésre.", height=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
CONFIG["GENERATION_TEMPERATURE"] = st.slider("Kreativitás (Temperature)", 0.0, 1.0, 0.6, 0.05)
|
| 83 |
|
| 84 |
st.write("---")
|
| 85 |
st.subheader("Utolsó Válasz Elemzése")
|
| 86 |
score = st.session_state.last_confidence_score
|
| 87 |
+
|
| 88 |
if score == "N/A":
|
| 89 |
level, help_text = "N/A", "Tegyen fel egy kérdést a megbízhatóság méréséhez."
|
| 90 |
elif score is None:
|
|
|
|
| 93 |
level, help_text = "Kurált Válasz", "Ez egy korábban megadott, pontosított válasz."
|
| 94 |
else:
|
| 95 |
help_text = f"Nyers pontszám: {score:.4f}"
|
| 96 |
+
if score > 1.0: level = "Magas"
|
| 97 |
+
elif score >= -1.5: level = "Közepes"
|
| 98 |
+
else: level = "Alacsony"
|
|
|
|
|
|
|
|
|
|
| 99 |
st.metric(label="Keresési Magabiztosság", value=level, help=help_text)
|
| 100 |
|
| 101 |
# Chat Előzmények Megjelenítése
|
|
|
|
| 103 |
with st.chat_message(message["role"]):
|
| 104 |
st.markdown(message["content"].replace('$', '\\$'))
|
| 105 |
if message["role"] == "assistant":
|
|
|
|
|
|
|
| 106 |
score_value = message.get("score")
|
| 107 |
if score_value is not None:
|
| 108 |
+
score_display = "Kurált válasz (legmagasabb)" if score_value == 10.0 else f"{score_value:.4f}"
|
|
|
|
|
|
|
|
|
|
| 109 |
st.caption(f"A válasz legjobb score értéke: **{score_display}**")
|
| 110 |
+
|
|
|
|
| 111 |
if message.get("sources"):
|
| 112 |
with st.expander("Felhasznált források"):
|
| 113 |
for source in message["sources"]:
|
| 114 |
st.caption(f"Forrás: {source.get('url', 'N/A')}")
|
| 115 |
st.markdown(f"> {source.get('content', '')[:250]}...")
|
| 116 |
+
|
| 117 |
feedback_key_prefix = f"feedback_{i}"
|
| 118 |
if not message.get("rated"):
|
| 119 |
st.write("---")
|
| 120 |
cols = st.columns(7)
|
| 121 |
if cols[0].button("👍 Jó", key=f"{feedback_key_prefix}_good"):
|
| 122 |
+
message["rated"] = "good"; st.toast("Köszönjük a visszajelzést!"); st.rerun()
|
|
|
|
|
|
|
| 123 |
if cols[1].button("👎 Rossz", key=f"{feedback_key_prefix}_bad"):
|
| 124 |
+
message["rated"] = "bad"; st.rerun()
|
|
|
|
| 125 |
|
| 126 |
if message.get("rated") == "bad":
|
| 127 |
with st.form(key=f"{feedback_key_prefix}_form"):
|
| 128 |
+
correction_text = st.text_area("Javítás:", key=f"{feedback_key_prefix}_text", value=message.get("correction", ""))
|
|
|
|
| 129 |
if st.form_submit_button("Javítás elküldése"):
|
| 130 |
+
index_feedback(backend["es_client"], backend["embedding_model"], message["original_question"], correction_text)
|
| 131 |
+
st.success("Javításodat rögzítettük!"); message["rated"] = "corrected"; st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
# Felhasználói Kérdés Feldolgozása
|
| 134 |
if prompt := st.chat_input("Kérdezz valamit a Dunaelektronikáról..."):
|
| 135 |
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 136 |
with st.spinner("Keresek és gondolkodom..."):
|
| 137 |
+
response_data = process_query(prompt, st.session_state.messages, backend, confidence_threshold, fallback_message)
|
|
|
|
|
|
|
| 138 |
|
| 139 |
st.session_state.last_confidence_score = response_data.get("confidence_score")
|
| 140 |
+
|
|
|
|
|
|
|
| 141 |
st.session_state.messages.append({
|
| 142 |
"role": "assistant",
|
| 143 |
"content": response_data.get("answer", "Hiba történt."),
|
| 144 |
"sources": response_data.get("sources", []),
|
| 145 |
"original_question": prompt,
|
| 146 |
"rated": False,
|
| 147 |
+
"score": response_data.get("confidence_score")
|
| 148 |
})
|
|
|
|
| 149 |
st.rerun()
|
| 150 |
|
| 151 |
# ==============================================================================
|
| 152 |
+
# = ADMIN OLDAL LOGIKÁJA =
|
| 153 |
# ==============================================================================
|
| 154 |
elif st.session_state.page == "Admin":
|
| 155 |
st.header("Rögzített Visszajelzések Kezelése")
|
|
|
|
| 157 |
if st.button("Lista frissítése"):
|
| 158 |
st.cache_data.clear()
|
| 159 |
|
|
|
|
| 160 |
@st.cache_data(ttl=60)
|
| 161 |
def get_cached_feedback():
|
|
|
|
| 162 |
return get_all_feedback(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"])
|
| 163 |
|
|
|
|
| 164 |
feedback_list = get_cached_feedback()
|
| 165 |
|
| 166 |
if not feedback_list:
|
| 167 |
st.warning("Nincsenek rögzített visszajelzések.")
|
| 168 |
else:
|
| 169 |
st.info(f"Összesen {len(feedback_list)} visszajelzés található.")
|
|
|
|
| 170 |
for item in feedback_list:
|
| 171 |
doc_id = item["_id"]
|
| 172 |
source = item["_source"]
|
|
|
|
| 173 |
with st.container(border=True):
|
| 174 |
st.markdown(f"**Kérdés:** `{source.get('question_text', 'N/A')}`")
|
|
|
|
| 175 |
with st.form(key=f"edit_form_{doc_id}"):
|
| 176 |
+
new_comment = st.text_area("Javítás/Megjegyzés:", value=source.get('correction_text', ''), key=f"text_{doc_id}", label_visibility="collapsed")
|
|
|
|
|
|
|
| 177 |
col1, col2 = st.columns([4, 1])
|
|
|
|
| 178 |
with col1:
|
| 179 |
if st.form_submit_button("💾 Mentés"):
|
| 180 |
+
if update_feedback_comment(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"], doc_id, new_comment):
|
| 181 |
+
st.success("Sikeresen frissítve!"); st.cache_data.clear(); st.rerun()
|
| 182 |
+
else: st.error("Hiba történt a frissítés során.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
with col2:
|
| 184 |
if st.form_submit_button("🗑️ Törlés"):
|
|
|
|
| 185 |
if delete_feedback_by_id(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"], doc_id):
|
| 186 |
+
st.success(f"Sikeresen törölve!"); st.cache_data.clear(); st.rerun()
|
| 187 |
+
else: st.error("Hiba történt a törlés során.")
|
| 188 |
+
st.caption(f"Elasticsearch ID: {doc_id} | Időbélyeg: {source.get('timestamp', 'N/A')}")
|
|
|
|
|
|
|
|
|
|
|
|
backendv1.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# backendv1.py
|
|
|
|
| 2 |
# A RAG rendszer motorja: adatfeldolgozás, keresés, generálás és tanulás.
|
| 3 |
-
# Végleges, refaktorált verzió. Gyors, egylépcsős generálással.
|
| 4 |
|
| 5 |
import os
|
| 6 |
import time
|
|
@@ -8,18 +8,23 @@ import datetime
|
|
| 8 |
import json
|
| 9 |
import re
|
| 10 |
from collections import defaultdict
|
| 11 |
-
from together import Together
|
| 12 |
from elasticsearch import Elasticsearch, exceptions as es_exceptions
|
| 13 |
import torch
|
| 14 |
from sentence_transformers import SentenceTransformer
|
| 15 |
from sentence_transformers.cross_encoder import CrossEncoder
|
| 16 |
from spellchecker import SpellChecker
|
| 17 |
-
import warnings
|
| 18 |
from dotenv import load_dotenv
|
| 19 |
import sys
|
| 20 |
import nltk
|
| 21 |
from concurrent.futures import ThreadPoolExecutor
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# === ANSI Színkódok (konzol loggoláshoz) ===
|
| 24 |
GREEN = '\033[92m'
|
| 25 |
YELLOW = '\033[93m'
|
|
@@ -30,9 +35,8 @@ CYAN = '\033[96m'
|
|
| 30 |
MAGENTA = '\033[95m'
|
| 31 |
|
| 32 |
# --- Konfiguráció ---
|
|
|
|
| 33 |
CONFIG = {
|
| 34 |
-
"ELASTIC_PASSWORD": os.environ.get("ES_PASSWORD", "T8xEbqQ4GAPkr73s2knN"),
|
| 35 |
-
"ELASTIC_HOST": "https://localhost:9200",
|
| 36 |
"VECTOR_INDEX_NAMES": ["duna", "dunawebindexai"],
|
| 37 |
"FEEDBACK_INDEX_NAME": "feedback_index",
|
| 38 |
"ES_CLIENT_TIMEOUT": 90,
|
|
@@ -49,134 +53,81 @@ CONFIG = {
|
|
| 49 |
"MAX_GENERATION_TOKENS": 1024,
|
| 50 |
"GENERATION_TEMPERATURE": 0.6,
|
| 51 |
"USE_QUERY_EXPANSION": True,
|
| 52 |
-
"SPELLCHECK_LANG": 'hu'
|
| 53 |
-
"MAX_HISTORY_TURNS": 3,
|
| 54 |
-
"HUNGARIAN_STOP_WORDS": set(
|
| 55 |
-
["a", "az", "egy", "és", "hogy", "ha", "is", "itt", "ki", "mi", "mit", "mikor", "hol", "hogyan", "nem", "ne",
|
| 56 |
-
"de", "csak", "meg", "megint", "már", "mint", "még", "vagy", "valamint", "van", "volt", "lesz", "kell",
|
| 57 |
-
"kellett", "lehet", "tud", "tudott", "fog", "fogja", "azt", "ezt", "ott", "ő", "ők", "én", "te", "mi", "ti",
|
| 58 |
-
"ön", "önök", "maga", "maguk", "ilyen", "olyan", "amely", "amelyek", "aki", "akik", "ahol", "amikor", "mert",
|
| 59 |
-
"ezért", "akkor", "így", "úgy", "pedig", "illetve", "továbbá", "azonban", "hanem", "viszont", "nélkül",
|
| 60 |
-
"alatt", "felett", "között", "előtt", "után", "mellett", "bele", "be", "fel", "le", "át", "szembe", "együtt",
|
| 61 |
-
"mindig", "soha", "gyakran", "néha", "talán", "esetleg", "biztosan", "nagyon", "kicsit", "éppen", "most",
|
| 62 |
-
"majd", "azután", "először", "utoljára", "igen", "sem", "túl", "kivéve", "szerint"])
|
| 63 |
}
|
| 64 |
|
| 65 |
-
|
| 66 |
# --- Segédfüggvények ---
|
| 67 |
|
| 68 |
def correct_spellings(text, spell_checker_instance):
|
| 69 |
-
|
| 70 |
-
Kijavítja a helyesírási hibákat a szövegben.
|
| 71 |
-
"""
|
| 72 |
-
if not spell_checker_instance:
|
| 73 |
-
return text
|
| 74 |
try:
|
| 75 |
words = re.findall(r'\b\w+\b', text.lower())
|
| 76 |
misspelled = spell_checker_instance.unknown(words)
|
| 77 |
-
if not misspelled:
|
| 78 |
-
return text
|
| 79 |
-
|
| 80 |
corrected_text = text
|
| 81 |
for word in misspelled:
|
| 82 |
correction = spell_checker_instance.correction(word)
|
| 83 |
if correction and correction != word:
|
| 84 |
-
corrected_text = re.sub(r'\b' + re.escape(word) + r'\b',
|
| 85 |
-
flags=re.IGNORECASE)
|
| 86 |
return corrected_text
|
| 87 |
except Exception as e:
|
| 88 |
print(f"{RED}Hiba a helyesírás javítása közben: {e}{RESET}")
|
| 89 |
return text
|
| 90 |
|
| 91 |
-
|
| 92 |
def get_query_category_with_llm(client, query):
|
| 93 |
-
|
| 94 |
-
LLM-et használ a felhasználói kérdés kategorizálására, előre definiált listából választva.
|
| 95 |
-
"""
|
| 96 |
-
if not client:
|
| 97 |
-
return None
|
| 98 |
print(f" {CYAN}-> Lekérdezés kategorizálása LLM-mel...{RESET}")
|
| 99 |
-
|
| 100 |
-
category_list = ['IT biztonsági szolgáltatások', 'szolgáltatások', 'hardver', 'szoftver', 'hírek',
|
| 101 |
-
'audiovizuális konferenciatechnika']
|
| 102 |
categories_text = ", ".join([f"'{cat}'" for cat in category_list])
|
| 103 |
-
|
| 104 |
-
prompt = f"""Adott egy felhasználói kérdés. Adj meg egyetlen, rövid kategóriát a következő listából, ami a legjobban jellemzi a kérdést. A válaszodban csak a kategória szerepeljen, más szöveg, magyarázat, vagy írásjelek nélkül.
|
| 105 |
Lehetséges kategóriák: {categories_text}
|
| 106 |
Kérdés: '{query}'
|
| 107 |
Kategória:"""
|
| 108 |
messages = [{"role": "user", "content": prompt}]
|
| 109 |
try:
|
| 110 |
-
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages,
|
| 111 |
-
temperature=0.1, max_tokens=30)
|
| 112 |
if response and response.choices:
|
| 113 |
-
category = response.choices[0].message.content.strip()
|
| 114 |
-
category = re.sub(r'\(.*?\)', '', category).strip()
|
| 115 |
-
category = re.sub(r'["\']', '', category).strip()
|
| 116 |
-
|
| 117 |
for cat in category_list:
|
| 118 |
if cat.lower() in category.lower():
|
| 119 |
print(f" {GREEN}-> A kérdés LLM által generált kategóriája: '{cat}'{RESET}")
|
| 120 |
return cat.lower()
|
| 121 |
-
|
| 122 |
-
print(f" {YELLOW}-> Az LLM nem talált megfelelő kategóriát, 'egyéb' kategória használata.{RESET}")
|
| 123 |
return 'egyéb'
|
| 124 |
except Exception as e:
|
| 125 |
print(f"{RED}Hiba LLM kategorizáláskor: {e}{RESET}")
|
| 126 |
return 'egyéb'
|
| 127 |
|
| 128 |
-
|
| 129 |
def expand_or_rewrite_query(original_query, client):
|
| 130 |
-
"""
|
| 131 |
-
Bővíti a felhasználói lekérdezést, hogy több releváns találat legyen.
|
| 132 |
-
"""
|
| 133 |
final_queries = [original_query]
|
| 134 |
-
if not CONFIG["USE_QUERY_EXPANSION"]:
|
| 135 |
return final_queries
|
| 136 |
-
|
| 137 |
print(f" {BLUE}-> Lekérdezés bővítése/átírása...{RESET}")
|
| 138 |
-
# JAVÍTOTT PROMPT: csak kulcsszavakat kérünk, magyarázat nélkül
|
| 139 |
prompt = f"Adott egy magyar nyelvű felhasználói kérdés: '{original_query}'. Generálj 2 db alternatív, releváns keresőkifejezést. A válaszodban csak ezeket add vissza, vesszővel (,) elválasztva, minden más szöveg nélkül."
|
| 140 |
messages = [{"role": "user", "content": prompt}]
|
| 141 |
try:
|
| 142 |
-
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages,
|
| 143 |
-
temperature=0.5, max_tokens=100)
|
| 144 |
if response and response.choices:
|
| 145 |
generated_text = response.choices[0].message.content.strip()
|
| 146 |
-
|
| 147 |
-
alternatives = [q.strip().replace('"', '').replace("'", '').replace('.', '') for q in
|
| 148 |
-
generated_text.split(',') if q.strip() and q.strip() != original_query]
|
| 149 |
final_queries.extend(alternatives)
|
| 150 |
print(f" {GREEN}-> Bővített lekérdezések: {final_queries}{RESET}")
|
| 151 |
except Exception as e:
|
| 152 |
print(f"{RED}Hiba a lekérdezés bővítése során: {e}{RESET}")
|
| 153 |
return final_queries
|
| 154 |
|
| 155 |
-
|
| 156 |
def run_separate_searches(es_client, query_text, embedding_model, expanded_queries, query_category=None):
|
| 157 |
-
"""
|
| 158 |
-
Párhuzamosan futtatja a kulcsszavas és a kNN kereséseket.
|
| 159 |
-
"""
|
| 160 |
results = {'knn': {}, 'keyword': {}}
|
| 161 |
es_client_with_timeout = es_client.options(request_timeout=CONFIG["ES_CLIENT_TIMEOUT"])
|
| 162 |
source_fields = ["text_content", "source_url", "summary", "category"]
|
| 163 |
-
|
| 164 |
filters = []
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
# Ha a probléma a szűrésben van, ezzel a lépéssel azonosítható.
|
| 169 |
-
# A felhasználó igénye szerint vissza lehet kapcsolni, de először a teljes működését kell biztosítani.
|
| 170 |
-
# if query_category and query_category != 'egyéb':
|
| 171 |
-
# print(f" {MAGENTA}-> Kategória-alapú szűrés hozzáadása a kereséshez: '{query_category}'{RESET}")
|
| 172 |
-
# filters.append({"match": {"category": query_category}})
|
| 173 |
|
| 174 |
def knn_search(index, query_vector):
|
| 175 |
try:
|
| 176 |
-
knn_query = {"field": "embedding", "query_vector": query_vector, "k": CONFIG["INITIAL_SEARCH_SIZE"],
|
| 177 |
-
|
| 178 |
-
response = es_client_with_timeout.search(index=index, knn=knn_query, _source=source_fields,
|
| 179 |
-
size=CONFIG["INITIAL_SEARCH_SIZE"])
|
| 180 |
return index, response.get('hits', {}).get('hits', [])
|
| 181 |
except Exception as e:
|
| 182 |
print(f"{RED}Hiba kNN keresés során ({index}): {e}{RESET}")
|
|
@@ -184,51 +135,34 @@ def run_separate_searches(es_client, query_text, embedding_model, expanded_queri
|
|
| 184 |
|
| 185 |
def keyword_search(index, expanded_queries):
|
| 186 |
try:
|
| 187 |
-
should_clauses = []
|
| 188 |
-
for q in expanded_queries:
|
| 189 |
-
should_clauses.append({"match": {"text_content": {"query": q, "operator": "OR", "fuzziness": "AUTO"}}})
|
| 190 |
-
|
| 191 |
query_body = {"query": {"bool": {"should": should_clauses, "minimum_should_match": 1, "filter": filters}}}
|
| 192 |
-
response = es_client_with_timeout.search(index=index, query=query_body['query'], _source=source_fields,
|
| 193 |
-
size=CONFIG["INITIAL_SEARCH_SIZE"])
|
| 194 |
return index, response.get('hits', {}).get('hits', [])
|
| 195 |
except Exception as e:
|
| 196 |
print(f"{RED}Hiba kulcsszavas keresés során ({index}): {e}{RESET}")
|
| 197 |
return index, []
|
| 198 |
|
| 199 |
-
query_vector = None
|
| 200 |
-
try:
|
| 201 |
-
query_vector = embedding_model.encode(query_text, normalize_embeddings=True).tolist()
|
| 202 |
-
except Exception as e:
|
| 203 |
-
print(f"{RED}Hiba az embedding generálásakor: {e}{RESET}")
|
| 204 |
|
| 205 |
with ThreadPoolExecutor(max_workers=len(CONFIG["VECTOR_INDEX_NAMES"]) * 2) as executor:
|
| 206 |
-
knn_futures = {executor.submit(knn_search, index, query_vector) for index in CONFIG["VECTOR_INDEX_NAMES"] if
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
CONFIG["VECTOR_INDEX_NAMES"]}
|
| 210 |
-
|
| 211 |
for future in knn_futures:
|
| 212 |
index, hits = future.result()
|
| 213 |
results['knn'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
| 214 |
-
|
| 215 |
for future in keyword_futures:
|
| 216 |
index, hits = future.result()
|
| 217 |
results['keyword'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
| 218 |
|
| 219 |
-
# ÚJ LOGOLÁS: Kiírjuk a keresési találatok számát
|
| 220 |
total_knn_hits = sum(len(h) for h in results['knn'].values())
|
| 221 |
total_keyword_hits = sum(len(h) for h in results['keyword'].values())
|
| 222 |
print(f"{CYAN}Vektorkeresési találatok száma: {total_knn_hits}{RESET}")
|
| 223 |
print(f"{CYAN}Kulcsszavas keresési találatok száma: {total_keyword_hits}{RESET}")
|
| 224 |
-
|
| 225 |
return results
|
| 226 |
|
| 227 |
-
|
| 228 |
def merge_results_rrf(search_results):
|
| 229 |
-
"""
|
| 230 |
-
Egyesíti a keresési eredményeket az RRF algoritmussal.
|
| 231 |
-
"""
|
| 232 |
rrf_scores = defaultdict(float)
|
| 233 |
all_hits_data = {}
|
| 234 |
for search_type in search_results:
|
|
@@ -238,184 +172,91 @@ def merge_results_rrf(search_results):
|
|
| 238 |
rrf_scores[doc_id] += 1.0 / (CONFIG["RRF_RANK_CONSTANT"] + rank)
|
| 239 |
if doc_id not in all_hits_data:
|
| 240 |
all_hits_data[doc_id] = hit
|
| 241 |
-
|
| 242 |
-
combined_results = [(doc_id, score, all_hits_data[doc_id]) for doc_id, score in rrf_scores.items()]
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
# ÚJ LOGOLÁS: Kiírjuk az RRF által rangsorolt top 5 pontszámot
|
| 246 |
-
print(
|
| 247 |
-
f"{CYAN}RRF által rangsorolt Top 5 pontszám: {[f'{score:.4f}' for doc_id, score, hit in combined_results[:5]]}{RESET}")
|
| 248 |
-
|
| 249 |
return combined_results
|
| 250 |
|
| 251 |
-
|
| 252 |
def retrieve_context_reranked(backend, query_text, confidence_threshold, fallback_message, query_category):
|
| 253 |
-
""
|
| 254 |
-
|
| 255 |
-
"""
|
| 256 |
-
es_client = backend["es_client"]
|
| 257 |
-
embedding_model = backend["embedding_model"]
|
| 258 |
-
cross_encoder = backend["cross_encoder"]
|
| 259 |
-
llm_client = backend["llm_client"]
|
| 260 |
-
|
| 261 |
-
# DRASZTIKUS VÁLTOZTATÁS: A kategória-alapú szűrés kikapcsolva.
|
| 262 |
-
expanded_queries = expand_or_rewrite_query(query_text, llm_client)
|
| 263 |
-
search_results = run_separate_searches(es_client, query_text, embedding_model, expanded_queries)
|
| 264 |
-
|
| 265 |
merged_results = merge_results_rrf(search_results)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
if not merged_results:
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
candidates_to_rerank = merged_results[:CONFIG["RE_RANK_CANDIDATE_COUNT"]]
|
| 273 |
hits_data_for_reranking = [hit for _, _, hit in candidates_to_rerank]
|
| 274 |
-
|
| 275 |
-
query_chunk_pairs = [[query_text, hit['_source'].get('summary', hit['_source'].get('text_content'))] for hit in
|
| 276 |
-
hits_data_for_reranking if hit and '_source' in hit]
|
| 277 |
|
| 278 |
ranked_by_ce = []
|
| 279 |
-
if cross_encoder and query_chunk_pairs:
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
print(f"{CYAN}Cross-Encoder pontszámok (Top 5):{RESET} {[f'{score:.4f}' for score, _ in ranked_by_ce[:5]]}")
|
| 284 |
-
except Exception as e:
|
| 285 |
-
print(f"{RED}Hiba a Cross-Encoder során: {e}{RESET}")
|
| 286 |
-
ranked_by_ce = []
|
| 287 |
-
|
| 288 |
-
if not ranked_by_ce and candidates_to_rerank:
|
| 289 |
-
print(f"{YELLOW}[INFO] Cross-Encoder nem futott, RRF sorrend használata.{RESET}")
|
| 290 |
-
ranked_by_ce = sorted([(score, hit) for _, score, hit in candidates_to_rerank], key=lambda x: x[0],
|
| 291 |
-
reverse=True)
|
| 292 |
|
| 293 |
if not ranked_by_ce:
|
| 294 |
-
return fallback_message, [],
|
| 295 |
|
| 296 |
top_score = float(ranked_by_ce[0][0])
|
| 297 |
-
print(f"{GREEN}Legjobb találat pontszáma: {top_score:.4f}{RESET}")
|
| 298 |
-
|
| 299 |
if top_score < confidence_threshold:
|
| 300 |
-
|
| 301 |
-
f"{YELLOW}A legjobb találat pontszáma ({top_score:.4f}) nem érte el a beállított küszöböt ({confidence_threshold}). A folyamat leáll.{RESET}")
|
| 302 |
-
dynamic_fallback = (
|
| 303 |
-
f"{fallback_message}\n\n"
|
| 304 |
-
f"A '{query_text}' kérdésre a legjobb találat megbízhatósági pontszáma ({top_score:.2f}) "
|
| 305 |
-
f"nem érte el a beállított küszöböt ({confidence_threshold:.2f})."
|
| 306 |
-
)
|
| 307 |
return dynamic_fallback, [], top_score
|
| 308 |
|
| 309 |
-
print(f"{GREEN}A Cross-Encoder magabiztos (legjobb score: {top_score:.4f}). A rangsorát használjuk.{RESET}")
|
| 310 |
final_hits_for_context = [hit for _, hit in ranked_by_ce[:CONFIG["NUM_CONTEXT_RESULTS"]]]
|
| 311 |
-
|
| 312 |
-
context_parts = [hit['_source'].get('summary', hit['_source'].get('text_content')) for hit in final_hits_for_context
|
| 313 |
-
if
|
| 314 |
-
hit and '_source' in hit and (hit['_source'].get('summary') or hit['_source'].get('text_content'))]
|
| 315 |
context_string = "\n\n---\n\n".join(context_parts)
|
| 316 |
-
|
| 317 |
-
sources = []
|
| 318 |
-
for hit_data in final_hits_for_context:
|
| 319 |
-
if hit_data and '_source' in hit_data:
|
| 320 |
-
source_info = {
|
| 321 |
-
"url": hit_data['_source'].get('source_url', hit_data.get('_index', '?')),
|
| 322 |
-
"content": hit_data['_source'].get('text_content', 'N/A')
|
| 323 |
-
}
|
| 324 |
-
if source_info not in sources:
|
| 325 |
-
sources.append(source_info)
|
| 326 |
-
|
| 327 |
return context_string, sources, top_score
|
| 328 |
|
| 329 |
-
|
| 330 |
def generate_answer_with_history(client, model_name, messages, temperature):
|
| 331 |
-
""
|
| 332 |
-
Válasz generálása LLM-mel, figyelembe véve az előzményeket.
|
| 333 |
-
"""
|
| 334 |
try:
|
| 335 |
-
response = client.chat.completions.create(
|
| 336 |
-
model=model_name,
|
| 337 |
-
messages=messages,
|
| 338 |
-
temperature=temperature,
|
| 339 |
-
max_tokens=CONFIG["MAX_GENERATION_TOKENS"],
|
| 340 |
-
timeout=CONFIG["LLM_CLIENT_TIMEOUT"]
|
| 341 |
-
)
|
| 342 |
if response and response.choices:
|
| 343 |
return response.choices[0].message.content.strip()
|
| 344 |
return "Hiba: Nem érkezett érvényes válasz az AI modelltől."
|
| 345 |
except Exception as e:
|
| 346 |
-
error_message = str(e)
|
| 347 |
-
if "429" in error_message:
|
| 348 |
-
wait_time = 100
|
| 349 |
-
print(f"{YELLOW}Rate limit elérve. A program vár {wait_time} másodpercet...{RESET}")
|
| 350 |
-
time.sleep(wait_time)
|
| 351 |
-
return generate_answer_with_history(client, model_name, messages, temperature)
|
| 352 |
print(f"{RED}Hiba a válasz generálásakor: {e}{RESET}")
|
| 353 |
return "Hiba történt az AI modell hívásakor."
|
| 354 |
|
| 355 |
-
|
| 356 |
def search_in_feedback_index(es_client, embedding_model, question, min_score=0.75):
|
| 357 |
-
"""
|
| 358 |
-
Keres a visszajelzési adatbázisban a hasonló kérdésekre.
|
| 359 |
-
"""
|
| 360 |
try:
|
|
|
|
| 361 |
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 362 |
knn_query = {"field": "embedding", "query_vector": embedding, "k": 1, "num_candidates": 10}
|
| 363 |
-
response = es_client.search(index=CONFIG["FEEDBACK_INDEX_NAME"], knn=knn_query,
|
| 364 |
-
_source=["question_text", "correction_text"])
|
| 365 |
hits = response.get('hits', {}).get('hits', [])
|
| 366 |
if hits and hits[0]['_score'] >= min_score:
|
| 367 |
-
top_hit = hits[0]
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
if score > 0.98:
|
| 372 |
-
return "direct_answer", source['correction_text']
|
| 373 |
-
|
| 374 |
-
instruction = f"Egy nagyon hasonló kérdésre ('{source['question_text']}') korábban a következő javítást/iránymutatást adtad: '{source['correction_text']}'. A válaszodat elsősorban ez alapján alkosd meg, még akkor is, ha a talált kontextus mást sugall!"
|
| 375 |
return "instruction", instruction
|
| 376 |
-
|
| 377 |
-
return None, None
|
| 378 |
-
except es_exceptions.NotFoundError:
|
| 379 |
-
return None, None
|
| 380 |
except Exception:
|
| 381 |
return None, None
|
| 382 |
-
|
| 383 |
|
| 384 |
def index_feedback(es_client, embedding_model, question, correction):
|
| 385 |
-
"""
|
| 386 |
-
Indexeli a visszajelzést.
|
| 387 |
-
"""
|
| 388 |
try:
|
| 389 |
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 390 |
-
doc = {"question_text": question, "correction_text": correction, "embedding": embedding,
|
| 391 |
-
"timestamp": datetime.datetime.now()}
|
| 392 |
es_client.index(index=CONFIG["FEEDBACK_INDEX_NAME"], document=doc)
|
| 393 |
-
print(f"Visszajelzés sikeresen indexelve a '{CONFIG['FEEDBACK_INDEX_NAME']}' indexbe.")
|
| 394 |
return True
|
| 395 |
except Exception as e:
|
| 396 |
print(f"{RED}Hiba a visszajelzés indexelése során: {e}{RESET}")
|
| 397 |
return False
|
| 398 |
|
| 399 |
-
|
| 400 |
def get_all_feedback(es_client, index_name):
|
| 401 |
-
"""
|
| 402 |
-
Lekéri az összes visszajelzést.
|
| 403 |
-
"""
|
| 404 |
try:
|
| 405 |
-
|
| 406 |
-
|
| 407 |
return response.get('hits', {}).get('hits', [])
|
| 408 |
-
except es_exceptions.NotFoundError:
|
| 409 |
-
return []
|
| 410 |
except Exception as e:
|
| 411 |
print(f"{RED}Hiba a visszajelzések listázása során: {e}{RESET}")
|
| 412 |
return []
|
| 413 |
|
| 414 |
-
|
| 415 |
def delete_feedback_by_id(es_client, index_name, doc_id):
|
| 416 |
-
"""
|
| 417 |
-
Töröl egy visszajelzést ID alapján.
|
| 418 |
-
"""
|
| 419 |
try:
|
| 420 |
es_client.delete(index=index_name, id=doc_id)
|
| 421 |
return True
|
|
@@ -423,11 +264,7 @@ def delete_feedback_by_id(es_client, index_name, doc_id):
|
|
| 423 |
print(f"{RED}Hiba a visszajelzés törlése során (ID: {doc_id}): {e}{RESET}")
|
| 424 |
return False
|
| 425 |
|
| 426 |
-
|
| 427 |
def update_feedback_comment(es_client, index_name, doc_id, new_comment):
|
| 428 |
-
"""
|
| 429 |
-
Frissít egy visszajelzést ID alapján.
|
| 430 |
-
"""
|
| 431 |
try:
|
| 432 |
es_client.update(index=index_name, id=doc_id, doc={"correction_text": new_comment})
|
| 433 |
return True
|
|
@@ -435,119 +272,91 @@ def update_feedback_comment(es_client, index_name, doc_id, new_comment):
|
|
| 435 |
print(f"{RED}Hiba a visszajelzés szerkesztése során (ID: {doc_id}): {e}{RESET}")
|
| 436 |
return False
|
| 437 |
|
| 438 |
-
|
| 439 |
def initialize_backend():
|
| 440 |
-
"""
|
| 441 |
-
Inicializálja a backend komponenseit.
|
| 442 |
-
"""
|
| 443 |
print("----- Backend Motor Inicializálása -----")
|
| 444 |
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
try:
|
| 446 |
nltk.data.find('tokenizers/punkt')
|
| 447 |
except LookupError:
|
| 448 |
nltk.download('punkt', quiet=True)
|
| 449 |
|
| 450 |
-
warnings.filterwarnings("ignore", message=".*verify_certs=False.*")
|
| 451 |
-
|
| 452 |
spell_checker = None
|
| 453 |
try:
|
| 454 |
spell_checker = SpellChecker(language=CONFIG["SPELLCHECK_LANG"])
|
| 455 |
-
custom_words = ["dunaelektronika", "kft", "outsourcing", "dell", "lenovo", "nis2", "szerver", "kliens",
|
| 456 |
-
"hálózati", "hpe"]
|
| 457 |
spell_checker.word_frequency.load_words(custom_words)
|
| 458 |
except Exception as e:
|
| 459 |
-
print(f"{RED}Helyesírás-
|
| 460 |
-
|
| 461 |
-
backend_objects = {
|
| 462 |
-
"es_client": Elasticsearch(CONFIG["ELASTIC_HOST"], basic_auth=("elastic", CONFIG["ELASTIC_PASSWORD"]),
|
| 463 |
-
verify_certs=False),
|
| 464 |
-
"embedding_model": SentenceTransformer(CONFIG["EMBEDDING_MODEL_NAME"],
|
| 465 |
-
device='cuda' if torch.cuda.is_available() else 'cpu'),
|
| 466 |
-
"cross_encoder": CrossEncoder(CONFIG["CROSS_ENCODER_MODEL_NAME"],
|
| 467 |
-
device='cuda' if torch.cuda.is_available() else 'cpu'),
|
| 468 |
-
"llm_client": Together(api_key=os.getenv("TOGETHER_API_KEY")),
|
| 469 |
-
"spell_checker": spell_checker
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
print(f"{GREEN}----- Backend Motor Készen Áll -----{RESET}")
|
| 473 |
-
return backend_objects
|
| 474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
def process_query(user_question, chat_history, backend, confidence_threshold, fallback_message):
|
| 477 |
-
"""
|
| 478 |
-
A teljes lekérdezés-feldolgozási munkafolyamatot vezérli.
|
| 479 |
-
"""
|
| 480 |
print(f"\n{BLUE}----- Új lekérdezés feldolgozása ----{RESET}")
|
| 481 |
print(f"{BLUE}Kérdés: {user_question}{RESET}")
|
| 482 |
|
| 483 |
corrected_question = correct_spellings(user_question, backend["spell_checker"])
|
| 484 |
print(f"{BLUE}Javított kérdés: {corrected_question}{RESET}")
|
| 485 |
|
| 486 |
-
feedback_type, feedback_content = search_in_feedback_index(
|
| 487 |
-
backend["es_client"], backend["embedding_model"], corrected_question
|
| 488 |
-
)
|
| 489 |
-
|
| 490 |
if feedback_type == "direct_answer":
|
| 491 |
print(f"{GREEN}Direkt válasz a visszajelzési adatbázisból.{RESET}")
|
| 492 |
-
return {
|
| 493 |
-
"answer": feedback_content,
|
| 494 |
-
"sources": [
|
| 495 |
-
{"url": "Személyes visszajelzés alapján", "content": "Ez egy korábban megadott, pontosított válasz."}],
|
| 496 |
-
"corrected_question": corrected_question,
|
| 497 |
-
"confidence_score": 10.0
|
| 498 |
-
}
|
| 499 |
-
|
| 500 |
-
feedback_instructions = feedback_content if feedback_type == "instruction" else None
|
| 501 |
|
|
|
|
| 502 |
query_category = get_query_category_with_llm(backend["llm_client"], corrected_question)
|
| 503 |
-
|
| 504 |
-
retrieved_context, sources, confidence_score = retrieve_context_reranked(backend, corrected_question,
|
| 505 |
-
confidence_threshold, fallback_message,
|
| 506 |
-
query_category)
|
| 507 |
|
| 508 |
if not sources and not feedback_instructions:
|
| 509 |
-
return {
|
| 510 |
-
"answer": retrieved_context,
|
| 511 |
-
"sources": [],
|
| 512 |
-
"corrected_question": corrected_question,
|
| 513 |
-
"confidence_score": confidence_score
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
prompt_instructions = ""
|
| 517 |
-
if feedback_instructions:
|
| 518 |
-
prompt_instructions = f"""
|
| 519 |
-
KÜLÖNLEGESEN FONTOS FEJLESZTŐI UTASÍTÁS (ezt vedd figyelembe a leginkább!):
|
| 520 |
-
---
|
| 521 |
-
{feedback_instructions}
|
| 522 |
-
---
|
| 523 |
-
"""
|
| 524 |
|
| 525 |
system_prompt = f"""Te egy professzionális, segítőkész AI asszisztens vagy.
|
| 526 |
A feladatod, hogy a KONTEXTUS-ból és a FEJLESZTŐI UTASÍTÁSOKBÓL származó információkat egyetlen, jól strukturált és ismétlés-mentes válasszá szintetizálld.
|
| 527 |
-
{
|
| 528 |
KRITIKUS SZABÁLY: Értékeld a kapott KONTEXTUS relevanciáját a felhasználó kérdéséhez képest. Ha egy kontextus-részlet nem kapcsolódik szorosan a kérdéshez, azt hagyd figyelmen kívül!
|
| 529 |
FIGYELEM: Szigorúan csak a megadott KONTEXTUS-ra és a fejlesztői utasításokra támaszkodj. Ha a releváns információk alapján nem tudsz válaszolni, add ezt a választ: '{fallback_message}'
|
| 530 |
KONTEXTUS:
|
| 531 |
---
|
| 532 |
{retrieved_context if sources else "A tudásbázisban nem található releváns információ."}
|
| 533 |
---
|
| 534 |
-
ELŐZMÉNYEK (ha releváns): Lásd a korábbi üzeneteket.
|
| 535 |
"""
|
| 536 |
-
|
| 537 |
-
messages_for_llm
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
messages_for_llm.append({"role": "user", "content": corrected_question})
|
| 543 |
-
|
| 544 |
-
answer = generate_answer_with_history(
|
| 545 |
-
backend["llm_client"], CONFIG["TOGETHER_MODEL_NAME"], messages_for_llm, CONFIG["GENERATION_TEMPERATURE"]
|
| 546 |
-
)
|
| 547 |
-
|
| 548 |
-
return {
|
| 549 |
-
"answer": answer,
|
| 550 |
-
"sources": sources,
|
| 551 |
-
"corrected_question": corrected_question,
|
| 552 |
-
"confidence_score": confidence_score
|
| 553 |
-
}
|
|
|
|
| 1 |
# backendv1.py
|
| 2 |
+
# VÉGLEGES, JAVÍTOTT VERZIÓ: Elastic Cloud és GitHub Secrets kompatibilis.
|
| 3 |
# A RAG rendszer motorja: adatfeldolgozás, keresés, generálás és tanulás.
|
|
|
|
| 4 |
|
| 5 |
import os
|
| 6 |
import time
|
|
|
|
| 8 |
import json
|
| 9 |
import re
|
| 10 |
from collections import defaultdict
|
|
|
|
| 11 |
from elasticsearch import Elasticsearch, exceptions as es_exceptions
|
| 12 |
import torch
|
| 13 |
from sentence_transformers import SentenceTransformer
|
| 14 |
from sentence_transformers.cross_encoder import CrossEncoder
|
| 15 |
from spellchecker import SpellChecker
|
|
|
|
| 16 |
from dotenv import load_dotenv
|
| 17 |
import sys
|
| 18 |
import nltk
|
| 19 |
from concurrent.futures import ThreadPoolExecutor
|
| 20 |
|
| 21 |
+
# Késleltetett importálás, hogy csak akkor legyen hiba, ha tényleg használjuk
|
| 22 |
+
try:
|
| 23 |
+
from together import Together
|
| 24 |
+
TOGETHER_AVAILABLE = True
|
| 25 |
+
except ImportError:
|
| 26 |
+
TOGETHER_AVAILABLE = False
|
| 27 |
+
|
| 28 |
# === ANSI Színkódok (konzol loggoláshoz) ===
|
| 29 |
GREEN = '\033[92m'
|
| 30 |
YELLOW = '\033[93m'
|
|
|
|
| 35 |
MAGENTA = '\033[95m'
|
| 36 |
|
| 37 |
# --- Konfiguráció ---
|
| 38 |
+
# JAVÍTVA: A hitelesítő adatok már nincsenek itt, a program a környezeti változókból olvassa őket.
|
| 39 |
CONFIG = {
|
|
|
|
|
|
|
| 40 |
"VECTOR_INDEX_NAMES": ["duna", "dunawebindexai"],
|
| 41 |
"FEEDBACK_INDEX_NAME": "feedback_index",
|
| 42 |
"ES_CLIENT_TIMEOUT": 90,
|
|
|
|
| 53 |
"MAX_GENERATION_TOKENS": 1024,
|
| 54 |
"GENERATION_TEMPERATURE": 0.6,
|
| 55 |
"USE_QUERY_EXPANSION": True,
|
| 56 |
+
"SPELLCHECK_LANG": 'hu'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
|
|
|
| 59 |
# --- Segédfüggvények ---
|
| 60 |
|
| 61 |
def correct_spellings(text, spell_checker_instance):
|
| 62 |
+
if not spell_checker_instance: return text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
words = re.findall(r'\b\w+\b', text.lower())
|
| 65 |
misspelled = spell_checker_instance.unknown(words)
|
| 66 |
+
if not misspelled: return text
|
|
|
|
|
|
|
| 67 |
corrected_text = text
|
| 68 |
for word in misspelled:
|
| 69 |
correction = spell_checker_instance.correction(word)
|
| 70 |
if correction and correction != word:
|
| 71 |
+
corrected_text = re.sub(r'\b' + re.escape(word) + r'\b', corrected_text, flags=re.IGNORECASE)
|
|
|
|
| 72 |
return corrected_text
|
| 73 |
except Exception as e:
|
| 74 |
print(f"{RED}Hiba a helyesírás javítása közben: {e}{RESET}")
|
| 75 |
return text
|
| 76 |
|
|
|
|
| 77 |
def get_query_category_with_llm(client, query):
|
| 78 |
+
if not client: return 'egyéb'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
print(f" {CYAN}-> Lekérdezés kategorizálása LLM-mel...{RESET}")
|
| 80 |
+
category_list = ['IT biztonsági szolgáltatások', 'szolgáltatások', 'hardver', 'szoftver', 'hírek', 'audiovizuális konferenciatechnika']
|
|
|
|
|
|
|
| 81 |
categories_text = ", ".join([f"'{cat}'" for cat in category_list])
|
| 82 |
+
prompt = f"""Adott egy felhasználói kérdés. Adj meg egyetlen, rövid kategóriát a következő listából, ami a legjobban jellemzi a kérdést. A válaszodban csak a kategória szerepeljen, más szöveg nélkül.
|
|
|
|
| 83 |
Lehetséges kategóriák: {categories_text}
|
| 84 |
Kérdés: '{query}'
|
| 85 |
Kategória:"""
|
| 86 |
messages = [{"role": "user", "content": prompt}]
|
| 87 |
try:
|
| 88 |
+
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages, temperature=0.1, max_tokens=30)
|
|
|
|
| 89 |
if response and response.choices:
|
| 90 |
+
category = response.choices[0].message.content.strip().replace("'", "").replace("`", "")
|
|
|
|
|
|
|
|
|
|
| 91 |
for cat in category_list:
|
| 92 |
if cat.lower() in category.lower():
|
| 93 |
print(f" {GREEN}-> A kérdés LLM által generált kategóriája: '{cat}'{RESET}")
|
| 94 |
return cat.lower()
|
|
|
|
|
|
|
| 95 |
return 'egyéb'
|
| 96 |
except Exception as e:
|
| 97 |
print(f"{RED}Hiba LLM kategorizáláskor: {e}{RESET}")
|
| 98 |
return 'egyéb'
|
| 99 |
|
|
|
|
| 100 |
def expand_or_rewrite_query(original_query, client):
|
|
|
|
|
|
|
|
|
|
| 101 |
final_queries = [original_query]
|
| 102 |
+
if not (CONFIG["USE_QUERY_EXPANSION"] and client):
|
| 103 |
return final_queries
|
|
|
|
| 104 |
print(f" {BLUE}-> Lekérdezés bővítése/átírása...{RESET}")
|
|
|
|
| 105 |
prompt = f"Adott egy magyar nyelvű felhasználói kérdés: '{original_query}'. Generálj 2 db alternatív, releváns keresőkifejezést. A válaszodban csak ezeket add vissza, vesszővel (,) elválasztva, minden más szöveg nélkül."
|
| 106 |
messages = [{"role": "user", "content": prompt}]
|
| 107 |
try:
|
| 108 |
+
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages, temperature=0.5, max_tokens=100)
|
|
|
|
| 109 |
if response and response.choices:
|
| 110 |
generated_text = response.choices[0].message.content.strip()
|
| 111 |
+
alternatives = [q.strip().replace('"', '').replace("'", '').replace('.', '') for q in generated_text.split(',') if q.strip() and q.strip() != original_query]
|
|
|
|
|
|
|
| 112 |
final_queries.extend(alternatives)
|
| 113 |
print(f" {GREEN}-> Bővített lekérdezések: {final_queries}{RESET}")
|
| 114 |
except Exception as e:
|
| 115 |
print(f"{RED}Hiba a lekérdezés bővítése során: {e}{RESET}")
|
| 116 |
return final_queries
|
| 117 |
|
|
|
|
| 118 |
def run_separate_searches(es_client, query_text, embedding_model, expanded_queries, query_category=None):
|
|
|
|
|
|
|
|
|
|
| 119 |
results = {'knn': {}, 'keyword': {}}
|
| 120 |
es_client_with_timeout = es_client.options(request_timeout=CONFIG["ES_CLIENT_TIMEOUT"])
|
| 121 |
source_fields = ["text_content", "source_url", "summary", "category"]
|
|
|
|
| 122 |
filters = []
|
| 123 |
+
|
| 124 |
+
if query_category and query_category != 'egyéb':
|
| 125 |
+
filters.append({"match": {"category": query_category}})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
def knn_search(index, query_vector):
|
| 128 |
try:
|
| 129 |
+
knn_query = {"field": "embedding", "query_vector": query_vector, "k": CONFIG["INITIAL_SEARCH_SIZE"], "num_candidates": CONFIG["KNN_NUM_CANDIDATES"], "filter": filters}
|
| 130 |
+
response = es_client_with_timeout.search(index=index, knn=knn_query, _source=source_fields, size=CONFIG["INITIAL_SEARCH_SIZE"])
|
|
|
|
|
|
|
| 131 |
return index, response.get('hits', {}).get('hits', [])
|
| 132 |
except Exception as e:
|
| 133 |
print(f"{RED}Hiba kNN keresés során ({index}): {e}{RESET}")
|
|
|
|
| 135 |
|
| 136 |
def keyword_search(index, expanded_queries):
|
| 137 |
try:
|
| 138 |
+
should_clauses = [{"match": {"text_content": {"query": q, "operator": "OR", "fuzziness": "AUTO"}}} for q in expanded_queries]
|
|
|
|
|
|
|
|
|
|
| 139 |
query_body = {"query": {"bool": {"should": should_clauses, "minimum_should_match": 1, "filter": filters}}}
|
| 140 |
+
response = es_client_with_timeout.search(index=index, query=query_body['query'], _source=source_fields, size=CONFIG["INITIAL_SEARCH_SIZE"])
|
|
|
|
| 141 |
return index, response.get('hits', {}).get('hits', [])
|
| 142 |
except Exception as e:
|
| 143 |
print(f"{RED}Hiba kulcsszavas keresés során ({index}): {e}{RESET}")
|
| 144 |
return index, []
|
| 145 |
|
| 146 |
+
query_vector = embedding_model.encode(query_text, normalize_embeddings=True).tolist() if embedding_model else None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
with ThreadPoolExecutor(max_workers=len(CONFIG["VECTOR_INDEX_NAMES"]) * 2) as executor:
|
| 149 |
+
knn_futures = {executor.submit(knn_search, index, query_vector) for index in CONFIG["VECTOR_INDEX_NAMES"] if query_vector}
|
| 150 |
+
keyword_futures = {executor.submit(keyword_search, index, expanded_queries) for index in CONFIG["VECTOR_INDEX_NAMES"]}
|
| 151 |
+
|
|
|
|
|
|
|
| 152 |
for future in knn_futures:
|
| 153 |
index, hits = future.result()
|
| 154 |
results['knn'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
|
|
|
| 155 |
for future in keyword_futures:
|
| 156 |
index, hits = future.result()
|
| 157 |
results['keyword'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
| 158 |
|
|
|
|
| 159 |
total_knn_hits = sum(len(h) for h in results['knn'].values())
|
| 160 |
total_keyword_hits = sum(len(h) for h in results['keyword'].values())
|
| 161 |
print(f"{CYAN}Vektorkeresési találatok száma: {total_knn_hits}{RESET}")
|
| 162 |
print(f"{CYAN}Kulcsszavas keresési találatok száma: {total_keyword_hits}{RESET}")
|
|
|
|
| 163 |
return results
|
| 164 |
|
|
|
|
| 165 |
def merge_results_rrf(search_results):
|
|
|
|
|
|
|
|
|
|
| 166 |
rrf_scores = defaultdict(float)
|
| 167 |
all_hits_data = {}
|
| 168 |
for search_type in search_results:
|
|
|
|
| 172 |
rrf_scores[doc_id] += 1.0 / (CONFIG["RRF_RANK_CONSTANT"] + rank)
|
| 173 |
if doc_id not in all_hits_data:
|
| 174 |
all_hits_data[doc_id] = hit
|
| 175 |
+
|
| 176 |
+
combined_results = sorted([(doc_id, score, all_hits_data[doc_id]) for doc_id, score in rrf_scores.items()], key=lambda item: item[1], reverse=True)
|
| 177 |
+
print(f"{CYAN}RRF által rangsorolt Top 5 pontszám: {[f'{score:.4f}' for doc_id, score, hit in combined_results[:5]]}{RESET}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
return combined_results
|
| 179 |
|
|
|
|
| 180 |
def retrieve_context_reranked(backend, query_text, confidence_threshold, fallback_message, query_category):
|
| 181 |
+
expanded_queries = expand_or_rewrite_query(query_text, backend["llm_client"])
|
| 182 |
+
search_results = run_separate_searches(backend["es_client"], query_text, backend["embedding_model"], expanded_queries, query_category)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
merged_results = merge_results_rrf(search_results)
|
| 184 |
+
|
|
|
|
| 185 |
if not merged_results:
|
| 186 |
+
return fallback_message, [], None
|
| 187 |
+
|
|
|
|
| 188 |
candidates_to_rerank = merged_results[:CONFIG["RE_RANK_CANDIDATE_COUNT"]]
|
| 189 |
hits_data_for_reranking = [hit for _, _, hit in candidates_to_rerank]
|
| 190 |
+
query_chunk_pairs = [[query_text, hit['_source'].get('summary', hit['_source'].get('text_content'))] for hit in hits_data_for_reranking if hit and '_source' in hit]
|
|
|
|
|
|
|
| 191 |
|
| 192 |
ranked_by_ce = []
|
| 193 |
+
if backend["cross_encoder"] and query_chunk_pairs:
|
| 194 |
+
ce_scores = backend["cross_encoder"].predict(query_chunk_pairs, show_progress_bar=False)
|
| 195 |
+
ranked_by_ce = sorted(zip(ce_scores, hits_data_for_reranking), key=lambda x: x[0], reverse=True)
|
| 196 |
+
print(f"{CYAN}Cross-Encoder pontszámok (Top 5):{RESET} {[f'{score:.4f}' for score, _ in ranked_by_ce[:5]]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
if not ranked_by_ce:
|
| 199 |
+
return fallback_message, [], None
|
| 200 |
|
| 201 |
top_score = float(ranked_by_ce[0][0])
|
|
|
|
|
|
|
| 202 |
if top_score < confidence_threshold:
|
| 203 |
+
dynamic_fallback = (f"{fallback_message}\n\nA '{query_text}' kérdésre a legjobb találat megbízhatósági pontszáma ({top_score:.2f}) nem érte el a beállított küszöböt ({confidence_threshold:.2f}).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
return dynamic_fallback, [], top_score
|
| 205 |
|
|
|
|
| 206 |
final_hits_for_context = [hit for _, hit in ranked_by_ce[:CONFIG["NUM_CONTEXT_RESULTS"]]]
|
| 207 |
+
context_parts = [hit['_source'].get('summary', hit['_source'].get('text_content')) for hit in final_hits_for_context]
|
|
|
|
|
|
|
|
|
|
| 208 |
context_string = "\n\n---\n\n".join(context_parts)
|
| 209 |
+
|
| 210 |
+
sources = [{"url": hit['_source'].get('source_url', '?'), "content": hit['_source'].get('text_content', 'N/A')} for hit in final_hits_for_context]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
return context_string, sources, top_score
|
| 212 |
|
|
|
|
| 213 |
def generate_answer_with_history(client, model_name, messages, temperature):
|
| 214 |
+
if not client: return "Hiba: Az AI kliens nincs inicializálva."
|
|
|
|
|
|
|
| 215 |
try:
|
| 216 |
+
response = client.chat.completions.create(model=model_name, messages=messages, temperature=temperature, max_tokens=CONFIG["MAX_GENERATION_TOKENS"], timeout=CONFIG["LLM_CLIENT_TIMEOUT"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
if response and response.choices:
|
| 218 |
return response.choices[0].message.content.strip()
|
| 219 |
return "Hiba: Nem érkezett érvényes válasz az AI modelltől."
|
| 220 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
print(f"{RED}Hiba a válasz generálásakor: {e}{RESET}")
|
| 222 |
return "Hiba történt az AI modell hívásakor."
|
| 223 |
|
|
|
|
| 224 |
def search_in_feedback_index(es_client, embedding_model, question, min_score=0.75):
|
|
|
|
|
|
|
|
|
|
| 225 |
try:
|
| 226 |
+
if not es_client.indices.exists(index=CONFIG["FEEDBACK_INDEX_NAME"]): return None, None
|
| 227 |
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 228 |
knn_query = {"field": "embedding", "query_vector": embedding, "k": 1, "num_candidates": 10}
|
| 229 |
+
response = es_client.search(index=CONFIG["FEEDBACK_INDEX_NAME"], knn=knn_query, _source=["question_text", "correction_text"])
|
|
|
|
| 230 |
hits = response.get('hits', {}).get('hits', [])
|
| 231 |
if hits and hits[0]['_score'] >= min_score:
|
| 232 |
+
top_hit = hits[0]; source = top_hit['_source']; score = top_hit['_score']
|
| 233 |
+
if score > 0.98: return "direct_answer", source['correction_text']
|
| 234 |
+
instruction = f"Egy nagyon hasonló kérdésre ('{source['question_text']}') korábban a következő javítást/iránymutatást adtad: '{source['correction_text']}'. A válaszodat elsősorban ez alapján alkosd meg!"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
return "instruction", instruction
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
except Exception:
|
| 237 |
return None, None
|
| 238 |
+
return None, None
|
| 239 |
|
| 240 |
def index_feedback(es_client, embedding_model, question, correction):
|
|
|
|
|
|
|
|
|
|
| 241 |
try:
|
| 242 |
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 243 |
+
doc = {"question_text": question, "correction_text": correction, "embedding": embedding, "timestamp": datetime.datetime.now()}
|
|
|
|
| 244 |
es_client.index(index=CONFIG["FEEDBACK_INDEX_NAME"], document=doc)
|
|
|
|
| 245 |
return True
|
| 246 |
except Exception as e:
|
| 247 |
print(f"{RED}Hiba a visszajelzés indexelése során: {e}{RESET}")
|
| 248 |
return False
|
| 249 |
|
|
|
|
| 250 |
def get_all_feedback(es_client, index_name):
|
|
|
|
|
|
|
|
|
|
| 251 |
try:
|
| 252 |
+
if not es_client.indices.exists(index=index_name): return []
|
| 253 |
+
response = es_client.search(index=index_name, query={"match_all": {}}, size=1000, sort=[{"timestamp": {"order": "desc"}}])
|
| 254 |
return response.get('hits', {}).get('hits', [])
|
|
|
|
|
|
|
| 255 |
except Exception as e:
|
| 256 |
print(f"{RED}Hiba a visszajelzések listázása során: {e}{RESET}")
|
| 257 |
return []
|
| 258 |
|
|
|
|
| 259 |
def delete_feedback_by_id(es_client, index_name, doc_id):
|
|
|
|
|
|
|
|
|
|
| 260 |
try:
|
| 261 |
es_client.delete(index=index_name, id=doc_id)
|
| 262 |
return True
|
|
|
|
| 264 |
print(f"{RED}Hiba a visszajelzés törlése során (ID: {doc_id}): {e}{RESET}")
|
| 265 |
return False
|
| 266 |
|
|
|
|
| 267 |
def update_feedback_comment(es_client, index_name, doc_id, new_comment):
|
|
|
|
|
|
|
|
|
|
| 268 |
try:
|
| 269 |
es_client.update(index=index_name, id=doc_id, doc={"correction_text": new_comment})
|
| 270 |
return True
|
|
|
|
| 272 |
print(f"{RED}Hiba a visszajelzés szerkesztése során (ID: {doc_id}): {e}{RESET}")
|
| 273 |
return False
|
| 274 |
|
|
|
|
| 275 |
def initialize_backend():
|
|
|
|
|
|
|
|
|
|
| 276 |
print("----- Backend Motor Inicializálása -----")
|
| 277 |
load_dotenv()
|
| 278 |
+
|
| 279 |
+
es_cloud_id = os.getenv("ES_CLOUD_ID")
|
| 280 |
+
es_api_key = os.getenv("ES_API_KEY")
|
| 281 |
+
together_api_key = os.getenv("TOGETHER_API_KEY")
|
| 282 |
+
|
| 283 |
+
if not all([es_cloud_id, es_api_key, together_api_key]):
|
| 284 |
+
print(f"{RED}Hiba: Hiányzó környezeti változók! Szükséges: ES_CLOUD_ID, ES_API_KEY, TOGETHER_API_KEY{RESET}")
|
| 285 |
+
return None
|
| 286 |
+
|
| 287 |
+
if not TOGETHER_AVAILABLE:
|
| 288 |
+
print(f"{RED}Hiba: A 'together' csomag nincs telepítve.{RESET}")
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
try:
|
| 292 |
nltk.data.find('tokenizers/punkt')
|
| 293 |
except LookupError:
|
| 294 |
nltk.download('punkt', quiet=True)
|
| 295 |
|
|
|
|
|
|
|
| 296 |
spell_checker = None
|
| 297 |
try:
|
| 298 |
spell_checker = SpellChecker(language=CONFIG["SPELLCHECK_LANG"])
|
| 299 |
+
custom_words = ["dunaelektronika", "kft", "outsourcing", "dell", "lenovo", "nis2", "szerver", "kliens", "hálózati", "hpe"]
|
|
|
|
| 300 |
spell_checker.word_frequency.load_words(custom_words)
|
| 301 |
except Exception as e:
|
| 302 |
+
print(f"{RED}Helyesírás-ellenőző hiba: {e}{RESET}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
+
try:
|
| 305 |
+
print(f"{CYAN}Elasticsearch kliens inicializálása...{RESET}")
|
| 306 |
+
es_client = Elasticsearch(cloud_id=es_cloud_id, api_key=es_api_key, request_timeout=CONFIG["ES_CLIENT_TIMEOUT"])
|
| 307 |
+
if not es_client.ping(): raise ConnectionError("Elasticsearch ping sikertelen.")
|
| 308 |
+
print(f"{GREEN}Elasticsearch kliens kész.{RESET}")
|
| 309 |
+
|
| 310 |
+
print(f"{CYAN}AI modellek betöltése...{RESET}")
|
| 311 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 312 |
+
embedding_model = SentenceTransformer(CONFIG["EMBEDDING_MODEL_NAME"], device=device)
|
| 313 |
+
cross_encoder = CrossEncoder(CONFIG["CROSS_ENCODER_MODEL_NAME"], device=device)
|
| 314 |
+
llm_client = Together(api_key=together_api_key)
|
| 315 |
+
print(f"{GREEN}AI modellek betöltve (eszköz: {device}).{RESET}")
|
| 316 |
+
|
| 317 |
+
backend_objects = {
|
| 318 |
+
"es_client": es_client, "embedding_model": embedding_model, "cross_encoder": cross_encoder,
|
| 319 |
+
"llm_client": llm_client, "spell_checker": spell_checker
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
print(f"{GREEN}----- Backend Motor Készen Áll -----{RESET}")
|
| 323 |
+
return backend_objects
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f"{RED}Hiba a backend inicializálása során: {e}{RESET}")
|
| 326 |
+
return None
|
| 327 |
|
| 328 |
def process_query(user_question, chat_history, backend, confidence_threshold, fallback_message):
|
|
|
|
|
|
|
|
|
|
| 329 |
print(f"\n{BLUE}----- Új lekérdezés feldolgozása ----{RESET}")
|
| 330 |
print(f"{BLUE}Kérdés: {user_question}{RESET}")
|
| 331 |
|
| 332 |
corrected_question = correct_spellings(user_question, backend["spell_checker"])
|
| 333 |
print(f"{BLUE}Javított kérdés: {corrected_question}{RESET}")
|
| 334 |
|
| 335 |
+
feedback_type, feedback_content = search_in_feedback_index(backend["es_client"], backend["embedding_model"], corrected_question)
|
|
|
|
|
|
|
|
|
|
| 336 |
if feedback_type == "direct_answer":
|
| 337 |
print(f"{GREEN}Direkt válasz a visszajelzési adatbázisból.{RESET}")
|
| 338 |
+
return {"answer": feedback_content, "sources": [{"url": "Személyes visszajelzés alapján", "content": "Ez egy korábban megadott, pontosított válasz."}], "corrected_question": corrected_question, "confidence_score": 10.0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
+
feedback_instructions = feedback_content if feedback_type == "instruction" else ""
|
| 341 |
query_category = get_query_category_with_llm(backend["llm_client"], corrected_question)
|
| 342 |
+
retrieved_context, sources, confidence_score = retrieve_context_reranked(backend, corrected_question, confidence_threshold, fallback_message, query_category)
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
if not sources and not feedback_instructions:
|
| 345 |
+
return {"answer": retrieved_context, "sources": [], "corrected_question": corrected_question, "confidence_score": confidence_score}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
system_prompt = f"""Te egy professzionális, segítőkész AI asszisztens vagy.
|
| 348 |
A feladatod, hogy a KONTEXTUS-ból és a FEJLESZTŐI UTASÍTÁSOKBÓL származó információkat egyetlen, jól strukturált és ismétlés-mentes válasszá szintetizálld.
|
| 349 |
+
{feedback_instructions}
|
| 350 |
KRITIKUS SZABÁLY: Értékeld a kapott KONTEXTUS relevanciáját a felhasználó kérdéséhez képest. Ha egy kontextus-részlet nem kapcsolódik szorosan a kérdéshez, azt hagyd figyelmen kívül!
|
| 351 |
FIGYELEM: Szigorúan csak a megadott KONTEXTUS-ra és a fejlesztői utasításokra támaszkodj. Ha a releváns információk alapján nem tudsz válaszolni, add ezt a választ: '{fallback_message}'
|
| 352 |
KONTEXTUS:
|
| 353 |
---
|
| 354 |
{retrieved_context if sources else "A tudásbázisban nem található releváns információ."}
|
| 355 |
---
|
|
|
|
| 356 |
"""
|
| 357 |
+
messages_for_llm = chat_history[-(CONFIG["MAX_HISTORY_TURNS"] * 2):] if chat_history else []
|
| 358 |
+
messages_for_llm.extend([{"role": "system", "content": system_prompt}, {"role": "user", "content": corrected_question}])
|
| 359 |
+
|
| 360 |
+
answer = generate_answer_with_history(backend["llm_client"], CONFIG["TOGETHER_MODEL_NAME"], messages_for_llm, CONFIG["GENERATION_TEMPERATURE"])
|
| 361 |
+
|
| 362 |
+
return {"answer": answer, "sources": sources, "corrected_question": corrected_question, "confidence_score": confidence_score}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|