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