import os from weaviate.connect import ConnectionParams from weaviate import WeaviateClient from langchain_huggingface import HuggingFaceEmbeddings from langchain_weaviate import WeaviateVectorStore from langchain_mistralai import ChatMistralAI from langchain.chains.conversational_retrieval.base import ConversationalRetrievalChain from langchain.memory import ConversationBufferMemory from langchain.prompts import PromptTemplate from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor from langserve import add_routes from fastapi import FastAPI, WebSocket from fastapi.responses import HTMLResponse import warnings import time from threading import Lock import logging from langchain_core.runnables import RunnablePassthrough, RunnableLambda from langchain_core.output_parsers import StrOutputParser from langchain_core.callbacks.base import BaseCallbackHandler from langchain.callbacks.manager import CallbackManager import json import atexit import asyncio from fastapi import WebSocketDisconnect # Logging config logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Rate limiting RATE_LIMIT = 3 RATE_WINDOW = 1 last_request_time = 0 request_lock = Lock() def wait_for_rate_limit(): global last_request_time with request_lock: current_time = time.time() time_since_last_request = current_time - last_request_time if time_since_last_request < RATE_WINDOW / RATE_LIMIT: sleep_time = (RATE_WINDOW / RATE_LIMIT) - time_since_last_request time.sleep(sleep_time) last_request_time = time.time() def setup_mistral(): logger.info("Configuration du modèle Mistral...") llm = ChatMistralAI( # Nom du modèle à utiliser (mistral-large-latest est le plus performant) model="mistral-large-latest", # Clé API pour l'authentification auprès de Mistral AI mistral_api_key=os.getenv("MISTRAL_API_KEY"), # Contrôle la créativité des réponses (0 = très déterministe, 1 = très créatif) temperature=0.2, # Contrôle la diversité des réponses (1 = toutes les options possibles, 0.1 = très sélectif) top_p=0.9, # Pénalité pour la répétition des mots (0 = pas de pénalité, 1 = forte pénalité) # frequency_penalty=0.5, # Pénalité pour encourager la diversité des sujets (0 = pas de pénalité, 1 = forte pénalité) # presence_penalty=0.5 ) logger.info("Modèle Mistral configuré avec succès") return llm def setup_weaviate(): logger.info("Connexion à Weaviate...") client = WeaviateClient( connection_params=ConnectionParams.from_url( os.getenv("WEAVIATE_URL"), 50051 ) ) client.connect() logger.info("Connexion à Weaviate établie avec succès") # Enregistrer la fermeture de la connexion à la sortie atexit.register(client.close) return client def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) def create_custom_prompt(): template = """Tu es un assistant viticole expert en pulvérisateurs et produits phytosanitaires. \ Voici les documents techniques pertinents pour répondre à la question : {context} Question : {question} Instructions : 1. Réponds de manière factuelle uniquement en te basant sur les documents fournis si cela est pertinent 2. Sois précis sur les caractéristiques techniques 3. Si la question n'est pas liée aux documents techniques, réponds de manière générale et concise Réponse :""" return PromptTemplate(template=template, input_variables=["context", "question"]) def setup_rag_chain(): logger.info("Initialisation de la chaîne RAG...") client = setup_weaviate() embeddings = HuggingFaceEmbeddings( model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", model_kwargs={'device': 'cpu'}, encode_kwargs={'normalize_embeddings': True} ) vectorstore = WeaviateVectorStore( client=client, embedding=embeddings, index_name="Document", text_key="page_content" ) llm = setup_mistral() memory = ConversationBufferMemory( memory_key="chat_history", return_messages=True, output_key="answer" ) retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 30}) prompt = create_custom_prompt() def extract_question(input_data): if isinstance(input_data, dict): question = input_data.get("question", "") else: question = str(input_data) if not question or not isinstance(question, str): raise ValueError("La question doit être une chaîne de caractères non vide") logger.info(f"🔍 Question extraite: {question}") return question chain = ( RunnableLambda(extract_question) | {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm ) logger.info("✅ Chaîne RAG initialisée avec succès") return chain # Init FastAPI app = FastAPI(title="Vitizen Chat API", description="API de chat pour l'assistant viticole", version="1.0.1") @app.get("/", response_class=HTMLResponse) async def home(): return """

Bienvenue sur l'API Vitizen Chat

""" # Create RAG chain rag_chain = setup_rag_chain() add_routes(app, rag_chain, path="/chat") @app.websocket("/chat/ws") async def stream_chat(websocket: WebSocket): await websocket.accept() logger.info("📡 WebSocket /chat/ws connecté") try: while True: try: data = await websocket.receive_json() question = data.get("message", "").strip() if not question: logger.warning("⚠️ Message vide reçu") await websocket.send_text("[START]") await websocket.send_text("[Erreur] Message vide") await websocket.send_text("[STOP]") continue logger.info(f"📝 Question reçue: {question}") input_data = {"question": question} logger.info(f"🔍 Entrée préparée pour la chaîne RAG: {input_data}") # Envoyer le marqueur START await websocket.send_text("[START]") logger.info("🔄 Début du streaming de la réponse") # Utiliser astream pour le streaming sans callback async for chunk in rag_chain.astream(input_data): if chunk: logger.info(f"📦 Chunk reçu: {chunk}") try: if hasattr(chunk, 'content'): content = chunk.content if content: logger.info(f"📤 Envoi du contenu: {content}") await websocket.send_text(content) elif isinstance(chunk, dict) and "answer" in chunk: logger.info(f"📤 Envoi de la réponse: {chunk['answer']}") await websocket.send_text(chunk["answer"]) else: logger.info(f"📤 Envoi du chunk: {str(chunk)}") await websocket.send_text(str(chunk)) except Exception as e: logger.error(f"❌ Erreur lors de l'envoi du chunk: {str(e)}") await websocket.send_text("[STOP]") break # Envoyer le marqueur STOP await websocket.send_text("[STOP]") logger.info("✅ Streaming terminé avec succès") except WebSocketDisconnect: logger.info("🔌 Client déconnecté proprement.") break except Exception as e: logger.error(f"❌ Erreur pendant traitement du message: {e}") try: await websocket.send_text("[START]") await websocket.send_text("[Erreur] Erreur API") await websocket.send_text("[STOP]") except: pass except Exception as e: logger.error(f"❌ Erreur fatale: {str(e)}") finally: try: if not websocket.client_state.disconnected: await websocket.send_text("[STOP]") await websocket.close() except: pass logger.info("🔌 Connexion WebSocket fermée") @app.get("/health") async def health_check(): return {"status": "ok"}