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.
Files changed (44) hide show
  1. django_lucy_assist-0.1.0.dist-info/METADATA +206 -0
  2. django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
  3. django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
  4. django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
  5. lucy_assist/__init__.py +11 -0
  6. lucy_assist/admin.py +22 -0
  7. lucy_assist/apps.py +10 -0
  8. lucy_assist/conf.py +103 -0
  9. lucy_assist/constantes.py +120 -0
  10. lucy_assist/context_processors.py +65 -0
  11. lucy_assist/migrations/0001_initial.py +92 -0
  12. lucy_assist/migrations/__init__.py +0 -0
  13. lucy_assist/models/__init__.py +14 -0
  14. lucy_assist/models/base.py +54 -0
  15. lucy_assist/models/configuration.py +175 -0
  16. lucy_assist/models/conversation.py +54 -0
  17. lucy_assist/models/message.py +45 -0
  18. lucy_assist/models/project_context_cache.py +213 -0
  19. lucy_assist/services/__init__.py +21 -0
  20. lucy_assist/services/bug_notification_service.py +183 -0
  21. lucy_assist/services/claude_service.py +417 -0
  22. lucy_assist/services/context_service.py +350 -0
  23. lucy_assist/services/crud_service.py +364 -0
  24. lucy_assist/services/gitlab_service.py +248 -0
  25. lucy_assist/services/project_context_service.py +412 -0
  26. lucy_assist/services/tool_executor_service.py +343 -0
  27. lucy_assist/services/tools_definition.py +229 -0
  28. lucy_assist/signals.py +25 -0
  29. lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
  30. lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
  31. lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
  32. lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
  33. lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
  34. lucy_assist/tests/__init__.py +0 -0
  35. lucy_assist/tests/factories/__init__.py +15 -0
  36. lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
  37. lucy_assist/tests/test_lucy_assist.py +186 -0
  38. lucy_assist/urls.py +36 -0
  39. lucy_assist/utils/__init__.py +7 -0
  40. lucy_assist/utils/log_utils.py +59 -0
  41. lucy_assist/utils/message_utils.py +130 -0
  42. lucy_assist/utils/token_utils.py +87 -0
  43. lucy_assist/views/__init__.py +13 -0
  44. lucy_assist/views/api_views.py +595 -0
