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,595 @@
1
+ import json
2
+
3
+ from django.conf import settings
4
+ from django.db.models import Count
5
+ from django.http import JsonResponse, StreamingHttpResponse
6
+ from django.views import View
7
+ from django.views.decorators.csrf import csrf_exempt
8
+ from django.utils.decorators import method_decorator
9
+ from django.contrib.auth.mixins import LoginRequiredMixin
10
+
11
+ from lucy_assist.utils.log_utils import LogUtils
12
+ from lucy_assist.models import Conversation, Message, ConfigurationLucyAssist
13
+ from lucy_assist.constantes import LucyAssistConstantes
14
+ from lucy_assist.services.claude_service import ClaudeService
15
+ from lucy_assist.services.context_service import ContextService
16
+ from lucy_assist.services.tool_executor_service import create_tool_executor
17
+
18
+ class LucyAssistAPIView(LoginRequiredMixin, View):
19
+ """Classe de base pour les vues API Lucy Assist."""
20
+
21
+ def dispatch(self, request, *args, **kwargs):
22
+ # Vérifier que Lucy Assist est activé
23
+ config = ConfigurationLucyAssist.get_config()
24
+ if not config.actif:
25
+ return JsonResponse({
26
+ 'error': 'Lucy est temporairement désactivé'
27
+ }, status=503)
28
+ return super().dispatch(request, *args, **kwargs)
29
+
30
+ def json_response(self, data, status=200):
31
+ return JsonResponse(data, status=status)
32
+
33
+ def error_response(self, message, status=400):
34
+ return JsonResponse({'error': message}, status=status)
35
+
36
+
37
+ class ConversationListCreateView(LucyAssistAPIView):
38
+ """Liste et création de conversations."""
39
+
40
+ def get(self, request):
41
+ """Liste les conversations de l'utilisateur."""
42
+ conversations = Conversation.objects.filter(
43
+ utilisateur=request.user,
44
+ is_active=True
45
+ ).order_by('-created_date')[:50]
46
+
47
+ data = [{
48
+ 'id': conv.id,
49
+ 'titre': conv.titre or f"Conversation #{conv.id}",
50
+ 'created_date': conv.created_date.isoformat(),
51
+ 'total_tokens': conv.total_tokens,
52
+ 'dernier_message': conv.dernier_message.contenu[:100] if conv.dernier_message else None
53
+ } for conv in conversations]
54
+
55
+ return self.json_response({'conversations': data})
56
+
57
+ def post(self, request):
58
+ """Crée une nouvelle conversation."""
59
+ try:
60
+ body = json.loads(request.body) if request.body else {}
61
+ except json.JSONDecodeError:
62
+ body = {}
63
+
64
+ page_contexte = body.get('page_contexte', '')
65
+
66
+ conversation = Conversation.objects.create(
67
+ utilisateur=request.user,
68
+ page_contexte=page_contexte
69
+ )
70
+
71
+ return self.json_response({
72
+ 'id': conversation.id,
73
+ 'titre': conversation.titre,
74
+ 'created_date': conversation.created_date.isoformat()
75
+ }, status=201)
76
+
77
+
78
+ class ConversationDetailView(LucyAssistAPIView):
79
+ """Détail, mise à jour et suppression d'une conversation."""
80
+
81
+ def get(self, request, pk):
82
+ """Récupère une conversation avec ses messages."""
83
+ try:
84
+ conversation = Conversation.objects.get(
85
+ pk=pk,
86
+ utilisateur=request.user
87
+ )
88
+ except Conversation.DoesNotExist:
89
+ return self.error_response('Conversation non trouvée', 404)
90
+
91
+ messages = [{
92
+ 'id': msg.id,
93
+ 'repondant': msg.repondant,
94
+ 'contenu': msg.contenu,
95
+ 'tokens_utilises': msg.tokens_utilises,
96
+ 'type_action': msg.type_action,
97
+ 'created_date': msg.created_date.isoformat()
98
+ } for msg in conversation.messages.all()]
99
+
100
+ return self.json_response({
101
+ 'id': conversation.id,
102
+ 'titre': conversation.titre,
103
+ 'page_contexte': conversation.page_contexte,
104
+ 'created_date': conversation.created_date.isoformat(),
105
+ 'total_tokens': conversation.total_tokens,
106
+ 'messages': messages
107
+ })
108
+
109
+ def delete(self, request, pk):
110
+ """Supprime une conversation."""
111
+ try:
112
+ conversation = Conversation.objects.get(
113
+ pk=pk,
114
+ utilisateur=request.user
115
+ )
116
+ except Conversation.DoesNotExist:
117
+ return self.error_response('Conversation non trouvée', 404)
118
+
119
+ conversation.delete()
120
+ return self.json_response({'success': True})
121
+
122
+
123
+ class MessageCreateView(LucyAssistAPIView):
124
+ """Création d'un message utilisateur."""
125
+
126
+ def post(self, request, conversation_id):
127
+ """Ajoute un message utilisateur à une conversation."""
128
+ try:
129
+ conversation = Conversation.objects.get(
130
+ pk=conversation_id,
131
+ utilisateur=request.user
132
+ )
133
+ except Conversation.DoesNotExist:
134
+ return self.error_response('Conversation non trouvée', 404)
135
+
136
+ try:
137
+ body = json.loads(request.body)
138
+ except json.JSONDecodeError:
139
+ return self.error_response('JSON invalide', 400)
140
+
141
+ contenu = body.get('contenu', '').strip()
142
+ if not contenu:
143
+ return self.error_response('Le message ne peut pas être vide', 400)
144
+
145
+ message = Message.objects.create(
146
+ conversation=conversation,
147
+ repondant=LucyAssistConstantes.Repondant.UTILISATEUR,
148
+ contenu=contenu
149
+ )
150
+
151
+ # Générer le titre si c'est le premier message
152
+ if conversation.messages.count() == 1:
153
+ conversation.generer_titre()
154
+
155
+ return self.json_response({
156
+ 'id': message.id,
157
+ 'contenu': message.contenu,
158
+ 'created_date': message.created_date.isoformat()
159
+ }, status=201)
160
+
161
+
162
+ @method_decorator(csrf_exempt, name='dispatch')
163
+ class ChatCompletionView(LucyAssistAPIView):
164
+ """Génération de réponse via Claude API avec streaming."""
165
+
166
+ def post(self, request, conversation_id):
167
+ """Génère une réponse Claude pour la conversation."""
168
+ try:
169
+ conversation = Conversation.objects.get(
170
+ pk=conversation_id,
171
+ utilisateur=request.user
172
+ )
173
+ except Conversation.DoesNotExist:
174
+ return self.error_response('Conversation non trouvée', 404)
175
+
176
+ # Vérifier les tokens disponibles
177
+ config = ConfigurationLucyAssist.get_config()
178
+ if not config.a_suffisamment_tokens():
179
+ return self.json_response({
180
+ 'error': 'tokens_insuffisants',
181
+ 'message': 'Vous n\'avez plus assez de crédits. Veuillez acheter un nouveau pack.',
182
+ 'tokens_disponibles': config.tokens_disponibles
183
+ }, status=402)
184
+
185
+ try:
186
+ body = json.loads(request.body) if request.body else {}
187
+ except json.JSONDecodeError:
188
+ body = {}
189
+
190
+ page_contexte = body.get('page_contexte', conversation.page_contexte)
191
+
192
+ # Construire le contexte de la conversation
193
+ messages_historique = list(conversation.messages.all().order_by('created_date'))
194
+
195
+ # Obtenir le contexte de la page
196
+ context_service = ContextService(request.user)
197
+ contexte_page = context_service.get_page_context(page_contexte)
198
+
199
+ # Appeler Claude avec le tool executor
200
+ claude_service = ClaudeService()
201
+ tool_executor = create_tool_executor(request.user)
202
+
203
+ try:
204
+ # Stream la réponse
205
+ def generate():
206
+ full_response = ""
207
+ total_tokens = 0
208
+ tool_actions = [] # Pour tracker les actions effectuées
209
+
210
+ for chunk in claude_service.chat_completion_stream(
211
+ messages=messages_historique,
212
+ page_context=contexte_page,
213
+ user=request.user,
214
+ tool_executor=tool_executor
215
+ ):
216
+ chunk_type = chunk.get('type')
217
+
218
+ if chunk_type == 'content':
219
+ full_response += chunk['content']
220
+ yield f"data: {json.dumps({'type': 'content', 'content': chunk['content']})}\n\n"
221
+
222
+ elif chunk_type == 'tool_use':
223
+ # Informer le client qu'un tool est appelé
224
+ yield f"data: {json.dumps({'type': 'tool_use', 'tool_name': chunk['tool_name']})}\n\n"
225
+
226
+ elif chunk_type == 'tool_result':
227
+ # Envoyer le résultat du tool au client
228
+ tool_actions.append({
229
+ 'tool': chunk['tool_name'],
230
+ 'result': chunk['result']
231
+ })
232
+ yield f"data: {json.dumps({'type': 'tool_result', 'tool_name': chunk['tool_name'], 'result': chunk['result']})}\n\n"
233
+
234
+ elif chunk_type == 'tool_error':
235
+ yield f"data: {json.dumps({'type': 'tool_error', 'tool_name': chunk['tool_name'], 'error': chunk['error']})}\n\n"
236
+
237
+ elif chunk_type == 'usage':
238
+ total_tokens = chunk.get('total_tokens', 0)
239
+
240
+ elif chunk_type == 'error':
241
+ yield f"data: {json.dumps({'type': 'error', 'error': chunk['error']})}\n\n"
242
+ return
243
+
244
+ # Sauvegarder la réponse complète
245
+ if full_response:
246
+ # Inclure les actions effectuées dans les métadonnées si nécessaire
247
+ type_action = None
248
+ if tool_actions:
249
+ # Déterminer le type d'action principal
250
+ for action in tool_actions:
251
+ if 'create' in action['tool']:
252
+ type_action = LucyAssistConstantes.TypeAction.CRUD
253
+ break
254
+ elif 'update' in action['tool'] or 'delete' in action['tool']:
255
+ type_action = LucyAssistConstantes.TypeAction.CRUD
256
+ break
257
+ elif 'search' in action['tool']:
258
+ type_action = LucyAssistConstantes.TypeAction.RECHERCHE
259
+ break
260
+
261
+ message = Message.objects.create(
262
+ conversation=conversation,
263
+ repondant=LucyAssistConstantes.Repondant.CHATBOT,
264
+ contenu=full_response,
265
+ tokens_utilises=total_tokens,
266
+ type_action=type_action
267
+ )
268
+
269
+ response_data = {
270
+ 'type': 'done',
271
+ 'message_id': message.id,
272
+ 'tokens_utilises': total_tokens
273
+ }
274
+
275
+ # Ajouter les actions effectuées
276
+ if tool_actions:
277
+ response_data['actions'] = tool_actions
278
+
279
+ yield f"data: {json.dumps(response_data)}\n\n"
280
+
281
+ response = StreamingHttpResponse(
282
+ generate(),
283
+ content_type='text/event-stream'
284
+ )
285
+ response['Cache-Control'] = 'no-cache'
286
+ response['X-Accel-Buffering'] = 'no'
287
+ return response
288
+
289
+ except Exception as e:
290
+ LogUtils.error("Erreur lors de l'appel à Claude")
291
+ return self.error_response(f'Erreur lors de la génération: {str(e)}', 500)
292
+
293
+
294
+ class TokenStatusView(LucyAssistAPIView):
295
+ """Statut des tokens disponibles."""
296
+
297
+ def get(self, request):
298
+ """Retourne le statut des tokens."""
299
+ config = ConfigurationLucyAssist.get_config()
300
+
301
+ return self.json_response({
302
+ 'tokens_disponibles': config.tokens_disponibles,
303
+ 'tokens_en_euros': round(config.tokens_restants_en_euros, 2),
304
+ 'conversations_estimees': config.conversations_estimees,
305
+ 'prix_par_million': float(config.prix_par_million_tokens),
306
+ 'actif': config.actif
307
+ })
308
+
309
+
310
+ class AcheterTokensView(LucyAssistAPIView):
311
+ """Achat de tokens via Lucy CRM."""
312
+
313
+ def post(self, request):
314
+ """Génère un lien d'achat de tokens."""
315
+ try:
316
+ body = json.loads(request.body)
317
+ except json.JSONDecodeError:
318
+ return self.error_response('JSON invalide', 400)
319
+
320
+ montant_ht = body.get('montant_ht')
321
+ if not montant_ht or montant_ht < 10:
322
+ return self.error_response('Le montant minimum est de 10 EUR', 400)
323
+
324
+ # Récupérer le SIREN client depuis les settings
325
+ siren_client = getattr(settings, 'SIREN_CLIENT', None)
326
+ if not siren_client:
327
+ return self.error_response('Configuration SIREN manquante', 500)
328
+
329
+ # Générer l'URL de souscription Lucy
330
+ url_souscription = f"https://app.lucy-crm.fr/sav/souscription-token-lucy-assist/{siren_client}/{montant_ht}"
331
+
332
+ # Calculer les tokens qui seront ajoutés
333
+ config = ConfigurationLucyAssist.get_config()
334
+ tokens_a_ajouter = int((montant_ht / float(config.prix_par_million_tokens)) * 1_000_000)
335
+
336
+ return self.json_response({
337
+ 'url_souscription': url_souscription,
338
+ 'montant_ht': montant_ht,
339
+ 'tokens_a_ajouter': tokens_a_ajouter,
340
+ 'conversations_estimees': int(tokens_a_ajouter / LucyAssistConstantes.TOKENS_MOYENS_PAR_CONVERSATION)
341
+ })
342
+
343
+
344
+ class SuggestionsView(LucyAssistAPIView):
345
+ """Suggestions de questions fréquentes."""
346
+
347
+ def get(self, request):
348
+ """Retourne les suggestions de questions."""
349
+ # Récupérer les questions les plus fréquentes des conversations précédentes
350
+ questions_frequentes = Message.objects.filter(
351
+ conversation__utilisateur=request.user,
352
+ repondant=LucyAssistConstantes.Repondant.UTILISATEUR
353
+ ).values('contenu').annotate(
354
+ count=Count('id')
355
+ ).order_by('-count')[:5]
356
+
357
+ suggestions = [q['contenu'][:100] for q in questions_frequentes]
358
+
359
+ # Compléter avec les suggestions configurées si nécessaire
360
+ if len(suggestions) < 5:
361
+ config = ConfigurationLucyAssist.get_config()
362
+ for question in config.get_questions_frequentes():
363
+ if question not in suggestions:
364
+ suggestions.append(question)
365
+ if len(suggestions) >= 5:
366
+ break
367
+
368
+ return self.json_response({'suggestions': suggestions})
369
+
370
+
371
+ class PageContextView(LucyAssistAPIView):
372
+ """Récupération du contexte de la page courante."""
373
+
374
+ def get(self, request):
375
+ """Retourne le contexte de la page."""
376
+ page_url = request.GET.get('url', '')
377
+
378
+ context_service = ContextService(request.user)
379
+ contexte = context_service.get_page_context(page_url)
380
+
381
+ return self.json_response(contexte)
382
+
383
+
384
+ class CacheStatsView(LucyAssistAPIView):
385
+ """Statistiques du cache de contexte projet."""
386
+
387
+ def get(self, request):
388
+ """Retourne les statistiques du cache."""
389
+ from lucy_assist.services.project_context_service import ProjectContextService
390
+
391
+ # Vérifier que l'utilisateur est staff
392
+ if not request.user.is_staff:
393
+ return self.error_response('Permission refusée', 403)
394
+
395
+ service = ProjectContextService()
396
+ stats = service.get_cache_stats()
397
+
398
+ # Calculer les économies en euros
399
+ config = ConfigurationLucyAssist.get_config()
400
+ tokens_saved = stats.get('total_tokens_saved', 0)
401
+ euros_saved = (tokens_saved / 1_000_000) * float(config.prix_par_million_tokens)
402
+
403
+ return self.json_response({
404
+ 'cache_stats': stats,
405
+ 'tokens_economises': tokens_saved,
406
+ 'euros_economises': round(euros_saved, 2),
407
+ 'message': f"Le cache a permis d'économiser ~{tokens_saved:,} tokens ({euros_saved:.2f}€)"
408
+ })
409
+
410
+
411
+ class CacheInvalidateView(LucyAssistAPIView):
412
+ """Invalidation du cache de contexte projet."""
413
+
414
+ def post(self, request):
415
+ """Invalide le cache (tout ou une clé spécifique)."""
416
+ from lucy_assist.services.project_context_service import ProjectContextService
417
+
418
+ # Vérifier que l'utilisateur est staff
419
+ if not request.user.is_staff:
420
+ return self.error_response('Permission refusée', 403)
421
+
422
+ try:
423
+ body = json.loads(request.body) if request.body else {}
424
+ except json.JSONDecodeError:
425
+ body = {}
426
+
427
+ cache_key = body.get('cache_key')
428
+
429
+ from lucy_assist.models import ProjectContextCache
430
+
431
+ if cache_key:
432
+ ProjectContextCache.invalidate_cache(cache_key)
433
+ return self.json_response({
434
+ 'success': True,
435
+ 'message': f"Cache '{cache_key}' invalidé"
436
+ })
437
+ else:
438
+ ProjectContextCache.invalidate_all()
439
+ return self.json_response({
440
+ 'success': True,
441
+ 'message': "Tous les caches ont été invalidés"
442
+ })
443
+
444
+
445
+ class FeedbackCreateView(LucyAssistAPIView):
446
+ """Envoi d'un feedback utilisateur pour signaler un problème avec Lucy Assist."""
447
+
448
+ # Email par défaut si aucun collaborateur associé
449
+ EMAIL_FALLBACK = 'maxence@revolucy.fr'
450
+
451
+ def post(self, request):
452
+ """Envoie un email avec la conversation à l'équipe Revolucy."""
453
+ import requests
454
+ from django.core.mail import send_mail
455
+ from django.utils import timezone
456
+
457
+ try:
458
+ body = json.loads(request.body)
459
+ except json.JSONDecodeError:
460
+ return self.error_response('JSON invalide', 400)
461
+
462
+ # Récupérer les données
463
+ conversation_id = body.get('conversation_id')
464
+ description = body.get('description', '').strip()
465
+ page_url = body.get('page_url', '')
466
+
467
+ # Récupérer la conversation avec tous ses messages
468
+ conversation = None
469
+ messages_conversation = []
470
+
471
+ if conversation_id:
472
+ try:
473
+ conversation = Conversation.objects.get(pk=conversation_id, utilisateur=request.user)
474
+ messages_conversation = list(conversation.messages.all().order_by('created_date'))
475
+ except Conversation.DoesNotExist:
476
+ return self.error_response('Conversation non trouvée', 404)
477
+ else:
478
+ return self.error_response('ID de conversation requis', 400)
479
+
480
+ # Construire le contenu de la conversation
481
+ conversation_content = self._formater_conversation(messages_conversation)
482
+
483
+ # Envoyer l'email
484
+ try:
485
+ # Récupérer l'email du collaborateur associé via l'API Lucy CRM
486
+ destinataire = self._get_collaborateur_email()
487
+
488
+ user_name = request.user.get_full_name() if hasattr(request.user, 'get_full_name') else str(request.user)
489
+ user_email = request.user.email if hasattr(request.user, 'email') else 'Non disponible'
490
+
491
+ sujet = f"[Lucy Assist] Feedback - {user_name}"
492
+
493
+ message_email = f"""
494
+ Un utilisateur a signalé un problème avec Lucy Assist.
495
+
496
+ ================================================================================
497
+ INFORMATIONS UTILISATEUR
498
+ ================================================================================
499
+ Utilisateur : {user_name}
500
+ Email : {user_email}
501
+ Date : {timezone.now().strftime('%d/%m/%Y à %H:%M')}
502
+ Page : {page_url or 'Non spécifiée'}
503
+
504
+ ================================================================================
505
+ DESCRIPTION DU PROBLÈME
506
+ ================================================================================
507
+ {description or 'Aucune description fournie'}
508
+
509
+ ================================================================================
510
+ CONVERSATION COMPLÈTE
511
+ ================================================================================
512
+ {conversation_content}
513
+
514
+ ================================================================================
515
+ INFORMATIONS TECHNIQUES
516
+ ================================================================================
517
+ Conversation ID : {conversation.id}
518
+ Titre : {conversation.titre or 'Sans titre'}
519
+ Total tokens : {conversation.total_tokens}
520
+ Créée le : {conversation.created_date.strftime('%d/%m/%Y à %H:%M')}
521
+ """
522
+
523
+ send_mail(
524
+ subject=sujet,
525
+ message=message_email,
526
+ from_email=settings.DEFAULT_FROM_EMAIL,
527
+ recipient_list=[destinataire],
528
+ fail_silently=False
529
+ )
530
+
531
+ LogUtils.info(f"Feedback Lucy Assist envoyé par {user_email} à {destinataire} - Conversation #{conversation.id}")
532
+
533
+ return self.json_response({
534
+ 'success': True,
535
+ 'message': 'Merci pour votre retour ! Notre équipe va analyser le problème.'
536
+ }, status=201)
537
+
538
+ except Exception as e:
539
+ LogUtils.error(f"Erreur envoi feedback Lucy Assist: {str(e)}")
540
+ return self.error_response('Erreur lors de l\'envoi du feedback', 500)
541
+
542
+ def _get_collaborateur_email(self):
543
+ """
544
+ Récupère l'email du collaborateur associé via l'API Lucy CRM.
545
+ Utilise le SIREN du client configuré dans les settings.
546
+ Retourne l'email par défaut si non trouvé.
547
+ """
548
+ import requests
549
+
550
+ try:
551
+ # Récupérer le SIREN depuis les settings
552
+ siren_client = getattr(settings, 'SIREN_CLIENT', None)
553
+ if not siren_client:
554
+ LogUtils.warning("SIREN_CLIENT non configuré, utilisation de l'email par défaut")
555
+ return self.EMAIL_FALLBACK
556
+
557
+ # Appel à l'API Lucy CRM
558
+ url = f"https://app.lucy-crm.fr/api/credit-client/{siren_client}"
559
+ response = requests.get(url, timeout=10)
560
+
561
+ if response.status_code == 200:
562
+ data = response.json()
563
+ collaborateur_email = data.get('collaborateur_associe')
564
+
565
+ if collaborateur_email:
566
+ LogUtils.info(f"Collaborateur associé trouvé: {collaborateur_email}")
567
+ return collaborateur_email
568
+ else:
569
+ LogUtils.info("Aucun collaborateur associé, utilisation de l'email par défaut")
570
+ return self.EMAIL_FALLBACK
571
+ else:
572
+ LogUtils.warning(f"API Lucy CRM a retourné le status {response.status_code}")
573
+ return self.EMAIL_FALLBACK
574
+
575
+ except requests.RequestException as e:
576
+ LogUtils.error(f"Erreur appel API Lucy CRM: {str(e)}")
577
+ return self.EMAIL_FALLBACK
578
+ except Exception as e:
579
+ LogUtils.error(f"Erreur récupération collaborateur: {str(e)}")
580
+ return self.EMAIL_FALLBACK
581
+
582
+ def _formater_conversation(self, messages):
583
+ """Formate la liste des messages en texte lisible."""
584
+ if not messages:
585
+ return "Aucun message dans la conversation"
586
+
587
+ lignes = []
588
+ for msg in messages:
589
+ repondant = "👤 UTILISATEUR" if msg.repondant == 'UTILISATEUR' else "🤖 LUCY"
590
+ date_str = msg.created_date.strftime('%H:%M:%S')
591
+ lignes.append(f"[{date_str}] {repondant}:")
592
+ lignes.append(f"{msg.contenu}")
593
+ lignes.append("-" * 40)
594
+
595
+ return "\n".join(lignes)