django-lucy-assist 1.2.3__py3-none-any.whl → 1.2.4__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-1.2.3.dist-info → django_lucy_assist-1.2.4.dist-info}/METADATA +1 -1
- django_lucy_assist-1.2.4.dist-info/RECORD +45 -0
- lucy_assist/__init__.py +1 -1
- lucy_assist/admin.py +6 -39
- lucy_assist/conf.py +4 -0
- lucy_assist/constantes.py +79 -29
- lucy_assist/migrations/0001_initial.py +3 -1
- lucy_assist/models/configuration.py +0 -116
- lucy_assist/services/context_service.py +234 -0
- lucy_assist/services/crud_service.py +97 -13
- lucy_assist/services/gitlab_service.py +79 -6
- lucy_assist/services/mistral_service.py +31 -11
- lucy_assist/services/tool_executor_service.py +91 -0
- lucy_assist/services/tools_definition.py +65 -0
- lucy_assist/services/view_discovery_service.py +47 -13
- lucy_assist/static/lucy_assist/css/lucy-assist.css +48 -1
- lucy_assist/static/lucy_assist/js/lucy-assist.js +11 -0
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +11 -1
- django_lucy_assist-1.2.3.dist-info/RECORD +0 -50
- lucy_assist/migrations/0002_configurationlucyassist_prompt_complementaire.py +0 -18
- lucy_assist/migrations/0003_configurationlucyassist_crud_views_mapping.py +0 -18
- lucy_assist/migrations/0004_configurationlucyassist_system_prompt.py +0 -22
- lucy_assist/migrations/0005_configurationlucyassist_project_context.py +0 -22
- lucy_assist/migrations/0006_remove_configurationlucyassist_prompt_complementaire.py +0 -17
- {django_lucy_assist-1.2.3.dist-info → django_lucy_assist-1.2.4.dist-info}/WHEEL +0 -0
- {django_lucy_assist-1.2.3.dist-info → django_lucy_assist-1.2.4.dist-info}/top_level.txt +0 -0
|
@@ -452,3 +452,237 @@ class ContextService:
|
|
|
452
452
|
|
|
453
453
|
except Exception:
|
|
454
454
|
return None
|
|
455
|
+
|
|
456
|
+
def get_page_help(self, url_path: str) -> Dict:
|
|
457
|
+
"""
|
|
458
|
+
Retourne une aide contextuelle enrichie pour la page actuelle.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
url_path: URL de la page
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Dict avec aide, actions suggerees, liens utiles
|
|
465
|
+
"""
|
|
466
|
+
context = self.get_page_context(url_path)
|
|
467
|
+
action = context.get('action')
|
|
468
|
+
model_name = context.get('model_name', 'element')
|
|
469
|
+
app_name = context.get('app_name', '')
|
|
470
|
+
|
|
471
|
+
help_info = {
|
|
472
|
+
'page_type': action,
|
|
473
|
+
'current_model': model_name,
|
|
474
|
+
'help_message': '',
|
|
475
|
+
'suggested_actions': [],
|
|
476
|
+
'useful_links': [],
|
|
477
|
+
'tips': []
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# Generer les URLs utiles
|
|
481
|
+
from django.urls import reverse
|
|
482
|
+
model_lower = model_name.lower() if model_name else ''
|
|
483
|
+
app_simple = app_name.split(':')[-1] if app_name else ''
|
|
484
|
+
|
|
485
|
+
def try_reverse(pattern, **kwargs):
|
|
486
|
+
try:
|
|
487
|
+
return reverse(pattern, kwargs=kwargs) if kwargs else reverse(pattern)
|
|
488
|
+
except Exception:
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
# Aide selon le type de page
|
|
492
|
+
if action == 'list':
|
|
493
|
+
help_info['help_message'] = f"Vous consultez la liste des {model_name}s."
|
|
494
|
+
help_info['suggested_actions'] = [
|
|
495
|
+
"Rechercher un element specifique",
|
|
496
|
+
"Filtrer la liste",
|
|
497
|
+
"Creer un nouvel element"
|
|
498
|
+
]
|
|
499
|
+
help_info['tips'] = [
|
|
500
|
+
"Utilisez la barre de recherche pour trouver rapidement",
|
|
501
|
+
"Cliquez sur une ligne pour voir les details"
|
|
502
|
+
]
|
|
503
|
+
# Lien creation
|
|
504
|
+
create_url = try_reverse(f'{app_simple}:{model_lower}-form')
|
|
505
|
+
if create_url:
|
|
506
|
+
help_info['useful_links'].append({
|
|
507
|
+
'label': f'Creer un nouveau {model_name}',
|
|
508
|
+
'url': create_url
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
elif action == 'create_or_edit':
|
|
512
|
+
if context.get('object_id'):
|
|
513
|
+
help_info['help_message'] = f"Vous modifiez un {model_name} existant."
|
|
514
|
+
help_info['suggested_actions'] = [
|
|
515
|
+
"Modifier les informations",
|
|
516
|
+
"Enregistrer les modifications",
|
|
517
|
+
"Annuler et revenir a la liste"
|
|
518
|
+
]
|
|
519
|
+
else:
|
|
520
|
+
help_info['help_message'] = f"Vous creez un nouveau {model_name}."
|
|
521
|
+
help_info['suggested_actions'] = [
|
|
522
|
+
"Remplir les champs obligatoires",
|
|
523
|
+
"Enregistrer le nouvel element"
|
|
524
|
+
]
|
|
525
|
+
help_info['tips'] = [
|
|
526
|
+
"Les champs marques * sont obligatoires",
|
|
527
|
+
"Verifiez les informations avant d'enregistrer"
|
|
528
|
+
]
|
|
529
|
+
# Lien liste
|
|
530
|
+
list_url = try_reverse(f'{app_simple}:{model_lower}-list')
|
|
531
|
+
if list_url:
|
|
532
|
+
help_info['useful_links'].append({
|
|
533
|
+
'label': f'Retour a la liste des {model_name}s',
|
|
534
|
+
'url': list_url
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
elif action == 'detail':
|
|
538
|
+
help_info['help_message'] = f"Vous consultez les details d'un {model_name}."
|
|
539
|
+
help_info['suggested_actions'] = [
|
|
540
|
+
"Modifier cet element",
|
|
541
|
+
"Supprimer cet element",
|
|
542
|
+
"Voir les elements lies"
|
|
543
|
+
]
|
|
544
|
+
object_id = context.get('object_id')
|
|
545
|
+
if object_id:
|
|
546
|
+
edit_url = try_reverse(f'{app_simple}:{model_lower}-form', pk=object_id)
|
|
547
|
+
if edit_url:
|
|
548
|
+
help_info['useful_links'].append({
|
|
549
|
+
'label': 'Modifier',
|
|
550
|
+
'url': edit_url
|
|
551
|
+
})
|
|
552
|
+
list_url = try_reverse(f'{app_simple}:{model_lower}-list')
|
|
553
|
+
if list_url:
|
|
554
|
+
help_info['useful_links'].append({
|
|
555
|
+
'label': f'Liste des {model_name}s',
|
|
556
|
+
'url': list_url
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
elif action == 'delete':
|
|
560
|
+
help_info['help_message'] = f"Attention : vous allez supprimer ce {model_name}."
|
|
561
|
+
help_info['suggested_actions'] = [
|
|
562
|
+
"Confirmer la suppression",
|
|
563
|
+
"Annuler et revenir"
|
|
564
|
+
]
|
|
565
|
+
help_info['tips'] = [
|
|
566
|
+
"Cette action est irreversible",
|
|
567
|
+
"Verifiez les elements lies qui seront aussi supprimes"
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
else:
|
|
571
|
+
help_info['help_message'] = "Comment puis-je vous aider sur cette page ?"
|
|
572
|
+
help_info['suggested_actions'] = [
|
|
573
|
+
"Poser une question",
|
|
574
|
+
"Rechercher un element",
|
|
575
|
+
"Signaler un probleme"
|
|
576
|
+
]
|
|
577
|
+
|
|
578
|
+
return help_info
|
|
579
|
+
|
|
580
|
+
def get_relevant_models(self, url_path: str) -> List[Dict]:
|
|
581
|
+
"""
|
|
582
|
+
Retourne uniquement les modeles pertinents pour la page actuelle.
|
|
583
|
+
Optimise l'utilisation des tokens.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
url_path: URL de la page
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Liste des modeles pertinents avec leurs champs
|
|
590
|
+
"""
|
|
591
|
+
context = self.get_page_context(url_path)
|
|
592
|
+
app_name = context.get('app_name', '')
|
|
593
|
+
model_name = context.get('model_name')
|
|
594
|
+
|
|
595
|
+
relevant_models = []
|
|
596
|
+
apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX
|
|
597
|
+
app_simple = app_name.split(':')[-1] if app_name else ''
|
|
598
|
+
|
|
599
|
+
# 1. Toujours inclure le modele de la page actuelle en premier
|
|
600
|
+
if model_name:
|
|
601
|
+
model_info = self._get_model_info(app_simple, model_name)
|
|
602
|
+
if model_info:
|
|
603
|
+
model_info['is_current'] = True
|
|
604
|
+
relevant_models.append(model_info)
|
|
605
|
+
|
|
606
|
+
# 2. Ajouter les modeles de la meme app (limiter a 5)
|
|
607
|
+
if app_simple:
|
|
608
|
+
try:
|
|
609
|
+
app_config = apps.get_app_config(app_simple)
|
|
610
|
+
for model in list(app_config.get_models())[:5]:
|
|
611
|
+
if model.__name__ != model_name:
|
|
612
|
+
model_info = self._get_model_info(app_simple, model.__name__)
|
|
613
|
+
if model_info:
|
|
614
|
+
model_info['is_current'] = False
|
|
615
|
+
relevant_models.append(model_info)
|
|
616
|
+
except LookupError:
|
|
617
|
+
pass
|
|
618
|
+
|
|
619
|
+
# 3. Si moins de 3 modeles, ajouter les modeles principaux du projet
|
|
620
|
+
if len(relevant_models) < 3:
|
|
621
|
+
for app_config in apps.get_app_configs():
|
|
622
|
+
if apps_prefix and not app_config.name.startswith(apps_prefix):
|
|
623
|
+
continue
|
|
624
|
+
if app_config.name.startswith('django.') or app_config.name == 'lucy_assist':
|
|
625
|
+
continue
|
|
626
|
+
if app_config.label == app_simple:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
for model in list(app_config.get_models())[:2]:
|
|
630
|
+
if len(relevant_models) >= 8:
|
|
631
|
+
break
|
|
632
|
+
model_info = self._get_model_info(app_config.label, model.__name__)
|
|
633
|
+
if model_info:
|
|
634
|
+
model_info['is_current'] = False
|
|
635
|
+
relevant_models.append(model_info)
|
|
636
|
+
|
|
637
|
+
if len(relevant_models) >= 8:
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
return relevant_models
|
|
641
|
+
|
|
642
|
+
def _get_model_info(self, app_label: str, model_name: str) -> Optional[Dict]:
|
|
643
|
+
"""Retourne les informations d'un modele."""
|
|
644
|
+
try:
|
|
645
|
+
model = apps.get_model(app_label, model_name)
|
|
646
|
+
fields = []
|
|
647
|
+
for field in model._meta.get_fields():
|
|
648
|
+
if hasattr(field, 'name') and not field.name.startswith('_'):
|
|
649
|
+
if hasattr(field, 'auto_created') and field.auto_created:
|
|
650
|
+
continue
|
|
651
|
+
if field.name in ('id', 'pk', 'created_at', 'updated_at'):
|
|
652
|
+
continue
|
|
653
|
+
fields.append(field.name)
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
'app': app_label,
|
|
657
|
+
'name': model_name,
|
|
658
|
+
'name_lower': model_name.lower(),
|
|
659
|
+
'fields': fields[:8],
|
|
660
|
+
'verbose_name': str(model._meta.verbose_name) if hasattr(model._meta, 'verbose_name') else model_name
|
|
661
|
+
}
|
|
662
|
+
except LookupError:
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
def get_all_model_names(self) -> List[str]:
|
|
666
|
+
"""
|
|
667
|
+
Retourne la liste de TOUS les noms de modeles du projet.
|
|
668
|
+
Utilise pour informer Lucy de ce qui existe vs ce qui n'existe pas.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Liste des noms de modeles (ex: ['Client', 'Reservation', 'Facture'])
|
|
672
|
+
"""
|
|
673
|
+
model_names = []
|
|
674
|
+
apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX
|
|
675
|
+
|
|
676
|
+
for app_config in apps.get_app_configs():
|
|
677
|
+
# Filtrer les apps si un préfixe est configuré
|
|
678
|
+
if apps_prefix and not app_config.name.startswith(apps_prefix):
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
# Ignorer les apps Django internes et lucy_assist
|
|
682
|
+
if app_config.name.startswith('django.') or app_config.name == 'lucy_assist':
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
for model in app_config.get_models():
|
|
686
|
+
model_names.append(model.__name__)
|
|
687
|
+
|
|
688
|
+
return sorted(set(model_names))
|
|
@@ -160,6 +160,8 @@ class CRUDService:
|
|
|
160
160
|
"""
|
|
161
161
|
Appelle une vue du projet via RequestFactory.
|
|
162
162
|
|
|
163
|
+
Utilise la découverte dynamique des vues (cache mémoire 5 min).
|
|
164
|
+
|
|
163
165
|
Args:
|
|
164
166
|
model_name: Nom du modèle
|
|
165
167
|
action: Action CRUD ('create', 'update', 'delete', 'list', 'detail')
|
|
@@ -169,13 +171,10 @@ class CRUDService:
|
|
|
169
171
|
Returns:
|
|
170
172
|
Dict avec 'success', 'response', 'messages' ou None si pas de vue
|
|
171
173
|
"""
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
from lucy_assist.services.view_discovery_service import ViewDiscoveryService
|
|
177
|
-
service = ViewDiscoveryService()
|
|
178
|
-
view_info = service.get_view_info(model_name, action)
|
|
174
|
+
# Utiliser la découverte dynamique des vues (cache mémoire)
|
|
175
|
+
from lucy_assist.services.view_discovery_service import ViewDiscoveryService
|
|
176
|
+
service = ViewDiscoveryService()
|
|
177
|
+
view_info = service.get_view_info(model_name, action)
|
|
179
178
|
|
|
180
179
|
if not view_info:
|
|
181
180
|
LogUtils.info(f"[CRUD] Pas de vue trouvée pour {model_name}.{action}")
|
|
@@ -187,6 +186,36 @@ class CRUDService:
|
|
|
187
186
|
|
|
188
187
|
LogUtils.info(f"[CRUD] Appel vue {url_name} ({method}) pour {model_name}.{action}")
|
|
189
188
|
|
|
189
|
+
# Vérifier que l'utilisateur est authentifié
|
|
190
|
+
if not self.user or not self.user.is_authenticated:
|
|
191
|
+
LogUtils.warning(f"[CRUD] Utilisateur non authentifié pour {model_name}.{action}")
|
|
192
|
+
return {
|
|
193
|
+
'success': False,
|
|
194
|
+
'error': "Vous devez être connecté pour effectuer cette action.",
|
|
195
|
+
'permission_denied': True,
|
|
196
|
+
'messages': []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Vérifier les permissions Django standard (optionnel mais rapide)
|
|
200
|
+
action_to_perm = {
|
|
201
|
+
'create': 'add',
|
|
202
|
+
'update': 'change',
|
|
203
|
+
'delete': 'delete',
|
|
204
|
+
'detail': 'view',
|
|
205
|
+
'list': 'view',
|
|
206
|
+
}
|
|
207
|
+
perm_action = action_to_perm.get(action)
|
|
208
|
+
if perm_action and not self.user.is_superuser:
|
|
209
|
+
# Trouver l'app_label du modèle
|
|
210
|
+
model = self.get_model('', model_name)
|
|
211
|
+
if model:
|
|
212
|
+
app_label = model._meta.app_label
|
|
213
|
+
perm_codename = f"{app_label}.{perm_action}_{model_name.lower()}"
|
|
214
|
+
if not self.user.has_perm(perm_codename):
|
|
215
|
+
LogUtils.warning(f"[CRUD] Permission Django manquante: {perm_codename}")
|
|
216
|
+
# On ne bloque pas ici car la vue peut avoir des permissions custom
|
|
217
|
+
# mais on log pour le debug
|
|
218
|
+
|
|
190
219
|
try:
|
|
191
220
|
# Construire l'URL
|
|
192
221
|
if requires_pk and object_id:
|
|
@@ -226,21 +255,49 @@ class CRUDService:
|
|
|
226
255
|
self._cleanup_thread_local()
|
|
227
256
|
|
|
228
257
|
# Analyser la réponse
|
|
229
|
-
|
|
258
|
+
redirect_url = response.get('Location', '') if response.status_code == 302 else ''
|
|
230
259
|
messages = self._extract_messages(request)
|
|
231
260
|
|
|
232
|
-
#
|
|
261
|
+
# Vérifier si c'est une redirection vers login (permission refusée)
|
|
262
|
+
is_permission_denied = False
|
|
263
|
+
if response.status_code == 302 and redirect_url:
|
|
264
|
+
login_patterns = ['/login', '/connexion', '/auth', 'accounts/login']
|
|
265
|
+
if any(pattern in redirect_url.lower() for pattern in login_patterns):
|
|
266
|
+
is_permission_denied = True
|
|
267
|
+
LogUtils.warning(f"[CRUD] Permission refusée - redirection vers {redirect_url}")
|
|
268
|
+
|
|
269
|
+
# Vérifier si c'est une erreur 403 (Forbidden)
|
|
270
|
+
if response.status_code == 403:
|
|
271
|
+
is_permission_denied = True
|
|
272
|
+
LogUtils.warning(f"[CRUD] Permission refusée - HTTP 403")
|
|
273
|
+
|
|
274
|
+
if is_permission_denied:
|
|
275
|
+
return {
|
|
276
|
+
'success': False,
|
|
277
|
+
'status_code': response.status_code,
|
|
278
|
+
'error': "Vous n'avez pas les permissions nécessaires pour effectuer cette action.",
|
|
279
|
+
'permission_denied': True,
|
|
280
|
+
'messages': messages
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Déterminer le succès
|
|
284
|
+
success = response.status_code in [200, 201, 302]
|
|
285
|
+
|
|
286
|
+
# Détecter les erreurs dans la réponse HTML
|
|
233
287
|
if hasattr(response, 'content'):
|
|
234
288
|
content = response.content.decode('utf-8', errors='replace')
|
|
235
289
|
# Chercher des indicateurs d'erreur dans le HTML
|
|
236
290
|
if 'class="error' in content or 'class="invalid' in content:
|
|
237
291
|
success = False
|
|
292
|
+
# Détecter les messages d'erreur de permission dans le contenu
|
|
293
|
+
if 'permission' in content.lower() and ('denied' in content.lower() or 'refusé' in content.lower()):
|
|
294
|
+
success = False
|
|
238
295
|
|
|
239
296
|
return {
|
|
240
297
|
'success': success,
|
|
241
298
|
'status_code': response.status_code,
|
|
242
299
|
'messages': messages,
|
|
243
|
-
'redirect_url':
|
|
300
|
+
'redirect_url': redirect_url if response.status_code == 302 else None
|
|
244
301
|
}
|
|
245
302
|
|
|
246
303
|
except NoReverseMatch as e:
|
|
@@ -480,7 +537,16 @@ class CRUDService:
|
|
|
480
537
|
'via_view': True
|
|
481
538
|
}
|
|
482
539
|
|
|
483
|
-
#
|
|
540
|
+
# Si CRUD_VIEWS_ONLY est activé, refuser l'opération
|
|
541
|
+
if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
|
|
542
|
+
LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.create et CRUD_VIEWS_ONLY=True")
|
|
543
|
+
return {
|
|
544
|
+
'success': False,
|
|
545
|
+
'errors': [f"Aucune vue de création trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
|
|
546
|
+
'no_view_found': True
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
# Fallback: création directe via le modèle (si autorisé)
|
|
484
550
|
LogUtils.info(f"[CRUD] Fallback création directe pour {model_name}")
|
|
485
551
|
return self._create_object_direct(app_name, model_name, data)
|
|
486
552
|
|
|
@@ -594,7 +660,16 @@ class CRUDService:
|
|
|
594
660
|
'via_view': True
|
|
595
661
|
}
|
|
596
662
|
|
|
597
|
-
#
|
|
663
|
+
# Si CRUD_VIEWS_ONLY est activé, refuser l'opération
|
|
664
|
+
if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
|
|
665
|
+
LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.update et CRUD_VIEWS_ONLY=True")
|
|
666
|
+
return {
|
|
667
|
+
'success': False,
|
|
668
|
+
'errors': [f"Aucune vue de modification trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
|
|
669
|
+
'no_view_found': True
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# Fallback: mise à jour directe via le modèle (si autorisé)
|
|
598
673
|
LogUtils.info(f"[CRUD] Fallback mise à jour directe pour {model_name}")
|
|
599
674
|
return self._update_object_direct(app_name, model_name, object_id, data)
|
|
600
675
|
|
|
@@ -715,7 +790,16 @@ class CRUDService:
|
|
|
715
790
|
'via_view': True
|
|
716
791
|
}
|
|
717
792
|
|
|
718
|
-
#
|
|
793
|
+
# Si CRUD_VIEWS_ONLY est activé, refuser l'opération
|
|
794
|
+
if lucy_assist_settings.get('CRUD_VIEWS_ONLY', True):
|
|
795
|
+
LogUtils.info(f"[CRUD] Aucune vue trouvée pour {model_name}.delete et CRUD_VIEWS_ONLY=True")
|
|
796
|
+
return {
|
|
797
|
+
'success': False,
|
|
798
|
+
'errors': [f"Aucune vue de suppression trouvée pour {model_name}. L'opération CRUD n'est pas disponible pour ce modèle."],
|
|
799
|
+
'no_view_found': True
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
# Fallback: suppression directe via le modèle (si autorisé)
|
|
719
803
|
LogUtils.info(f"[CRUD] Fallback suppression directe pour {model_name}")
|
|
720
804
|
return self._delete_object_direct(app_name, model_name, object_id)
|
|
721
805
|
|
|
@@ -35,6 +35,26 @@ class GitLabService:
|
|
|
35
35
|
"""Construit l'URL de l'API."""
|
|
36
36
|
return f"{self.base_url}/api/v4{endpoint}"
|
|
37
37
|
|
|
38
|
+
def get_default_branch(self) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Recupere la branche par defaut du projet.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Nom de la branche par defaut ou 'main' si erreur
|
|
44
|
+
"""
|
|
45
|
+
if not self.token or not self.project_id:
|
|
46
|
+
return 'main'
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
url = self._api_url(f"/projects/{self.project_id}")
|
|
50
|
+
response = requests.get(url, headers=self.headers, timeout=10)
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
project_info = response.json()
|
|
53
|
+
return project_info.get('default_branch', 'main')
|
|
54
|
+
except requests.RequestException as e:
|
|
55
|
+
LogUtils.error(f"Erreur lors de la recuperation de la branche par defaut: {e}")
|
|
56
|
+
return 'main'
|
|
57
|
+
|
|
38
58
|
def search_code(
|
|
39
59
|
self,
|
|
40
60
|
query: str,
|
|
@@ -76,14 +96,14 @@ class GitLabService:
|
|
|
76
96
|
def get_file_content(
|
|
77
97
|
self,
|
|
78
98
|
file_path: str,
|
|
79
|
-
ref: str =
|
|
99
|
+
ref: str = None
|
|
80
100
|
) -> Optional[str]:
|
|
81
101
|
"""
|
|
82
|
-
|
|
102
|
+
Recupere le contenu d'un fichier.
|
|
83
103
|
|
|
84
104
|
Args:
|
|
85
105
|
file_path: Chemin du fichier dans le repo
|
|
86
|
-
ref: Branche ou tag
|
|
106
|
+
ref: Branche ou tag (None = branche par defaut)
|
|
87
107
|
|
|
88
108
|
Returns:
|
|
89
109
|
Contenu du fichier ou None
|
|
@@ -91,6 +111,10 @@ class GitLabService:
|
|
|
91
111
|
if not self.token or not self.project_id:
|
|
92
112
|
return None
|
|
93
113
|
|
|
114
|
+
# Utiliser la branche par defaut si non specifiee
|
|
115
|
+
if ref is None:
|
|
116
|
+
ref = self.get_default_branch()
|
|
117
|
+
|
|
94
118
|
try:
|
|
95
119
|
encoded_path = quote(file_path, safe='')
|
|
96
120
|
url = self._api_url(
|
|
@@ -104,16 +128,16 @@ class GitLabService:
|
|
|
104
128
|
return response.text
|
|
105
129
|
|
|
106
130
|
except requests.RequestException as e:
|
|
107
|
-
LogUtils.error(f"Erreur lors de la
|
|
131
|
+
LogUtils.error(f"Erreur lors de la recuperation du fichier {file_path}: {e}")
|
|
108
132
|
return None
|
|
109
133
|
|
|
110
134
|
def get_file_blame(
|
|
111
135
|
self,
|
|
112
136
|
file_path: str,
|
|
113
|
-
ref: str =
|
|
137
|
+
ref: str = None
|
|
114
138
|
) -> List[Dict]:
|
|
115
139
|
"""
|
|
116
|
-
|
|
140
|
+
Recupere le blame d'un fichier (qui a modifie quoi).
|
|
117
141
|
|
|
118
142
|
Returns:
|
|
119
143
|
Liste des blocs de blame
|
|
@@ -121,6 +145,10 @@ class GitLabService:
|
|
|
121
145
|
if not self.token or not self.project_id:
|
|
122
146
|
return []
|
|
123
147
|
|
|
148
|
+
# Utiliser la branche par defaut si non specifiee
|
|
149
|
+
if ref is None:
|
|
150
|
+
ref = self.get_default_branch()
|
|
151
|
+
|
|
124
152
|
try:
|
|
125
153
|
encoded_path = quote(file_path, safe='')
|
|
126
154
|
url = self._api_url(
|
|
@@ -218,6 +246,51 @@ class GitLabService:
|
|
|
218
246
|
|
|
219
247
|
return result
|
|
220
248
|
|
|
249
|
+
def get_repository_tree(
|
|
250
|
+
self,
|
|
251
|
+
path: str = '',
|
|
252
|
+
ref: str = None,
|
|
253
|
+
recursive: bool = True,
|
|
254
|
+
per_page: int = 100
|
|
255
|
+
) -> List[Dict]:
|
|
256
|
+
"""
|
|
257
|
+
Liste les fichiers et dossiers du repository.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
path: Chemin du dossier (vide = racine)
|
|
261
|
+
ref: Branche ou tag (None = branche par defaut)
|
|
262
|
+
recursive: Inclure les sous-dossiers
|
|
263
|
+
per_page: Nombre de resultats par page
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Liste des fichiers/dossiers avec 'name', 'path', 'type'
|
|
267
|
+
"""
|
|
268
|
+
if not self.token or not self.project_id:
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
# Utiliser la branche par defaut si non specifiee
|
|
272
|
+
if ref is None:
|
|
273
|
+
ref = self.get_default_branch()
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
url = self._api_url(f"/projects/{self.project_id}/repository/tree")
|
|
277
|
+
params = {
|
|
278
|
+
'ref': ref,
|
|
279
|
+
'recursive': str(recursive).lower(),
|
|
280
|
+
'per_page': per_page
|
|
281
|
+
}
|
|
282
|
+
if path:
|
|
283
|
+
params['path'] = path
|
|
284
|
+
|
|
285
|
+
response = requests.get(url, headers=self.headers, params=params, timeout=15)
|
|
286
|
+
response.raise_for_status()
|
|
287
|
+
|
|
288
|
+
return response.json()
|
|
289
|
+
|
|
290
|
+
except requests.RequestException as e:
|
|
291
|
+
LogUtils.error(f"Erreur lors de la recuperation de l'arborescence: {e}")
|
|
292
|
+
return []
|
|
293
|
+
|
|
221
294
|
def get_recent_commits(self, per_page: int = 10) -> List[Dict]:
|
|
222
295
|
"""
|
|
223
296
|
Récupère les commits récents.
|
|
@@ -60,6 +60,8 @@ class MistralService:
|
|
|
60
60
|
Utilise le prompt stocke en base de donnees et le cache
|
|
61
61
|
pour reduire la redondance des informations sur le projet.
|
|
62
62
|
"""
|
|
63
|
+
from lucy_assist.services.context_service import ContextService
|
|
64
|
+
|
|
63
65
|
# Recuperer les permissions utilisateur (compressees)
|
|
64
66
|
user_permissions = []
|
|
65
67
|
if hasattr(user, 'get_all_permissions'):
|
|
@@ -69,25 +71,43 @@ class MistralService:
|
|
|
69
71
|
|
|
70
72
|
# Recuperer le contexte projet optimise depuis le cache
|
|
71
73
|
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
74
|
|
|
77
|
-
#
|
|
75
|
+
# Utiliser ContextService pour obtenir les modeles pertinents (optimisation tokens)
|
|
76
|
+
context_service = ContextService(user)
|
|
77
|
+
relevant_models = context_service.get_relevant_models(page_url)
|
|
78
|
+
|
|
79
|
+
# Formater les modeles pertinents (avec details)
|
|
80
|
+
models_description = []
|
|
81
|
+
for model in relevant_models:
|
|
82
|
+
prefix = "[PAGE ACTUELLE] " if model.get('is_current') else ""
|
|
83
|
+
fields_str = ", ".join(model.get('fields', []))
|
|
84
|
+
models_description.append(
|
|
85
|
+
f"{prefix}{model['name']} ({model['name_lower']}): {fields_str}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Ajouter la liste complete de TOUS les modeles disponibles (juste les noms)
|
|
89
|
+
all_model_names = context_service.get_all_model_names()
|
|
90
|
+
if all_model_names:
|
|
91
|
+
models_description.append("")
|
|
92
|
+
models_description.append("Liste complete des modeles existants: " + ", ".join(all_model_names))
|
|
93
|
+
|
|
94
|
+
available_models = "\n".join(models_description) if models_description else "Aucun modele"
|
|
95
|
+
|
|
96
|
+
# Enrichir le contexte de page avec l'aide contextuelle
|
|
97
|
+
page_help = context_service.get_page_help(page_url)
|
|
78
98
|
enriched_context = {
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
99
|
+
'url': page_url,
|
|
100
|
+
'type_page': page_help.get('page_type'),
|
|
101
|
+
'modele_actuel': page_help.get('current_model'),
|
|
102
|
+
'aide': page_help.get('help_message'),
|
|
103
|
+
'actions_suggerees': page_help.get('suggested_actions', []),
|
|
104
|
+
'liens_utiles': page_help.get('useful_links', [])
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
# Recuperer la configuration avec le prompt systeme
|
|
85
108
|
from lucy_assist.models import ConfigurationLucyAssist
|
|
86
109
|
config = ConfigurationLucyAssist.get_config()
|
|
87
110
|
|
|
88
|
-
# Generer la description des modeles disponibles
|
|
89
|
-
available_models = config.get_available_models_description()
|
|
90
|
-
|
|
91
111
|
# Construire le prompt depuis la configuration
|
|
92
112
|
prompt = config.get_system_prompt(
|
|
93
113
|
page_context=json.dumps(enriched_context, ensure_ascii=False, indent=2),
|