darkisz commited on
Commit
92f70a2
·
unverified ·
1 Parent(s): e9ce8ca

Add files via upload

Browse files
Files changed (3) hide show
  1. appv1.py +220 -0
  2. backendv1.py +553 -0
  3. requirements.txt +249 -0
appv1.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
43
+ st.session_state.last_confidence_score = "N/A"
44
+ if "page" not in st.session_state:
45
+ st.session_state.page = "Chat"
46
+
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
+ # ==============================================================================
62
+ # = CHAT OLDAL LOGIKÁJA =
63
+ # ==============================================================================
64
+ if st.session_state.page == "Chat":
65
+ with st.sidebar:
66
+ st.header("Beállítások")
67
+ # A 0.1 egy jó 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:
81
+ level, help_text = "Alap Rangsor (RRF)", "A Cross-Encoder bizonytalan volt."
82
+ elif score == 10.0:
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
95
+ for i, message in enumerate(st.session_state.messages):
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")
168
+
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')}")
backendv1.py ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
7
+ 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'
26
+ RED = '\033[91m'
27
+ RESET = '\033[0m'
28
+ BLUE = '\033[94m'
29
+ 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,
39
+ "EMBEDDING_MODEL_NAME": 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2',
40
+ "CROSS_ENCODER_MODEL_NAME": 'cross-encoder/mmarco-mMiniLMv2-L12-H384-v1',
41
+ "TOGETHER_MODEL_NAME": "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free",
42
+ "QUERY_EXPANSION_MODEL": "mistralai/Mixtral-8x7B-Instruct-v0.1",
43
+ "LLM_CLIENT_TIMEOUT": 120,
44
+ "NUM_CONTEXT_RESULTS": 5,
45
+ "RE_RANK_CANDIDATE_COUNT": 50,
46
+ "RRF_RANK_CONSTANT": 60,
47
+ "INITIAL_SEARCH_SIZE": 150,
48
+ "KNN_NUM_CANDIDATES": 200,
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}")
183
+ return index, []
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:
235
+ for index_name in search_results[search_type]:
236
+ for rank, hit in search_results[search_type][index_name]:
237
+ doc_id = hit['_id']
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
422
+ except Exception as e:
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
434
+ except Exception as e:
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
+ }
requirements.txt ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ absl-py==2.2.1
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.11.16
4
+ aiolimiter==1.1.0
5
+ aiosignal==1.3.2
6
+ albucore==0.0.23
7
+ albumentations==2.0.5
8
+ altair==5.5.0
9
+ annotated-types==0.7.0
10
+ anyio==4.6.2.post1
11
+ astor==0.8.1
12
+ astunparse==1.6.3
13
+ attrs==24.2.0
14
+ banks==2.1.1
15
+ beautifulsoup4==4.13.3
16
+ blinker==1.8.2
17
+ blis==1.2.0
18
+ boto3==1.35.44
19
+ botocore==1.35.44
20
+ cachelib==0.13.0
21
+ cachetools==5.5.0
22
+ catalogue==2.0.10
23
+ certifi==2024.8.30
24
+ cffi==1.17.1
25
+ charset-normalizer==3.3.2
26
+ click==8.1.7
27
+ cloudpathlib==0.21.0
28
+ colorama==0.4.6
29
+ confection==0.1.5
30
+ contourpy==1.3.1
31
+ cryptography==44.0.2
32
+ cycler==0.12.1
33
+ cymem==2.0.11
34
+ Cython==3.0.12
35
+ dataclasses-json==0.6.7
36
+ decorator==5.2.1
37
+ deep-translator==1.11.4
38
+ Deprecated==1.2.15
39
+ dirtyjson==1.0.8
40
+ docopt==0.6.2
41
+ easyocr==1.7.2
42
+ elastic-transport==8.17.1
43
+ elasticsearch==8.17.2
44
+ et_xmlfile==2.0.0
45
+ eval_type_backport==0.2.2
46
+ filelock==3.18.0
47
+ filetype==1.2.0
48
+ fire==0.7.0
49
+ Flask==3.0.3
50
+ Flask-Cors==5.0.0
51
+ Flask-Session==0.8.0
52
+ flatbuffers==25.2.10
53
+ fonttools==4.56.0
54
+ frozenlist==1.5.0
55
+ fsspec==2025.3.2
56
+ gast==0.6.0
57
+ gitdb==4.0.12
58
+ GitPython==3.1.45
59
+ google-ai-generativelanguage==0.6.15
60
+ google-api-core==2.20.0
61
+ google-api-python-client==2.166.0
62
+ google-auth==2.35.0
63
+ google-auth-httplib2==0.2.0
64
+ google-cloud-core==2.4.1
65
+ google-cloud-speech==2.27.0
66
+ google-cloud-storage==2.18.2
67
+ google-crc32c==1.6.0
68
+ google-generativeai==0.8.4
69
+ google-pasta==0.2.0
70
+ google-resumable-media==2.7.2
71
+ googleapis-common-protos==1.65.0
72
+ greenlet==3.2.0
73
+ griffe==1.7.2
74
+ grpcio==1.66.1
75
+ grpcio-status==1.66.1
76
+ h11==0.14.0
77
+ h5py==3.13.0
78
+ httpcore==1.0.7
79
+ httplib2==0.22.0
80
+ httpx==0.27.2
81
+ httpx-sse==0.4.0
82
+ huggingface-hub==0.30.1
83
+ ibm-cloud-sdk-core==3.21.0
84
+ ibm-cos-sdk==2.13.6
85
+ ibm-cos-sdk-core==2.13.6
86
+ ibm-cos-sdk-s3transfer==2.13.6
87
+ ibm-generative-ai==3.0.0
88
+ ibm-watson==8.1.0
89
+ idna==3.10
90
+ imageio==2.37.0
91
+ itsdangerous==2.2.0
92
+ Jinja2==3.1.4
93
+ jmespath==1.0.1
94
+ joblib==1.4.2
95
+ jsonschema==4.25.0
96
+ jsonschema-specifications==2025.4.1
97
+ keras==3.9.2
98
+ keybert==0.9.0
99
+ kiwisolver==1.4.8
100
+ langcodes==3.5.0
101
+ language_data==1.3.0
102
+ lazy_loader==0.4
103
+ Levenshtein==0.27.1
104
+ libclang==18.1.1
105
+ llama-index-core==0.12.31
106
+ llama-index-embeddings-huggingface==0.5.3
107
+ lmdb==1.6.2
108
+ lxml==5.3.1
109
+ marisa-trie==1.2.1
110
+ Markdown==3.7
111
+ markdown-it-py==3.0.0
112
+ MarkupSafe==2.1.5
113
+ marshmallow==3.26.1
114
+ matplotlib==3.10.1
115
+ mdurl==0.1.2
116
+ ml_dtypes==0.5.1
117
+ mosestokenizer==1.2.1
118
+ mpmath==1.3.0
119
+ msgspec==0.18.6
120
+ multidict==6.4.2
121
+ murmurhash==1.0.12
122
+ mypy-extensions==1.0.0
123
+ namex==0.0.8
124
+ narwhals==2.1.2
125
+ nest-asyncio==1.6.0
126
+ networkx==3.4.2
127
+ ninja==1.11.1.4
128
+ nltk==3.9.1
129
+ numpy==2.1.3
130
+ opencv-contrib-python==4.11.0.86
131
+ opencv-python==4.11.0.86
132
+ opencv-python-headless==4.11.0.86
133
+ openfile==0.0.7
134
+ openpyxl==3.1.5
135
+ opt-einsum==3.3.0
136
+ optree==0.15.0
137
+ outcome==1.3.0.post0
138
+ packaging==24.2
139
+ paddleocr==2.10.0
140
+ paddlepaddle==3.0.0
141
+ pandas==2.2.3
142
+ pdf2image==1.17.0
143
+ pdfminer.six==20250327
144
+ pdfplumber==0.11.6
145
+ pillow==11.1.0
146
+ platformdirs==4.3.7
147
+ preshed==3.0.9
148
+ propcache==0.3.1
149
+ proto-plus==1.24.0
150
+ protobuf==5.28.2
151
+ pyarrow==19.0.1
152
+ pyasn1==0.6.1
153
+ pyasn1_modules==0.4.1
154
+ pyclipper==1.3.0.post6
155
+ pycparser==2.22
156
+ pydantic==2.10.1
157
+ pydantic_core==2.27.1
158
+ pydeck==0.9.1
159
+ pydub==0.25.1
160
+ Pygments==2.19.1
161
+ PyJWT==2.9.0
162
+ PyMuPDF==1.25.4
163
+ pyparsing==3.2.3
164
+ pypdfium2==4.30.1
165
+ PySocks==1.7.1
166
+ pyspellchecker==0.8.2
167
+ pytesseract==0.3.13
168
+ python-bidi==0.6.6
169
+ python-dateutil==2.9.0.post0
170
+ python-docx==1.1.2
171
+ python-dotenv==1.1.0
172
+ python-Levenshtein==0.27.1
173
+ pytz==2024.2
174
+ PyYAML==6.0.2
175
+ RapidFuzz==3.12.2
176
+ redis==5.1.1
177
+ referencing==0.36.2
178
+ regex==2024.11.6
179
+ requests==2.32.3
180
+ rich==13.9.4
181
+ rpds-py==0.27.0
182
+ rsa==4.9
183
+ s3transfer==0.10.3
184
+ sacremoses==0.1.1
185
+ safetensors==0.5.3
186
+ scikit-image==0.25.2
187
+ scikit-learn==1.6.1
188
+ scipy==1.15.2
189
+ selenium==4.27.1
190
+ sentence-transformers==4.0.1
191
+ sentencepiece==0.2.0
192
+ setuptools==78.1.0
193
+ shapely==2.0.7
194
+ shellingham==1.5.4
195
+ simsimd==6.2.1
196
+ six==1.16.0
197
+ smart-open==7.1.0
198
+ smmap==5.0.2
199
+ sniffio==1.3.1
200
+ sortedcontainers==2.4.0
201
+ soupsieve==2.6
202
+ spacy==3.8.4
203
+ spacy-legacy==3.0.12
204
+ spacy-loggers==1.0.5
205
+ SQLAlchemy==2.0.40
206
+ srsly==2.5.1
207
+ streamlit==1.48.1
208
+ stringzilla==3.12.3
209
+ sympy==1.13.1
210
+ tabulate==0.9.0
211
+ tenacity==9.1.2
212
+ tensorboard==2.19.0
213
+ tensorboard-data-server==0.7.2
214
+ tensorflow==2.19.0
215
+ termcolor==3.0.0
216
+ tf_keras==2.19.0
217
+ thinc==8.3.4
218
+ threadpoolctl==3.6.0
219
+ tifffile==2025.3.30
220
+ tika==3.1.0
221
+ tiktoken==0.9.0
222
+ together==1.5.5
223
+ tokenizers==0.21.1
224
+ toml==0.10.2
225
+ toolwrapper==2.1.0
226
+ torch==2.6.0
227
+ torchaudio==2.6.0
228
+ torchvision==0.21.0
229
+ tornado==6.5.2
230
+ tqdm==4.67.1
231
+ transformers==4.50.3
232
+ trio==0.27.0
233
+ trio-websocket==0.11.1
234
+ typer==0.15.2
235
+ typing-inspect==0.9.0
236
+ typing_extensions==4.12.2
237
+ tzdata==2024.2
238
+ uctools==1.3.0
239
+ uritemplate==4.1.1
240
+ urllib3==2.3.0
241
+ wasabi==1.1.3
242
+ watchdog==6.0.0
243
+ weasel==0.4.1
244
+ websocket-client==1.8.0
245
+ Werkzeug==3.0.4
246
+ wheel==0.45.1
247
+ wrapt==1.17.0
248
+ wsproto==1.2.0
249
+ yarl==1.19.0