django-lucy-assist 1.2.2__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.2.dist-info → django_lucy_assist-1.2.4.dist-info}/METADATA +1 -1
- {django_lucy_assist-1.2.2.dist-info → django_lucy_assist-1.2.4.dist-info}/RECORD +20 -23
- lucy_assist/__init__.py +1 -1
- lucy_assist/admin.py +13 -13
- 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 +5 -13
- 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
- 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
- {django_lucy_assist-1.2.2.dist-info → django_lucy_assist-1.2.4.dist-info}/WHEEL +0 -0
- {django_lucy_assist-1.2.2.dist-info → django_lucy_assist-1.2.4.dist-info}/top_level.txt +0 -0
|
@@ -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),
|
|
@@ -54,6 +54,9 @@ class ToolExecutorService:
|
|
|
54
54
|
'get_form_fields': self._handle_get_form_fields,
|
|
55
55
|
'navigate_to_page': self._handle_navigate_to_page,
|
|
56
56
|
'analyze_bug': self._handle_analyze_bug,
|
|
57
|
+
'get_page_help': self._handle_get_page_help,
|
|
58
|
+
'search_code_gitlab': self._handle_search_code_gitlab,
|
|
59
|
+
'get_file_content_gitlab': self._handle_get_file_content_gitlab,
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
handler = handlers.get(tool_name)
|
|
@@ -371,6 +374,94 @@ class ToolExecutorService:
|
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
|
|
377
|
+
def _handle_get_page_help(self, params: Dict) -> Dict:
|
|
378
|
+
"""
|
|
379
|
+
Recupere l'aide contextuelle pour la page actuelle.
|
|
380
|
+
"""
|
|
381
|
+
url_path = params.get('url_path', '')
|
|
382
|
+
|
|
383
|
+
if not url_path:
|
|
384
|
+
return {'error': 'url_path est requis'}
|
|
385
|
+
|
|
386
|
+
help_info = self.context_service.get_page_help(url_path)
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
'success': True,
|
|
390
|
+
**help_info
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
def _handle_search_code_gitlab(self, params: Dict) -> Dict:
|
|
394
|
+
"""
|
|
395
|
+
Recherche dans le code source du projet via GitLab.
|
|
396
|
+
"""
|
|
397
|
+
query = params.get('query', '')
|
|
398
|
+
file_type = params.get('file_type', 'all')
|
|
399
|
+
|
|
400
|
+
if not query:
|
|
401
|
+
return {'error': 'query est requis'}
|
|
402
|
+
|
|
403
|
+
# Adapter la recherche selon le type de fichier
|
|
404
|
+
search_query = query
|
|
405
|
+
if file_type != 'all':
|
|
406
|
+
# Ajouter un filtre de chemin pour cibler le type de fichier
|
|
407
|
+
search_query = f"{query} path:{file_type}"
|
|
408
|
+
|
|
409
|
+
results = self.gitlab_service.search_code(search_query, per_page=10)
|
|
410
|
+
|
|
411
|
+
if not results:
|
|
412
|
+
return {
|
|
413
|
+
'success': True,
|
|
414
|
+
'count': 0,
|
|
415
|
+
'message': f'Aucun resultat trouve pour "{query}"',
|
|
416
|
+
'results': []
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
# Formater les resultats
|
|
420
|
+
formatted_results = []
|
|
421
|
+
for result in results[:10]:
|
|
422
|
+
formatted_results.append({
|
|
423
|
+
'file': result.get('filename', ''),
|
|
424
|
+
'content_preview': result.get('data', '')[:500],
|
|
425
|
+
'ref': result.get('ref', '')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
'success': True,
|
|
430
|
+
'count': len(formatted_results),
|
|
431
|
+
'query': query,
|
|
432
|
+
'results': formatted_results
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
def _handle_get_file_content_gitlab(self, params: Dict) -> Dict:
|
|
436
|
+
"""
|
|
437
|
+
Recupere le contenu complet d'un fichier depuis GitLab.
|
|
438
|
+
"""
|
|
439
|
+
file_path = params.get('file_path', '')
|
|
440
|
+
|
|
441
|
+
if not file_path:
|
|
442
|
+
return {'error': 'file_path est requis'}
|
|
443
|
+
|
|
444
|
+
content = self.gitlab_service.get_file_content(file_path)
|
|
445
|
+
|
|
446
|
+
if content is None:
|
|
447
|
+
return {
|
|
448
|
+
'success': False,
|
|
449
|
+
'error': f'Fichier non trouve: {file_path}'
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Limiter la taille du contenu retourne
|
|
453
|
+
max_content_size = 10000
|
|
454
|
+
truncated = len(content) > max_content_size
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
'success': True,
|
|
458
|
+
'file_path': file_path,
|
|
459
|
+
'content': content[:max_content_size],
|
|
460
|
+
'truncated': truncated,
|
|
461
|
+
'total_size': len(content)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
374
465
|
def create_tool_executor(user):
|
|
375
466
|
"""
|
|
376
467
|
Factory function pour créer un callable d'exécution de tools.
|
|
@@ -274,6 +274,71 @@ LUCY_ASSIST_TOOLS = [
|
|
|
274
274
|
"required": ["user_description"]
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"type": "function",
|
|
280
|
+
"function": {
|
|
281
|
+
"name": "get_page_help",
|
|
282
|
+
"description": """Recupere l'aide contextuelle pour la page actuelle.
|
|
283
|
+
Utilise ce tool pour obtenir des informations sur ce que l'utilisateur peut faire sur la page.
|
|
284
|
+
Retourne les actions suggerees, liens utiles et conseils.""",
|
|
285
|
+
"parameters": {
|
|
286
|
+
"type": "object",
|
|
287
|
+
"properties": {
|
|
288
|
+
"url_path": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"description": "URL de la page actuelle (disponible dans le contexte)"
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
"required": ["url_path"]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"type": "function",
|
|
299
|
+
"function": {
|
|
300
|
+
"name": "search_code_gitlab",
|
|
301
|
+
"description": """USAGE INTERNE - Recherche dans le code source pour comprendre le fonctionnement.
|
|
302
|
+
Utilise ce tool pour comprendre comment fonctionne une fonctionnalite.
|
|
303
|
+
IMPORTANT: Les resultats sont pour TON usage interne uniquement.
|
|
304
|
+
Ne JAMAIS montrer les chemins de fichiers ou le code a l'utilisateur.
|
|
305
|
+
Utilise ces informations pour repondre en termes simples avec des liens vers les pages.""",
|
|
306
|
+
"parameters": {
|
|
307
|
+
"type": "object",
|
|
308
|
+
"properties": {
|
|
309
|
+
"query": {
|
|
310
|
+
"type": "string",
|
|
311
|
+
"description": "Terme de recherche (nom de classe, fonction, variable, texte)"
|
|
312
|
+
},
|
|
313
|
+
"file_type": {
|
|
314
|
+
"type": "string",
|
|
315
|
+
"enum": ["models", "views", "urls", "forms", "admin", "all"],
|
|
316
|
+
"description": "Type de fichier a rechercher (defaut: all)"
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
"required": ["query"]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"type": "function",
|
|
325
|
+
"function": {
|
|
326
|
+
"name": "get_file_content_gitlab",
|
|
327
|
+
"description": """USAGE INTERNE - Recupere le contenu d'un fichier pour comprendre le fonctionnement.
|
|
328
|
+
IMPORTANT: Les resultats sont pour TON usage interne uniquement.
|
|
329
|
+
Ne JAMAIS montrer le code source ou les chemins de fichiers a l'utilisateur.
|
|
330
|
+
Utilise ces informations pour repondre en termes simples avec des liens vers les pages.""",
|
|
331
|
+
"parameters": {
|
|
332
|
+
"type": "object",
|
|
333
|
+
"properties": {
|
|
334
|
+
"file_path": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"description": "Chemin du fichier dans le repository"
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"required": ["file_path"]
|
|
340
|
+
}
|
|
341
|
+
}
|
|
277
342
|
}
|
|
278
343
|
]
|
|
279
344
|
|
|
@@ -16,8 +16,16 @@ from lucy_assist.conf import lucy_assist_settings
|
|
|
16
16
|
class ViewDiscoveryService:
|
|
17
17
|
"""
|
|
18
18
|
Service pour découvrir automatiquement les vues CRUD d'un projet Django.
|
|
19
|
+
|
|
20
|
+
La découverte est mise en cache en mémoire pour éviter de scanner
|
|
21
|
+
les URLs à chaque requête.
|
|
19
22
|
"""
|
|
20
23
|
|
|
24
|
+
# Cache en mémoire pour les vues découvertes (partagé entre instances)
|
|
25
|
+
_views_cache = None
|
|
26
|
+
_cache_timestamp = None
|
|
27
|
+
CACHE_TTL = 300 # 5 minutes
|
|
28
|
+
|
|
21
29
|
# Patterns de nommage courants pour les vues CRUD
|
|
22
30
|
# L'ordre est important : les patterns plus spécifiques doivent être en premier
|
|
23
31
|
CRUD_PATTERNS = {
|
|
@@ -65,6 +73,35 @@ class ViewDiscoveryService:
|
|
|
65
73
|
def __init__(self):
|
|
66
74
|
self.apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX or ''
|
|
67
75
|
|
|
76
|
+
@classmethod
|
|
77
|
+
def get_cached_views(cls) -> Dict:
|
|
78
|
+
"""
|
|
79
|
+
Retourne les vues CRUD depuis le cache mémoire.
|
|
80
|
+
Redécouvre automatiquement si le cache est expiré.
|
|
81
|
+
"""
|
|
82
|
+
import time
|
|
83
|
+
|
|
84
|
+
now = time.time()
|
|
85
|
+
|
|
86
|
+
# Vérifier si le cache est valide
|
|
87
|
+
if (cls._views_cache is not None and
|
|
88
|
+
cls._cache_timestamp is not None and
|
|
89
|
+
now - cls._cache_timestamp < cls.CACHE_TTL):
|
|
90
|
+
return cls._views_cache
|
|
91
|
+
|
|
92
|
+
# Redécouvrir les vues
|
|
93
|
+
service = cls()
|
|
94
|
+
cls._views_cache = service.discover_crud_views()
|
|
95
|
+
cls._cache_timestamp = now
|
|
96
|
+
|
|
97
|
+
return cls._views_cache
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def invalidate_cache(cls):
|
|
101
|
+
"""Force la redécouverte des vues au prochain appel."""
|
|
102
|
+
cls._views_cache = None
|
|
103
|
+
cls._cache_timestamp = None
|
|
104
|
+
|
|
68
105
|
def discover_crud_views(self) -> Dict:
|
|
69
106
|
"""
|
|
70
107
|
Découvre toutes les vues CRUD du projet.
|
|
@@ -293,6 +330,9 @@ class ViewDiscoveryService:
|
|
|
293
330
|
"""
|
|
294
331
|
Récupère les informations d'une vue pour un modèle et une action.
|
|
295
332
|
|
|
333
|
+
Utilise le cache mémoire pour éviter de scanner les URLs à chaque requête.
|
|
334
|
+
Le cache est automatiquement rafraîchi après CACHE_TTL secondes.
|
|
335
|
+
|
|
296
336
|
Args:
|
|
297
337
|
model_name: Nom du modèle
|
|
298
338
|
action: Action CRUD ('list', 'create', 'detail', 'update', 'delete')
|
|
@@ -300,11 +340,8 @@ class ViewDiscoveryService:
|
|
|
300
340
|
Returns:
|
|
301
341
|
Dict avec 'url_name', 'url', 'method', 'requires_pk' ou None
|
|
302
342
|
"""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if not config.crud_views_mapping:
|
|
307
|
-
config.refresh_crud_views_mapping()
|
|
343
|
+
# Utiliser le cache mémoire (découverte dynamique)
|
|
344
|
+
crud_views_mapping = self.get_cached_views()
|
|
308
345
|
|
|
309
346
|
model_lower = model_name.lower()
|
|
310
347
|
|
|
@@ -317,10 +354,12 @@ class ViewDiscoveryService:
|
|
|
317
354
|
]
|
|
318
355
|
|
|
319
356
|
for variant in variations:
|
|
320
|
-
model_views =
|
|
357
|
+
model_views = crud_views_mapping.get(variant, {})
|
|
321
358
|
if model_views and action in model_views:
|
|
359
|
+
LogUtils.info(f"[ViewDiscovery] Vue trouvée: {variant}.{action} -> {model_views[action].get('url_name')}")
|
|
322
360
|
return model_views.get(action)
|
|
323
361
|
|
|
362
|
+
LogUtils.info(f"[ViewDiscovery] Aucune vue trouvée pour {model_name}.{action}")
|
|
324
363
|
return None
|
|
325
364
|
|
|
326
365
|
def get_all_discovered_models(self) -> List[str]:
|
|
@@ -330,10 +369,5 @@ class ViewDiscoveryService:
|
|
|
330
369
|
Returns:
|
|
331
370
|
Liste des noms de modèles
|
|
332
371
|
"""
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if not config.crud_views_mapping:
|
|
337
|
-
config.refresh_crud_views_mapping()
|
|
338
|
-
|
|
339
|
-
return list(config.crud_views_mapping.keys())
|
|
372
|
+
crud_views_mapping = self.get_cached_views()
|
|
373
|
+
return list(crud_views_mapping.keys())
|