diff --git a/Backend/Dockerfile b/Backend/Dockerfile index 5858505..14b5f64 100644 --- a/Backend/Dockerfile +++ b/Backend/Dockerfile @@ -1,17 +1,17 @@ -# Используем базовый образ Python + FROM python:3.12 -# Устанавливаем рабочую директорию в контейнере + WORKDIR /app -# Копируем все файлы проекта в контейнер + COPY . . -# Устанавливаем зависимости из requirements.txt + RUN pip install -r requirements.txt -# Делаем скрипт ожидания исполняемым + RUN chmod +x wait-for-elasticsearch.sh -# Запускаем скрипт ожидания перед запуском бэкенда + CMD ["./wait-for-elasticsearch.sh", "python", "server.py"] diff --git a/Backend/__pycache__/model.cpython-311.pyc b/Backend/__pycache__/model.cpython-311.pyc index ea50eff..5463232 100644 Binary files a/Backend/__pycache__/model.cpython-311.pyc and b/Backend/__pycache__/model.cpython-311.pyc differ diff --git a/Backend/indexCloud.py b/Backend/indexCloud.py index bb897d9..32e2665 100644 --- a/Backend/indexCloud.py +++ b/Backend/indexCloud.py @@ -30,10 +30,10 @@ def index_documents(data): 'full_data': item }) - sys.stdout.write(f"\rПроиндексировано {i} из {total_documents} документов") + sys.stdout.flush() - print("\nИндексирование завершено.") + data_path = "../../data_adc_databaza/cleaned_general_info_additional.json" diff --git a/Backend/index_JSON.py b/Backend/index_JSON.py index 56fb09f..c6230c6 100644 --- a/Backend/index_JSON.py +++ b/Backend/index_JSON.py @@ -8,7 +8,7 @@ es = Elasticsearch([{'host': 'localhost', 'port': 9200, 'scheme': 'http'}]) embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") def create_index(): - # Определяем маппинг для индекса + mapping = { "mappings": { "properties": { @@ -18,11 +18,11 @@ def create_index(): }, "vector": { "type": "dense_vector", - "dims": 384 # Размерность векторного представления + "dims": 384 }, "full_data": { "type": "object", - "enabled": False # Отключаем индексацию вложенных данных + "enabled": False } } } @@ -53,19 +53,15 @@ def index_documents(data): } actions.append(action) - # Отображение прогресса - print(f"Индексируется документ {i}/{total_docs}", end='\r') - # Опционально: индексируем пакетами по N документов + if i % 100 == 0 or i == total_docs: bulk(es, actions) actions = [] - # Если остались неиндексированные документы if actions: bulk(es, actions) - print("\nИндексирование завершено.") if __name__ == "__main__": create_index() diff --git a/Backend/model.py b/Backend/model.py index 58ca380..474f402 100644 --- a/Backend/model.py +++ b/Backend/model.py @@ -3,20 +3,18 @@ import requests import logging import time import re -import difflib from requests.exceptions import HTTPError from elasticsearch import Elasticsearch -from langchain.chains import SequentialChain -from langchain.chains import LLMChain, SequentialChain from langchain_huggingface import HuggingFaceEmbeddings from langchain_elasticsearch import ElasticsearchStore from langchain.text_splitter import RecursiveCharacterTextSplitter -from langchain.docstore.document import Document -# from googletrans import Translator +import psycopg2 +from psycopg2.extras import RealDictCursor logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + config_file_path = "config.json" with open(config_file_path, 'r') as config_file: config = json.load(config_file) @@ -26,69 +24,90 @@ if not mistral_api_key: raise ValueError("Mistral API key not found in configuration.") ############################################################################### -# translate all answer to slovak(temporary closed :) ) # +# Simple functions for translation (stub) ############################################################################### -# translator = Translator() def translate_to_slovak(text: str) -> str: - """ - Переводит весь текст на словацкий с логированием изменений. - Сейчас функция является заглушкой и возвращает исходный текст без изменений. - """ - # if not text.strip(): - # return text - # - # logger.info("Translation - Before: " + text) - # try: - # mid_result = translator.translate(text, src='auto', dest='en').text - # final_result = translator.translate(mid_result, src='en', dest='sk').text - # logger.info("Translation - After: " + final_result) - # before_words = text.split() - # after_words = final_result.split() - # diff = list(difflib.ndiff(before_words, after_words)) - # changed_words = [word[2:] for word in diff if word.startswith('+ ')] - # if changed_words: - # logger.info("Changed words: " + ", ".join(changed_words)) - # else: - # logger.info("No changed words detected.") - # return final_result - # except Exception as e: - # logger.error(f"Translation error: {e}") - # return text return text -############################################################################### -# Функция перевода описания лекарства с сохранением названия (до двоеточия) # -############################################################################### def translate_preserving_medicine_names(text: str) -> str: - """ - Ищет строки вида "номер. Название лекарства: описание..." и переводит только описание, - оставляя название без изменений. - Сейчас функция является заглушкой и возвращает исходный текст без изменений. - """ - # pattern = re.compile(r'^(\d+\.\s*[^:]+:\s*)(.*)$', re.MULTILINE) - # - # def replacer(match): - # prefix = match.group(1) - # description = match.group(2) - # logger.info("Translating description: " + description) - # translated_description = translate_to_slovak(description) - # logger.info("Translated description: " + translated_description) - # diff = list(difflib.ndiff(description.split(), translated_description.split())) - # changed_words = [word[2:] for word in diff if word.startswith('+ ')] - # if changed_words: - # logger.info("Changed words in description: " + ", ".join(changed_words)) - # else: - # logger.info("No changed words in description detected.") - # return prefix + translated_description - # - # if pattern.search(text): - # return pattern.sub(replacer, text) - # else: - # return translate_to_slovak(text) return text ############################################################################### -# Custom Mistral LLM # +# Function for evaluating the completeness of the answer +############################################################################### +def evaluate_complete_answer(query: str, answer: str) -> dict: + evaluation_prompt = ( + f"Vyhodnoť nasledujúcu odpoveď na základe týchto kritérií:\n" + f"1. Odpoveď obsahuje odporúčania liekov vrátane názvu, stručného vysvetlenia a, ak bolo žiadané, aj dávkovanie alebo čas užívania.\n" + f"2. Ak otázka obsahovala dodatočné požiadavky, odpoveď má samostatnú časť, ktorá tieto požiadavky rieši.\n\n" + f"Otázka: '{query}'\n" + f"Odpoveď: '{answer}'\n\n" + "Na základe týchto kritérií daj odpovedi hodnotenie od 0 do 10, kde 10 znamená, že odpoveď je úplne logická a obsahuje všetky požadované informácie. " + "Vráť len číslo." + ) + score_str = llm_small.generate_text(prompt=evaluation_prompt, max_tokens=50, temperature=0.3) + try: + score = float(score_str.strip()) + except Exception as e: + logger.error(f"Error parsing evaluation score: {e}") + score = 0.0 + return {"rating": round(score, 2), "explanation": "Evaluation based on required criteria."} + +############################################################################### +# Function for validating the response logic +############################################################################### +def validate_answer_logic(query: str, answer: str) -> str: + validation_prompt = ( + f"Otázka: '{query}'\n" + f"Odpoveď: '{answer}'\n\n" + "Analyzuj prosím túto odpoveď. Ak odpoveď neobsahuje všetky dodatočné informácie, na ktoré sa pýtal používateľ, " + "alebo ak odporúčania liekov nie sú úplné (napr. chýba dávkovanie alebo čas užívania, ak boli takéto požiadavky v otázke), " + "vytvor opravenú odpoveď, ktorá je logicky konzistentná s otázkou. " + "Odpovedz v slovenčine a iba čistou, konečnou odpoveďou bez ďalších komentárov." + ) + try: + validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=800, temperature=0.5) + logger.info(f"Validated answer: {validated_answer}") + return validated_answer + except Exception as e: + logger.error(f"Error during answer validation: {e}") + return answer + +############################################################################### +# Function for creating a dynamic prompt with information from documents +############################################################################### +def build_dynamic_prompt(query: str, documents: list) -> str: + documents_str = "\n".join(documents) + prompt = ( + f"Otázka: '{query}'.\n" + "Na základe nasledujúcich informácií o liekoch:\n" + f"{documents_str}\n\n" + "Analyzuj uvedenú otázku a zisti, či obsahuje dodatočné požiadavky okrem odporúčania liekov. " + "Ak áno, v odpovedi najprv uveď odporúčané lieky – pre každý liek uveď jeho názov, stručné vysvetlenie a, ak je to relevantné, " + "odporúčané dávkovanie alebo čas užívania, a potom v ďalšej časti poskytn ú odpoveď na dodatočné požiadavky. " + "Odpovedaj priamo a ľudským, priateľským tónom v číslovanom zozname, bez zbytočných úvodných fráz. " + "Odpoveď musí byť v slovenčine. " + "Prosím, odpovedaj v priateľskom, zdvorilom a profesionálnom tóne, bez akýchkoľvek agresívnych či drzých výrazov." + ) + return prompt + +############################################################################### +# Function to get user data from the database via endpoint /api/get_user_data +############################################################################### +def get_user_data_from_db(chat_id: str) -> str: + try: + response = requests.get("http://localhost:5000/api/get_user_data", params={"chatId": chat_id}) + if response.status_code == 200: + data = response.json() + return data.get("user_data", "") + else: + logger.warning(f"Nezískané user_data, status: {response.status_code}") + except Exception as e: + logger.error(f"Error retrieving user_data from DB: {e}", exc_info=True) + return "" + +############################################################################### +# Class for calling Mistral LLM ############################################################################### class CustomMistralLLM: def __init__(self, api_key: str, endpoint_url: str, model_name: str): @@ -96,7 +115,7 @@ class CustomMistralLLM: self.endpoint_url = endpoint_url self.model_name = model_name - def generate_text(self, prompt: str, max_tokens=512, temperature=0.7, retries=3, delay=2): + def generate_text(self, prompt: str, max_tokens=812, temperature=0.7, retries=3, delay=2): headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json" @@ -123,25 +142,24 @@ class CustomMistralLLM: else: logger.error(f"HTTP Error: {e}") raise e - except Exception as e: - logger.error(f"Error: {str(e)}") - raise e + except Exception as ex: + logger.error(f"Error: {str(ex)}") + raise ex raise Exception("Reached maximum number of retries for API request") ############################################################################### -# Initialize embeddings and Elasticsearch store # +# Initialisation of Embeddings and Elasticsearch ############################################################################### logger.info("Loading HuggingFaceEmbeddings model...") embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") -index_name = 'drug_docs' - +index_name = "drug_docs" if config.get("useCloud", False): logger.info("Using cloud Elasticsearch.") cloud_id = "tt:dXMtZWFzdC0yLmF3cy5lbGFzdGljLWNsb3VkLmNvbTo0NDMkOGM3ODQ0ZWVhZTEyNGY3NmFjNjQyNDFhNjI4NmVhYzMkZTI3YjlkNTQ0ODdhNGViNmEyMTcxMjMxNmJhMWI0ZGU=" vectorstore = ElasticsearchStore( es_cloud_id=cloud_id, - index_name='drug_docs', + index_name=index_name, embedding=embeddings, es_user="elastic", es_password="sSz2BEGv56JRNjGFwoQ191RJ" @@ -149,22 +167,21 @@ if config.get("useCloud", False): else: logger.info("Using local Elasticsearch.") vectorstore = ElasticsearchStore( - es_url="http://localhost:9200", + es_url="http://elasticsearch:9200", index_name=index_name, embedding=embeddings, ) -logger.info(f"Connected to {'cloud' if config.get('useCloud', False) else 'local'} Elasticsearch.") +logger.info("Connected to Elasticsearch.") ############################################################################### -# Initialize Mistral models (small & large) # +# Initialisation of LLM small & large ############################################################################### llm_small = CustomMistralLLM( api_key=mistral_api_key, endpoint_url="https://api.mistral.ai/v1/chat/completions", model_name="mistral-small-latest" ) - llm_large = CustomMistralLLM( api_key=mistral_api_key, endpoint_url="https://api.mistral.ai/v1/chat/completions", @@ -172,144 +189,310 @@ llm_large = CustomMistralLLM( ) ############################################################################### -# Helper function to evaluate model output # +# Request classification function: vyhladavanie vs. upresnenie ############################################################################### -def evaluate_results(query, summaries, model_name): - query_keywords = query.split() - total_score = 0 - explanation = [] - for i, summary in enumerate(summaries): - length_score = min(len(summary) / 100, 10) - total_score += length_score - explanation.append(f"Document {i+1}: Length score - {length_score}") - keyword_matches = sum(1 for word in query_keywords if word.lower() in summary.lower()) - keyword_score = min(keyword_matches * 2, 10) - total_score += keyword_score - explanation.append(f"Document {i+1}: Keyword match score - {keyword_score}") - final_score = total_score / len(summaries) if summaries else 0 - explanation_summary = "\n".join(explanation) - logger.info(f"Evaluation for model {model_name}: {final_score}/10") - logger.info(f"Explanation:\n{explanation_summary}") - return {"rating": round(final_score, 2), "explanation": explanation_summary} - -############################################################################### -# validation of recieved answer is it correct for user question # -############################################################################### -def validate_answer_logic(query: str, answer: str) -> str: - """ - Проверяет, соответствует ли ответ логике вопроса. - Если, например, вопрос относится к ľudským liekom a obsahuje otázku na dávkovanie, - odpoveď musí obsahovať iba lieky vhodné pre ľudí s uvedením správneho dávkovania. - """ - validation_prompt = ( - f"Otázka: '{query}'\n" - f"Odpoveď: '{answer}'\n\n" - "Analyzuj prosím túto odpoveď. Ak odpoveď obsahuje odporúčania liekov, ktoré nie sú vhodné pre ľudí, " - "alebo ak neobsahuje správne informácie o dávkovaní, oprav ju tak, aby bola logicky konzistentná s otázkou. " - "Odpoveď musí obsahovať iba lieky určené pre ľudí a pri potrebe aj presné informácie o dávkovaní (napr. v gramoch). " - "Ak je odpoveď logická a korektná, vráť pôvodnú odpoveď bez zmien. " - "Odpovedz v slovenčine a iba čistou, konečnou odpoveďou bez ďalších komentárov." +def classify_query(query: str, chat_history: str = "") -> str: + if not chat_history.strip(): + return "vyhladavanie" + prompt = ( + "Ty si zdravotnícky expert, ktorý analyzuje otázky používateľov. " + "Analyzuj nasledujúci dopyt a urči, či ide o dopyt na vyhľadanie liekov alebo " + "o upresnenie/doplnenie už poskytnutej odpovede.\n" + "Ak dopyt obsahuje výrazy ako 'čo pit', 'aké lieky', 'odporuč liek', 'hľadám liek', " + "odpovedaj slovom 'vyhľadávanie'.\n" + "Ak dopyt slúži na upresnenie, napríklad obsahuje výrazy ako 'a nie na predpis', " + "'upresni', 'este raz', odpovedaj slovom 'upresnenie'.\n" + f"Dopyt: \"{query}\"" ) - try: - validated_answer = llm_small.generate_text(prompt=validation_prompt, max_tokens=500, temperature=0.5) - logger.info(f"Validated answer: {validated_answer}") - return validated_answer - except Exception as e: - logger.error(f"Error during answer validation: {e}") - return answer + classification = llm_small.generate_text(prompt=prompt, max_tokens=20, temperature=0.3) + classification = classification.strip().lower() + logger.info(f"Klasifikácia dopytu: {classification}") + if "vyhládzanie" in classification or "vyhľadávanie" in classification: + return "vyhladavanie" + elif "upresnenie" in classification: + return "upresnenie" + return "vyhladavanie" ############################################################################### -# Main function: process_query_with_mistral (Slovak prompt) # +# Template for upresnenie dopytu ############################################################################### -def process_query_with_mistral(query, k=10): +def build_upresnenie_prompt_no_history(chat_history: str, user_query: str) -> str: + prompt = f""" +Ty si zdravotnícky expert. Máš k dispozícii históriu chatu a novú upresňujúcu otázku. + +Ak v histórii chatu už existuje jasná odpoveď na túto upresňujúcu otázku, napíš: +"FOUND_IN_HISTORY: <ľudský vysvetľajúci text>" + +Ak však v histórii chatu nie je dostatok informácií, napíš: +"NO_ANSWER_IN_HISTORY: " + +V časti zahrň kľúčové slová z pôvodnej otázky aj z upresnenia. + +=== ZAČIATOK HISTÓRIE CHatu === +{chat_history} +=== KONIEC HISTÓRIE CHatu === + +Upresňujúca otázka od používateľa: +"{user_query}" +""" + return prompt + +############################################################################### +# Function for retrieving the last vyhladavacieho dopytu z histórie +############################################################################### +def extract_last_vyhladavacie_query(chat_history: str) -> str: + lines = chat_history.splitlines() + last_query = "" + for line in reversed(lines): + if line.startswith("User:"): + last_query = line[len("User:"):].strip() + break + return last_query + +############################################################################### +# Agent class for data storage: vek, anamneza, predpis, user_data, search_query +############################################################################### +class ConversationalAgent: + def __init__(self): + self.long_term_memory = { + "vek": None, + "anamneza": None, + "predpis": None, + "user_data": None, + "search_query": None + } + + def update_memory(self, key, value): + self.long_term_memory[key] = value + + def get_memory(self, key): + return self.long_term_memory.get(key, None) + + def load_memory_from_history(self, chat_history: str): + memory_match = re.search(r"\[MEMORY\](.*?)\[/MEMORY\]", chat_history, re.DOTALL) + if memory_match: + try: + memory_data = json.loads(memory_match.group(1)) + self.long_term_memory.update(memory_data) + logger.info(f"Nahraná pamäť z histórie: {self.long_term_memory}") + except Exception as e: + logger.error(f"Chyba pri načítaní pamäte: {e}") + + def parse_user_info(self, query: str): + text_lower = query.lower() + if re.search(r"\d+\s*(rok(ov|y)?|years?)", text_lower): + self.update_memory("user_data", query) + age_match = re.search(r"(\d{1,3})\s*(rok(ov|y)?|years?)", text_lower) + if age_match: + self.update_memory("vek", age_match.group(1)) + if ("nemá" in text_lower or "nema" in text_lower) and ("chronické" in text_lower or "alerg" in text_lower): + self.update_memory("anamneza", "Žiadne chronické ochorenia ani alergie") + elif (("chronické" in text_lower or "alerg" in text_lower) and ("má" in text_lower or "ma" in text_lower)): + self.update_memory("anamneza", "Má chronické ochorenie alebo alergie (nespecifikované)") + if "voľnopredajný" in text_lower: + self.update_memory("predpis", "volnopredajny") + elif "na predpis" in text_lower: + self.update_memory("predpis", "na predpis") + + def analyze_input(self, query: str) -> dict: + self.parse_user_info(query) + missing_info = {} + if not self.get_memory("vek"): + missing_info["vek"] = "Prosím, uveďte vek pacienta." + if not self.get_memory("anamneza"): + missing_info["anamnéza"] = "Má pacient nejaké chronické ochorenia alebo alergie?" + if not self.get_memory("predpis"): + missing_info["predpis"] = "Ide o liek na predpis alebo voľnopredajný liek?" + return missing_info + + def ask_follow_up(self, missing_info: dict) -> str: + return " ".join(missing_info.values()) + +############################################################################### +# Main function process_query_with_mistral with updated logic +############################################################################### +CHAT_HISTORY_ENDPOINT = "http://localhost:5000/api/chat_history_detail" + +def process_query_with_mistral(query: str, chat_id: str, chat_context: str, k=10): logger.info("Processing query started.") - try: - # --- Vector search --- - vector_results = vectorstore.similarity_search(query, k=k) - vector_documents = [hit.metadata.get('text', '') for hit in vector_results] + + + chat_history = "" + if chat_context: + chat_history = chat_context + elif chat_id: + try: + params = {"id": chat_id} + r = requests.get(CHAT_HISTORY_ENDPOINT, params=params) + if r.status_code == 200: + data = r.json() + chat_data = data.get("chat", "") + if isinstance(chat_data, dict): + chat_history = chat_data.get("chat", "") + else: + chat_history = chat_data or "" + logger.info(f"História chatu načítaná pre chatId: {chat_id}") + else: + logger.warning(f"Nepodarilo sa načítať históriu (status {r.status_code}) pre chatId: {chat_id}") + except Exception as e: + logger.error(f"Chyba pri načítaní histórie: {e}") + + + agent = ConversationalAgent() + if chat_history: + agent.load_memory_from_history(chat_history) + + + existing_user_data = "" + if chat_id: + existing_user_data = get_user_data_from_db(chat_id) + + + agent.parse_user_info(query) + missing_info = agent.analyze_input(query) + + + if not existing_user_data: + + if "Prosím, uveďte vek pacienta" in chat_history: + if chat_id: + update_payload = {"chatId": chat_id, "userData": query} + try: + update_response = requests.post("http://localhost:5000/api/save_user_data", json=update_payload) + if update_response.status_code == 200: + logger.info("User data was successfully updated via endpoint /api/save_user_data (data question flag).") + else: + logger.warning(f"Failed to update data (data question flag): {update_response.text}") + except Exception as e: + logger.error(f"Error when updating user_data via endpoint (data question flag): {e}") + + if missing_info: + logger.info(f"Chýbajúce informácie: {missing_info}") + combined_missing_text = " ".join(missing_info.values()) + if query.strip() not in combined_missing_text: + if chat_id: + update_payload = {"chatId": chat_id, "userData": query} + try: + update_response = requests.post("http://localhost:5000/api/save_user_data", json=update_payload) + if update_response.status_code == 200: + logger.info("User data was successfully updated via endpoint /api/save_user_data.") + else: + logger.warning(f"Failed to update the data: {update_response.text}") + except Exception as e: + logger.error(f"Error when updating user_data via endpoint: {e}") + return { + "best_answer": combined_missing_text, + "model": "FollowUp (new chat)", + "rating": 0, + "explanation": "Additional data pre pokračovanie is required.", + "patient_data": query + } + + + qtype = classify_query(query, chat_history) + logger.info(f"Typ dopytu: {qtype}") + logger.info(f"Chat context (snippet): {chat_history[:200]}...") + + + if qtype == "vyhladavanie": + user_data_db = get_user_data_from_db(chat_id) + if user_data_db: + query = query + " Udaje cloveka: " + user_data_db + agent.long_term_memory["search_query"] = query + + + if qtype == "upresnenie": + original_search = agent.long_term_memory.get("search_query") + if not original_search: + original_search = extract_last_vyhladavacie_query(chat_history) + if original_search is None: + original_search = "" + combined_query = (original_search + " " + query).strip() + user_data_db = get_user_data_from_db(chat_id) + if user_data_db: + combined_query += " Udaje cloveka: " + user_data_db + logger.info(f"Combined query for search: {combined_query}") + + upres_prompt = build_upresnenie_prompt_no_history(chat_history, combined_query) + response_str = llm_small.generate_text(upres_prompt, max_tokens=1200, temperature=0.5) + normalized = response_str.strip() + logger.info(f"Upresnenie prompt response: {normalized}") + + if re.match(r"(?i)^found_in_history:\s*", normalized): + logger.info("Zistený FOUND_IN_HISTORY – vykonávame vyhľadávanie s kombinovaným dopytom.") + elif re.match(r"(?i)^no_answer_in_history:\s*", normalized): + parts = re.split(r"(?i)^no_answer_in_history:\s*", normalized, maxsplit=1) + if len(parts) >= 2: + combined_query = parts[1].strip() + logger.info(f"Upravený vyhľadávací dopyт z NO_ANSWER_IN_HISTORY: {combined_query}") + + vector_results = vectorstore.similarity_search(combined_query, k=k) max_docs = 5 - max_doc_length = 1000 - vector_documents = [doc[:max_doc_length] for doc in vector_documents[:max_docs]] - if vector_documents: - vector_prompt = ( - f"Otázka: '{query}'.\n" - "Na základe nasledujúcich informácií o liekoch:\n" - f"{vector_documents}\n\n" - "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. " - "Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, " - "aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). " - "Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. " - "Odpoveď musí byť v slovenčine." - ) - summary_small_vector = llm_small.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7) - summary_large_vector = llm_large.generate_text(prompt=vector_prompt, max_tokens=700, temperature=0.7) - splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20) - split_summary_small_vector = splitter.split_text(summary_small_vector) - split_summary_large_vector = splitter.split_text(summary_large_vector) - small_vector_eval = evaluate_results(query, split_summary_small_vector, 'Mistral Small') - large_vector_eval = evaluate_results(query, split_summary_large_vector, 'Mistral Large') - else: - small_vector_eval = {"rating": 0, "explanation": "No results"} - large_vector_eval = {"rating": 0, "explanation": "No results"} - summary_small_vector = "" - summary_large_vector = "" - - # --- Text search --- - es_results = vectorstore.client.search( - index=index_name, - body={"size": k, "query": {"match": {"text": query}}} + max_len = 1000 + vector_docs = [hit.metadata.get("text", "")[:max_len] for hit in vector_results[:max_docs]] + if not vector_docs: + return { + "best_answer": "Ľutujem, nenašli sa žiadne relevantné informácie.", + "model": "Upresnenie-NoResults", + "rating": 0, + "explanation": "No results from search." + } + joined_docs = "\n".join(vector_docs) + final_prompt = ( + f"Otázka: {combined_query}\n\n" + "Na základe týchto informácií:\n" + f"{joined_docs}\n\n" + "Vygeneruj odporúčanie liekov alebo vysvetlenie, ak je to relevantné." ) - text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']] - text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]] - if text_documents: - text_prompt = ( - f"Otázka: '{query}'.\n" - "Na základe nasledujúcich informácií o liekoch:\n" - f"{text_documents}\n\n" - "Prosím, uveďte tri najvhodnejšie lieky alebo riešenia pre daný problém. " - "Pre každý liek uveďte jeho názov, stručné a jasné vysvetlenie, prečo je vhodný, a ak je to relevantné, " - "aj odporúčané dávkovanie (napr. v gramoch alebo v iných vhodných jednotkách). " - "Odpovedajte priamo a ľudským, priateľským tónom v číslovanom zozname, bez nepotrebných úvodných fráz. " - "Odpoveď musí byť v slovenčine." - ) - summary_small_text = llm_small.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7) - summary_large_text = llm_large.generate_text(prompt=text_prompt, max_tokens=700, temperature=0.7) - splitter_text = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20) - split_summary_small_text = splitter_text.split_text(summary_small_text) - split_summary_large_text = splitter_text.split_text(summary_large_text) - small_text_eval = evaluate_results(query, split_summary_small_text, 'Mistral Small') - large_text_eval = evaluate_results(query, split_summary_large_text, 'Mistral Large') - else: - small_text_eval = {"rating": 0, "explanation": "No results"} - large_text_eval = {"rating": 0, "explanation": "No results"} - summary_small_text = "" - summary_large_text = "" - - # Porovnanie výsledkov a výber najlepšieho - all_results = [ - {"eval": small_vector_eval, "summary": summary_small_vector, "model": "Mistral Small Vector"}, - {"eval": large_vector_eval, "summary": summary_large_vector, "model": "Mistral Large Vector"}, - {"eval": small_text_eval, "summary": summary_small_text, "model": "Mistral Small Text"}, - {"eval": large_text_eval, "summary": summary_large_text, "model": "Mistral Large Text"}, + ans_small = llm_small.generate_text(final_prompt, max_tokens=1200, temperature=0.7) + ans_large = llm_large.generate_text(final_prompt, max_tokens=1200, temperature=0.7) + val_small = validate_answer_logic(combined_query, ans_small) + val_large = validate_answer_logic(combined_query, ans_large) + eval_small = evaluate_complete_answer(combined_query, val_small) + eval_large = evaluate_complete_answer(combined_query, val_large) + candidates = [ + {"summary": val_small, "eval": eval_small, "model": "Mistral Small"}, + {"summary": val_large, "eval": eval_large, "model": "Mistral Large"}, ] - best_result = max(all_results, key=lambda x: x["eval"]["rating"]) - logger.info(f"Best result from model {best_result['model']} with score {best_result['eval']['rating']}.") - - # Dodatočná kontrola logiky odpovede - validated_answer = validate_answer_logic(query, best_result["summary"]) - - - polished_answer = translate_preserving_medicine_names(validated_answer) + best = max(candidates, key=lambda x: x["eval"]["rating"]) + logger.info(f"Odpoveď od modelu {best['model']} má rating: {best['eval']['rating']}/10") + final_answer = translate_preserving_medicine_names(best["summary"]) + memory_json = json.dumps(agent.long_term_memory) + memory_block = f"[MEMORY]{memory_json}[/MEMORY]" + final_answer_with_memory = final_answer + "\n\n" return { - "best_answer": polished_answer, - "model": best_result["model"], - "rating": best_result["eval"]["rating"], - "explanation": best_result["eval"]["explanation"] - } - except Exception as e: - logger.error(f"Error: {str(e)}") - return { - "best_answer": "An error occurred during query processing.", - "error": str(e) + "best_answer": final_answer_with_memory, + "model": best["model"], + "rating": best["eval"]["rating"], + "explanation": best["eval"]["explanation"] } + + vector_results = vectorstore.similarity_search(query, k=k) + max_docs = 5 + max_len = 1000 + vector_docs = [hit.metadata.get("text", "")[:max_len] for hit in vector_results[:max_docs]] + if not vector_docs: + return { + "best_answer": "Ľutujem, nenašli sa žiadne relevantné informácie.", + "model": "Vyhladavanie-NoDocs", + "rating": 0, + "explanation": "No results" + } + joined_docs = "\n".join(vector_docs) + final_prompt = ( + f"Otázka: {query}\n\n" + "Na základe týchto informácií:\n" + f"{joined_docs}\n\n" + "Vygeneruj odporúčanie liekov alebo vysvetlenie, ak je to relevantné." + ) + answer = llm_small.generate_text(final_prompt, max_tokens=1200, temperature=0.7) + memory_json = json.dumps(agent.long_term_memory) + memory_block = f"[MEMORY]{memory_json}[/MEMORY]" + answer_with_memory = answer + "\n\n" + return { + "best_answer": answer_with_memory, + "model": "Vyhladavanie-Final", + "rating": 9, + "explanation": "Vyhľadávacia cesta" + } diff --git a/Backend/qwen72-test.py b/Backend/qwen72-test.py deleted file mode 100644 index b615116..0000000 --- a/Backend/qwen72-test.py +++ /dev/null @@ -1,77 +0,0 @@ -import torch -import logging -from transformers import AutoModelForCausalLM, AutoTokenizer -from elasticsearch import Elasticsearch - -# Настройка логирования -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -# Подключение к Elasticsearch -es = Elasticsearch( - ["https://localhost:9200"], - basic_auth=("elastic", "S7DoO3ma=G=9USBPbqq3"), # Ваш пароль - verify_certs=False -) -index_name = 'drug_docs' - -# Загрузка токенизатора и модели -model_name = "Qwen/Qwen2.5-7B-Instruct" -device = "cuda:0" if torch.cuda.is_available() else "cpu" - -model = AutoModelForCausalLM.from_pretrained( - model_name, - torch_dtype="auto", - device_map="auto" -) -tokenizer = AutoTokenizer.from_pretrained(model_name) - -# Проверка наличия pad_token -if tokenizer.pad_token is None: - tokenizer.pad_token = tokenizer.eos_token - -def text_search(query, k=10, max_doc_length=300, max_docs=3): - try: - es_results = es.search( - index=index_name, - body={"size": k, "query": {"match": {"text": query}}} - ) - text_documents = [hit['_source'].get('text', '') for hit in es_results['hits']['hits']] - text_documents = [doc[:max_doc_length] for doc in text_documents[:max_docs]] - return text_documents - except Exception as e: - logger.error(f"Ошибка поиска: {str(e)}") - return [] - -# Пример запроса для поиска -query = "čo piť pri horúčke" -text_documents = text_search(query) - -# Обрезаем текст, если он превышает предел токенов модели -max_tokens_per_input = 1024 # Установим более низкое значение для max_tokens -context_text = ' '.join(text_documents) -input_text = ( - f"Informácie o liekoch: {context_text[:max_tokens_per_input]}\n" - "Uveďte tri konkrétne lieky alebo riešenia s veľmi krátkym vysvetlením pre každý z nich.\n" - "Odpoveď v slovenčine:" -) - -# Токенизация входного текста -inputs = tokenizer(input_text, return_tensors="pt", max_length=max_tokens_per_input, truncation=True).to(device) - -try: - generated_ids = model.generate( - inputs.input_ids, - attention_mask=inputs.attention_mask, - max_new_tokens=300, # Снижено значение - temperature=0.7, - top_k=50, - top_p=0.9, - do_sample=False, # Отключено семплирование для детерминированного вывода - pad_token_id=tokenizer.pad_token_id - ) - response = tokenizer.decode(generated_ids[0], skip_special_tokens=True, errors='ignore') - print("Сгенерированный текст:", response) -except RuntimeError as e: - print(f"Произошла ошибка во время генерации: {e}") - diff --git a/Backend/qwen7b-test.py b/Backend/qwen7b-test.py deleted file mode 100644 index f407b87..0000000 --- a/Backend/qwen7b-test.py +++ /dev/null @@ -1,308 +0,0 @@ -"""A simple command-line interactive chat demo for Qwen2.5-Instruct model with left-padding using bos_token.""" - -import argparse -import os -import platform -import shutil -from copy import deepcopy -from threading import Thread - -import torch -from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer -from transformers.trainer_utils import set_seed - -DEFAULT_CKPT_PATH = "Qwen/Qwen2.5-7B-Instruct" - -_WELCOME_MSG = """\ -Welcome to use Qwen2.5-Instruct model, type text to start chat, type :h to show command help. -""" -_HELP_MSG = """\ -Commands: - :help / :h Show this help message - :exit / :quit / :q Exit the demo - :clear / :cl Clear screen - :clear-history / :clh Clear history - :history / :his Show history - :seed Show current random seed - :seed Set random seed to - :conf Show current generation config - :conf = Change generation config - :reset-conf Reset generation config -""" - -_ALL_COMMAND_NAMES = [ - "help", - "h", - "exit", - "quit", - "q", - "clear", - "cl", - "clear-history", - "clh", - "history", - "his", - "seed", - "conf", - "reset-conf", -] - - -def _setup_readline(): - try: - import readline - except ImportError: - return - - _matches = [] - - def _completer(text, state): - nonlocal _matches - - if state == 0: - _matches = [ - cmd_name for cmd_name in _ALL_COMMAND_NAMES if cmd_name.startswith(text) - ] - if 0 <= state < len(_matches): - return _matches[state] - return None - - readline.set_completer(_completer) - readline.parse_and_bind("tab: complete") - - -def _load_model_tokenizer(args): - tokenizer = AutoTokenizer.from_pretrained( - args.checkpoint_path, - resume_download=True, - ) - - # Set bos_token for left-padding - if tokenizer.pad_token is None: - tokenizer.pad_token = tokenizer.bos_token - - device_map = "cpu" if args.cpu_only else "auto" - - model = AutoModelForCausalLM.from_pretrained( - args.checkpoint_path, - torch_dtype="auto", - device_map=device_map, - resume_download=True, - ).eval() - - # Conservative generation config - model.generation_config.max_new_tokens = 256 - model.generation_config.temperature = 0.7 - model.generation_config.top_k = 50 - model.generation_config.top_p = 0.9 - model.generation_config.pad_token_id = tokenizer.pad_token_id - model.generation_config.eos_token_id = tokenizer.eos_token_id - model.generation_config.do_sample = False - - return model, tokenizer - - -def _gc(): - import gc - - gc.collect() - if torch.cuda.is_available(): - torch.cuda.empty_cache() - - -def _clear_screen(): - if platform.system() == "Windows": - os.system("cls") - else: - os.system("clear") - - -def _print_history(history): - terminal_width = shutil.get_terminal_size()[0] - print(f"History ({len(history)})".center(terminal_width, "=")) - for index, (query, response) in enumerate(history): - print(f"User[{index}]: {query}") - print(f"Qwen[{index}]: {response}") - print("=" * terminal_width) - - -def _get_input() -> str: - while True: - try: - message = input("User> ").strip() - except UnicodeDecodeError: - print("[ERROR] Encoding error in input") - continue - except KeyboardInterrupt: - exit(1) - if message: - return message - print("[ERROR] Query is empty") - - -def _chat_stream(model, tokenizer, query, history): - conversation = [] - for query_h, response_h in history: - conversation.append({"role": "user", "content": query_h}) - conversation.append({"role": "assistant", "content": response_h}) - conversation.append({"role": "user", "content": query}) - input_text = tokenizer.apply_chat_template( - conversation, - add_generation_prompt=True, - tokenize=False, - ) - # Perform left-padding with bos_token - inputs = tokenizer( - [input_text], - return_tensors="pt", - padding="longest", - truncation=True, - pad_to_multiple_of=8, - max_length=1024, - add_special_tokens=False - ).to(model.device) - - # Update attention_mask for left-padding compatibility - inputs["attention_mask"] = inputs["attention_mask"].flip(dims=[1]) - - streamer = TextIteratorStreamer( - tokenizer=tokenizer, skip_prompt=True, timeout=60.0, skip_special_tokens=True - ) - generation_kwargs = { - **inputs, - "streamer": streamer, - } - thread = Thread(target=model.generate, kwargs=generation_kwargs) - thread.start() - - for new_text in streamer: - yield new_text - - -def main(): - parser = argparse.ArgumentParser( - description="Qwen2.5-Instruct command-line interactive chat demo." - ) - parser.add_argument( - "-c", - "--checkpoint-path", - type=str, - default=DEFAULT_CKPT_PATH, - help="Checkpoint name or path, default to %(default)r", - ) - parser.add_argument("-s", "--seed", type=int, default=1234, help="Random seed") - parser.add_argument( - "--cpu-only", action="store_true", help="Run demo with CPU only" - ) - args = parser.parse_args() - - history, response = [], "" - - model, tokenizer = _load_model_tokenizer(args) - orig_gen_config = deepcopy(model.generation_config) - - _setup_readline() - - _clear_screen() - print(_WELCOME_MSG) - - seed = args.seed - - while True: - query = _get_input() - - # Process commands. - if query.startswith(":"): - command_words = query[1:].strip().split() - if not command_words: - command = "" - else: - command = command_words[0] - - if command in ["exit", "quit", "q"]: - break - elif command in ["clear", "cl"]: - _clear_screen() - print(_WELCOME_MSG) - _gc() - continue - elif command in ["clear-history", "clh"]: - print(f"[INFO] All {len(history)} history cleared") - history.clear() - _gc() - continue - elif command in ["help", "h"]: - print(_HELP_MSG) - continue - elif command in ["history", "his"]: - _print_history(history) - continue - elif command in ["seed"]: - if len(command_words) == 1: - print(f"[INFO] Current random seed: {seed}") - continue - else: - new_seed_s = command_words[1] - try: - new_seed = int(new_seed_s) - except ValueError: - print( - f"[WARNING] Fail to change random seed: {new_seed_s!r} is not a valid number" - ) - else: - print(f"[INFO] Random seed changed to {new_seed}") - seed = new_seed - continue - elif command in ["conf"]: - if len(command_words) == 1: - print(model.generation_config) - else: - for key_value_pairs_str in command_words[1:]: - eq_idx = key_value_pairs_str.find("=") - if eq_idx == -1: - print("[WARNING] format: =") - continue - conf_key, conf_value_str = ( - key_value_pairs_str[:eq_idx], - key_value_pairs_str[eq_idx + 1 :], - ) - try: - conf_value = eval(conf_value_str) - except Exception as e: - print(e) - continue - else: - print( - f"[INFO] Change config: model.generation_config.{conf_key} = {conf_value}" - ) - setattr(model.generation_config, conf_key, conf_value) - continue - elif command in ["reset-conf"]: - print("[INFO] Reset generation config") - model.generation_config = deepcopy(orig_gen_config) - print(model.generation_config) - continue - - # Run chat. - set_seed(seed) - _clear_screen() - print(f"\nUser: {query}") - print(f"\nQwen: ", end="") - try: - partial_text = "" - for new_text in _chat_stream(model, tokenizer, query, history): - print(new_text, end="", flush=True) - partial_text += new_text - response = partial_text - print() - - except KeyboardInterrupt: - print("[WARNING] Generation interrupted") - continue - - history.append((query, response)) - - -if __name__ == "__main__": - main() - diff --git a/Backend/requirements.txt b/Backend/requirements.txt index 18d0bb7..ac9d103 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -189,3 +189,8 @@ wrapt==1.16.0 xformers==0.0.28.post1 yarl==1.9.4 zipp==3.20.2 +flask +flask-cors +psycopg2-binary +google-auth + diff --git a/Backend/server.py b/Backend/server.py index 4583644..471ec38 100644 --- a/Backend/server.py +++ b/Backend/server.py @@ -1,50 +1,76 @@ import time import re - -# Сохраняем оригинальную функцию time.time -_real_time = time.time -# Переопределяем time.time для смещения времени на 1 секунду назад -time.time = lambda: _real_time() - 1 - from flask import Flask, request, jsonify from flask_cors import CORS from google.oauth2 import id_token -from google.auth.transport import requests +from google.auth.transport import requests as google_requests import logging - -# Импортируем функцию обработки из model.py -from model import process_query_with_mistral import psycopg2 from psycopg2.extras import RealDictCursor +from model import process_query_with_mistral -# Параметры подключения к базе данных +_real_time = time.time +time.time = lambda: _real_time() - 1 + +# Database connection parameters DATABASE_CONFIG = { - "dbname": "postgres", + "dbname": "HealthAIDB", "user": "postgres", - "password": "healthai!", - "host": "health-ai-user-db.cxeum6cmct3r.eu-west-1.rds.amazonaws.com", + "password": "Oleg2005", + "host": "postgres", "port": 5432, } -# Подключение к базе данных -try: - conn = psycopg2.connect(**DATABASE_CONFIG) - print("Подключение к базе данных успешно установлено") -except Exception as e: - print(f"Ошибка подключения к базе данных: {e}") - conn = None - +import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Создаем Flask приложение -app = Flask(__name__) -CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) +try: + conn = psycopg2.connect(**DATABASE_CONFIG) + logger.info("Database connection established successfully") +except Exception as e: + logger.error(f"Error connecting to database: {e}", exc_info=True) + conn = None + +def init_db(): + create_users_query = """ + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + google_id TEXT, + password TEXT + ); + """ + create_chat_history_query = """ + CREATE TABLE IF NOT EXISTS chat_history ( + id SERIAL PRIMARY KEY, + user_email TEXT NOT NULL, + chat TEXT NOT NULL, + user_data TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """ + try: + with conn.cursor() as cur: + cur.execute(create_users_query) + cur.execute(create_chat_history_query) + conn.commit() + logger.info("Database tables initialized successfully") + except Exception as e: + logger.error(f"Error initializing database tables: {e}", exc_info=True) + conn.rollback() + +if conn: + init_db() + +app = Flask(__name__) +CORS(app, resources={r"/api/*": {"origins": "*"}}) -# Ваш Google Client ID CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com" def save_user_to_db(name, email, google_id=None, password=None): + logger.info(f"Saving user {name} with email: {email} to the database") try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( @@ -56,94 +82,115 @@ def save_user_to_db(name, email, google_id=None, password=None): (name, email, google_id, password) ) conn.commit() - print(f"User {name} ({email}) saved successfully!") + logger.info(f"User {name} ({email}) saved successfully") except Exception as e: - print(f"Error saving user to database: {e}") + logger.error(f"Error saving user {name} ({email}) to database: {e}", exc_info=True) -# Эндпоинт для верификации токенов Google OAuth @app.route('/api/verify', methods=['POST']) def verify_token(): + logger.info("Received token verification request") data = request.get_json() token = data.get('token') if not token: + logger.warning("Token not provided in request") return jsonify({'error': 'No token provided'}), 400 try: - id_info = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID) + id_info = id_token.verify_oauth2_token(token, google_requests.Request(), CLIENT_ID) user_email = id_info.get('email') user_name = id_info.get('name') - google_id = id_info.get('sub') # Уникальный идентификатор пользователя Google + google_id = id_info.get('sub') + logger.info(f"Token verified for user: {user_name} ({user_email})") save_user_to_db(name=user_name, email=user_email, google_id=google_id) - logger.info(f"User authenticated and saved: {user_name} ({user_email})") return jsonify({'message': 'Authentication successful', 'user': {'email': user_email, 'name': user_name}}), 200 except ValueError as e: - logger.error(f"Token verification failed: {e}") + logger.error(f"Token verification error: {e}", exc_info=True) return jsonify({'error': 'Invalid token'}), 400 -# Эндпоинт для регистрации пользователя с проверкой на дублирование @app.route('/api/register', methods=['POST']) def register(): + logger.info("Received new user registration request") data = request.get_json() name = data.get('name') email = data.get('email') - password = data.get('password') # Рекомендуется хэшировать пароль + password = data.get('password') if not all([name, email, password]): + logger.warning("Not all required fields provided for registration") return jsonify({'error': 'All fields are required'}), 400 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT * FROM users WHERE email = %s", (email,)) existing_user = cur.fetchone() if existing_user: + logger.warning(f"User with email {email} already exists") return jsonify({'error': 'User already exists'}), 409 save_user_to_db(name=name, email=email, password=password) + logger.info(f"User {name} ({email}) registered successfully") return jsonify({'message': 'User registered successfully'}), 201 except Exception as e: + logger.error(f"Error during user registration: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 -# Эндпоинт для логина пользователя @app.route('/api/login', methods=['POST']) def login(): + logger.info("Received login request") data = request.get_json() email = data.get('email') password = data.get('password') if not all([email, password]): + logger.warning("Email or password not provided") return jsonify({'error': 'Email and password are required'}), 400 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT * FROM users WHERE email = %s", (email,)) user = cur.fetchone() - if not user: - return jsonify({'error': 'Invalid credentials'}), 401 - if user.get('password') != password: + if not user or user.get('password') != password: + logger.warning(f"Invalid credentials for email: {email}") return jsonify({'error': 'Invalid credentials'}), 401 + logger.info(f"User {user.get('name')} ({email}) logged in successfully") return jsonify({'message': 'Login successful', 'user': {'name': user.get('name'), 'email': user.get('email')}}), 200 except Exception as e: + logger.error(f"Error during user login: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 -# Объединённый эндпоинт для обработки запроса чата @app.route('/api/chat', methods=['POST']) def chat(): + logger.info("Received chat request") data = request.get_json() query = data.get('query', '') - user_email = data.get('email') # email пользователя (если передается) - chat_id = data.get('chatId') # параметр для обновления существующего чата + user_email = data.get('email') + chat_id = data.get('chatId') if not query: + logger.warning("No query provided") return jsonify({'error': 'No query provided'}), 400 - # Вызов функции для обработки запроса (например, чат-бота) - response_obj = process_query_with_mistral(query) - best_answer = "" - if isinstance(response_obj, dict): - best_answer = response_obj.get("best_answer", "") - else: - best_answer = str(response_obj) + logger.info(f"Processing request for chatId: {chat_id if chat_id else 'new chat'} | Query: {query}") + + # Retrieve chat context from the database + chat_context = "" + if chat_id: + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute("SELECT chat, user_data FROM chat_history WHERE id = %s", (chat_id,)) + result = cur.fetchone() + if result: + chat_context = result.get("chat", "") + logger.info(f"Loaded chat context for chatId {chat_id}: {chat_context}") + else: + logger.info(f"No chat context found for chatId {chat_id}") + except Exception as e: + logger.error(f"Error loading chat context from DB: {e}", exc_info=True) + + logger.info("Calling process_query_with_mistral function") + response_obj = process_query_with_mistral(query, chat_id=chat_id, chat_context=chat_context) + best_answer = response_obj.get("best_answer", "") if isinstance(response_obj, dict) else str(response_obj) + logger.info(f"Response from process_query_with_mistral: {best_answer}") - # Форматирование ответа с использованием re.sub best_answer = re.sub(r'[*#]', '', best_answer) best_answer = re.sub(r'(\d\.\s)', r'\n\n\1', best_answer) best_answer = re.sub(r':\s-', r':\n-', best_answer) - # Если chatId передан, обновляем существующий чат, иначе создаем новый чат + # Update or create chat_history record including user_data if available if chat_id: try: with conn.cursor(cursor_factory=RealDictCursor) as cur: @@ -151,8 +198,13 @@ def chat(): existing_chat = cur.fetchone() if existing_chat: updated_chat = existing_chat['chat'] + f"\nUser: {query}\nBot: {best_answer}" - cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id)) + if "patient_data" in response_obj: + cur.execute("UPDATE chat_history SET chat = %s, user_data = %s WHERE id = %s", + (updated_chat, response_obj["patient_data"], chat_id)) + else: + cur.execute("UPDATE chat_history SET chat = %s WHERE id = %s", (updated_chat, chat_id)) conn.commit() + logger.info(f"Chat history for chatId {chat_id} updated successfully") else: with conn.cursor(cursor_factory=RealDictCursor) as cur2: cur2.execute( @@ -162,7 +214,9 @@ def chat(): new_chat_id = cur2.fetchone()['id'] conn.commit() chat_id = new_chat_id + logger.info(f"New chat created with chatId: {chat_id}") except Exception as e: + logger.error(f"Error updating/creating chat history: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 else: try: @@ -174,45 +228,98 @@ def chat(): new_chat_id = cur.fetchone()['id'] conn.commit() chat_id = new_chat_id + logger.info(f"New chat created with chatId: {chat_id}") except Exception as e: + logger.error(f"Error creating new chat: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 - # Возвращаем текстовый ответ и новый chatId, если чат был создан - return jsonify({'response': {'best_answer': best_answer, 'model': 'Mistral Small Vector', 'chatId': chat_id}}), 200 + return jsonify({'response': {'best_answer': best_answer, 'model': response_obj.get("model", ""), 'chatId': chat_id}}), 200 + +@app.route('/api/save_user_data', methods=['POST']) +def save_user_data(): + logger.info("Received request to save user data") + data = request.get_json() + chat_id = data.get('chatId') + user_data = data.get('userData') + if not chat_id or not user_data: + return jsonify({'error': 'chatId and userData are required'}), 400 + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute("UPDATE chat_history SET user_data = %s WHERE id = %s", (user_data, chat_id)) + conn.commit() + logger.info(f"User data for chatId {chat_id} updated successfully") + return jsonify({'message': 'User data updated successfully'}), 200 + except Exception as e: + logger.error(f"Error updating user data: {e}", exc_info=True) + return jsonify({'error': str(e)}), 500 -# Эндпоинт для получения истории чатов конкретного пользователя @app.route('/api/chat_history', methods=['GET']) def get_chat_history(): + logger.info("Received request to get chat history") user_email = request.args.get('email') if not user_email: return jsonify({'error': 'User email is required'}), 400 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( - "SELECT id, chat, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC", + "SELECT id, chat, user_data, created_at FROM chat_history WHERE user_email = %s ORDER BY created_at DESC", (user_email,) ) history = cur.fetchall() return jsonify({'history': history}), 200 except Exception as e: + logger.error(f"Error getting chat history for {user_email}: {e}", exc_info=True) + return jsonify({'error': str(e)}), 500 + +@app.route('/api/chat_history', methods=['DELETE']) +def delete_chat(): + logger.info("Received request to delete chat") + chat_id = request.args.get('id') + if not chat_id: + return jsonify({'error': 'Chat id is required'}), 400 + try: + with conn.cursor() as cur: + cur.execute("DELETE FROM chat_history WHERE id = %s", (chat_id,)) + conn.commit() + return jsonify({'message': 'Chat deleted successfully'}), 200 + except Exception as e: + logger.error(f"Error deleting chat with chatId {chat_id}: {e}", exc_info=True) + return jsonify({'error': str(e)}), 500 + +@app.route('/api/get_user_data', methods=['GET']) +def get_user_data(): + chat_id = request.args.get('chatId') + if not chat_id: + return jsonify({'error': 'Chat id is required'}), 400 + try: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute("SELECT user_data FROM chat_history WHERE id = %s", (chat_id,)) + result = cur.fetchone() + if result and result.get("user_data"): + return jsonify({'user_data': result.get("user_data")}), 200 + else: + return jsonify({'user_data': None}), 200 + except Exception as e: + logger.error(f"Error retrieving user data: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 -# Эндпоинт для получения деталей чата по ID @app.route('/api/chat_history_detail', methods=['GET']) def chat_history_detail(): + logger.info("Received request to get chat details") chat_id = request.args.get('id') if not chat_id: return jsonify({'error': 'Chat id is required'}), 400 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: - cur.execute("SELECT id, chat, created_at FROM chat_history WHERE id = %s", (chat_id,)) + cur.execute("SELECT id, chat, user_data, created_at FROM chat_history WHERE id = %s", (chat_id,)) chat = cur.fetchone() if not chat: return jsonify({'error': 'Chat not found'}), 404 return jsonify({'chat': chat}), 200 except Exception as e: + logger.error(f"Error getting chat details for chatId {chat_id}: {e}", exc_info=True) return jsonify({'error': str(e)}), 500 if __name__ == '__main__': + logger.info("Starting Flask application") app.run(host='0.0.0.0', port=5000, debug=True) - diff --git a/Backend/test_slovakbert-skquad.py b/Backend/test_slovakbert-skquad.py index ff8a0cd..08c7db2 100644 --- a/Backend/test_slovakbert-skquad.py +++ b/Backend/test_slovakbert-skquad.py @@ -1,19 +1,18 @@ from sentence_transformers import SentenceTransformer, util -# Загрузка модели из Hugging Face -model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr") # Замените на ID нужной модели -# Пример предложений на словацком языке +model = SentenceTransformer("TUKE-DeutscheTelekom/slovakbert-skquad-mnlr") + + sentences = [ "Prvý most cez Zlatý roh nechal vybudovať cisár Justinián I. V roku 1502 vypísal sultán Bajezid II. súťaž na nový most.", "V ktorom roku vznikol druhý drevený most cez záliv Zlatý roh?", "Aká je priemerná dĺžka života v Eritrei?" ] -# Получение эмбеддингов для каждого предложения embeddings = model.encode(sentences) -print("Shape of embeddings:", embeddings.shape) # Вывод формы эмбеддингов, например (3, 768) +print("Shape of embeddings:", embeddings.shape) + -# Вычисление сходства между предложениями similarities = util.cos_sim(embeddings, embeddings) print("Similarity matrix:\n", similarities) diff --git a/Backend/wait-for-elasticsearch.sh b/Backend/wait-for-elasticsearch.sh index 85069c2..cd6e8bc 100644 --- a/Backend/wait-for-elasticsearch.sh +++ b/Backend/wait-for-elasticsearch.sh @@ -1,13 +1,11 @@ #!/bin/sh -echo "Ожидание готовности Elasticsearch..." +echo "Waiting for Elasticsearch..." -# Проверяем доступность Elasticsearch while ! curl -s http://elasticsearch:9200 > /dev/null; do sleep 5 done -echo "Elasticsearch готов. Запуск бэкенда..." +echo "Elasticsearch is ready. Starting backend..." -# Запускаем бэкенд exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index 51bbf86..f62629a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,20 @@ services: retries: 5 start_period: 40s + postgres: + image: postgres:14 + container_name: postgres_db + environment: + - POSTGRES_DB=HealthAIDB + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=Oleg2005 + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - app-network + backend: container_name: backend_container build: @@ -30,9 +44,12 @@ services: - "5000:5000" environment: - ELASTICSEARCH_HOST=http://elasticsearch:9200 + - DATABASE_HOST=postgres depends_on: elasticsearch: condition: service_healthy + postgres: + condition: service_started networks: - app-network @@ -42,7 +59,7 @@ services: context: ./frontend dockerfile: Dockerfile ports: - - "3000:3000" + - "5173:5173" depends_on: - backend networks: @@ -51,3 +68,6 @@ services: networks: app-network: driver: bridge + +volumes: + pgdata: diff --git a/elasticsearch/Dockerfile b/elasticsearch/Dockerfile index 8acbcb2..756ad21 100644 --- a/elasticsearch/Dockerfile +++ b/elasticsearch/Dockerfile @@ -1,15 +1,15 @@ FROM docker.elastic.co/elasticsearch/elasticsearch:8.14.3 -# Отключаем безопасность для упрощения доступа + ENV discovery.type=single-node ENV xpack.security.enabled=false -# Копируем проиндексированные данные в директорию данных Elasticsearch + COPY --chown=elasticsearch:elasticsearch data/ /usr/share/elasticsearch/data -# Устанавливаем права доступа + RUN chmod -R 0775 /usr/share/elasticsearch/data -# Удаляем файлы блокировок (добавьте эти команды) + RUN find /usr/share/elasticsearch/data -type f -name "*.lock" -delete RUN rm -f /usr/share/elasticsearch/data/nodes/0/node.lock diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 0f34e59..5b4a0bd 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,26 +1,26 @@ -# Используем базовый образ Node.js + FROM node:18-alpine -# Устанавливаем рабочую директорию + WORKDIR /app -# Копируем package.json и package-lock.json + COPY package*.json ./ -# Устанавливаем зависимости + RUN npm install -# Копируем файлы проекта + COPY . . -# Сборка приложения + RUN npm run build -# Устанавливаем сервер для обслуживания статических файлов + RUN npm install -g serve -# Открываем порт -EXPOSE 3000 -# Запуск фронтенда -CMD ["serve", "-s", "dist", "-l", "3000"] +EXPOSE 5173 + + +CMD ["serve", "-s", "dist", "-l", "5173"] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 408819a..e53c710 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,14 @@ import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom'; import Navigation from './Components/Navigation'; -import LandingPage from './pages/LandingPage'; +import {Home} from './pages/LandingPage'; import RegistrationForm from "./Components/RegistrationForm"; import LoginForm from "./Components/LoginForm"; import ChatHistory from "./Components/ChatHistory"; import HomePage from './pages/HomePage'; import NewChatPage from "./Components/NewChatPage"; +import About from "./Components/About.tsx"; +import Contact from "./Components/Contact.tsx"; +import Profile from "./Components/Profile.tsx"; const Layout = () => (
@@ -22,13 +25,14 @@ function App() { return ( - } /> + } /> } /> } /> + } /> + } /> + } /> }> - {/* Новый чат */} } /> - {/* Существующий чат (после создания нового, URL обновится) */} } /> } /> } /> diff --git a/frontend/src/Components/About.tsx b/frontend/src/Components/About.tsx new file mode 100644 index 0000000..1605479 --- /dev/null +++ b/frontend/src/Components/About.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Typography, Grid, Paper } from '@mui/material'; +import {Navbar} from '../pages/LandingPage'; +import MedicalServicesIcon from '@mui/icons-material/MedicalServices'; +import LocalHospitalIcon from '@mui/icons-material/LocalHospital'; +import CodeIcon from '@mui/icons-material/Code'; + +const About: React.FC = () => { + const [user, setUser] = useState(null); + + useEffect(() => { + const storedUser = localStorage.getItem('user'); + if (storedUser) { + setUser(JSON.parse(storedUser)); + } + }, []); + + return ( + + {/* Navigation bar */} + + + {/* Main content with top padding to account for fixed Navbar */} + + + About Health AI + + + Your Personal AI Assistant for Tailored Drug Recommendations + + + {/* Project Information Card */} + + + + + + About the Project + + + + Health AI is a cutting-edge application specializing in providing personalized drug recommendations and medication advice. + Leveraging advanced AI models like Mistral and powerful search technologies such as Elasticsearch, our platform delivers accurate, + context-aware suggestions for both over-the-counter and prescription medications. + + + Our backend utilizes modern technologies including Flask, PostgreSQL, and Google OAuth, ensuring robust security and reliable performance. + We also use long-term conversational memory to continuously enhance our responses. + + + + {/* How It Works Card */} + + + + + + How It Works + + + + Our system uses natural language processing to understand user queries and extract key details such as age, medical history, + and medication type. It then employs vector search techniques to fetch the most relevant information from a comprehensive drug database, + ensuring precise recommendations. + + + Health AI validates its responses to guarantee consistency and reliability, making it an innovative solution for personalized healthcare guidance. + + + + {/* Future Enhancements Card */} + + + + + + What's Next? + + + + We are continuously improving Health AI by integrating additional data sources and refining our AI algorithms. + Future enhancements include real-time drug interaction checks, comprehensive patient monitoring, + and seamless integration with healthcare providers. Stay tuned for more exciting updates and features as we strive to make healthcare more accessible and efficient. + + + + + {/* Footer */} + + + © {new Date().getFullYear()} Health AI. All rights reserved. + + + + + ); +}; + +export default About; diff --git a/frontend/src/Components/ChatDetails.tsx b/frontend/src/Components/ChatDetails.tsx index 26977ec..252b4cd 100644 --- a/frontend/src/Components/ChatDetails.tsx +++ b/frontend/src/Components/ChatDetails.tsx @@ -15,7 +15,6 @@ const ChatDetails: React.FC = () => { useEffect(() => { if (!chat && id) { - // Если данные не переданы через state, можно попробовать получить их с сервера fetch(`http://localhost:5000/api/chat_history_detail?id=${encodeURIComponent(id)}`) .then((res) => { if (!res.ok) { diff --git a/frontend/src/Components/ChatHistory.tsx b/frontend/src/Components/ChatHistory.tsx index 4911f01..6708563 100644 --- a/frontend/src/Components/ChatHistory.tsx +++ b/frontend/src/Components/ChatHistory.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Box, Typography, Paper, IconButton } from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; interface ChatHistoryItem { id: number; @@ -32,48 +34,124 @@ const ChatHistory: React.FC = () => { } }, []); - // При клике перенаправляем пользователя на /dashboard/chat/{chatId} const handleClick = (item: ChatHistoryItem) => { navigate(`/dashboard/chat/${item.id}`, { state: { selectedChat: item } }); }; + const handleDelete = (chatId: number) => { + if (window.confirm('Are you sure that you want to delete that chat?')) { + fetch(`http://localhost:5000/api/chat_history?id=${chatId}`, { + method: 'DELETE' + }) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + setError(data.error); + } else { + setHistory(history.filter((chat) => chat.id !== chatId)); + } + }) + .catch(() => setError('Error deleting chat')); + } + }; + return ( -
-

Chat History

- {error &&

{error}

} - {history.length === 0 && !error ? ( -

No chat history found.

+ + + Chat History + + {error ? ( + + {error} + ) : ( -
    - {history.map((item) => { - // Извлекаем первую строку из сохранённого чата. - // Предполагаем, что чат хранится в формате: "User: <вопрос>\nBot: <ответ>\n..." - const lines = item.chat.split("\n"); - let firstUserMessage = lines[0]; - if (firstUserMessage.startsWith("User:")) { - firstUserMessage = firstUserMessage.replace("User:", "").trim(); - } - return ( -
  • handleClick(item)} - > -
    - {firstUserMessage} -
    - {new Date(item.created_at).toLocaleString()} -
  • - ); - })} -
+ + {history.length === 0 ? ( + + No chat history found. + + ) : ( + history.map((item) => { + const lines = item.chat.split("\n"); + let firstUserMessage = lines[0]; + if (firstUserMessage.startsWith("User:")) { + firstUserMessage = firstUserMessage.replace("User:", "").trim(); + } + return ( + + handleClick(item)} + > + + {firstUserMessage} + + + {new Date(item.created_at).toLocaleString()} + + + { + e.stopPropagation(); + handleDelete(item.id); + }} + sx={{ color: '#d32f2f' }} + > + + + + ); + }) + )} + )} -
+ ); }; diff --git a/frontend/src/Components/Contact.tsx b/frontend/src/Components/Contact.tsx new file mode 100644 index 0000000..4ac3622 --- /dev/null +++ b/frontend/src/Components/Contact.tsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Typography, Paper, Grid } from '@mui/material'; +import {Navbar} from '../pages/LandingPage'; +import SchoolIcon from '@mui/icons-material/School'; +import DeveloperModeIcon from '@mui/icons-material/DeveloperMode'; +import EmailIcon from '@mui/icons-material/Email'; + +const Contact: React.FC = () => { + const [user, setUser] = useState(null); + + useEffect(() => { + const storedUser = localStorage.getItem('user'); + if (storedUser) { + setUser(JSON.parse(storedUser)); + } + }, []); + + return ( + + {/* Navbar with navigation links */} + + + {/* Main content with spacing for fixed Navbar */} + + + + Contact + + + {/* University Info */} + + + + + + Technical University of Košice + + + KEMT Department + + + + + {/* Developer Info */} + + + + + + Developer + + + oleh.poiasnik@student.tuke.sk + + + + + {/* Additional Contact Option */} + + + + + + Email Us + + + For any inquiries or further information about Health AI, please get in touch! + + + + + + + + © {new Date().getFullYear()} Health AI. All rights reserved. + + + + + + ); +}; + +export default Contact; diff --git a/frontend/src/Components/EatingForm.tsx b/frontend/src/Components/EatingForm.tsx deleted file mode 100644 index ad90f4b..0000000 --- a/frontend/src/Components/EatingForm.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { Form, Field } from 'react-final-form'; - -interface FormValues { - healthGoal: string; - dietType?: string; - exerciseLevel?: string; - hydrationGoal?: string; - userInput: string; -} - -const EatingForm: React.FC = () => { - const onSubmit = (values: FormValues) => { - console.log('Form values:', values); - }; - - return ( - - onSubmit={onSubmit} - render={({ handleSubmit, form }) => { - const healthGoal = form.getFieldState("healthGoal")?.value; - - return ( -
-

Select Your Health Goal

- - {/* Health Goal Selection */} -
- - name="healthGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - - - -
- - {/* Dynamic Fields Based on Health Goal */} - {healthGoal === 'weight_loss' && ( -
- - name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - - -
- )} - - {healthGoal === 'muscle_gain' && ( -
- - name="exerciseLevel" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - -
- )} - - {healthGoal === 'improve_energy' && ( -
- - name="hydrationGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - -
- )} - - {/* User Input */} -
- - - name="userInput" - component="input" - type="text" - placeholder="Enter your preferences or comments" - className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500" - /> -
- - -
- ); - }} - /> - ); -}; - -export default EatingForm; diff --git a/frontend/src/Components/Human.tsx b/frontend/src/Components/Human.tsx deleted file mode 100644 index ac6fefe..0000000 --- a/frontend/src/Components/Human.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React, { useState } from 'react'; - -export type Muscle = 'neck' | 'chest' | 'biceps' | 'forearms' | 'quadriceps' | 'calves' | 'abs' | 'shoulders' | 'trapezius'; - -const MuscleDiagram: React.FC = () => { - const [highlightedMuscle, setHighlightedMuscle] = useState(null); - - const handleMouseEnter = (muscle: Muscle) => { - setHighlightedMuscle(muscle); - }; - - const handleMouseLeave = () => { - setHighlightedMuscle(null); - }; - - // const handleMuscleClick = (muscle: Muscle) => { - // - // } - - const getDarkerColor = (baseColor: string) => { - const colorValue = parseInt(baseColor.slice(1), 16); - const r = Math.max((colorValue >> 16) - 20, 0); - const g = Math.max(((colorValue >> 8) & 0x00ff) - 20, 0); - const b = Math.max((colorValue & 0x0000ff) - 20, 0); - return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`; - }; - - const baseColor = "#f1c27d"; - - return ( -
- - {/* Голова */} - - - {/* Шея */} - handleMouseEnter('neck')} - onMouseLeave={handleMouseLeave} - /> - - {/* Трапеции */} - handleMouseEnter('trapezius')} - onMouseLeave={handleMouseLeave} - /> - - {/* Грудные мышцы */} - handleMouseEnter('chest')} - onMouseLeave={handleMouseLeave} - /> - - {/* Плечи */} - handleMouseEnter('shoulders')} - onMouseLeave={handleMouseLeave} - /> - handleMouseEnter('shoulders')} - onMouseLeave={handleMouseLeave} - /> - - {/* Бицепсы */} - - handleMouseEnter('biceps')} - onMouseLeave={handleMouseLeave} - /> - - - handleMouseEnter('biceps')} - onMouseLeave={handleMouseLeave} - /> - - - {/* Предплечья */} - - handleMouseEnter('forearms')} - onMouseLeave={handleMouseLeave} - /> - - - handleMouseEnter('forearms')} - onMouseLeave={handleMouseLeave} - /> - - - {/* Пресс */} - handleMouseEnter('abs')} - onMouseLeave={handleMouseLeave} - /> - - {/* Квадрицепсы */} - - handleMouseEnter('quadriceps')} - onMouseLeave={handleMouseLeave} - /> - - - handleMouseEnter('quadriceps')} - onMouseLeave={handleMouseLeave} - /> - - - {/* Икроножные мышцы */} - - handleMouseEnter('calves')} - onMouseLeave={handleMouseLeave} - /> - - - handleMouseEnter('calves')} - onMouseLeave={handleMouseLeave} - /> - - - - {/* Отображение названия мышечной группы */} -
- {highlightedMuscle - ? `Выделено: ${highlightedMuscle}` - : 'Наведите на мышцу, чтобы увидеть название'} -
-
- ); -}; - -export default MuscleDiagram; diff --git a/frontend/src/Components/Human2d.tsx b/frontend/src/Components/Human2d.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/Components/LoginForm.tsx b/frontend/src/Components/LoginForm.tsx index 6c22c76..63c8423 100644 --- a/frontend/src/Components/LoginForm.tsx +++ b/frontend/src/Components/LoginForm.tsx @@ -10,12 +10,11 @@ const LoginFormContent: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Приведение типов для получения значений инпутов const emailElement = document.getElementById('email') as HTMLInputElement | null; const passwordElement = document.getElementById('password') as HTMLInputElement | null; if (!emailElement || !passwordElement) { - console.error('Один или несколько инпутов отсутствуют'); + console.error('One or many inputs are missing'); return; } @@ -41,10 +40,10 @@ const LoginFormContent: React.FC = () => { localStorage.setItem('user', JSON.stringify(loggedInUser)); navigate('/dashboard'); } else { - console.error('Ошибка:', data.error); + console.error('Error:', data.error); } } catch (error) { - console.error('Ошибка при входе:', error); + console.error('Error loginning:', error); } }; @@ -64,14 +63,15 @@ const LoginFormContent: React.FC = () => { localStorage.setItem('user', JSON.stringify(loggedInUser)); navigate('/dashboard'); } catch (error) { - console.error('Ошибка верификации токена:', error); + console.error('Error token verification:', error); } }; - const handleGoogleLoginError = (error: any) => { - console.error('Ошибка аутентификации через Google:', error); + const handleGoogleLoginError = () => { + console.error('Error auth through Google'); }; + return (
{ - const [formValues, setFormValues] = useState({}); - const [stage, setStage] = useState(1); - const [data, setData] = useState(null) - - const [sendTestMessage, { isLoading, isFetching }] = useLazySendTestVersionQuery() - - const nextStage = () => setStage((prev) => prev + 1); - const previousStage = () => setStage((prev) => prev - 1); - - const saveFormData = (values: FormValues) => { - setFormValues((prev) => ({ - ...prev, - ...values, - })); - }; - - const onSubmit = (values: FormValues) => { - saveFormData(values); - nextStage(); - }; - console.log(isLoading) - - const finalSubmit = async () => { - const res = await sendTestMessage(formValues).unwrap() - setData(res) - }; - - const selectEmoji = ( - value: number | undefined, - thresholds: number[], - emojis: string[] - ) => { - if (value === undefined) return null; - if (value <= thresholds[0]) return emojis[0]; - if (value <= thresholds[1]) return emojis[1]; - return emojis[2]; - }; - - return !data ? ( -
- -

- Fill in your profile and get some advices -

- - - onSubmit={onSubmit} - initialValues={formValues} - render={({ handleSubmit, values }) => ( -
- {stage === 1 && (<>
-

- Stage 1: Base information -

-
- {selectEmoji(values.age, [17, 50], ["👶", "🧑", "👴"])} -
- (value === "" ? 0 : Number(value))} - > - {({ input }) => ( - - )} - - -
- - - -
- -
- {selectEmoji(values.height, [150, 175], ["🌱", "🌳", "🌲"])} -
- (value === "" ? 0 : Number(value))} - > - {({ input }) => ( - - )} - - -
-
-

-

- name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - - - - -
- - -
-
- {selectEmoji(values.weight, [70, 99], ["🐭", "🐱", "🐘"])} -
- (value === "" ? 0 : Number(value))} - > - {({ input }) => ( -
- - input.onChange( - Array.isArray(value) ? value[0] : value - ) - } - min={0} - max={200} - className="text-indigo-500" - /> -
- Current Weight: {input.value || 0} kg -
-
- )} -
-
- - -
-
- )} - - - {stage === 2 && ( - <> -
-

- Stage 2: Details -

- - -
- - -
-
- - name="dietType" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - - -
-
- - name="exerciseLevel" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - -
-
- - name="hydrationGoal" component="select" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500"> - - - - - -
-
- - name="userInput" component="input" type="text" placeholder="Enter your preferences or comments" className="w-full p-3 border border-gray-300 rounded-lg text-gray-800 focus:outline-none focus:border-blue-500" /> -
-
- - -
-
- - )} - {stage === 3 && ( -
-

Summary

-

Age: {formValues.age}

-

Height: {formValues.height} cm

-

Weight: {formValues.weight} kg

-

Health Goal: {formValues.healthGoal}

-

Diet Type: {formValues.dietType || "Not specified"}

-

Exercise Level: {formValues.exerciseLevel || "Not specified"}

-

Hydration Goal: {formValues.hydrationGoal || "Not specified"}

-

User Input: {formValues.userInput || "Not specified"}

-
- - -
-
- )} -
- )} - /> -
- ) : (
-

- Advices for your health -

-

{data}

-
- - - - - -
-
- ) -}; - -export default MultiStepForm; diff --git a/frontend/src/Components/Navigation.tsx b/frontend/src/Components/Navigation.tsx index 04a00c0..1db872c 100644 --- a/frontend/src/Components/Navigation.tsx +++ b/frontend/src/Components/Navigation.tsx @@ -2,9 +2,8 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import IconButton from '@mui/material/IconButton'; import Avatar from '@mui/material/Avatar'; -import { MdAddCircleOutline, MdOutlineDarkMode } from "react-icons/md"; +import { MdAddCircleOutline } from "react-icons/md"; import { GoHistory } from "react-icons/go"; -import { CiLight } from "react-icons/ci"; import { CgLogIn } from "react-icons/cg"; import BackImage from '../assets/smallheadicon.png'; @@ -17,7 +16,7 @@ export interface NavigationItem { const NavigationItems: NavigationItem[] = [ { title: 'New Chat', - link: '/dashboard/new-chat', // Перенаправляем сразу на новый чат + link: '/dashboard/new-chat', icon: }, { @@ -50,11 +49,8 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => { } }, [theme]); - const handleThemeSwitch = () => { - setTheme(theme === "dark" ? "light" : "dark"); - }; - // Загружаем данные пользователя из localStorage (если имеются) + const [user, setUser] = useState(null); useEffect(() => { const storedUser = localStorage.getItem('user'); @@ -106,7 +102,6 @@ const Navigation = ({ isExpanded = false }: NavigationProps) => { ))}
- {/* Блок с иконкой пользователя и переключателем темы */}
{ )} - + {/**/}
diff --git a/frontend/src/Components/NewChatPage.tsx b/frontend/src/Components/NewChatPage.tsx index 21233bd..39b1422 100644 --- a/frontend/src/Components/NewChatPage.tsx +++ b/frontend/src/Components/NewChatPage.tsx @@ -72,7 +72,7 @@ const NewChatPage: React.FC = () => { console.error('Error:', error); setChatHistory(prev => [ ...prev, - { sender: 'Assistant', text: 'Что-то пошло не так' } + { sender: 'Assistant', text: 'Something went wrong' } ]); } } diff --git a/frontend/src/Components/Profile.tsx b/frontend/src/Components/Profile.tsx new file mode 100644 index 0000000..9f75b06 --- /dev/null +++ b/frontend/src/Components/Profile.tsx @@ -0,0 +1,233 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Typography, + Avatar, + Paper, + Button, + TextField, + IconButton, + Divider +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { Navbar } from '../pages/LandingPage'; +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; + +const Profile: React.FC = () => { + const [user, setUser] = useState(null); + const [editing, setEditing] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + phone: '', + role: '', + bio: '', + picture: '', + }); + const navigate = useNavigate(); + + useEffect(() => { + const storedUser = localStorage.getItem('user'); + if (storedUser) { + const parsedUser = JSON.parse(storedUser); + setUser(parsedUser); + setFormData({ + name: parsedUser.name || '', + email: parsedUser.email || '', + phone: parsedUser.phone || '', + role: parsedUser.role || '', + bio: parsedUser.bio || '', + picture: parsedUser.picture || '', + }); + } else { + navigate('/login'); + } + }, [navigate]); + + const handleChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value, + })); + }; + + const handleCancelEdit = () => { + setFormData({ + name: user.name || '', + email: user.email || '', + phone: user.phone || '', + role: user.role || '', + bio: user.bio || '', + picture: user.picture || '', + }); + setEditing(false); + }; + + const handleSaveEdit = async () => { + try { + const response = await fetch('http://localhost:5000/api/update_profile', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); + const data = await response.json(); + if (response.ok) { + const updatedUser = { ...user, ...formData }; + setUser(updatedUser); + localStorage.setItem('user', JSON.stringify(updatedUser)); + setEditing(false); + } else { + alert(data.error || 'Error updating profile'); + } + } catch (err) { + console.error(err); + alert('Error updating profile'); + } + }; + + if (!user) { + return null; + } + + return ( + + + + + + {editing ? ( + <> + + + + + + + + + + + + + + + + ) : ( + <> + + {user.name} + + + {user.email} + + + + Phone: {user.phone || 'Not provided'} + + + Role: {user.role || 'User'} + + + Bio: {user.bio || 'No bio available'} + + + + )} + + + + ); +}; + +export default Profile; diff --git a/frontend/src/Components/RegistrationForm.tsx b/frontend/src/Components/RegistrationForm.tsx index a020f9c..657345d 100644 --- a/frontend/src/Components/RegistrationForm.tsx +++ b/frontend/src/Components/RegistrationForm.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'; import { Link, useNavigate } from 'react-router-dom'; -import gsap from 'gsap'; const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"; @@ -11,7 +10,6 @@ const RegistrationFormContent: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Приведение типов для получения значений инпутов const nameElement = document.getElementById('name') as HTMLInputElement | null; const emailElement = document.getElementById('email') as HTMLInputElement | null; const passwordElement = document.getElementById('password') as HTMLInputElement | null; @@ -27,7 +25,6 @@ const RegistrationFormContent: React.FC = () => { const password = passwordElement.value; const confirmPassword = confirmPasswordElement.value; - // Проверка совпадения паролей if (password !== confirmPassword) { console.error('Passwords do not match'); alert('Passwords do not match'); @@ -45,7 +42,6 @@ const RegistrationFormContent: React.FC = () => { if (response.ok) { console.log('User registered successfully:', data.message); - // Создаем объект пользователя для авторизации (placeholder для аватара) const loggedInUser = { name, email, @@ -55,7 +51,7 @@ const RegistrationFormContent: React.FC = () => { navigate('/dashboard'); } else { console.error('Error:', data.error); - alert(data.error); // Показываем сообщение об ошибке, например "User already exists" + alert(data.error); } } catch (error) { console.error('Error registering user:', error); @@ -79,14 +75,15 @@ const RegistrationFormContent: React.FC = () => { localStorage.setItem('user', JSON.stringify(loggedInUser)); navigate('/dashboard'); } catch (error) { - console.error('Ошибка верификации токена:', error); + console.error('Error tiken verification:', error); } }; - const handleGoogleLoginError = (error: any) => { - console.error('Ошибка аутентификации:', error); + const handleGoogleLoginError = () => { + console.error('Error auth: Google login failed'); }; + return (
{ - const [stage, setStage] = useState(1); - - const next = () => setStage((prev) => prev + 1); - const previous = () => setStage((prev) => prev - 1); - - const onSubmit = (values: FormValues) => { - console.log("Form submitted:", values); - }; - - // Helper function to select emoji based on value - const selectEmoji = (value: number | undefined, thresholds: number[], emojis: (string | JSX.Element)[]) => { - if (value === undefined) return null; - if (value <= thresholds[0]) return emojis[0]; - if (value <= thresholds[1]) return emojis[1]; - return emojis[2]; - }; - - return ( -
-
-

- User Metrics Form -

- - onSubmit={onSubmit} - render={({ handleSubmit, values }) => ( -
- {/* Stage 1: Age */} - {stage === 1 && ( -
-

- Stage 1: Age -

-
- {selectEmoji(values.age, [17, 50], ["👶", "🧑", "👴"])} -
-
- - - name="age" - parse={(value) => (value === undefined ? 0 : Number(value))} // Changed to return 0 instead of undefined - > - {({ input, meta }) => ( -
- - {meta.touched && meta.error && ( - {meta.error} - )} -
- )} - -
-
- -
-
- )} - - {/* Stage 2: Height */} - {stage === 2 && ( -
-

- Stage 2: Height -

-
- {selectEmoji(values.height, [150, 175], ["🌼", "🧍🏻", "🦒"])} -
-
- - - name="height" - parse={(value) => (value === undefined ? 0 : Number(value))} // Changed to return 0 instead of undefined - > - {({ input, meta }) => ( -
- - {meta.touched && meta.error && ( - {meta.error} - )} -
- )} - -
-
- - -
-
- )} - - {/* Stage 3: Weight */} - {stage === 3 && ( -
-

- Stage 3: Weight -

-
- {selectEmoji(values.weight, [70, 99], ["🐭", "🐱", "🐘"])} -
-
- - name="weight"> - {({ input, meta }: FieldRenderProps) => ( -
- { - const newValue = Array.isArray(value) ? value[0] : value; - input.onChange(newValue); - }} - min={0} - max={200} - className="text-indigo-500" - /> -
- Weight: {input.value || 0} kg -
- {meta.touched && meta.error && ( - {meta.error} - )} -
- )} - -
-
- - -
-
- )} -
- )} - /> -
-
- ); -}; - -export default UserMetricsForm; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 81df1eb..a9cab6a 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -30,7 +30,7 @@ const HomePage: React.FC = () => { if (!isNewChat && selectedChat && selectedChat.chat) { const messages: ChatMessage[] = selectedChat.chat .split(/(?=^(User:|Bot:))/m) - .map((msg) => { + .map((msg:any) => { const trimmed = msg.trim(); const sender = trimmed.startsWith('User:') ? 'User' : 'Assistant'; return { @@ -44,10 +44,7 @@ const HomePage: React.FC = () => { } }, [isNewChat, selectedChat]); - /** - * Функция форматирования сообщения. - * Если в ответе отсутствуют символы перевода строки, пытаемся разбить текст по нумерованным пунктам. - */ + const formatMessage = (text: string) => { let lines: string[] = []; diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index e44760a..c4bbb90 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -1,17 +1,14 @@ import React, { useState, useEffect } from 'react'; -import { CgLogIn } from "react-icons/cg"; + import BackImage from '../assets/smallheadicon.png'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import { Box, Button, Avatar, Modal, Typography } from '@mui/material'; +import { Box, Avatar } from '@mui/material'; import gsap from 'gsap'; import { useGSAP } from '@gsap/react'; -import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google'; +import LogoutIcon from '@mui/icons-material/Logout'; +import LoginIcon from '@mui/icons-material/Login'; import { Link, useNavigate } from 'react-router-dom'; -import RegistrationForm from "../Components/RegistrationForm"; -const CLIENT_ID = "532143017111-4eqtlp0oejqaovj6rf5l1ergvhrp4vao.apps.googleusercontent.com"; - -// Компонент для анимации стрелки вниз const BouncingArrow = () => { return ( = ({ user, setUser }) => {
  • - + Home
  • @@ -75,19 +72,17 @@ const Navbar: React.FC = ({ user, setUser }) => {
    {user ? (
    - - + navigate('/profile')} /> +
    ) : ( - + sx={{ cursor: 'pointer', color: '#0d47a1', fontSize: '30px' }} + /> )}
    @@ -98,7 +93,6 @@ const Home: React.FC = () => { const navigate = useNavigate(); const [user, setUser] = useState(null); - // При загрузке страницы пытаемся загрузить данные пользователя из localStorage useEffect(() => { const storedUser = localStorage.getItem('user'); if (storedUser) { @@ -106,7 +100,6 @@ const Home: React.FC = () => { } }, []); - // Анимация GSAP для элементов страницы useGSAP(() => { gsap.from('#mainheading', { opacity: 0.3, ease: 'power2.inOut', duration: 0.5 }); gsap.from('#secondheading', { opacity: 0, y: 5, ease: 'power2.inOut', delay: 0.3, duration: 0.5 }); @@ -116,23 +109,20 @@ const Home: React.FC = () => { gsap.to('#button', { opacity: 1, ease: 'power2.inOut', delay: 2.5, duration: 0.5 }); }, []); - // Обработчик нажатия на кнопку "Get started" const handleGetStartedClick = () => { if (!user) { - // Если пользователь не авторизован — переходим на страницу регистрации navigate('/register'); } else { - // Если авторизован — переходим на страницу dashboard navigate('/dashboard'); } }; return ( -
    -
    +
    +
    -
    +

    {

    -
    ); }; -export default Home; +export { Home, Navbar }; diff --git a/prepare.sh b/prepare.sh new file mode 100644 index 0000000..41e3132 --- /dev/null +++ b/prepare.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Подготовка окружения: сборка Docker образов..." +docker-compose build +echo "Подготовка завершена."