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,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
|
+
}
|