File size: 8,972 Bytes
fe4792e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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