Spaces:
Running
Running
Király Zoltán
commited on
Commit
·
90e8782
1
Parent(s):
b5d1360
API backend
Browse files- .gitignore +10 -0
- api_main.py +415 -0
- requirements.txt +2 -0
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Virtual Environment
|
| 2 |
+
.venv/
|
| 3 |
+
venv/
|
| 4 |
+
|
| 5 |
+
# Environment variables
|
| 6 |
+
.env
|
| 7 |
+
|
| 8 |
+
# Python cache
|
| 9 |
+
__pycache__/
|
| 10 |
+
*.pyc
|
api_main.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# api_main.py
|
| 2 |
+
# A te teljes backendv1.py kódod FastAPI keretrendszerbe illesztve.
|
| 3 |
+
|
| 4 |
+
# === FastAPI és alapvető importok ===
|
| 5 |
+
from fastapi import FastAPI, HTTPException
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
import uvicorn
|
| 8 |
+
from typing import List, Dict, Any
|
| 9 |
+
|
| 10 |
+
# === A TE KÓDOD (backendv1.py) ===
|
| 11 |
+
# Itt kezdődik a te kódod, minimális változtatással
|
| 12 |
+
import os
|
| 13 |
+
import time
|
| 14 |
+
import datetime
|
| 15 |
+
import traceback
|
| 16 |
+
import re
|
| 17 |
+
from collections import defaultdict
|
| 18 |
+
from elasticsearch import Elasticsearch, exceptions as es_exceptions
|
| 19 |
+
import torch
|
| 20 |
+
from sentence_transformers import SentenceTransformer
|
| 21 |
+
from sentence_transformers.cross_encoder import CrossEncoder
|
| 22 |
+
from spellchecker import SpellChecker
|
| 23 |
+
from dotenv import load_dotenv
|
| 24 |
+
import sys
|
| 25 |
+
import nltk
|
| 26 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 27 |
+
|
| 28 |
+
# Késleltetett importálás, hogy csak akkor legyen hiba, ha tényleg használjuk
|
| 29 |
+
try:
|
| 30 |
+
from together import Together
|
| 31 |
+
TOGETHER_AVAILABLE = True
|
| 32 |
+
except ImportError:
|
| 33 |
+
TOGETHER_AVAILABLE = False
|
| 34 |
+
|
| 35 |
+
# === ANSI Színkódok (konzol loggoláshoz) ===
|
| 36 |
+
GREEN = '\033[92m'
|
| 37 |
+
YELLOW = '\033[93m'
|
| 38 |
+
RED = '\033[91m'
|
| 39 |
+
RESET = '\033[0m'
|
| 40 |
+
BLUE = '\033[94m'
|
| 41 |
+
CYAN = '\033[96m'
|
| 42 |
+
MAGENTA = '\033[95m'
|
| 43 |
+
|
| 44 |
+
# --- Konfiguráció ---
|
| 45 |
+
# A hitelesítő adatok a környezeti változókból kerülnek beolvasásra.
|
| 46 |
+
CONFIG = {
|
| 47 |
+
"VECTOR_INDEX_NAMES": ["duna", "dunawebindexai"],
|
| 48 |
+
"FEEDBACK_INDEX_NAME": "feedback_index",
|
| 49 |
+
"ES_CLIENT_TIMEOUT": 90,
|
| 50 |
+
"EMBEDDING_MODEL_NAME": 'sentence-transformers/paraphrase-multilingual-mpnet-base-v2',
|
| 51 |
+
"CROSS_ENCODER_MODEL_NAME": 'cross-encoder/mmarco-mMiniLMv2-L12-H384-v1',
|
| 52 |
+
"TOGETHER_MODEL_NAME": "meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", # Frissítve a valós modellnévre
|
| 53 |
+
"QUERY_EXPANSION_MODEL": "mistralai/Mixtral-8x7B-Instruct-v0.1",
|
| 54 |
+
"LLM_CLIENT_TIMEOUT": 120,
|
| 55 |
+
"NUM_CONTEXT_RESULTS": 5,
|
| 56 |
+
"RE_RANK_CANDIDATE_COUNT": 50,
|
| 57 |
+
"RRF_RANK_CONSTANT": 60,
|
| 58 |
+
"INITIAL_SEARCH_SIZE": 150,
|
| 59 |
+
"KNN_NUM_CANDIDATES": 200,
|
| 60 |
+
"MAX_GENERATION_TOKENS": 1024,
|
| 61 |
+
"GENERATION_TEMPERATURE": 0.6,
|
| 62 |
+
"USE_QUERY_EXPANSION": True,
|
| 63 |
+
"SPELLCHECK_LANG": 'hu',
|
| 64 |
+
"MAX_HISTORY_TURNS": 3
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# --- Segédfüggvények (A TE FÜGGVÉNYEID) ---
|
| 68 |
+
|
| 69 |
+
def correct_spellings(text, spell_checker_instance):
|
| 70 |
+
if not spell_checker_instance: return text
|
| 71 |
+
try:
|
| 72 |
+
words = re.findall(r'\b\w+\b', text.lower())
|
| 73 |
+
misspelled = spell_checker_instance.unknown(words)
|
| 74 |
+
if not misspelled: return text
|
| 75 |
+
corrected_text = text
|
| 76 |
+
for word in misspelled:
|
| 77 |
+
correction = spell_checker_instance.correction(word)
|
| 78 |
+
if correction and correction != word:
|
| 79 |
+
# Ezt a sort javítottam az előző verziódban, mert hibás volt a re.sub hívás
|
| 80 |
+
corrected_text = re.sub(r'\b' + re.escape(word) + r'\b', correction, corrected_text, flags=re.IGNORECASE)
|
| 81 |
+
return corrected_text
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"{RED}Hiba a helyesírás javítása közben: {e}{RESET}")
|
| 84 |
+
return text
|
| 85 |
+
|
| 86 |
+
def get_query_category_with_llm(client, query):
|
| 87 |
+
if not client: return 'egyéb'
|
| 88 |
+
print(f" {CYAN}-> Lekérdezés kategorizálása LLM-mel...{RESET}")
|
| 89 |
+
category_list = ['IT biztonsági szolgáltatások', 'szolgáltatások', 'hardver', 'szoftver', 'hírek', 'audiovizuális konferenciatechnika']
|
| 90 |
+
categories_text = ", ".join([f"'{cat}'" for cat in category_list])
|
| 91 |
+
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.
|
| 92 |
+
Lehetséges kategóriák: {categories_text}
|
| 93 |
+
Kérdés: '{query}'
|
| 94 |
+
Kategória:"""
|
| 95 |
+
messages = [{"role": "user", "content": prompt}]
|
| 96 |
+
try:
|
| 97 |
+
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages, temperature=0.1, max_tokens=30)
|
| 98 |
+
if response and response.choices:
|
| 99 |
+
category = response.choices[0].message.content.strip().replace("'", "").replace("`", "")
|
| 100 |
+
for cat in category_list:
|
| 101 |
+
if cat.lower() in category.lower():
|
| 102 |
+
print(f" {GREEN}-> A kérdés LLM által generált kategóriája: '{cat}'{RESET}")
|
| 103 |
+
return cat.lower()
|
| 104 |
+
return 'egyéb'
|
| 105 |
+
except Exception as e:
|
| 106 |
+
print(f"{RED}Hiba LLM kategorizáláskor: {e}{RESET}")
|
| 107 |
+
return 'egyéb'
|
| 108 |
+
|
| 109 |
+
def expand_or_rewrite_query(original_query, client):
|
| 110 |
+
final_queries = [original_query]
|
| 111 |
+
if not (CONFIG["USE_QUERY_EXPANSION"] and client):
|
| 112 |
+
return final_queries
|
| 113 |
+
print(f" {BLUE}-> Lekérdezés bővítése/átírása...{RESET}")
|
| 114 |
+
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."
|
| 115 |
+
messages = [{"role": "user", "content": prompt}]
|
| 116 |
+
try:
|
| 117 |
+
response = client.chat.completions.create(model=CONFIG["QUERY_EXPANSION_MODEL"], messages=messages, temperature=0.5, max_tokens=100)
|
| 118 |
+
if response and response.choices:
|
| 119 |
+
generated_text = response.choices[0].message.content.strip()
|
| 120 |
+
alternatives = [q.strip().replace('"', '').replace("'", '').replace('.', '') for q in generated_text.split(',') if q.strip() and q.strip() != original_query]
|
| 121 |
+
final_queries.extend(alternatives)
|
| 122 |
+
print(f" {GREEN}-> Bővített lekérdezések: {final_queries}{RESET}")
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"{RED}Hiba a lekérdezés bővítése során: {e}{RESET}")
|
| 125 |
+
return final_queries
|
| 126 |
+
|
| 127 |
+
def run_separate_searches(es_client, query_text, embedding_model, expanded_queries, query_category=None):
|
| 128 |
+
results = {'knn': {}, 'keyword': {}}
|
| 129 |
+
es_client_with_timeout = es_client.options(request_timeout=CONFIG["ES_CLIENT_TIMEOUT"])
|
| 130 |
+
source_fields = ["text_content", "source_url", "summary", "category"]
|
| 131 |
+
filters = []
|
| 132 |
+
|
| 133 |
+
def knn_search(index, query_vector):
|
| 134 |
+
try:
|
| 135 |
+
knn_query = {"field": "embedding", "query_vector": query_vector, "k": CONFIG["INITIAL_SEARCH_SIZE"], "num_candidates": CONFIG["KNN_NUM_CANDIDATES"], "filter": filters}
|
| 136 |
+
response = es_client_with_timeout.search(index=index, knn=knn_query, _source=source_fields, size=CONFIG["INITIAL_SEARCH_SIZE"])
|
| 137 |
+
return index, response.get('hits', {}).get('hits', [])
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"{RED}Hiba kNN keresés során ({index}): {e}{RESET}")
|
| 140 |
+
return index, []
|
| 141 |
+
|
| 142 |
+
def keyword_search(index, expanded_queries):
|
| 143 |
+
try:
|
| 144 |
+
should_clauses = [{"match": {"text_content": {"query": q, "operator": "OR", "fuzziness": "AUTO"}}} for q in expanded_queries]
|
| 145 |
+
query_body = {"query": {"bool": {"should": should_clauses, "minimum_should_match": 1, "filter": filters}}}
|
| 146 |
+
response = es_client_with_timeout.search(index=index, query=query_body['query'], _source=source_fields, size=CONFIG["INITIAL_SEARCH_SIZE"])
|
| 147 |
+
return index, response.get('hits', {}).get('hits', [])
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print(f"{RED}Hiba kulcsszavas keresés során ({index}): {e}{RESET}")
|
| 150 |
+
return index, []
|
| 151 |
+
|
| 152 |
+
query_vector = embedding_model.encode(query_text, normalize_embeddings=True).tolist() if embedding_model else None
|
| 153 |
+
|
| 154 |
+
with ThreadPoolExecutor(max_workers=len(CONFIG["VECTOR_INDEX_NAMES"]) * 2) as executor:
|
| 155 |
+
knn_futures = {executor.submit(knn_search, index, query_vector) for index in CONFIG["VECTOR_INDEX_NAMES"] if query_vector}
|
| 156 |
+
keyword_futures = {executor.submit(keyword_search, index, expanded_queries) for index in CONFIG["VECTOR_INDEX_NAMES"]}
|
| 157 |
+
|
| 158 |
+
for future in knn_futures:
|
| 159 |
+
index, hits = future.result()
|
| 160 |
+
results['knn'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
| 161 |
+
for future in keyword_futures:
|
| 162 |
+
index, hits = future.result()
|
| 163 |
+
results['keyword'][index] = [(rank + 1, hit) for rank, hit in enumerate(hits)]
|
| 164 |
+
|
| 165 |
+
total_knn_hits = sum(len(h) for h in results['knn'].values())
|
| 166 |
+
total_keyword_hits = sum(len(h) for h in results['keyword'].values())
|
| 167 |
+
print(f"{CYAN}Vektorkeresési találatok száma: {total_knn_hits}{RESET}")
|
| 168 |
+
print(f"{CYAN}Kulcsszavas keresési találatok száma: {total_keyword_hits}{RESET}")
|
| 169 |
+
return results
|
| 170 |
+
|
| 171 |
+
def merge_results_rrf(search_results):
|
| 172 |
+
rrf_scores = defaultdict(float)
|
| 173 |
+
all_hits_data = {}
|
| 174 |
+
for search_type in search_results:
|
| 175 |
+
for index_name in search_results[search_type]:
|
| 176 |
+
for rank, hit in search_results[search_type][index_name]:
|
| 177 |
+
doc_id = hit['_id']
|
| 178 |
+
rrf_scores[doc_id] += 1.0 / (CONFIG["RRF_RANK_CONSTANT"] + rank)
|
| 179 |
+
if doc_id not in all_hits_data:
|
| 180 |
+
all_hits_data[doc_id] = hit
|
| 181 |
+
|
| 182 |
+
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)
|
| 183 |
+
print(f"{CYAN}RRF által rangsorolt Top 5 pontszám: {[f'{score:.4f}' for doc_id, score, hit in combined_results[:5]]}{RESET}")
|
| 184 |
+
return combined_results
|
| 185 |
+
|
| 186 |
+
def retrieve_context_reranked(backend, query_text, confidence_threshold, fallback_message, query_category):
|
| 187 |
+
expanded_queries = expand_or_rewrite_query(query_text, backend["llm_client"])
|
| 188 |
+
search_results = run_separate_searches(backend["es_client"], query_text, backend["embedding_model"], expanded_queries, query_category)
|
| 189 |
+
merged_results = merge_results_rrf(search_results)
|
| 190 |
+
|
| 191 |
+
if not merged_results:
|
| 192 |
+
return fallback_message, [], None
|
| 193 |
+
|
| 194 |
+
candidates_to_rerank = merged_results[:CONFIG["RE_RANK_CANDIDATE_COUNT"]]
|
| 195 |
+
hits_data_for_reranking = [hit for _, _, hit in candidates_to_rerank]
|
| 196 |
+
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]
|
| 197 |
+
|
| 198 |
+
ranked_by_ce = []
|
| 199 |
+
if backend["cross_encoder"] and query_chunk_pairs:
|
| 200 |
+
ce_scores = backend["cross_encoder"].predict(query_chunk_pairs, show_progress_bar=False)
|
| 201 |
+
ranked_by_ce = sorted(zip(ce_scores, hits_data_for_reranking), key=lambda x: x[0], reverse=True)
|
| 202 |
+
print(f"{CYAN}Cross-Encoder pontszámok (Top 5):{RESET} {[f'{score:.4f}' for score, _ in ranked_by_ce[:5]]}")
|
| 203 |
+
|
| 204 |
+
if not ranked_by_ce:
|
| 205 |
+
return fallback_message, [], None
|
| 206 |
+
|
| 207 |
+
top_score = float(ranked_by_ce[0][0])
|
| 208 |
+
if top_score < confidence_threshold:
|
| 209 |
+
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}).")
|
| 210 |
+
return dynamic_fallback, [], top_score
|
| 211 |
+
|
| 212 |
+
final_hits_for_context = [hit for _, hit in ranked_by_ce[:CONFIG["NUM_CONTEXT_RESULTS"]]]
|
| 213 |
+
context_parts = [hit['_source'].get('summary', hit['_source'].get('text_content')) for hit in final_hits_for_context]
|
| 214 |
+
context_string = "\n\n---\n\n".join(context_parts)
|
| 215 |
+
|
| 216 |
+
sources = [{"url": hit['_source'].get('source_url', '?'), "content": hit['_source'].get('text_content', 'N/A')} for hit in final_hits_for_context]
|
| 217 |
+
return context_string, sources, top_score
|
| 218 |
+
|
| 219 |
+
def generate_answer_with_history(client, model_name, messages, temperature):
|
| 220 |
+
if not client: return "Hiba: Az AI kliens nincs inicializálva."
|
| 221 |
+
try:
|
| 222 |
+
response = client.chat.completions.create(model=model_name, messages=messages, temperature=temperature, max_tokens=CONFIG["MAX_GENERATION_TOKENS"], timeout=CONFIG["LLM_CLIENT_TIMEOUT"])
|
| 223 |
+
if response and response.choices:
|
| 224 |
+
return response.choices[0].message.content.strip()
|
| 225 |
+
return "Hiba: Nem érkezett érvényes válasz az AI modelltől."
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"{RED}Hiba a válasz generálásakor: {e}{RESET}")
|
| 228 |
+
return "Hiba történt az AI modell hívásakor."
|
| 229 |
+
|
| 230 |
+
def search_in_feedback_index(es_client, embedding_model, question, min_score=0.75):
|
| 231 |
+
try:
|
| 232 |
+
if not es_client.indices.exists(index=CONFIG["FEEDBACK_INDEX_NAME"]): return None, None
|
| 233 |
+
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 234 |
+
knn_query = {"field": "embedding", "query_vector": embedding, "k": 1, "num_candidates": 10}
|
| 235 |
+
response = es_client.search(index=CONFIG["FEEDBACK_INDEX_NAME"], knn=knn_query, _source=["question_text", "correction_text"])
|
| 236 |
+
hits = response.get('hits', {}).get('hits', [])
|
| 237 |
+
if hits and hits[0]['_score'] >= min_score:
|
| 238 |
+
top_hit = hits[0]; source = top_hit['_source']; score = top_hit['_score']
|
| 239 |
+
if score > 0.98: return "direct_answer", source['correction_text']
|
| 240 |
+
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!"
|
| 241 |
+
return "instruction", instruction
|
| 242 |
+
except Exception:
|
| 243 |
+
return None, None
|
| 244 |
+
return None, None
|
| 245 |
+
|
| 246 |
+
def index_feedback(es_client, embedding_model, question, correction):
|
| 247 |
+
try:
|
| 248 |
+
embedding = embedding_model.encode(question, normalize_embeddings=True).tolist()
|
| 249 |
+
doc = {"question_text": question, "correction_text": correction, "embedding": embedding, "timestamp": datetime.datetime.now()}
|
| 250 |
+
es_client.index(index=CONFIG["FEEDBACK_INDEX_NAME"], document=doc)
|
| 251 |
+
return True
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"{RED}Hiba a visszajelzés indexelése során: {e}{RESET}")
|
| 254 |
+
return False
|
| 255 |
+
|
| 256 |
+
def get_all_feedback(es_client, index_name):
|
| 257 |
+
try:
|
| 258 |
+
if not es_client.indices.exists(index=index_name): return []
|
| 259 |
+
response = es_client.search(index=index_name, query={"match_all": {}}, size=1000, sort=[{"timestamp": {"order": "desc"}}])
|
| 260 |
+
return response.get('hits', {}).get('hits', [])
|
| 261 |
+
except Exception as e:
|
| 262 |
+
print(f"{RED}Hiba a visszajelzések listázása során: {e}{RESET}")
|
| 263 |
+
return []
|
| 264 |
+
|
| 265 |
+
def delete_feedback_by_id(es_client, index_name, doc_id):
|
| 266 |
+
try:
|
| 267 |
+
es_client.delete(index=index_name, id=doc_id)
|
| 268 |
+
return True
|
| 269 |
+
except Exception as e:
|
| 270 |
+
print(f"{RED}Hiba a visszajelzés törlése során (ID: {doc_id}): {e}{RESET}")
|
| 271 |
+
return False
|
| 272 |
+
|
| 273 |
+
def update_feedback_comment(es_client, index_name, doc_id, new_comment):
|
| 274 |
+
try:
|
| 275 |
+
es_client.update(index=index_name, id=doc_id, doc={"correction_text": new_comment})
|
| 276 |
+
return True
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print(f"{RED}Hiba a visszajelzés szerkesztése során (ID: {doc_id}): {e}{RESET}")
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
def initialize_backend():
|
| 282 |
+
print("----- Backend Motor Inicializálása -----")
|
| 283 |
+
load_dotenv()
|
| 284 |
+
|
| 285 |
+
es_cloud_id = os.getenv("ES_CLOUD_ID")
|
| 286 |
+
es_api_key = os.getenv("ES_API_KEY")
|
| 287 |
+
together_api_key = os.getenv("TOGETHER_API_KEY")
|
| 288 |
+
|
| 289 |
+
if not all([es_cloud_id, es_api_key, together_api_key]):
|
| 290 |
+
print(f"{RED}Hiba: Hiányzó környezeti változók! Szükséges: ES_CLOUD_ID, ES_API_KEY, TOGETHER_API_KEY{RESET}")
|
| 291 |
+
return None
|
| 292 |
+
|
| 293 |
+
if not TOGETHER_AVAILABLE:
|
| 294 |
+
print(f"{RED}Hiba: A 'together' csomag nincs telepítve.{RESET}")
|
| 295 |
+
return None
|
| 296 |
+
|
| 297 |
+
try:
|
| 298 |
+
nltk.data.find('tokenizers/punkt')
|
| 299 |
+
except LookupError:
|
| 300 |
+
nltk.download('punkt', quiet=True)
|
| 301 |
+
|
| 302 |
+
spell_checker = None
|
| 303 |
+
try:
|
| 304 |
+
spell_checker = SpellChecker(language=CONFIG["SPELLCHECK_LANG"])
|
| 305 |
+
custom_words = ["dunaelektronika", "kft", "outsourcing", "dell", "lenovo", "nis2", "szerver", "kliens", "hálózati", "hpe"]
|
| 306 |
+
spell_checker.word_frequency.load_words(custom_words)
|
| 307 |
+
except Exception as e:
|
| 308 |
+
print(f"{RED}Helyesírás-ellenőrző hiba: {e}{RESET}")
|
| 309 |
+
|
| 310 |
+
try:
|
| 311 |
+
print(f"{CYAN}Elasticsearch kliens inicializálása...{RESET}")
|
| 312 |
+
es_client = Elasticsearch(cloud_id=es_cloud_id, api_key=es_api_key, request_timeout=CONFIG["ES_CLIENT_TIMEOUT"])
|
| 313 |
+
if not es_client.ping(): raise ConnectionError("Elasticsearch ping sikertelen.")
|
| 314 |
+
print(f"{GREEN}Elasticsearch kliens kész.{RESET}")
|
| 315 |
+
|
| 316 |
+
print(f"{CYAN}AI modellek betöltése...{RESET}")
|
| 317 |
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 318 |
+
embedding_model = SentenceTransformer(CONFIG["EMBEDDING_MODEL_NAME"], device=device)
|
| 319 |
+
cross_encoder = CrossEncoder(CONFIG["CROSS_ENCODER_MODEL_NAME"], device=device)
|
| 320 |
+
llm_client = Together(api_key=together_api_key)
|
| 321 |
+
print(f"{GREEN}AI modellek betöltve (eszköz: {device}).{RESET}")
|
| 322 |
+
|
| 323 |
+
backend_objects = {
|
| 324 |
+
"es_client": es_client, "embedding_model": embedding_model, "cross_encoder": cross_encoder,
|
| 325 |
+
"llm_client": llm_client, "spell_checker": spell_checker
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
print(f"{GREEN}----- Backend Motor Készen Áll -----{RESET}")
|
| 329 |
+
return backend_objects
|
| 330 |
+
except Exception as e:
|
| 331 |
+
print(f"{RED}Hiba a backend inicializálása során: {e}{RESET}")
|
| 332 |
+
traceback.print_exc()
|
| 333 |
+
return None
|
| 334 |
+
|
| 335 |
+
def process_query(user_question, chat_history, backend, confidence_threshold, fallback_message):
|
| 336 |
+
print(f"\n{BLUE}----- Új lekérdezés feldolgozása ----{RESET}")
|
| 337 |
+
print(f"{BLUE}Kérdés: {user_question}{RESET}")
|
| 338 |
+
|
| 339 |
+
corrected_question = correct_spellings(user_question, backend["spell_checker"])
|
| 340 |
+
print(f"{BLUE}Javított kérdés: {corrected_question}{RESET}")
|
| 341 |
+
|
| 342 |
+
feedback_type, feedback_content = search_in_feedback_index(backend["es_client"], backend["embedding_model"], corrected_question)
|
| 343 |
+
if feedback_type == "direct_answer":
|
| 344 |
+
print(f"{GREEN}Direkt válasz a visszajelzési adatbázisból.{RESET}")
|
| 345 |
+
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}
|
| 346 |
+
|
| 347 |
+
feedback_instructions = feedback_content if feedback_type == "instruction" else ""
|
| 348 |
+
query_category = get_query_category_with_llm(backend["llm_client"], corrected_question)
|
| 349 |
+
retrieved_context, sources, confidence_score = retrieve_context_reranked(backend, corrected_question, confidence_threshold, fallback_message, query_category)
|
| 350 |
+
|
| 351 |
+
if not sources and not feedback_instructions:
|
| 352 |
+
return {"answer": retrieved_context, "sources": [], "corrected_question": corrected_question, "confidence_score": confidence_score}
|
| 353 |
+
|
| 354 |
+
system_prompt = f"""Te egy professzionális, segítőkész AI asszisztens vagy.
|
| 355 |
+
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.
|
| 356 |
+
{feedback_instructions}
|
| 357 |
+
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!
|
| 358 |
+
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}'
|
| 359 |
+
KONTEXTUS:
|
| 360 |
+
---
|
| 361 |
+
{retrieved_context if sources else "A tudásbázisban nem található releváns információ."}
|
| 362 |
+
---
|
| 363 |
+
"""
|
| 364 |
+
messages_for_llm = chat_history[-(CONFIG["MAX_HISTORY_TURNS"] * 2):] if chat_history else []
|
| 365 |
+
messages_for_llm.extend([{"role": "system", "content": system_prompt}, {"role": "user", "content": corrected_question}])
|
| 366 |
+
|
| 367 |
+
answer = generate_answer_with_history(backend["llm_client"], CONFIG["TOGETHER_MODEL_NAME"], messages_for_llm, CONFIG["GENERATION_TEMPERATURE"])
|
| 368 |
+
|
| 369 |
+
return {"answer": answer, "sources": sources, "corrected_question": corrected_question, "confidence_score": confidence_score}
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
# === FastAPI KERETRENDSZER ===
|
| 373 |
+
|
| 374 |
+
# FastAPI alkalmazás létrehozása
|
| 375 |
+
app = FastAPI()
|
| 376 |
+
|
| 377 |
+
# A backend objektumok inicializálása a szerver indulásakor
|
| 378 |
+
# Ezt egy globális változóban tároljuk, hogy minden API hívás elérje
|
| 379 |
+
backend_objects = initialize_backend()
|
| 380 |
+
|
| 381 |
+
# Ez a modell határozza meg, milyen formátumban várjuk a kérést
|
| 382 |
+
# Hozzáadtuk a chat_history-t is, mint opcionális mezőt
|
| 383 |
+
class UserQuery(BaseModel):
|
| 384 |
+
question: str
|
| 385 |
+
chat_history: List[Dict[str, str]] = [] # pl. [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
|
| 386 |
+
|
| 387 |
+
# Ez hozza létre a /ask API végpontot
|
| 388 |
+
@app.post("/ask")
|
| 389 |
+
def handle_question(query: UserQuery):
|
| 390 |
+
# Ellenőrizzük, hogy a backend sikeresen elindult-e
|
| 391 |
+
if not backend_objects:
|
| 392 |
+
raise HTTPException(status_code=503, detail="A backend szolgáltatás nem érhető el vagy hibával indult el.")
|
| 393 |
+
|
| 394 |
+
try:
|
| 395 |
+
# Meghívjuk a te logikádat tartalmazó process_query függvényt
|
| 396 |
+
# A chat_history-t is átadjuk a kérésből
|
| 397 |
+
result = process_query(
|
| 398 |
+
user_question=query.question,
|
| 399 |
+
chat_history=query.chat_history,
|
| 400 |
+
backend=backend_objects,
|
| 401 |
+
confidence_threshold=-3.8, # Ezt az értéket később akár a kérésből is kaphatja
|
| 402 |
+
fallback_message="Sajnos nem találtam releváns információt a kérdésedre a tudásbázisban."
|
| 403 |
+
)
|
| 404 |
+
return result
|
| 405 |
+
except Exception as e:
|
| 406 |
+
# Általános hibakezelés
|
| 407 |
+
print(f"{RED}Váratlan hiba az API végpontban: {e}{RESET}")
|
| 408 |
+
traceback.print_exc()
|
| 409 |
+
raise HTTPException(status_code=500, detail="Belső szerverhiba történt a kérés feldolgozása közben.")
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# Fő indítási pont a helyi teszteléshez
|
| 413 |
+
if __name__ == "__main__":
|
| 414 |
+
# Az --host 0.0.0.0 lehetővé teszi, hogy a hálózat más gépeiről is elérd a szervert
|
| 415 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
requirements.txt
CHANGED
|
@@ -9,3 +9,5 @@ streamlit
|
|
| 9 |
together
|
| 10 |
torch
|
| 11 |
tiktoken
|
|
|
|
|
|
|
|
|
| 9 |
together
|
| 10 |
torch
|
| 11 |
tiktoken
|
| 12 |
+
fastapi
|
| 13 |
+
uvicorn[standard]
|