django-lucy-assist 1.1.0__py3-none-any.whl → 1.2.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-1.1.0.dist-info → django_lucy_assist-1.2.0.dist-info}/METADATA +39 -26
- {django_lucy_assist-1.1.0.dist-info → django_lucy_assist-1.2.0.dist-info}/RECORD +17 -17
- lucy_assist/__init__.py +2 -2
- lucy_assist/conf.py +19 -19
- lucy_assist/constantes.py +2 -2
- lucy_assist/context_processors.py +2 -2
- lucy_assist/migrations/0001_initial.py +1 -1
- lucy_assist/services/__init__.py +3 -2
- lucy_assist/services/mistral_service.py +489 -0
- lucy_assist/services/tool_executor_service.py +10 -10
- lucy_assist/services/tools_definition.py +238 -208
- lucy_assist/static/lucy_assist/css/lucy-assist.css +4 -0
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +2 -2
- lucy_assist/utils/token_utils.py +2 -2
- lucy_assist/views/api_views.py +7 -7
- lucy_assist/services/claude_service.py +0 -423
- {django_lucy_assist-1.1.0.dist-info → django_lucy_assist-1.2.0.dist-info}/WHEEL +0 -0
- {django_lucy_assist-1.1.0.dist-info → django_lucy_assist-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,423 +0,0 @@
|
|
|
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_LUCY_ASSIST_API_KEY
|
|
35
|
-
if not self.api_key:
|
|
36
|
-
raise ValueError("CLAUDE_LUCY_ASSIST_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
|
-
# Ajouter les instructions complémentaires si configurées
|
|
91
|
-
from lucy_assist.models import ConfigurationLucyAssist
|
|
92
|
-
config = ConfigurationLucyAssist.get_config()
|
|
93
|
-
if config.prompt_complementaire:
|
|
94
|
-
prompt += f"\n\n## Instructions complémentaires\n{config.prompt_complementaire}"
|
|
95
|
-
|
|
96
|
-
return prompt
|
|
97
|
-
|
|
98
|
-
def _optimize_messages(self, messages: List) -> List[Dict]:
|
|
99
|
-
"""
|
|
100
|
-
Optimise l'historique des messages pour réduire les tokens.
|
|
101
|
-
|
|
102
|
-
Pour les conversations longues, résume les anciens messages
|
|
103
|
-
au lieu de les envoyer en entier.
|
|
104
|
-
"""
|
|
105
|
-
formatted = self._format_messages(messages)
|
|
106
|
-
|
|
107
|
-
if len(formatted) <= self.MAX_MESSAGES_BEFORE_SUMMARY:
|
|
108
|
-
return formatted
|
|
109
|
-
|
|
110
|
-
# Résumer la conversation
|
|
111
|
-
summary_data = self.project_context_service.summarize_conversation(
|
|
112
|
-
formatted,
|
|
113
|
-
max_tokens=500
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if not summary_data:
|
|
117
|
-
return formatted
|
|
118
|
-
|
|
119
|
-
# Reconstruire les messages avec le résumé
|
|
120
|
-
optimized = []
|
|
121
|
-
|
|
122
|
-
# Ajouter les premiers messages
|
|
123
|
-
optimized.extend(summary_data['first_messages'])
|
|
124
|
-
|
|
125
|
-
# Ajouter le résumé comme message système
|
|
126
|
-
optimized.append({
|
|
127
|
-
'role': 'user',
|
|
128
|
-
'content': f"[Note: {summary_data['original_count'] - 4} messages résumés]\n{summary_data['summary']}"
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
# Ajouter les derniers messages
|
|
132
|
-
optimized.extend(summary_data['last_messages'])
|
|
133
|
-
|
|
134
|
-
LogUtils.info(
|
|
135
|
-
f"Conversation optimisée: {len(formatted)} -> {len(optimized)} messages, "
|
|
136
|
-
f"~{summary_data.get('tokens_saved_estimate', 0)} tokens économisés"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
return optimized
|
|
140
|
-
|
|
141
|
-
def _format_messages(self, messages: List) -> List[Dict]:
|
|
142
|
-
"""Formate les messages pour l'API Claude."""
|
|
143
|
-
formatted = []
|
|
144
|
-
|
|
145
|
-
for msg in messages:
|
|
146
|
-
role = "user" if msg.repondant == LucyAssistConstantes.Repondant.UTILISATEUR else "assistant"
|
|
147
|
-
formatted.append({
|
|
148
|
-
"role": role,
|
|
149
|
-
"content": msg.contenu
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
return formatted
|
|
153
|
-
|
|
154
|
-
def chat_completion_stream(
|
|
155
|
-
self,
|
|
156
|
-
messages: List,
|
|
157
|
-
page_context: Dict,
|
|
158
|
-
user,
|
|
159
|
-
tool_executor=None
|
|
160
|
-
) -> Generator[Dict[str, Any], None, None]:
|
|
161
|
-
"""
|
|
162
|
-
Génère une réponse en streaming avec support des tools.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
messages: Liste des messages de la conversation
|
|
166
|
-
page_context: Contexte de la page courante
|
|
167
|
-
user: Utilisateur Django
|
|
168
|
-
tool_executor: Callable pour exécuter les tools (optionnel)
|
|
169
|
-
|
|
170
|
-
Yields:
|
|
171
|
-
Dict avec 'type' (content/tool_use/tool_result/usage/error) et les données associées
|
|
172
|
-
"""
|
|
173
|
-
try:
|
|
174
|
-
# Extraire la question utilisateur du dernier message
|
|
175
|
-
user_question = ""
|
|
176
|
-
if messages:
|
|
177
|
-
last_msg = messages[-1] if hasattr(messages[-1], 'contenu') else messages[-1]
|
|
178
|
-
user_question = getattr(last_msg, 'contenu', '') if hasattr(last_msg, 'contenu') else str(last_msg)
|
|
179
|
-
|
|
180
|
-
system_prompt = self._build_system_prompt(page_context, user, user_question)
|
|
181
|
-
|
|
182
|
-
# Utiliser l'optimisation des messages pour les longues conversations
|
|
183
|
-
formatted_messages = self._optimize_messages(messages)
|
|
184
|
-
|
|
185
|
-
if not formatted_messages:
|
|
186
|
-
yield {'type': 'error', 'error': 'Aucun message à traiter'}
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
# Boucle pour gérer les appels de tools
|
|
190
|
-
current_messages = formatted_messages.copy()
|
|
191
|
-
max_tool_iterations = 5 # Limite de sécurité
|
|
192
|
-
|
|
193
|
-
for iteration in range(max_tool_iterations):
|
|
194
|
-
with self.client.messages.stream(
|
|
195
|
-
model=self._model,
|
|
196
|
-
max_tokens=self.MAX_TOKENS,
|
|
197
|
-
system=system_prompt,
|
|
198
|
-
messages=current_messages,
|
|
199
|
-
tools=self._tools
|
|
200
|
-
) as stream:
|
|
201
|
-
response_text = ""
|
|
202
|
-
tool_uses = []
|
|
203
|
-
|
|
204
|
-
for text in stream.text_stream:
|
|
205
|
-
response_text += text
|
|
206
|
-
yield {'type': 'content', 'content': text}
|
|
207
|
-
|
|
208
|
-
# Récupérer la réponse finale pour les tools
|
|
209
|
-
response = stream.get_final_message()
|
|
210
|
-
|
|
211
|
-
# Extraire les appels de tools
|
|
212
|
-
for block in response.content:
|
|
213
|
-
if block.type == "tool_use":
|
|
214
|
-
tool_uses.append({
|
|
215
|
-
'id': block.id,
|
|
216
|
-
'name': block.name,
|
|
217
|
-
'input': block.input
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
# Si pas de tool_use, on a fini
|
|
221
|
-
if not tool_uses or response.stop_reason != "tool_use":
|
|
222
|
-
if response.usage:
|
|
223
|
-
total_tokens = response.usage.input_tokens + response.usage.output_tokens
|
|
224
|
-
yield {
|
|
225
|
-
'type': 'usage',
|
|
226
|
-
'input_tokens': response.usage.input_tokens,
|
|
227
|
-
'output_tokens': response.usage.output_tokens,
|
|
228
|
-
'total_tokens': total_tokens
|
|
229
|
-
}
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
# Exécuter les tools
|
|
233
|
-
tool_results = []
|
|
234
|
-
for tool_use in tool_uses:
|
|
235
|
-
yield {
|
|
236
|
-
'type': 'tool_use',
|
|
237
|
-
'tool_name': tool_use['name'],
|
|
238
|
-
'tool_input': tool_use['input']
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
# Exécuter le tool si un executor est fourni
|
|
242
|
-
if tool_executor:
|
|
243
|
-
try:
|
|
244
|
-
result = tool_executor(
|
|
245
|
-
tool_use['name'],
|
|
246
|
-
tool_use['input'],
|
|
247
|
-
user
|
|
248
|
-
)
|
|
249
|
-
tool_results.append({
|
|
250
|
-
'type': 'tool_result',
|
|
251
|
-
'tool_use_id': tool_use['id'],
|
|
252
|
-
'content': json.dumps(result, ensure_ascii=False)
|
|
253
|
-
})
|
|
254
|
-
yield {
|
|
255
|
-
'type': 'tool_result',
|
|
256
|
-
'tool_name': tool_use['name'],
|
|
257
|
-
'result': result
|
|
258
|
-
}
|
|
259
|
-
except Exception as e:
|
|
260
|
-
error_result = {'error': str(e)}
|
|
261
|
-
tool_results.append({
|
|
262
|
-
'type': 'tool_result',
|
|
263
|
-
'tool_use_id': tool_use['id'],
|
|
264
|
-
'content': json.dumps(error_result),
|
|
265
|
-
'is_error': True
|
|
266
|
-
})
|
|
267
|
-
yield {
|
|
268
|
-
'type': 'tool_error',
|
|
269
|
-
'tool_name': tool_use['name'],
|
|
270
|
-
'error': str(e)
|
|
271
|
-
}
|
|
272
|
-
else:
|
|
273
|
-
# Pas d'executor, on ne peut pas exécuter le tool
|
|
274
|
-
tool_results.append({
|
|
275
|
-
'type': 'tool_result',
|
|
276
|
-
'tool_use_id': tool_use['id'],
|
|
277
|
-
'content': json.dumps({'error': 'Tool executor not available'}),
|
|
278
|
-
'is_error': True
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
# Ajouter les messages pour continuer la conversation
|
|
282
|
-
current_messages.append({
|
|
283
|
-
'role': 'assistant',
|
|
284
|
-
'content': response.content
|
|
285
|
-
})
|
|
286
|
-
current_messages.append({
|
|
287
|
-
'role': 'user',
|
|
288
|
-
'content': tool_results
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
except anthropic.APIConnectionError as e:
|
|
292
|
-
LogUtils.error(f"Erreur de connexion API Claude: {e}")
|
|
293
|
-
yield {'type': 'error', 'error': 'Impossible de se connecter au service IA'}
|
|
294
|
-
|
|
295
|
-
except anthropic.RateLimitError as e:
|
|
296
|
-
LogUtils.error(f"Rate limit API Claude: {e}")
|
|
297
|
-
yield {'type': 'error', 'error': 'Service temporairement surchargé, veuillez réessayer'}
|
|
298
|
-
|
|
299
|
-
except anthropic.APIStatusError as e:
|
|
300
|
-
LogUtils.error(f"Erreur API Claude: {e}")
|
|
301
|
-
yield {'type': 'error', 'error': f'Erreur du service IA: {e.message}'}
|
|
302
|
-
|
|
303
|
-
except Exception as e:
|
|
304
|
-
LogUtils.error("Erreur inattendue lors de l'appel Claude")
|
|
305
|
-
yield {'type': 'error', 'error': str(e)}
|
|
306
|
-
|
|
307
|
-
def chat_completion(
|
|
308
|
-
self,
|
|
309
|
-
messages: List,
|
|
310
|
-
page_context: Dict,
|
|
311
|
-
user
|
|
312
|
-
) -> Dict[str, Any]:
|
|
313
|
-
"""
|
|
314
|
-
Génère une réponse complète (non-streaming).
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
Dict avec 'content', 'tokens_utilises', ou 'error'
|
|
318
|
-
"""
|
|
319
|
-
try:
|
|
320
|
-
# Extraire la question utilisateur
|
|
321
|
-
user_question = ""
|
|
322
|
-
if messages:
|
|
323
|
-
last_msg = messages[-1] if hasattr(messages[-1], 'contenu') else messages[-1]
|
|
324
|
-
user_question = getattr(last_msg, 'contenu', '') if hasattr(last_msg, 'contenu') else str(last_msg)
|
|
325
|
-
|
|
326
|
-
system_prompt = self._build_system_prompt(page_context, user, user_question)
|
|
327
|
-
|
|
328
|
-
# Utiliser l'optimisation des messages
|
|
329
|
-
formatted_messages = self._optimize_messages(messages)
|
|
330
|
-
|
|
331
|
-
if not formatted_messages:
|
|
332
|
-
return {'error': 'Aucun message à traiter'}
|
|
333
|
-
|
|
334
|
-
response = self.client.messages.create(
|
|
335
|
-
model=self._model,
|
|
336
|
-
max_tokens=self.MAX_TOKENS,
|
|
337
|
-
system=system_prompt,
|
|
338
|
-
messages=formatted_messages
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
content = ""
|
|
342
|
-
for block in response.content:
|
|
343
|
-
if block.type == "text":
|
|
344
|
-
content += block.text
|
|
345
|
-
|
|
346
|
-
total_tokens = 0
|
|
347
|
-
if response.usage:
|
|
348
|
-
total_tokens = response.usage.input_tokens + response.usage.output_tokens
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
'content': content,
|
|
352
|
-
'tokens_utilises': total_tokens,
|
|
353
|
-
'input_tokens': response.usage.input_tokens if response.usage else 0,
|
|
354
|
-
'output_tokens': response.usage.output_tokens if response.usage else 0
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
except Exception as e:
|
|
358
|
-
LogUtils.error("Erreur lors de l'appel Claude")
|
|
359
|
-
return {'error': str(e)}
|
|
360
|
-
|
|
361
|
-
def analyze_code_for_bug(
|
|
362
|
-
self,
|
|
363
|
-
error_message: str,
|
|
364
|
-
code_context: str,
|
|
365
|
-
user_description: str
|
|
366
|
-
) -> Dict[str, Any]:
|
|
367
|
-
"""
|
|
368
|
-
Analyse du code pour détecter un bug potentiel.
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
Dict avec 'is_bug', 'analysis', 'recommendation'
|
|
372
|
-
"""
|
|
373
|
-
prompt = f"""Analyse le problème suivant signalé par un utilisateur:
|
|
374
|
-
|
|
375
|
-
Description de l'utilisateur: {user_description}
|
|
376
|
-
|
|
377
|
-
Message d'erreur (si disponible): {error_message}
|
|
378
|
-
|
|
379
|
-
Code source pertinent:
|
|
380
|
-
```
|
|
381
|
-
{code_context}
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
Réponds au format JSON avec les clés suivantes:
|
|
385
|
-
- is_bug: boolean (true si c'est un bug dans le code, false si c'est une erreur utilisateur)
|
|
386
|
-
- analysis: string (explication du problème)
|
|
387
|
-
- recommendation: string (recommandation pour résoudre le problème)
|
|
388
|
-
- severity: string (low/medium/high si c'est un bug)
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
try:
|
|
392
|
-
response = self.client.messages.create(
|
|
393
|
-
model=self._model,
|
|
394
|
-
max_tokens=1024,
|
|
395
|
-
messages=[{"role": "user", "content": prompt}]
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
content = response.content[0].text if response.content else "{}"
|
|
399
|
-
|
|
400
|
-
# Essayer de parser le JSON
|
|
401
|
-
try:
|
|
402
|
-
# Extraire le JSON de la réponse
|
|
403
|
-
import re
|
|
404
|
-
json_match = re.search(r'\{[^{}]*\}', content, re.DOTALL)
|
|
405
|
-
if json_match:
|
|
406
|
-
return json.loads(json_match.group())
|
|
407
|
-
except json.JSONDecodeError:
|
|
408
|
-
pass
|
|
409
|
-
|
|
410
|
-
return {
|
|
411
|
-
'is_bug': False,
|
|
412
|
-
'analysis': content,
|
|
413
|
-
'recommendation': 'Contactez le support si le problème persiste.'
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
except Exception as e:
|
|
417
|
-
LogUtils.error("Erreur lors de l'analyse de bug")
|
|
418
|
-
return {
|
|
419
|
-
'error': str(e),
|
|
420
|
-
'is_bug': False,
|
|
421
|
-
'analysis': 'Impossible d\'analyser le problème',
|
|
422
|
-
'recommendation': 'Veuillez contacter le support technique.'
|
|
423
|
-
}
|
|
File without changes
|
|
File without changes
|