import logging import os import shutil from enum import Enum from openai import OpenAI from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings import gradio as gr import asyncio import edge_tts from pathlib import Path from app.config import OPENAI_API_KEY from app.functions.database_handling import BASE_DB_PATH # Aggiungi questo import from app.configs.prompts import SYSTEM_PROMPTS logging.basicConfig(level=logging.INFO) class LLMType(Enum): OPENAI = "openai" LOCAL = "local" # Client OpenAI standard openai_client = OpenAI(api_key=OPENAI_API_KEY) # Client LM Studio locale local_client = OpenAI( base_url="http://192.168.140.5:1234/v1", api_key="not-needed" ) # Voci italiane edge-tts VOICE_USER = "it-IT-DiegoNeural" # Voce maschile utente VOICE_ASSISTANT = "it-IT-ElsaNeural" # Voce femminile assistente async def text_to_speech(text, voice_name, output_file): """Genera audio usando edge-tts""" communicate = edge_tts.Communicate(text, voice_name) await communicate.save(output_file) def generate_speech(text, is_user=True): try: # Crea directory per audio temporanei audio_dir = Path("temp_audio") audio_dir.mkdir(exist_ok=True) # Seleziona voce e genera nome file voice = VOICE_USER if is_user else VOICE_ASSISTANT file_name = f"speech_{hash(text)}.mp3" output_path = audio_dir / file_name # Genera audio asyncio.run(text_to_speech(text, voice, str(output_path))) return str(output_path) except Exception as e: logging.error(f"Errore TTS: {e}") return None import re def clean_markdown(text): """Rimuove markdown dal testo""" text = re.sub(r'```[\s\S]*?```', '', text) # blocchi codice text = re.sub(r'`.*?`', '', text) # codice inline text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text) # link text = re.sub(r'\*\*(.*?)\*\*', r'\1', text) # bold text = re.sub(r'\*(.*?)\*', r'\1', text) # italic return text.strip() def generate_chat_audio(chat_history): """Genera audio della conversazione con voci alternate""" try: audio_files = [] audio_dir = Path("temp_audio") audio_dir.mkdir(exist_ok=True) # Genera audio per ogni messaggio for msg in chat_history: content = clean_markdown(msg["content"]) if not content.strip(): continue voice = VOICE_USER if msg["role"] == "user" else VOICE_ASSISTANT file_name = f"chat_{msg['role']}_{hash(content)}.mp3" output_path = audio_dir / file_name # Genera audio senza prefissi asyncio.run(text_to_speech(content, voice, str(output_path))) audio_files.append(str(output_path)) # Combina tutti gli audio if audio_files: from pydub import AudioSegment combined = AudioSegment.empty() for audio_file in audio_files: segment = AudioSegment.from_mp3(audio_file) combined += segment final_path = audio_dir / f"chat_complete_{hash(str(chat_history))}.mp3" combined.export(str(final_path), format="mp3") return str(final_path) return None except Exception as e: logging.error(f"Errore generazione audio: {e}") return None def get_system_prompt(prompt_type="tutor"): """Seleziona il prompt di sistema appropriato""" return SYSTEM_PROMPTS.get(prompt_type, SYSTEM_PROMPTS["tutor"]) def answer_question(question, db_name, prompt_type="tutor", chat_history=None, llm_type=LLMType.OPENAI): """ Risponde alla domanda 'question' usando i documenti del database 'db_name'. Restituisce una lista di 2 messaggi in formato: [ {"role": "user", "content": }, {"role": "assistant", "content": } ] In questa versione, viene effettuato il log dei 'chunk' recuperati durante la ricerca di similarità. """ if chat_history is None: chat_history = [] logging.info(f"Inizio elaborazione domanda: {question} per database: {db_name}") try: embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") db_path = os.path.join(BASE_DB_PATH, f"faiss_index_{db_name}") # Percorso corretto logging.info(f"Verifico esistenza database in: {db_path}") if not os.path.exists(db_path): logging.warning(f"Database {db_name} non trovato in {db_path}") return [ {"role": "user", "content": question}, {"role": "assistant", "content": f"Database non trovato in {db_path}"} ] # Carica l'indice FAISS vectorstore = FAISS.load_local(db_path, embeddings, allow_dangerous_deserialization=True) # Cerca i documenti (chunk) più simili relevant_docs = vectorstore.similarity_search(question, k=3) # Logga i chunk recuperati for idx, doc in enumerate(relevant_docs): logging.info(f"--- Chunk {idx+1} ---") logging.info(doc.page_content) logging.info("---------------------") # Prepara il contesto dai documenti context = "\n".join([doc.page_content for doc in relevant_docs]) prompt = SYSTEM_PROMPTS[prompt_type].format(context=context) # Prepara la cronologia completa delle conversazioni conversation_history = [] for msg in chat_history: # Rimuovo limite di 4 messaggi conversation_history.append({ "role": msg["role"], "content": msg["content"] }) # Costruisci messaggio con contesto completo messages = [ {"role": "system", "content": prompt}, *conversation_history, # Includi tutta la cronologia {"role": "user", "content": question} ] if llm_type == LLMType.OPENAI: response = openai_client.chat.completions.create( model="gpt-4o-mini", messages=messages, temperature=0.7, max_tokens=2048 # Aumenta token per gestire conversazioni lunghe ) answer = response.choices[0].message.content else: # LOCAL response = local_client.chat.completions.create( model="qwen2.5-coder-7b-instruct", messages=messages, temperature=0.7 ) answer = response.choices[0].message.content # Genera audio per domanda e risposta user_audio = generate_speech(question, is_user=True) assistant_audio = generate_speech(answer, is_user=False) return [ {"role": "user", "content": question, "audio": user_audio}, {"role": "assistant", "content": answer, "audio": assistant_audio} ] except Exception as e: logging.error(f"Errore durante la generazione della risposta: {e}") return [ {"role": "user", "content": question}, {"role": "assistant", "content": f"Si è verificato un errore: {str(e)}"} ] if __name__ == "__main__": pass