django-lucy-assist 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. django_lucy_assist-0.1.0.dist-info/METADATA +206 -0
  2. django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
  3. django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
  4. django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
  5. lucy_assist/__init__.py +11 -0
  6. lucy_assist/admin.py +22 -0
  7. lucy_assist/apps.py +10 -0
  8. lucy_assist/conf.py +103 -0
  9. lucy_assist/constantes.py +120 -0
  10. lucy_assist/context_processors.py +65 -0
  11. lucy_assist/migrations/0001_initial.py +92 -0
  12. lucy_assist/migrations/__init__.py +0 -0
  13. lucy_assist/models/__init__.py +14 -0
  14. lucy_assist/models/base.py +54 -0
  15. lucy_assist/models/configuration.py +175 -0
  16. lucy_assist/models/conversation.py +54 -0
  17. lucy_assist/models/message.py +45 -0
  18. lucy_assist/models/project_context_cache.py +213 -0
  19. lucy_assist/services/__init__.py +21 -0
  20. lucy_assist/services/bug_notification_service.py +183 -0
  21. lucy_assist/services/claude_service.py +417 -0
  22. lucy_assist/services/context_service.py +350 -0
  23. lucy_assist/services/crud_service.py +364 -0
  24. lucy_assist/services/gitlab_service.py +248 -0
  25. lucy_assist/services/project_context_service.py +412 -0
  26. lucy_assist/services/tool_executor_service.py +343 -0
  27. lucy_assist/services/tools_definition.py +229 -0
  28. lucy_assist/signals.py +25 -0
  29. lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
  30. lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
  31. lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
  32. lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
  33. lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
  34. lucy_assist/tests/__init__.py +0 -0
  35. lucy_assist/tests/factories/__init__.py +15 -0
  36. lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
  37. lucy_assist/tests/test_lucy_assist.py +186 -0
  38. lucy_assist/urls.py +36 -0
  39. lucy_assist/utils/__init__.py +7 -0
  40. lucy_assist/utils/log_utils.py +59 -0
  41. lucy_assist/utils/message_utils.py +130 -0
  42. lucy_assist/utils/token_utils.py +87 -0
  43. lucy_assist/views/__init__.py +13 -0
  44. lucy_assist/views/api_views.py +595 -0
