Spaces:
Sleeping
Sleeping
import asyncio | |
import aiohttp | |
import logging | |
from typing import Dict, Optional, Any, AsyncGenerator | |
import json | |
from datetime import datetime, timedelta | |
import pytz | |
from utils.logging_utils import log_to_file | |
import os | |
import hashlib | |
from dotenv import load_dotenv | |
from utils.rate_limiter import RateLimiter | |
from utils.circuit_breaker import AsyncCircuitBreaker | |
from utils.cache_manager import CacheManager | |
FLOWISE_API_URL_SANTE = os.getenv("FLOWISE_API_URL_SANTE") | |
FLOWISE_API_URL_CAR = os.getenv("FLOWISE_API_URL_CAR") | |
FLOWISE_API_URL_BTP = os.getenv("FLOWISE_API_URL_BTP") | |
FLOWISE_API_URL_RH = os.getenv("FLOWISE_API_URL_RH") | |
FLOWISE_API_URL_PILOTAGE = os.getenv("FLOWISE_API_URL_PILOTAGE") | |
class AsyncFlowiseClient: | |
def __init__(self): | |
"""Initialise le client asynchrone pour Flowise""" | |
self.api_urls = { | |
'insuranceSANTE': FLOWISE_API_URL_SANTE, | |
'insuranceCAR': FLOWISE_API_URL_CAR, | |
'insuranceBTP': FLOWISE_API_URL_BTP, | |
'RH': FLOWISE_API_URL_RH, | |
'Pilotage': FLOWISE_API_URL_PILOTAGE | |
} | |
self.session = None | |
self.rate_limiter = RateLimiter(requests_per_second=5, burst_limit=10) | |
self.circuit_breaker = AsyncCircuitBreaker( | |
failure_threshold=5, | |
recovery_timeout=60, | |
half_open_timeout=30 | |
) | |
self.cache_manager = CacheManager() | |
def _generate_cache_key(self, question: str, assistant_type: str) -> str: | |
"""Génère une clé de cache unique pour une question""" | |
question_hash = hashlib.md5(question.encode()).hexdigest() | |
return f"assistant_response:{assistant_type}:{question_hash}" | |
def _clean_question(self, question: str) -> str: | |
"""Nettoie la question des métadonnées""" | |
try: | |
data = json.loads(question) | |
if isinstance(data, dict) and "next_inputs" in data and "query" in data["next_inputs"]: | |
return data["next_inputs"]["query"] | |
except json.JSONDecodeError: | |
pass | |
return question | |
def _process_response(self, response_data: Any) -> Dict[str, str]: | |
"""Extrait uniquement le texte de la réponse""" | |
try: | |
# Si c'est une chaîne, essayer de la parser comme JSON | |
if isinstance(response_data, str): | |
try: | |
data = json.loads(response_data) | |
if isinstance(data, dict) and "text" in data: | |
return {"answer": data["text"]} | |
except json.JSONDecodeError: | |
return {"answer": response_data} | |
# Si c'est déjà un dictionnaire | |
if isinstance(response_data, dict) and "text" in response_data: | |
return {"answer": response_data["text"]} | |
# Si on ne peut pas extraire le texte, retourner une erreur | |
return {"error": "Format de réponse non reconnu"} | |
except Exception as e: | |
log_to_file(f"Erreur lors du traitement de la réponse : {str(e)}", level=logging.ERROR) | |
return {"error": "Format de réponse non reconnu"} | |
async def query_assistant_stream(self, question: str, assistant_type: str, user_id: Optional[str] = None) -> AsyncGenerator[str, None]: | |
"""Envoie une requête en streaming à Flowise""" | |
if assistant_type not in self.api_urls: | |
log_to_file(f"Type d'assistant non valide : {assistant_type}", level=logging.ERROR) | |
yield json.dumps({"error": f"Type d'assistant non valide : {assistant_type}"}) | |
return | |
if not await self.rate_limiter.acquire(user_id): | |
yield json.dumps({"error": "Trop de requêtes. Veuillez réessayer dans quelques instants."}) | |
return | |
if not self.session: | |
self.session = aiohttp.ClientSession() | |
try: | |
url = self.api_urls[assistant_type] | |
clean_question = self._clean_question(question) | |
payload = {"question": clean_question} | |
async with self.session.post(url, json=payload) as response: | |
if response.status != 200: | |
error_text = await response.text() | |
log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_text}", level=logging.ERROR) | |
yield json.dumps({"error": f"Erreur {response.status}: {error_text}"}) | |
return | |
async for chunk in response.content.iter_any(): | |
if chunk: | |
try: | |
text = chunk.decode('utf-8') | |
processed_response = self._process_response(text) | |
if "answer" in processed_response: | |
yield processed_response["answer"] | |
except Exception as e: | |
log_to_file(f"Erreur de décodage du chunk : {str(e)}", level=logging.ERROR) | |
continue | |
except Exception as e: | |
error_message = str(e) | |
log_to_file(f"Erreur lors du streaming pour {assistant_type}: {error_message}", level=logging.ERROR) | |
yield json.dumps({"error": error_message}) | |
async def query_assistant(self, question: str, assistant_type: str, user_id: Optional[str] = None) -> Dict[str, Any]: | |
"""Envoie une requête asynchrone à Flowise avec cache""" | |
if assistant_type not in self.api_urls: | |
log_to_file(f"Type d'assistant non valide : {assistant_type}", level=logging.ERROR) | |
return {"error": f"Type d'assistant non valide : {assistant_type}"} | |
if not await self.rate_limiter.acquire(user_id): | |
return {"error": "Trop de requêtes. Veuillez réessayer dans quelques instants."} | |
cache_key = self._generate_cache_key(question, assistant_type) | |
cached_response = await self.cache_manager.get(cache_key) | |
if cached_response: | |
log_to_file(f"Réponse trouvée dans le cache pour {assistant_type}", level=logging.INFO) | |
return cached_response | |
if not self.session: | |
self.session = aiohttp.ClientSession() | |
try: | |
url = self.api_urls[assistant_type] | |
clean_question = self._clean_question(question) | |
payload = {"question": clean_question} | |
async with self.session.post(url, json=payload) as response: | |
if response.status != 200: | |
error_text = await response.text() | |
log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_text}", level=logging.ERROR) | |
return {"error": f"Erreur {response.status}: {error_text}"} | |
response_text = await response.text() | |
processed_response = self._process_response(response_text) | |
if "error" not in processed_response: | |
await self.cache_manager.set( | |
cache_key, | |
processed_response, | |
expiry=timedelta(hours=1) | |
) | |
log_to_file(f"Réponse mise en cache pour {assistant_type}", level=logging.INFO) | |
return processed_response | |
except Exception as e: | |
error_message = str(e) | |
if "Circuit breaker ouvert" in error_message: | |
log_to_file(f"Service {assistant_type} temporairement indisponible (Circuit breaker)", level=logging.ERROR) | |
return {"error": "Service temporairement indisponible. Veuillez réessayer plus tard."} | |
log_to_file(f"Erreur lors de la requête à {assistant_type}: {error_message}", level=logging.ERROR) | |
return {"error": error_message} | |
async def check_health(self) -> Dict[str, bool]: | |
"""Vérifie la santé des connexions aux différents assistants""" | |
if not self.session: | |
self.session = aiohttp.ClientSession() | |
results = {} | |
for assistant_type, url in self.api_urls.items(): | |
try: | |
base_url = '/'.join(url.split('/')[:-4]) # Obtenir l'URL de base | |
async with self.session.get( | |
f"{base_url}/health", | |
timeout=aiohttp.ClientTimeout(total=5) | |
) as response: | |
results[assistant_type] = response.status == 200 | |
log_to_file(f"Statut santé {assistant_type}: {response.status}", level=logging.INFO) | |
except Exception as e: | |
log_to_file(f"Erreur de santé pour {assistant_type}: {str(e)}", level=logging.ERROR) | |
results[assistant_type] = False | |
return results | |
async def close(self): | |
"""Ferme la session client""" | |
if self.session: | |
await self.session.close() | |
self.session = None |