django-lucy-assist 1.0.5__py3-none-any.whl → 1.0.7__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.
@@ -47,6 +47,7 @@ class ToolExecutorService:
47
47
  handlers = {
48
48
  'create_object': self._handle_create_object,
49
49
  'update_object': self._handle_update_object,
50
+ 'get_deletion_impact': self._handle_get_deletion_impact,
50
51
  'delete_object': self._handle_delete_object,
51
52
  'search_objects': self._handle_search_objects,
52
53
  'get_object_details': self._handle_get_object_details,
@@ -110,11 +111,41 @@ class ToolExecutorService:
110
111
 
111
112
  return self.crud_service.update_object(app_name, model_name, object_id, data)
112
113
 
114
+ def _handle_get_deletion_impact(self, params: Dict) -> Dict:
115
+ """
116
+ Analyse l'impact d'une suppression avant de l'exécuter.
117
+ Retourne la liste de toutes les conséquences (cascade, SET_NULL, etc.)
118
+ """
119
+ app_name = params.get('app_name') or get_app_for_model(params.get('model_name', ''))
120
+ model_name = params.get('model_name', '')
121
+ object_id = params.get('object_id')
122
+
123
+ if not model_name:
124
+ return {'error': 'model_name est requis'}
125
+
126
+ if not object_id:
127
+ return {'error': 'object_id est requis'}
128
+
129
+ # Analyser l'impact
130
+ impact = self.crud_service.get_deletion_impact(app_name, model_name, object_id)
131
+
132
+ # Formater le message pour l'utilisateur
133
+ impact['formatted_message'] = self.crud_service.format_deletion_impact_message(impact)
134
+
135
+ return {
136
+ 'success': True,
137
+ 'impact': impact
138
+ }
139
+
113
140
  def _handle_delete_object(self, params: Dict) -> Dict:
114
- """Supprime un objet."""
141
+ """
142
+ Supprime un objet.
143
+ Requiert que l'utilisateur ait explicitement confirmé après avoir vu l'impact.
144
+ """
115
145
  app_name = params.get('app_name') or get_app_for_model(params.get('model_name', ''))
116
146
  model_name = params.get('model_name', '')
117
147
  object_id = params.get('object_id')
148
+ confirmed = params.get('confirmed', False)
118
149
 
119
150
  if not model_name:
120
151
  return {'error': 'model_name est requis'}
@@ -122,6 +153,15 @@ class ToolExecutorService:
122
153
  if not object_id:
123
154
  return {'error': 'object_id est requis'}
124
155
 
156
+ # Vérifier que la confirmation a été donnée
157
+ if not confirmed:
158
+ return {
159
+ 'error': 'Confirmation requise. Utilisez d\'abord get_deletion_impact pour voir les conséquences, '
160
+ 'puis demandez confirmation à l\'utilisateur avant de supprimer.',
161
+ 'requires_confirmation': True
162
+ }
163
+
164
+ # Exécuter la suppression
125
165
  return self.crud_service.delete_object(app_name, model_name, object_id)
126
166
 
127
167
  def _handle_search_objects(self, params: Dict) -> Dict:
@@ -10,7 +10,7 @@ LUCY_ASSIST_TOOLS = [
10
10
  {
11
11
  "name": "create_object",
12
12
  "description": """Crée un nouvel objet dans la base de données.
13
- Utilise ce tool quand l'utilisateur demande de créer un objet métier (client, réservation, etc.).
13
+ Utilise ce tool quand l'utilisateur demande de créer un objet métier (client, facture, etc.).
14
14
  IMPORTANT: Utilise ce tool en priorité quand l'utilisateur demande une création.
15
15
  Ne demande pas de confirmation, exécute directement l'action.""",
16
16
  "input_schema": {
@@ -18,11 +18,11 @@ LUCY_ASSIST_TOOLS = [
18
18
  "properties": {
19
19
  "app_name": {
20
20
  "type": "string",
21
- "description": "Nom de l'application Django (ex: client, reservation, franchise)"
21
+ "description": "Nom de l'application Django (ex: client, facture)"
22
22
  },
23
23
  "model_name": {
24
24
  "type": "string",
25
- "description": "Nom du modèle Django (ex: Client, Reservation, Franchise)"
25
+ "description": "Nom du modèle Django (ex: Client, Facture)"
26
26
  },
27
27
  "data": {
28
28
  "type": "object",
@@ -62,11 +62,37 @@ LUCY_ASSIST_TOOLS = [
62
62
  "required": ["app_name", "model_name", "object_id", "data"]
63
63
  }
64
64
  },
65
+ {
66
+ "name": "get_deletion_impact",
67
+ "description": """Analyse l'impact d'une suppression AVANT de supprimer.
68
+ OBLIGATOIRE: Utilise TOUJOURS ce tool AVANT delete_object pour montrer à l'utilisateur
69
+ toutes les conséquences de la suppression (objets supprimés en cascade, champs mis à NULL, etc.)
70
+ Affiche le résultat à l'utilisateur et demande sa confirmation explicite avant de supprimer.""",
71
+ "input_schema": {
72
+ "type": "object",
73
+ "properties": {
74
+ "app_name": {
75
+ "type": "string",
76
+ "description": "Nom de l'application Django"
77
+ },
78
+ "model_name": {
79
+ "type": "string",
80
+ "description": "Nom du modèle Django"
81
+ },
82
+ "object_id": {
83
+ "type": "integer",
84
+ "description": "ID de l'objet à analyser"
85
+ }
86
+ },
87
+ "required": ["app_name", "model_name", "object_id"]
88
+ }
89
+ },
65
90
  {
66
91
  "name": "delete_object",
67
92
  "description": """Supprime un objet de la base de données.
68
- Utilise ce tool quand l'utilisateur demande explicitement de supprimer un objet.
69
- ATTENTION: Demande confirmation avant de supprimer.""",
93
+ IMPORTANT: Tu DOIS d'abord utiliser get_deletion_impact pour analyser les conséquences,
94
+ puis afficher le résultat à l'utilisateur, et obtenir sa CONFIRMATION EXPLICITE avant d'exécuter ce tool.
95
+ Ne JAMAIS supprimer sans avoir montré l'impact et obtenu confirmation.""",
70
96
  "input_schema": {
71
97
  "type": "object",
72
98
  "properties": {
@@ -81,9 +107,13 @@ LUCY_ASSIST_TOOLS = [
81
107
  "object_id": {
82
108
  "type": "integer",
83
109
  "description": "ID de l'objet à supprimer"
110
+ },
111
+ "confirmed": {
112
+ "type": "boolean",
113
+ "description": "OBLIGATOIRE: true si l'utilisateur a explicitement confirmé la suppression après avoir vu l'impact"
84
114
  }
85
115
  },
86
- "required": ["app_name", "model_name", "object_id"]
116
+ "required": ["app_name", "model_name", "object_id", "confirmed"]
87
117
  }
88
118
  },
89
119
  {
@@ -0,0 +1,339 @@
1
+ """
2
+ Service de découverte automatique des vues CRUD du projet.
3
+
4
+ Parcourt les URL patterns Django pour identifier les vues CRUD
5
+ et construire un mapping utilisable par Lucy Assist.
6
+ """
7
+ import re
8
+ from typing import Dict, List, Optional, Tuple
9
+ from django.urls import URLPattern, URLResolver, get_resolver
10
+ from django.urls.resolvers import RoutePattern
11
+
12
+ from lucy_assist.utils.log_utils import LogUtils
13
+ from lucy_assist.conf import lucy_assist_settings
14
+
15
+
16
+ class ViewDiscoveryService:
17
+ """
18
+ Service pour découvrir automatiquement les vues CRUD d'un projet Django.
19
+ """
20
+
21
+ # Patterns de nommage courants pour les vues CRUD
22
+ # L'ordre est important : les patterns plus spécifiques doivent être en premier
23
+ CRUD_PATTERNS = {
24
+ 'list': [
25
+ r'-list$', r'-liste$', r'-gestion$', r'-index$',
26
+ r'_list$', r'_liste$', r'_gestion$', r'_index$',
27
+ r'list$', r'liste$', r'gestion$'
28
+ ],
29
+ 'create': [
30
+ r'-create$', r'-add$', r'-nouveau$', r'-new$',
31
+ r'_create$', r'_add$', r'_nouveau$', r'_new$',
32
+ r'create$', r'add$', r'nouveau$'
33
+ ],
34
+ 'detail': [
35
+ r'-detail$', r'-view$', r'-show$', r'-fiche$', r'-consulter$',
36
+ r'_detail$', r'_view$', r'_show$', r'_fiche$',
37
+ r'detail$', r'view$', r'fiche$'
38
+ ],
39
+ 'update': [
40
+ r'-update$', r'-edit$', r'-modifier$', r'-modification$',
41
+ r'_update$', r'_edit$', r'_modifier$',
42
+ r'update$', r'edit$', r'modifier$'
43
+ ],
44
+ 'delete': [
45
+ r'-delete$', r'-suppression$', r'-supprimer$', r'-remove$',
46
+ r'_delete$', r'_suppression$', r'_supprimer$', r'_remove$',
47
+ r'delete$', r'suppression$', r'supprimer$'
48
+ ],
49
+ # Formulaire peut être create ou update selon le contexte
50
+ 'formulaire': [
51
+ r'-formulaire$', r'_formulaire$', r'formulaire$',
52
+ r'-form$', r'_form$', r'form$'
53
+ ]
54
+ }
55
+
56
+ # Patterns d'URL pour détecter les paramètres
57
+ URL_PARAM_PATTERNS = [
58
+ (r'<int:pk>', 'pk'),
59
+ (r'<pk>', 'pk'),
60
+ (r'<int:id>', 'id'),
61
+ (r'<id>', 'id'),
62
+ (r'<slug:slug>', 'slug'),
63
+ ]
64
+
65
+ def __init__(self):
66
+ self.apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX or ''
67
+
68
+ def discover_crud_views(self) -> Dict:
69
+ """
70
+ Découvre toutes les vues CRUD du projet.
71
+
72
+ Returns:
73
+ Dict avec le mapping modèle -> actions -> infos vue
74
+ """
75
+ LogUtils.info("[ViewDiscovery] Début de la découverte des vues CRUD")
76
+
77
+ crud_mapping = {}
78
+ resolver = get_resolver()
79
+
80
+ # Parcourir tous les URL patterns
81
+ self._scan_url_patterns(resolver.url_patterns, '', '', crud_mapping)
82
+
83
+ LogUtils.info(f"[ViewDiscovery] {len(crud_mapping)} modèles découverts")
84
+ for model, actions in crud_mapping.items():
85
+ LogUtils.info(f"[ViewDiscovery] {model}: {list(actions.keys())}")
86
+
87
+ return crud_mapping
88
+
89
+ def _scan_url_patterns(
90
+ self,
91
+ patterns: List,
92
+ namespace: str,
93
+ url_prefix: str,
94
+ crud_mapping: Dict
95
+ ):
96
+ """
97
+ Parcourt récursivement les URL patterns.
98
+ """
99
+ for pattern in patterns:
100
+ if isinstance(pattern, URLResolver):
101
+ # C'est un include() - descendre récursivement
102
+ new_namespace = pattern.namespace
103
+ if namespace and new_namespace:
104
+ new_namespace = f"{namespace}:{new_namespace}"
105
+ elif namespace:
106
+ new_namespace = namespace
107
+
108
+ # Construire le préfixe URL
109
+ pattern_str = self._get_pattern_string(pattern.pattern)
110
+ new_url_prefix = url_prefix + pattern_str
111
+
112
+ # Filtrer par préfixe d'apps si configuré
113
+ app_name = getattr(pattern, 'app_name', '') or ''
114
+ if self.apps_prefix:
115
+ # Vérifier si c'est une app du projet
116
+ if app_name and not app_name.startswith(self.apps_prefix.rstrip('.')):
117
+ # Permettre aussi les apps dont le namespace correspond
118
+ if new_namespace and not any(
119
+ new_namespace.startswith(prefix)
120
+ for prefix in ['admin', 'lucy_assist']
121
+ ):
122
+ pass # Continuer l'exploration
123
+ else:
124
+ continue
125
+
126
+ self._scan_url_patterns(
127
+ pattern.url_patterns,
128
+ new_namespace or '',
129
+ new_url_prefix,
130
+ crud_mapping
131
+ )
132
+
133
+ elif isinstance(pattern, URLPattern):
134
+ # C'est une vue finale
135
+ self._process_url_pattern(
136
+ pattern,
137
+ namespace,
138
+ url_prefix,
139
+ crud_mapping
140
+ )
141
+
142
+ def _process_url_pattern(
143
+ self,
144
+ pattern: URLPattern,
145
+ namespace: str,
146
+ url_prefix: str,
147
+ crud_mapping: Dict
148
+ ):
149
+ """
150
+ Analyse un URL pattern pour détecter s'il s'agit d'une vue CRUD.
151
+ """
152
+ url_name = pattern.name
153
+ if not url_name:
154
+ return
155
+
156
+ # Construire l'URL complète
157
+ pattern_str = self._get_pattern_string(pattern.pattern)
158
+ full_url = '/' + url_prefix + pattern_str
159
+ full_url = re.sub(r'/+', '/', full_url) # Normaliser les slashes
160
+
161
+ # Construire le nom complet de la vue
162
+ full_name = f"{namespace}:{url_name}" if namespace else url_name
163
+
164
+ # Détecter le modèle et l'action depuis le nom de l'URL
165
+ model_name, action = self._detect_model_and_action(url_name)
166
+
167
+ if not model_name or not action:
168
+ return
169
+
170
+ # Détecter si l'URL nécessite un paramètre (pk, id, etc.)
171
+ requires_pk = self._url_requires_pk(full_url)
172
+
173
+ # Déterminer la méthode HTTP
174
+ http_method = self._detect_http_method(pattern, action)
175
+
176
+ # Ajouter au mapping
177
+ model_lower = model_name.lower()
178
+ if model_lower not in crud_mapping:
179
+ crud_mapping[model_lower] = {}
180
+
181
+ # Les vues "formulaire" gèrent généralement create ET update
182
+ if action == 'formulaire':
183
+ crud_mapping[model_lower]['create'] = {
184
+ 'url_name': full_name,
185
+ 'url': full_url,
186
+ 'method': 'POST',
187
+ 'requires_pk': False
188
+ }
189
+ crud_mapping[model_lower]['update'] = {
190
+ 'url_name': full_name,
191
+ 'url': full_url,
192
+ 'method': 'POST',
193
+ 'requires_pk': True # pk passé en POST ou GET
194
+ }
195
+ return
196
+
197
+ # Pour les autres actions create/update
198
+ if action == 'create' and requires_pk:
199
+ # C'est probablement une vue qui gère create et update
200
+ action = 'update'
201
+
202
+ crud_mapping[model_lower][action] = {
203
+ 'url_name': full_name,
204
+ 'url': full_url,
205
+ 'method': http_method,
206
+ 'requires_pk': requires_pk
207
+ }
208
+
209
+ def _detect_model_and_action(self, url_name: str) -> Tuple[Optional[str], Optional[str]]:
210
+ """
211
+ Détecte le nom du modèle et l'action CRUD depuis le nom de l'URL.
212
+
213
+ Args:
214
+ url_name: Nom de l'URL (ex: 'client-list', 'reservation-formulaire', 'client-b2b-gestion')
215
+
216
+ Returns:
217
+ Tuple (model_name, action) ou (None, None)
218
+ """
219
+ url_name_lower = url_name.lower()
220
+
221
+ # Chercher l'action dans le nom
222
+ detected_action = None
223
+ matched_pattern = None
224
+
225
+ for action, patterns in self.CRUD_PATTERNS.items():
226
+ for pattern in patterns:
227
+ if re.search(pattern, url_name_lower):
228
+ detected_action = action
229
+ matched_pattern = pattern
230
+ break
231
+ if detected_action:
232
+ break
233
+
234
+ if not detected_action:
235
+ return None, None
236
+
237
+ # Extraire le nom du modèle en retirant le pattern d'action trouvé
238
+ model_name = re.sub(matched_pattern, '', url_name_lower)
239
+
240
+ # Nettoyer le nom du modèle
241
+ model_name = model_name.strip('-_')
242
+
243
+ # Convertir les tirets en underscores pour la cohérence
244
+ # mais garder aussi la version avec tirets pour le mapping
245
+ model_name_normalized = model_name.replace('-', '_')
246
+
247
+ if not model_name:
248
+ return None, None
249
+
250
+ # Retourner la version normalisée (avec underscores)
251
+ return model_name_normalized, detected_action
252
+
253
+ def _get_pattern_string(self, pattern) -> str:
254
+ """
255
+ Extrait la chaîne de pattern depuis un RoutePattern ou RegexPattern.
256
+ """
257
+ if hasattr(pattern, '_route'):
258
+ return pattern._route
259
+ elif hasattr(pattern, '_regex'):
260
+ # Convertir le regex en quelque chose de lisible
261
+ regex = pattern._regex
262
+ # Simplifier les captures
263
+ regex = re.sub(r'\(\?P<(\w+)>[^)]+\)', r'<\1>', regex)
264
+ regex = regex.strip('^$')
265
+ return regex
266
+ return ''
267
+
268
+ def _url_requires_pk(self, url: str) -> bool:
269
+ """
270
+ Vérifie si l'URL nécessite un paramètre pk/id.
271
+ """
272
+ pk_patterns = ['<pk>', '<int:pk>', '<id>', '<int:id>', '<slug>']
273
+ return any(p in url for p in pk_patterns)
274
+
275
+ def _detect_http_method(self, pattern: URLPattern, action: str) -> str:
276
+ """
277
+ Détecte la méthode HTTP appropriée pour une action.
278
+ """
279
+ # Pour les vues basées sur des classes, on peut essayer de détecter
280
+ # les méthodes supportées, mais par défaut on utilise les conventions
281
+
282
+ method_map = {
283
+ 'list': 'GET',
284
+ 'detail': 'GET',
285
+ 'create': 'POST',
286
+ 'update': 'POST', # Souvent POST dans Django (pas PUT)
287
+ 'delete': 'POST', # Souvent POST dans Django (pas DELETE)
288
+ }
289
+
290
+ return method_map.get(action, 'GET')
291
+
292
+ def get_view_info(self, model_name: str, action: str) -> Optional[Dict]:
293
+ """
294
+ Récupère les informations d'une vue pour un modèle et une action.
295
+
296
+ Args:
297
+ model_name: Nom du modèle
298
+ action: Action CRUD ('list', 'create', 'detail', 'update', 'delete')
299
+
300
+ Returns:
301
+ Dict avec 'url_name', 'url', 'method', 'requires_pk' ou None
302
+ """
303
+ from lucy_assist.models import ConfigurationLucyAssist
304
+ config = ConfigurationLucyAssist.get_config()
305
+
306
+ if not config.crud_views_mapping:
307
+ config.refresh_crud_views_mapping()
308
+
309
+ model_lower = model_name.lower()
310
+
311
+ # Essayer plusieurs variations du nom de modèle
312
+ variations = [
313
+ model_lower,
314
+ model_lower.replace('_', '-'), # client_b2b -> client-b2b
315
+ model_lower.replace('-', '_'), # client-b2b -> client_b2b
316
+ model_lower.replace('_', ''), # client_b2b -> clientb2b
317
+ ]
318
+
319
+ for variant in variations:
320
+ model_views = config.crud_views_mapping.get(variant, {})
321
+ if model_views and action in model_views:
322
+ return model_views.get(action)
323
+
324
+ return None
325
+
326
+ def get_all_discovered_models(self) -> List[str]:
327
+ """
328
+ Retourne la liste de tous les modèles découverts avec des vues CRUD.
329
+
330
+ Returns:
331
+ Liste des noms de modèles
332
+ """
333
+ from lucy_assist.models import ConfigurationLucyAssist
334
+ config = ConfigurationLucyAssist.get_config()
335
+
336
+ if not config.crud_views_mapping:
337
+ config.refresh_crud_views_mapping()
338
+
339
+ return list(config.crud_views_mapping.keys())