Király Zoltán commited on
Commit
138e4b0
·
1 Parent(s): 79aa6e9

Fix: Clean up requirements.txt to resolve build conflicts

Browse files
Files changed (2) hide show
  1. appv1.py +64 -96
  2. 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
- # Végleges verzió, Chat és Admin felülettel.
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
- # A backendv1.py importálása, a futtatható könyvtárhoz hozzáadása.
12
- # Feltételezi, hogy a backendv1.py és az appv1.py ugyanabban a mappában van.
13
- sys.path.append(os.path.dirname(__file__))
14
-
15
- # Az összes szükséges függvény importálása a backendből
16
- from backendv1 import (
17
- initialize_backend,
18
- process_query,
19
- index_feedback,
20
- get_all_feedback,
21
- delete_feedback_by_id,
22
- update_feedback_comment,
23
- CONFIG
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
- return initialize_backend()
35
-
 
 
 
 
 
 
 
 
 
36
 
 
37
  backend = load_backend_components()
 
 
 
 
 
 
38
 
39
- # --- Session State Inicializálása ---
 
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
- # A 0.1 egy alapértelmezett érték, de a pontos tartomány a Cross-Encoder modell kimenetétől függ
68
- confidence_threshold = st.slider("Minimális pontossági küszöb", min_value=-5.0, max_value=5.0, value=0.1,
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
- level = "Magas"
88
- elif score >= -1.5:
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
- # --- HOZZÁADOTT RÉSZ VÉGE ---
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
- # Hívás a backendv1 függvényre
134
- index_feedback(backend["es_client"], backend["embedding_model"],
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
- # Hívás a backendv1 függvényre
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") # Itt adjuk hozzá a pontszámot
159
  })
160
- # --- MÓDOSÍTOTT RÉSZ VÉGE ---
161
  st.rerun()
162
 
163
  # ==============================================================================
164
- # = ADMIN OLDAL LOGIKÁJA =
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
- # Hívás a backendv1 függvényre
202
- if update_feedback_comment(backend["es_client"], CONFIG["FEEDBACK_INDEX_NAME"], doc_id,
203
- new_comment):
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
- st.cache_data.clear()
216
- st.rerun()
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', correction, corrected_text,
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
- # Módosítva: eltávolítjuk a felesleges karaktereket és magyarázó szöveget
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
- # DRASZTIKUS VÁLTOZTATÁS:
167
- # A kategóriaszűrés logikája kikapcsolva. A lekérdezés a teljes indexben fut.
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
- "num_candidates": CONFIG["KNN_NUM_CANDIDATES"], "filter": filters}
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
- query_vector}
208
- keyword_futures = {executor.submit(keyword_search, index, expanded_queries) for index in
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
- combined_results.sort(key=lambda item: item[1], reverse=True)
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
- Lekéri a kontextust a rangsorolás után.
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
- top_score = None
267
-
268
  if not merged_results:
269
- print(f"{YELLOW}A keresés nem hozott eredményt.{RESET}")
270
- return fallback_message, [], top_score
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
- try:
281
- ce_scores = cross_encoder.predict(query_chunk_pairs, show_progress_bar=False)
282
- ranked_by_ce = sorted(zip(ce_scores, hits_data_for_reranking), key=lambda x: x[0], reverse=True)
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, [], top_score
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
- print(
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
- source = top_hit['_source']
369
- score = top_hit['_score']
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
- response = es_client.search(index=index_name, query={"match_all": {}}, size=1000,
406
- sort=[{"timestamp": {"order": "desc"}}])
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-ellenőrző hiba: {e}{RESET}")
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
- {prompt_instructions}
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
- if chat_history:
539
- messages_for_llm.extend(chat_history[-(CONFIG["MAX_HISTORY_TURNS"] * 2):])
540
-
541
- messages_for_llm.append({"role": "system", "content": system_prompt})
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}