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.
@@ -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
- }