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.
- django_lucy_assist-0.1.0.dist-info/METADATA +206 -0
- django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
- django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
- django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
- lucy_assist/__init__.py +11 -0
- lucy_assist/admin.py +22 -0
- lucy_assist/apps.py +10 -0
- lucy_assist/conf.py +103 -0
- lucy_assist/constantes.py +120 -0
- lucy_assist/context_processors.py +65 -0
- lucy_assist/migrations/0001_initial.py +92 -0
- lucy_assist/migrations/__init__.py +0 -0
- lucy_assist/models/__init__.py +14 -0
- lucy_assist/models/base.py +54 -0
- lucy_assist/models/configuration.py +175 -0
- lucy_assist/models/conversation.py +54 -0
- lucy_assist/models/message.py +45 -0
- lucy_assist/models/project_context_cache.py +213 -0
- lucy_assist/services/__init__.py +21 -0
- lucy_assist/services/bug_notification_service.py +183 -0
- lucy_assist/services/claude_service.py +417 -0
- lucy_assist/services/context_service.py +350 -0
- lucy_assist/services/crud_service.py +364 -0
- lucy_assist/services/gitlab_service.py +248 -0
- lucy_assist/services/project_context_service.py +412 -0
- lucy_assist/services/tool_executor_service.py +343 -0
- lucy_assist/services/tools_definition.py +229 -0
- lucy_assist/signals.py +25 -0
- lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
- lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
- lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
- lucy_assist/tests/__init__.py +0 -0
- lucy_assist/tests/factories/__init__.py +15 -0
- lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
- lucy_assist/tests/test_lucy_assist.py +186 -0
- lucy_assist/urls.py +36 -0
- lucy_assist/utils/__init__.py +7 -0
- lucy_assist/utils/log_utils.py +59 -0
- lucy_assist/utils/message_utils.py +130 -0
- lucy_assist/utils/token_utils.py +87 -0
- lucy_assist/views/__init__.py +13 -0
- 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)
|