@@ -0,0 +1,183 @@
1
+ """
2
+ Service de notification automatique des bugs détectés.
3
+
4
+ Ce service est appelé quand Lucy détecte un bug lors de l'analyse du code
5
+ via GitLab. Il envoie automatiquement un email au gestionnaire de projet.
6
+ """
7
+ import requests
8
+ from typing import Dict, Any, Optional
9
+
10
+ from django.conf import settings
11
+ from django.core.mail import send_mail
12
+ from django.utils import timezone
13
+
14
+ from lucy_assist.utils.log_utils import LogUtils
15
+
16
+
17
+ class BugNotificationService:
18
+ """Service pour notifier automatiquement quand un bug est détecté."""
19
+
20
+ # Email par défaut si aucun collaborateur associé
21
+ EMAIL_FALLBACK = 'maxence@revolucy.fr'
22
+
23
+ def __init__(self, user=None):
24
+ self.user = user
25
+
26
+ def get_collaborateur_email(self) -> str:
27
+ """
28
+ Récupère l'email du collaborateur associé via l'API Lucy CRM.
29
+ Utilise le SIREN du client configuré dans les settings.
30
+ Retourne l'email par défaut si non trouvé.
31
+ """
32
+ try:
33
+ # Récupérer le SIREN depuis les settings
34
+ siren_client = getattr(settings, 'SIREN_CLIENT', None)
35
+ if not siren_client:
36
+ LogUtils.warning("SIREN_CLIENT non configuré, utilisation de l'email par défaut")
37
+ return self.EMAIL_FALLBACK
38
+
39
+ # Appel à l'API Lucy CRM
40
+ url = f"https://app.lucy-crm.fr/api/credit-client/{siren_client}"
41
+ response = requests.get(url, timeout=10)
42
+
43
+ if response.status_code == 200:
44
+ data = response.json()
45
+ collaborateur_email = data.get('collaborateur_associe')
46
+
47
+ if collaborateur_email:
48
+ LogUtils.info(f"Collaborateur associé trouvé: {collaborateur_email}")
49
+ return collaborateur_email
50
+ else:
51
+ LogUtils.info("Aucun collaborateur associé, utilisation de l'email par défaut")
52
+ return self.EMAIL_FALLBACK
53
+ else:
54
+ LogUtils.warning(f"API Lucy CRM a retourné le status {response.status_code}")
55
+ return self.EMAIL_FALLBACK
56
+
57
+ except requests.RequestException as e:
58
+ LogUtils.error(f"Erreur appel API Lucy CRM: {str(e)}")
59
+ return self.EMAIL_FALLBACK
60
+ except Exception as e:
61
+ LogUtils.error(f"Erreur récupération collaborateur: {str(e)}")
62
+ return self.EMAIL_FALLBACK
63
+
64
+ def notify_bug_detected(
65
+ self,
66
+ bug_analysis: Dict[str, Any],
67
+ user_description: str,
68
+ error_message: str = "",
69
+ code_context: str = "",
70
+ file_path: str = "",
71
+ page_url: str = ""
72
+ ) -> Dict[str, Any]:
73
+ """
74
+ Envoie une notification email quand un bug est détecté.
75
+
76
+ Args:
77
+ bug_analysis: Résultat de l'analyse du bug (is_bug, analysis, recommendation, severity)
78
+ user_description: Description du problème par l'utilisateur
79
+ error_message: Message d'erreur signalé (optionnel)
80
+ code_context: Code source analysé (optionnel)
81
+ file_path: Chemin du fichier contenant le bug (optionnel)
82
+ page_url: URL de la page où le bug a été signalé (optionnel)
83
+
84
+ Returns:
85
+ Dict avec 'success' et 'message'
86
+ """
87
+ # Vérifier si c'est bien un bug
88
+ if not bug_analysis.get('is_bug', False):
89
+ return {
90
+ 'success': False,
91
+ 'message': "Ce n'est pas un bug, notification non envoyée.",
92
+ 'notified': False
93
+ }
94
+
95
+ try:
96
+ destinataire = self.get_collaborateur_email()
97
+
98
+ # Informations utilisateur
99
+ user_name = "Utilisateur"
100
+ user_email = "Non disponible"
101
+ if self.user:
102
+ user_name = self.user.get_full_name() if hasattr(self.user, 'get_full_name') else str(self.user)
103
+ user_email = self.user.email if hasattr(self.user, 'email') else 'Non disponible'
104
+
105
+ # Déterminer la sévérité
106
+ severity = bug_analysis.get('severity', 'medium')
107
+ severity_label = {
108
+ 'low': '🟡 Faible',
109
+ 'medium': '🟠 Moyenne',
110
+ 'high': '🔴 Élevée'
111
+ }.get(severity, '🟠 Moyenne')
112
+
113
+ sujet = f"[Lucy Assist] 🐛 Bug détecté - Sévérité {severity_label}"
114
+
115
+ message_email = f"""
116
+ Lucy Assist a détecté un bug dans le code.
117
+
118
+ ================================================================================
119
+ 📊 ANALYSE DU BUG
120
+ ================================================================================
121
+ Sévérité : {severity_label}
122
+ Fichier : {file_path or 'Non identifié'}
123
+ Page : {page_url or 'Non spécifiée'}
124
+
125
+ 📋 Analyse :
126
+ {bug_analysis.get('analysis', 'Non disponible')}
127
+
128
+ 💡 Recommandation :
129
+ {bug_analysis.get('recommendation', 'Non disponible')}
130
+
131
+ ================================================================================
132
+ 👤 INFORMATIONS UTILISATEUR
133
+ ================================================================================
134
+ Utilisateur : {user_name}
135
+ Email : {user_email}
136
+ Date : {timezone.now().strftime('%d/%m/%Y à %H:%M')}
137
+
138
+ 📝 Description du problème par l'utilisateur :
139
+ {user_description or 'Non fournie'}
140
+
141
+ ⚠️ Message d'erreur signalé :
142
+ {error_message or 'Aucun'}
143
+
144
+ ================================================================================
145
+ 💻 CODE SOURCE ANALYSÉ
146
+ ================================================================================
147
+ {code_context[:2000] if code_context else 'Non disponible'}
148
+ {('...[tronqué]' if code_context and len(code_context) > 2000 else '')}
149
+
150
+ ================================================================================
151
+ Cette notification a été envoyée automatiquement par Lucy Assist
152
+ suite à la détection d'un bug lors de l'analyse du code via GitLab.
153
+ ================================================================================
154
+ """
155
+
156
+ send_mail(
157
+ subject=sujet,
158
+ message=message_email,
159
+ from_email=settings.DEFAULT_FROM_EMAIL,
160
+ recipient_list=[destinataire],
161
+ fail_silently=False
162
+ )
163
+
164
+ LogUtils.info(
165
+ f"Notification bug envoyée à {destinataire} - "
166
+ f"Sévérité: {severity}, Fichier: {file_path or 'inconnu'}"
167
+ )
168
+
169
+ return {
170
+ 'success': True,
171
+ 'message': f"Bug détecté et signalé à Revolucy ({destinataire})",
172
+ 'notified': True,
173
+ 'recipient': destinataire,
174
+ 'severity': severity
175
+ }
176
+
177
+ except Exception as e:
178
+ LogUtils.error(f"Erreur envoi notification bug: {str(e)}")
179
+ return {
180
+ 'success': False,
181
+ 'message': f"Erreur lors de l'envoi de la notification: {str(e)}",
182
+ 'notified': False
183
+ }
@@ -0,0 +1,417 @@
1
+ """
2
+ Service d'intégration avec Claude API (Anthropic).
3
+
4
+ Optimisations tokens:
5
+ - Cache du contexte projet via ProjectContextService
6
+ - Résumé des conversations longues
7
+ - Compression intelligente du contexte
8
+
9
+ Tools CRUD:
10
+ - Claude peut exécuter des actions CRUD via les tools
11
+ - Les tools sont exécutés côté serveur avec les permissions de l'utilisateur
12
+ """
13
+ import json
14
+ from typing import Generator, List, Dict, Any
15
+
16
+ import anthropic
17
+
18
+ from lucy_assist.utils.log_utils import LogUtils
19
+ from lucy_assist.constantes import LucyAssistConstantes
20
+ from lucy_assist.services.tools_definition import LUCY_ASSIST_TOOLS
21
+ from lucy_assist.conf import lucy_assist_settings
22
+
23
+
24
+ class ClaudeService:
25
+ """Service pour interagir avec l'API Claude d'Anthropic."""
26
+
27
+ MAX_TOKENS = 4096
28
+
29
+ # Seuils pour l'optimisation
30
+ MAX_MESSAGES_BEFORE_SUMMARY = 10 # Résumer après 10 messages
31
+ MAX_CONTEXT_TOKENS = 2000 # Limiter le contexte à 2000 tokens estimés
32
+
33
+ def __init__(self):
34
+ self.api_key = lucy_assist_settings.CLAUDE_API_KEY
35
+ if not self.api_key:
36
+ raise ValueError("CLAUDE_API_KEY non configurée dans les settings")
37
+
38
+ self.client = anthropic.Anthropic(api_key=self.api_key)
39
+ self._project_context_service = None
40
+ self._tools = LUCY_ASSIST_TOOLS
41
+ self._model = lucy_assist_settings.CLAUDE_MODEL
42
+
43
+ @property
44
+ def project_context_service(self):
45
+ """Lazy loading du service de contexte projet."""
46
+ if self._project_context_service is None:
47
+ from lucy_assist.services.project_context_service import ProjectContextService
48
+ self._project_context_service = ProjectContextService()
49
+ return self._project_context_service
50
+
51
+ def _build_system_prompt(
52
+ self,
53
+ page_context: Dict,
54
+ user,
55
+ user_question: str = ""
56
+ ) -> str:
57
+ """
58
+ Construit le prompt système avec le contexte optimisé.
59
+
60
+ Utilise le cache pour réduire la redondance des informations
61
+ sur le projet.
62
+ """
63
+ # Récupérer les permissions utilisateur (compressées)
64
+ user_permissions = []
65
+ if hasattr(user, 'get_all_permissions'):
66
+ # Ne garder que les permissions pertinentes (sans préfixe d'app commun)
67
+ all_perms = list(user.get_all_permissions())
68
+ user_permissions = [p.split('.')[-1] for p in all_perms[:15]]
69
+
70
+ # Récupérer le contexte projet optimisé depuis le cache
71
+ page_url = page_context.get('page_url', page_context.get('url', ''))
72
+ optimized_context = self.project_context_service.get_optimized_context(
73
+ page_url=page_url,
74
+ user_question=user_question
75
+ )
76
+
77
+ # Fusionner le contexte de page avec le contexte projet caché
78
+ enriched_context = {
79
+ 'page': page_context,
80
+ 'projet': optimized_context.get('relevant_info', {}),
81
+ 'cache_stats': optimized_context.get('stats', {})
82
+ }
83
+
84
+ # Construire le prompt avec contexte compact
85
+ prompt = LucyAssistConstantes.SYSTEM_PROMPTS['default'].format(
86
+ page_context=json.dumps(enriched_context, ensure_ascii=False, indent=2),
87
+ user_permissions=', '.join(user_permissions)
88
+ )
89
+
90
+ return prompt
91
+
92
+ def _optimize_messages(self, messages: List) -> List[Dict]:
93
+ """
94
+ Optimise l'historique des messages pour réduire les tokens.
95
+
96
+ Pour les conversations longues, résume les anciens messages
97
+ au lieu de les envoyer en entier.
98
+ """
99
+ formatted = self._format_messages(messages)
100
+
101
+ if len(formatted) <= self.MAX_MESSAGES_BEFORE_SUMMARY:
102
+ return formatted
103
+
104
+ # Résumer la conversation
105
+ summary_data = self.project_context_service.summarize_conversation(
106
+ formatted,
107
+ max_tokens=500
108
+ )
109
+
110
+ if not summary_data:
111
+ return formatted
112
+
113
+ # Reconstruire les messages avec le résumé
114
+ optimized = []
115
+
116
+ # Ajouter les premiers messages
117
+ optimized.extend(summary_data['first_messages'])
118
+
119
+ # Ajouter le résumé comme message système
120
+ optimized.append({
121
+ 'role': 'user',
122
+ 'content': f"[Note: {summary_data['original_count'] - 4} messages résumés]\n{summary_data['summary']}"
123
+ })
124
+
125
+ # Ajouter les derniers messages
126
+ optimized.extend(summary_data['last_messages'])
127
+
128
+ LogUtils.info(
129
+ f"Conversation optimisée: {len(formatted)} -> {len(optimized)} messages, "
130
+ f"~{summary_data.get('tokens_saved_estimate', 0)} tokens économisés"
131
+ )
132
+
133
+ return optimized
134
+
135
+ def _format_messages(self, messages: List) -> List[Dict]:
136
+ """Formate les messages pour l'API Claude."""
137
+ formatted = []
138
+
139
+ for msg in messages:
140
+ role = "user" if msg.repondant == LucyAssistConstantes.Repondant.UTILISATEUR else "assistant"
141
+ formatted.append({
142
+ "role": role,
143
+ "content": msg.contenu
144
+ })
145
+
146
+ return formatted
147
+
148
+ def chat_completion_stream(
149
+ self,
150
+ messages: List,
151
+ page_context: Dict,
152
+ user,
153
+ tool_executor=None
154
+ ) -> Generator[Dict[str, Any], None, None]:
155
+ """
156
+ Génère une réponse en streaming avec support des tools.
157
+
158
+ Args:
159
+ messages: Liste des messages de la conversation
160
+ page_context: Contexte de la page courante
161
+ user: Utilisateur Django
162
+ tool_executor: Callable pour exécuter les tools (optionnel)
163
+
164
+ Yields:
165
+ Dict avec 'type' (content/tool_use/tool_result/usage/error) et les données associées
166
+ """
167
+ try:
168
+ # Extraire la question utilisateur du dernier message
169
+ user_question = ""
170
+ if messages:
171
+ last_msg = messages[-1] if hasattr(messages[-1], 'contenu') else messages[-1]
172
+ user_question = getattr(last_msg, 'contenu', '') if hasattr(last_msg, 'contenu') else str(last_msg)
173
+
174
+ system_prompt = self._build_system_prompt(page_context, user, user_question)
175
+
176
+ # Utiliser l'optimisation des messages pour les longues conversations
177
+ formatted_messages = self._optimize_messages(messages)
178
+
179
+ if not formatted_messages:
180
+ yield {'type': 'error', 'error': 'Aucun message à traiter'}
181
+ return
182
+
183
+ # Boucle pour gérer les appels de tools
184
+ current_messages = formatted_messages.copy()
185
+ max_tool_iterations = 5 # Limite de sécurité
186
+
187
+ for iteration in range(max_tool_iterations):
188
+ with self.client.messages.stream(
189
+ model=self._model,
190
+ max_tokens=self.MAX_TOKENS,
191
+ system=system_prompt,
192
+ messages=current_messages,
193
+ tools=self._tools
194
+ ) as stream:
195
+ response_text = ""
196
+ tool_uses = []
197
+
198
+ for text in stream.text_stream:
199
+ response_text += text
200
+ yield {'type': 'content', 'content': text}
201
+
202
+ # Récupérer la réponse finale pour les tools
203
+ response = stream.get_final_message()
204
+
205
+ # Extraire les appels de tools
206
+ for block in response.content:
207
+ if block.type == "tool_use":
208
+ tool_uses.append({
209
+ 'id': block.id,
210
+ 'name': block.name,
211
+ 'input': block.input
212
+ })
213
+
214
+ # Si pas de tool_use, on a fini
215
+ if not tool_uses or response.stop_reason != "tool_use":
216
+ if response.usage:
217
+ total_tokens = response.usage.input_tokens + response.usage.output_tokens
218
+ yield {
219
+ 'type': 'usage',
220
+ 'input_tokens': response.usage.input_tokens,
221
+ 'output_tokens': response.usage.output_tokens,
222
+ 'total_tokens': total_tokens
223
+ }
224
+ return
225
+
226
+ # Exécuter les tools
227
+ tool_results = []
228
+ for tool_use in tool_uses:
229
+ yield {
230
+ 'type': 'tool_use',
231
+ 'tool_name': tool_use['name'],
232
+ 'tool_input': tool_use['input']
233
+ }
234
+
235
+ # Exécuter le tool si un executor est fourni
236
+ if tool_executor:
237
+ try:
238
+ result = tool_executor(
239
+ tool_use['name'],
240
+ tool_use['input'],
241
+ user
242
+ )
243
+ tool_results.append({
244
+ 'type': 'tool_result',
245
+ 'tool_use_id': tool_use['id'],
246
+ 'content': json.dumps(result, ensure_ascii=False)
247
+ })
248
+ yield {
249
+ 'type': 'tool_result',
250
+ 'tool_name': tool_use['name'],
251
+ 'result': result
252
+ }
253
+ except Exception as e:
254
+ error_result = {'error': str(e)}
255
+ tool_results.append({
256
+ 'type': 'tool_result',
257
+ 'tool_use_id': tool_use['id'],
258
+ 'content': json.dumps(error_result),
259
+ 'is_error': True
260
+ })
261
+ yield {
262
+ 'type': 'tool_error',
263
+ 'tool_name': tool_use['name'],
264
+ 'error': str(e)
265
+ }
266
+ else:
267
+ # Pas d'executor, on ne peut pas exécuter le tool
268
+ tool_results.append({
269
+ 'type': 'tool_result',
270
+ 'tool_use_id': tool_use['id'],
271
+ 'content': json.dumps({'error': 'Tool executor not available'}),
272
+ 'is_error': True
273
+ })
274
+
275
+ # Ajouter les messages pour continuer la conversation
276
+ current_messages.append({
277
+ 'role': 'assistant',
278
+ 'content': response.content
279
+ })
280
+ current_messages.append({
281
+ 'role': 'user',
282
+ 'content': tool_results
283
+ })
284
+
285
+ except anthropic.APIConnectionError as e:
286
+ LogUtils.error(f"Erreur de connexion API Claude: {e}")
287
+ yield {'type': 'error', 'error': 'Impossible de se connecter au service IA'}
288
+
289
+ except anthropic.RateLimitError as e:
290
+ LogUtils.error(f"Rate limit API Claude: {e}")
291
+ yield {'type': 'error', 'error': 'Service temporairement surchargé, veuillez réessayer'}
292
+
293
+ except anthropic.APIStatusError as e:
294
+ LogUtils.error(f"Erreur API Claude: {e}")
295
+ yield {'type': 'error', 'error': f'Erreur du service IA: {e.message}'}
296
+
297
+ except Exception as e:
298
+ LogUtils.error("Erreur inattendue lors de l'appel Claude")
299
+ yield {'type': 'error', 'error': str(e)}
300
+
301
+ def chat_completion(
302
+ self,
303
+ messages: List,
304
+ page_context: Dict,
305
+ user
306
+ ) -> Dict[str, Any]:
307
+ """
308
+ Génère une réponse complète (non-streaming).
309
+
310
+ Returns:
311
+ Dict avec 'content', 'tokens_utilises', ou 'error'
312
+ """
313
+ try:
314
+ # Extraire la question utilisateur
315
+ user_question = ""
316
+ if messages:
317
+ last_msg = messages[-1] if hasattr(messages[-1], 'contenu') else messages[-1]
318
+ user_question = getattr(last_msg, 'contenu', '') if hasattr(last_msg, 'contenu') else str(last_msg)
319
+
320
+ system_prompt = self._build_system_prompt(page_context, user, user_question)
321
+
322
+ # Utiliser l'optimisation des messages
323
+ formatted_messages = self._optimize_messages(messages)
324
+
325
+ if not formatted_messages:
326
+ return {'error': 'Aucun message à traiter'}
327
+
328
+ response = self.client.messages.create(
329
+ model=self._model,
330
+ max_tokens=self.MAX_TOKENS,
331
+ system=system_prompt,
332
+ messages=formatted_messages
333
+ )
334
+
335
+ content = ""
336
+ for block in response.content:
337
+ if block.type == "text":
338
+ content += block.text
339
+
340
+ total_tokens = 0
341
+ if response.usage:
342
+ total_tokens = response.usage.input_tokens + response.usage.output_tokens
343
+
344
+ return {
345
+ 'content': content,
346
+ 'tokens_utilises': total_tokens,
347
+ 'input_tokens': response.usage.input_tokens if response.usage else 0,
348
+ 'output_tokens': response.usage.output_tokens if response.usage else 0
349
+ }
350
+
351
+ except Exception as e:
352
+ LogUtils.error("Erreur lors de l'appel Claude")
353
+ return {'error': str(e)}
354
+
355
+ def analyze_code_for_bug(
356
+ self,
357
+ error_message: str,
358
+ code_context: str,
359
+ user_description: str
360
+ ) -> Dict[str, Any]:
361
+ """
362
+ Analyse du code pour détecter un bug potentiel.
363
+
364
+ Returns:
365
+ Dict avec 'is_bug', 'analysis', 'recommendation'
366
+ """
367
+ prompt = f"""Analyse le problème suivant signalé par un utilisateur:
368
+
369
+ Description de l'utilisateur: {user_description}
370
+
371
+ Message d'erreur (si disponible): {error_message}
372
+
373
+ Code source pertinent:
374
+ ```
375
+ {code_context}
376
+ ```
377
+
378
+ Réponds au format JSON avec les clés suivantes:
379
+ - is_bug: boolean (true si c'est un bug dans le code, false si c'est une erreur utilisateur)
380
+ - analysis: string (explication du problème)
381
+ - recommendation: string (recommandation pour résoudre le problème)
382
+ - severity: string (low/medium/high si c'est un bug)
383
+ """
384
+
385
+ try:
386
+ response = self.client.messages.create(
387
+ model=self._model,
388
+ max_tokens=1024,
389
+ messages=[{"role": "user", "content": prompt}]
390
+ )
391
+
392
+ content = response.content[0].text if response.content else "{}"
393
+
394
+ # Essayer de parser le JSON
395
+ try:
396
+ # Extraire le JSON de la réponse
397
+ import re
398
+ json_match = re.search(r'\{[^{}]*\}', content, re.DOTALL)
399
+ if json_match:
400
+ return json.loads(json_match.group())
401
+ except json.JSONDecodeError:
402
+ pass
403
+
404
+ return {
405
+ 'is_bug': False,
406
+ 'analysis': content,
407
+ 'recommendation': 'Contactez le support si le problème persiste.'
408
+ }
409
+
410
+ except Exception as e:
411
+ LogUtils.error("Erreur lors de l'analyse de bug")
412
+ return {
413
+ 'error': str(e),
414
+ 'is_bug': False,
415
+ 'analysis': 'Impossible d\'analyser le problème',
416
+ 'recommendation': 'Veuillez contacter le support technique.'
417
+ }