@@ -0,0 +1,350 @@
1
+ """
2
+ Service de détection et construction du contexte de page.
3
+ """
4
+ import logging
5
+ import re
6
+ from typing import Dict, List, Optional
7
+ from urllib.parse import urlparse
8
+
9
+ from django.urls import resolve, Resolver404
10
+ from django.apps import apps
11
+
12
+ from lucy_assist.utils.log_utils import LogUtils
13
+
14
+
15
+ class ContextService:
16
+ """Service pour construire le contexte de la page courante."""
17
+
18
+ def __init__(self, user):
19
+ self.user = user
20
+
21
+ def get_page_context(self, url_path: str) -> Dict:
22
+ """
23
+ Construit le contexte complet d'une page.
24
+
25
+ Args:
26
+ url_path: URL de la page (ex: /membre/list)
27
+
28
+ Returns:
29
+ Dict avec les informations de contexte
30
+ """
31
+ context = {
32
+ 'url': url_path,
33
+ 'app_name': None,
34
+ 'view_name': None,
35
+ 'model_name': None,
36
+ 'object_id': None,
37
+ 'action': None,
38
+ 'available_actions': [],
39
+ 'help_text': None
40
+ }
41
+
42
+ if not url_path:
43
+ return context
44
+
45
+ # Parser l'URL
46
+ parsed = urlparse(url_path)
47
+ path = parsed.path
48
+
49
+ try:
50
+ # Résoudre l'URL Django
51
+ match = resolve(path)
52
+
53
+ context['app_name'] = match.app_name
54
+ context['view_name'] = match.url_name
55
+
56
+ # Extraire l'ID d'objet si présent
57
+ if 'pk' in match.kwargs:
58
+ context['object_id'] = match.kwargs['pk']
59
+ elif 'id' in match.kwargs:
60
+ context['object_id'] = match.kwargs['id']
61
+
62
+ # Déterminer l'action
63
+ context['action'] = self._detect_action(match.url_name)
64
+
65
+ # Trouver le modèle associé
66
+ context['model_name'] = self._find_model_name(match)
67
+
68
+ # Déterminer les actions disponibles
69
+ context['available_actions'] = self._get_available_actions(
70
+ match.app_name,
71
+ context['model_name']
72
+ )
73
+
74
+ # Générer le texte d'aide
75
+ context['help_text'] = self._generate_help_text(context)
76
+
77
+ except Resolver404:
78
+ LogUtils.info(f"URL non résolue: {path}")
79
+ # Essayer d'extraire les infos depuis le path
80
+ context.update(self._parse_url_manually(path))
81
+
82
+ return context
83
+
84
+ def _detect_action(self, url_name: str) -> Optional[str]:
85
+ """Détecte l'action à partir du nom de l'URL."""
86
+ if not url_name:
87
+ return None
88
+
89
+ url_name_lower = url_name.lower()
90
+
91
+ if 'list' in url_name_lower:
92
+ return 'list'
93
+ elif 'form' in url_name_lower or 'create' in url_name_lower or 'add' in url_name_lower:
94
+ return 'create_or_edit'
95
+ elif 'detail' in url_name_lower or 'view' in url_name_lower:
96
+ return 'detail'
97
+ elif 'delete' in url_name_lower:
98
+ return 'delete'
99
+ elif 'edit' in url_name_lower or 'update' in url_name_lower:
100
+ return 'edit'
101
+
102
+ return 'unknown'
103
+
104
+ def _find_model_name(self, match) -> Optional[str]:
105
+ """Trouve le nom du modèle à partir de la vue."""
106
+ if not match.url_name:
107
+ return None
108
+
109
+ # Extraire le nom du modèle depuis le nom de l'URL
110
+ # Pattern: model-action (ex: membre-list, paiement-form)
111
+ parts = match.url_name.split('-')
112
+ if parts:
113
+ model_name = parts[0].capitalize()
114
+
115
+ # Vérifier si le modèle existe
116
+ if match.app_name:
117
+ try:
118
+ app_config = apps.get_app_config(match.app_name.split(':')[-1])
119
+ for model in app_config.get_models():
120
+ if model.__name__.lower() == parts[0].lower():
121
+ return model.__name__
122
+ except LookupError:
123
+ pass
124
+
125
+ return model_name
126
+
127
+ return None
128
+
129
+ def _get_available_actions(self, app_name: str, model_name: str) -> List[Dict]:
130
+ """Retourne les actions disponibles pour l'utilisateur."""
131
+ actions = []
132
+
133
+ if not app_name or not model_name:
134
+ return actions
135
+
136
+ model_lower = model_name.lower()
137
+ app_simple = app_name.split(':')[-1] if app_name else ''
138
+
139
+ # Vérifier les permissions
140
+ permission_map = {
141
+ 'view': f'{app_simple}.view_{model_lower}',
142
+ 'add': f'{app_simple}.add_{model_lower}',
143
+ 'change': f'{app_simple}.change_{model_lower}',
144
+ 'delete': f'{app_simple}.delete_{model_lower}',
145
+ }
146
+
147
+ for action, perm in permission_map.items():
148
+ has_perm = self.user.has_perm(perm) if self.user else False
149
+ actions.append({
150
+ 'action': action,
151
+ 'permission': perm,
152
+ 'allowed': has_perm or (self.user and self.user.is_superuser)
153
+ })
154
+
155
+ return actions
156
+
157
+ def _generate_help_text(self, context: Dict) -> str:
158
+ """Génère un texte d'aide contextuel."""
159
+ action = context.get('action')
160
+ model = context.get('model_name', 'élément')
161
+
162
+ help_texts = {
163
+ 'list': f"Vous êtes sur la liste des {model}s. Vous pouvez filtrer, rechercher ou créer un nouveau {model}.",
164
+ 'create_or_edit': f"Vous êtes sur le formulaire de {model}. Remplissez les champs requis et cliquez sur Enregistrer.",
165
+ 'detail': f"Vous consultez les détails d'un {model}. Vous pouvez le modifier ou le supprimer si vous avez les droits.",
166
+ 'delete': f"Vous allez supprimer ce {model}. Cette action est irréversible.",
167
+ 'edit': f"Vous modifiez un {model} existant. Les modifications seront enregistrées après validation.",
168
+ }
169
+
170
+ return help_texts.get(action, f"Vous êtes sur la page {context.get('view_name', 'inconnue')}.")
171
+
172
+ def _parse_url_manually(self, path: str) -> Dict:
173
+ """Parse l'URL manuellement si Django ne peut pas la résoudre."""
174
+ result = {
175
+ 'app_name': None,
176
+ 'view_name': None,
177
+ 'action': None
178
+ }
179
+
180
+ parts = path.strip('/').split('/')
181
+ if parts:
182
+ result['app_name'] = parts[0]
183
+
184
+ if len(parts) > 1:
185
+ action_part = parts[1]
186
+ if 'list' in action_part:
187
+ result['action'] = 'list'
188
+ elif 'form' in action_part:
189
+ result['action'] = 'create_or_edit'
190
+ elif 'detail' in action_part:
191
+ result['action'] = 'detail'
192
+
193
+ result['view_name'] = action_part
194
+
195
+ return result
196
+
197
+ def get_form_info(self, app_name: str, model_name: str) -> Optional[Dict]:
198
+ """
199
+ Récupère les informations sur un formulaire.
200
+
201
+ Returns:
202
+ Dict avec 'fields', 'required_fields', 'help_texts'
203
+ """
204
+ if not app_name or not model_name:
205
+ return None
206
+
207
+ try:
208
+ # Trouver le modèle
209
+ app_simple = app_name.split(':')[-1]
210
+ model = apps.get_model(app_simple, model_name)
211
+
212
+ # Extraire les informations des champs
213
+ fields = []
214
+ required_fields = []
215
+
216
+ for field in model._meta.get_fields():
217
+ if hasattr(field, 'verbose_name'):
218
+ field_info = {
219
+ 'name': field.name,
220
+ 'verbose_name': str(field.verbose_name),
221
+ 'type': field.get_internal_type() if hasattr(field, 'get_internal_type') else 'Unknown',
222
+ 'required': not getattr(field, 'blank', True),
223
+ 'help_text': str(field.help_text) if hasattr(field, 'help_text') else ''
224
+ }
225
+ fields.append(field_info)
226
+
227
+ if field_info['required']:
228
+ required_fields.append(field_info['verbose_name'])
229
+
230
+ return {
231
+ 'model': model_name,
232
+ 'fields': fields,
233
+ 'required_fields': required_fields
234
+ }
235
+
236
+ except LookupError:
237
+ LogUtils.info(f"Modèle non trouvé: {app_name}.{model_name}")
238
+ return None
239
+
240
+ def search_objects(
241
+ self,
242
+ query: str,
243
+ model_name: Optional[str] = None,
244
+ limit: int = 10
245
+ ) -> List[Dict]:
246
+ """
247
+ Recherche des objets dans la base de données.
248
+
249
+ Args:
250
+ query: Terme de recherche
251
+ model_name: Nom du modèle (optionnel, cherche dans tous si None)
252
+ limit: Nombre max de résultats
253
+
254
+ Returns:
255
+ Liste de résultats avec 'model', 'id', 'str', 'url'
256
+ """
257
+ results = []
258
+
259
+ # Liste des modèles à rechercher
260
+ models_to_search = []
261
+
262
+ if model_name:
263
+ # Chercher le modèle spécifique
264
+ for app_config in apps.get_app_configs():
265
+ try:
266
+ model = app_config.get_model(model_name)
267
+ models_to_search.append(model)
268
+ break
269
+ except LookupError:
270
+ continue
271
+ else:
272
+ # Chercher dans les principaux modèles métier
273
+ model_names = ['Membre', 'Structure', 'Paiement', 'Adhesion']
274
+ for name in model_names:
275
+ for app_config in apps.get_app_configs():
276
+ try:
277
+ model = app_config.get_model(name)
278
+ models_to_search.append(model)
279
+ break
280
+ except LookupError:
281
+ continue
282
+
283
+ # Rechercher dans chaque modèle
284
+ for model in models_to_search:
285
+ try:
286
+ # Trouver les champs texte pour la recherche
287
+ search_fields = []
288
+ for field in model._meta.get_fields():
289
+ if hasattr(field, 'get_internal_type'):
290
+ if field.get_internal_type() in ['CharField', 'TextField']:
291
+ search_fields.append(field.name)
292
+
293
+ if not search_fields:
294
+ continue
295
+
296
+ # Construire la requête
297
+ from django.db.models import Q
298
+ q_objects = Q()
299
+ for field_name in search_fields[:5]: # Limiter à 5 champs
300
+ q_objects |= Q(**{f'{field_name}__icontains': query})
301
+
302
+ # Filtrer par permissions si le manager user_filter existe
303
+ queryset = model.objects
304
+ if hasattr(model, 'user_filter'):
305
+ queryset = model.user_filter
306
+
307
+ objects = queryset.filter(q_objects)[:limit]
308
+
309
+ for obj in objects:
310
+ result = {
311
+ 'model': model.__name__,
312
+ 'id': obj.pk,
313
+ 'str': str(obj),
314
+ 'url': self._get_object_url(model, obj)
315
+ }
316
+ results.append(result)
317
+
318
+ if len(results) >= limit:
319
+ return results
320
+
321
+ except Exception as e:
322
+ LogUtils.info(f"Erreur recherche dans {model.__name__}: {e}")
323
+ continue
324
+
325
+ return results
326
+
327
+ def _get_object_url(self, model, obj) -> Optional[str]:
328
+ """Génère l'URL de détail d'un objet."""
329
+ try:
330
+ from django.urls import reverse
331
+ app_label = model._meta.app_label
332
+ model_name = model.__name__.lower()
333
+
334
+ # Essayer différents patterns d'URL
335
+ url_patterns = [
336
+ f'{app_label}:{model_name}-detail',
337
+ f'{app_label}:{model_name}-form',
338
+ f'{model_name}-detail',
339
+ ]
340
+
341
+ for pattern in url_patterns:
342
+ try:
343
+ return reverse(pattern, kwargs={'pk': obj.pk})
344
+ except Exception:
345
+ continue
346
+
347
+ return None
348
+
349
+ except Exception:
350
+ return None