django-lucy-assist 1.0.5__py3-none-any.whl → 1.0.6__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.0.5.dist-info → django_lucy_assist-1.0.6.dist-info}/METADATA +1 -1
- {django_lucy_assist-1.0.5.dist-info → django_lucy_assist-1.0.6.dist-info}/RECORD +7 -7
- lucy_assist/__init__.py +1 -1
- lucy_assist/services/context_service.py +83 -10
- lucy_assist/services/crud_service.py +50 -6
- {django_lucy_assist-1.0.5.dist-info → django_lucy_assist-1.0.6.dist-info}/WHEEL +0 -0
- {django_lucy_assist-1.0.5.dist-info → django_lucy_assist-1.0.6.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
lucy_assist/__init__.py,sha256=
|
|
1
|
+
lucy_assist/__init__.py,sha256=GmRLNDDU0LJkL3EhCcm2DaUIX4Ix7ybTCn-fu5n2k4E,335
|
|
2
2
|
lucy_assist/admin.py,sha256=-hNfuwuMfxgZVFQc_ODy6WcyZPxrM_8TfKsRMd0fj38,694
|
|
3
3
|
lucy_assist/apps.py,sha256=zHZtlBXs5ML4CKtGg7xDyptSWzLfB1ks2VvbXF50hdo,264
|
|
4
4
|
lucy_assist/conf.py,sha256=sWcAdJTSE3Hn_guTifZaRCLKIuJMvR9RwJk2GEKCFOI,3520
|
|
@@ -18,8 +18,8 @@ lucy_assist/models/project_context_cache.py,sha256=Bnb0VU7pv7QEvjOI6JSLEPvL4Bxsk
|
|
|
18
18
|
lucy_assist/services/__init__.py,sha256=I0brW674WNIKkGHj2lj4sGEDD7HUAr5Z254dsbirdLk,691
|
|
19
19
|
lucy_assist/services/bug_notification_service.py,sha256=OyowCvAs-QDlsGQ_WTFoc4lRe9detD7r6ZyYK0JD2Sc,7217
|
|
20
20
|
lucy_assist/services/claude_service.py,sha256=vYeotZKwFghbWNmN_VM0uggnFQgtNNK0SP3e9QPQzgc,16218
|
|
21
|
-
lucy_assist/services/context_service.py,sha256=
|
|
22
|
-
lucy_assist/services/crud_service.py,sha256=
|
|
21
|
+
lucy_assist/services/context_service.py,sha256=e7ByX0pmCBET9odM4ePWSt66sZohR6WOr8AljELuq1I,16499
|
|
22
|
+
lucy_assist/services/crud_service.py,sha256=IQ0CwEgag-rTwmgA0lCqbKS5tEK5YjeiWmuxXainli8,14935
|
|
23
23
|
lucy_assist/services/gitlab_service.py,sha256=uH83fwRSCwiRItznENpYQG4aPckjafYIV9z6OChUrZg,8056
|
|
24
24
|
lucy_assist/services/project_context_service.py,sha256=bIuqTanc59gP_BLod3oQgWplxpiCgByg-kbUMe_57CQ,14053
|
|
25
25
|
lucy_assist/services/tool_executor_service.py,sha256=fXLH4Aaip-HPX1nNRs8UQ1N77rGloBahSwOR9gPmjfU,13583
|
|
@@ -39,7 +39,7 @@ lucy_assist/utils/message_utils.py,sha256=YzcLHnl1ig4d5_utHCJwgxS7tKmd49Q-tuo78e
|
|
|
39
39
|
lucy_assist/utils/token_utils.py,sha256=rxe9jHjcRJcaIlcw0QuVmYXOjscTsUsxnhhI6RMBzDM,2608
|
|
40
40
|
lucy_assist/views/__init__.py,sha256=uUPYpuHlBC8j7zKS_DDoWjwpCpRnOIXETY-S2-Ss0cY,288
|
|
41
41
|
lucy_assist/views/api_views.py,sha256=iCvdTTTJ73r3jfyZVjcEDi3Of2wP_N24G_QsXwc-Euk,23617
|
|
42
|
-
django_lucy_assist-1.0.
|
|
43
|
-
django_lucy_assist-1.0.
|
|
44
|
-
django_lucy_assist-1.0.
|
|
45
|
-
django_lucy_assist-1.0.
|
|
42
|
+
django_lucy_assist-1.0.6.dist-info/METADATA,sha256=7CCVUKQWHPNS52cMi6YpPNyrRtu_eQilAHlBd_gu_fI,5543
|
|
43
|
+
django_lucy_assist-1.0.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
44
|
+
django_lucy_assist-1.0.6.dist-info/top_level.txt,sha256=T-UCiwpn5yF3Oem3234TUpSVnEgbkrM2rGz9Tz5N-QA,12
|
|
45
|
+
django_lucy_assist-1.0.6.dist-info/RECORD,,
|
lucy_assist/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Un chatbot IA basé sur Claude d'Anthropic, intégrable dans n'importe quelle
|
|
|
5
5
|
application Django pour fournir une assistance contextuelle aux utilisateurs.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = '1.0.
|
|
8
|
+
__version__ = '1.0.6'
|
|
9
9
|
__author__ = 'Revolucy'
|
|
10
10
|
|
|
11
11
|
default_app_config = 'lucy_assist.apps.LucyAssistConfig'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Service de détection et construction du contexte de page.
|
|
3
3
|
"""
|
|
4
|
-
import logging
|
|
5
4
|
import re
|
|
5
|
+
from datetime import datetime
|
|
6
6
|
from typing import Dict, List, Optional
|
|
7
7
|
from urllib.parse import urlparse
|
|
8
8
|
|
|
@@ -16,9 +16,38 @@ from lucy_assist.conf import lucy_assist_settings
|
|
|
16
16
|
class ContextService:
|
|
17
17
|
"""Service pour construire le contexte de la page courante."""
|
|
18
18
|
|
|
19
|
+
# Patterns pour détecter les dates dans les requêtes
|
|
20
|
+
DATE_PATTERNS = [
|
|
21
|
+
(r'(\d{1,2})/(\d{1,2})/(\d{4})', '%d/%m/%Y'), # 18/10/2025
|
|
22
|
+
(r'(\d{1,2})-(\d{1,2})-(\d{4})', '%d-%m-%Y'), # 18-10-2025
|
|
23
|
+
(r'(\d{4})/(\d{1,2})/(\d{1,2})', '%Y/%m/%d'), # 2025/10/18
|
|
24
|
+
(r'(\d{4})-(\d{1,2})-(\d{1,2})', '%Y-%m-%d'), # 2025-10-18
|
|
25
|
+
]
|
|
26
|
+
|
|
19
27
|
def __init__(self, user):
|
|
20
28
|
self.user = user
|
|
21
29
|
|
|
30
|
+
def _extract_date_from_query(self, query: str) -> Optional[datetime]:
|
|
31
|
+
"""
|
|
32
|
+
Extrait une date d'une requête texte.
|
|
33
|
+
Supporte les formats: 18/10/2025, 18-10-2025, 2025/10/18, 2025-10-18
|
|
34
|
+
"""
|
|
35
|
+
for pattern, date_format in self.DATE_PATTERNS:
|
|
36
|
+
match = re.search(pattern, query)
|
|
37
|
+
if match:
|
|
38
|
+
try:
|
|
39
|
+
date_str = match.group(0)
|
|
40
|
+
return datetime.strptime(date_str, date_format)
|
|
41
|
+
except ValueError:
|
|
42
|
+
continue
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _remove_date_from_query(self, query: str) -> str:
|
|
46
|
+
"""Supprime la date de la requête pour garder les autres mots-clés."""
|
|
47
|
+
for pattern, _ in self.DATE_PATTERNS:
|
|
48
|
+
query = re.sub(pattern, '', query)
|
|
49
|
+
return query.strip()
|
|
50
|
+
|
|
22
51
|
def get_page_context(self, url_path: str) -> Dict:
|
|
23
52
|
"""
|
|
24
53
|
Construit le contexte complet d'une page.
|
|
@@ -293,25 +322,69 @@ class ContextService:
|
|
|
293
322
|
# Log des modèles découverts pour debug
|
|
294
323
|
LogUtils.info(f"[search_objects] Recherche '{query}' dans {len(models_to_search)} modèles: {[m.__name__ for m in models_to_search]}")
|
|
295
324
|
|
|
325
|
+
# Extraire une date de la requête si présente
|
|
326
|
+
search_date = self._extract_date_from_query(query)
|
|
327
|
+
text_query = self._remove_date_from_query(query) if search_date else query
|
|
328
|
+
|
|
329
|
+
if search_date:
|
|
330
|
+
LogUtils.info(f"[search_objects] Date détectée: {search_date.date()}")
|
|
331
|
+
|
|
296
332
|
# Rechercher dans chaque modèle
|
|
297
333
|
for model in models_to_search:
|
|
298
334
|
try:
|
|
299
|
-
# Trouver les champs texte pour la recherche
|
|
335
|
+
# Trouver les champs texte et date pour la recherche
|
|
300
336
|
search_fields = []
|
|
337
|
+
date_fields = []
|
|
301
338
|
for field in model._meta.get_fields():
|
|
302
339
|
if hasattr(field, 'get_internal_type'):
|
|
303
|
-
|
|
340
|
+
field_type = field.get_internal_type()
|
|
341
|
+
if field_type in ['CharField', 'TextField']:
|
|
304
342
|
search_fields.append(field.name)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
LogUtils.info(f"[search_objects] {model.__name__}: aucun champ texte trouvé")
|
|
308
|
-
continue
|
|
343
|
+
elif field_type in ['DateField', 'DateTimeField']:
|
|
344
|
+
date_fields.append(field.name)
|
|
309
345
|
|
|
310
346
|
# Construire la requête
|
|
311
347
|
from django.db.models import Q
|
|
312
|
-
q_objects =
|
|
313
|
-
|
|
314
|
-
|
|
348
|
+
q_objects = None
|
|
349
|
+
has_criteria = False
|
|
350
|
+
|
|
351
|
+
# Si on a une date, chercher dans les champs date
|
|
352
|
+
if search_date and date_fields:
|
|
353
|
+
date_q = Q()
|
|
354
|
+
for field_name in date_fields:
|
|
355
|
+
date_q |= Q(**{f'{field_name}__date': search_date.date()})
|
|
356
|
+
q_objects = date_q
|
|
357
|
+
has_criteria = True
|
|
358
|
+
LogUtils.info(f"[search_objects] {model.__name__}: recherche date dans {date_fields}")
|
|
359
|
+
|
|
360
|
+
# Si on a du texte, chercher dans les champs texte
|
|
361
|
+
if text_query.strip() and search_fields:
|
|
362
|
+
query_words = text_query.strip().split()
|
|
363
|
+
text_q = Q()
|
|
364
|
+
|
|
365
|
+
if len(query_words) > 1:
|
|
366
|
+
# Recherche multi-mots: chaque mot doit matcher au moins un champ
|
|
367
|
+
for word in query_words:
|
|
368
|
+
word_q = Q()
|
|
369
|
+
for field_name in search_fields[:5]:
|
|
370
|
+
word_q |= Q(**{f'{field_name}__icontains': word})
|
|
371
|
+
text_q &= word_q
|
|
372
|
+
else:
|
|
373
|
+
# Recherche simple
|
|
374
|
+
for field_name in search_fields[:5]:
|
|
375
|
+
text_q |= Q(**{f'{field_name}__icontains': text_query})
|
|
376
|
+
|
|
377
|
+
if q_objects is not None:
|
|
378
|
+
q_objects &= text_q
|
|
379
|
+
else:
|
|
380
|
+
q_objects = text_q
|
|
381
|
+
has_criteria = True
|
|
382
|
+
|
|
383
|
+
# Si pas de critère de recherche valide, skip
|
|
384
|
+
if not has_criteria:
|
|
385
|
+
if not search_fields and not date_fields:
|
|
386
|
+
LogUtils.info(f"[search_objects] {model.__name__}: aucun champ recherchable")
|
|
387
|
+
continue
|
|
315
388
|
|
|
316
389
|
# Filtrer par permissions si possible
|
|
317
390
|
# Note: certains modèles utilisent des managers customs qui
|
|
@@ -17,6 +17,44 @@ class CRUDService:
|
|
|
17
17
|
def __init__(self, user):
|
|
18
18
|
self.user = user
|
|
19
19
|
|
|
20
|
+
def _serialize_value(self, value):
|
|
21
|
+
"""
|
|
22
|
+
Convertit une valeur en un format sérialisable JSON.
|
|
23
|
+
"""
|
|
24
|
+
from datetime import datetime, date, time
|
|
25
|
+
from decimal import Decimal
|
|
26
|
+
import uuid
|
|
27
|
+
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
elif isinstance(value, (datetime,)):
|
|
31
|
+
return value.isoformat()
|
|
32
|
+
elif isinstance(value, (date,)):
|
|
33
|
+
return value.isoformat()
|
|
34
|
+
elif isinstance(value, (time,)):
|
|
35
|
+
return value.isoformat()
|
|
36
|
+
elif isinstance(value, Decimal):
|
|
37
|
+
return float(value)
|
|
38
|
+
elif isinstance(value, uuid.UUID):
|
|
39
|
+
return str(value)
|
|
40
|
+
elif hasattr(value, 'pk'):
|
|
41
|
+
# ForeignKey ou relation
|
|
42
|
+
return {'id': value.pk, 'str': str(value)}
|
|
43
|
+
elif hasattr(value, 'all'):
|
|
44
|
+
# ManyToMany ou reverse FK - retourner juste le count
|
|
45
|
+
return {'count': value.count()}
|
|
46
|
+
elif isinstance(value, bytes):
|
|
47
|
+
return value.decode('utf-8', errors='replace')
|
|
48
|
+
else:
|
|
49
|
+
# Essayer de retourner directement, sinon convertir en string
|
|
50
|
+
try:
|
|
51
|
+
# Types simples (str, int, float, bool, list, dict)
|
|
52
|
+
import json
|
|
53
|
+
json.dumps(value)
|
|
54
|
+
return value
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
return str(value)
|
|
57
|
+
|
|
20
58
|
def can_perform_action(self, app_name: str, model_name: str, action: str) -> bool:
|
|
21
59
|
"""
|
|
22
60
|
Vérifie si l'utilisateur peut effectuer l'action.
|
|
@@ -364,16 +402,21 @@ class CRUDService:
|
|
|
364
402
|
Returns:
|
|
365
403
|
Dict avec les données de l'objet ou None
|
|
366
404
|
"""
|
|
405
|
+
LogUtils.info(f"[CRUD] get_object: {app_name}.{model_name} #{object_id}")
|
|
406
|
+
|
|
367
407
|
# Vérifier les permissions
|
|
368
408
|
if not self.can_perform_action(app_name, model_name, 'view'):
|
|
409
|
+
LogUtils.info(f"[CRUD] get_object: permission refusée pour {model_name}")
|
|
369
410
|
return None
|
|
370
411
|
|
|
371
412
|
model = self.get_model(app_name, model_name)
|
|
372
413
|
if not model:
|
|
414
|
+
LogUtils.warning(f"[CRUD] get_object: modèle {model_name} non trouvé")
|
|
373
415
|
return None
|
|
374
416
|
|
|
375
417
|
try:
|
|
376
|
-
|
|
418
|
+
# Utiliser objects.all() pour éviter les problèmes avec les managers customs
|
|
419
|
+
obj = model.objects.all().get(pk=object_id)
|
|
377
420
|
|
|
378
421
|
# Construire un dict avec les données
|
|
379
422
|
data = {'id': obj.pk, 'str': str(obj)}
|
|
@@ -382,15 +425,16 @@ class CRUDService:
|
|
|
382
425
|
if hasattr(field, 'verbose_name'):
|
|
383
426
|
try:
|
|
384
427
|
value = getattr(obj, field.name)
|
|
385
|
-
|
|
386
|
-
if hasattr(value, 'pk'):
|
|
387
|
-
data[field.name] = {'id': value.pk, 'str': str(value)}
|
|
388
|
-
else:
|
|
389
|
-
data[field.name] = value
|
|
428
|
+
data[field.name] = self._serialize_value(value)
|
|
390
429
|
except Exception:
|
|
391
430
|
pass
|
|
392
431
|
|
|
432
|
+
LogUtils.info(f"[CRUD] get_object: {model_name} #{object_id} récupéré avec succès")
|
|
393
433
|
return data
|
|
394
434
|
|
|
395
435
|
except model.DoesNotExist:
|
|
436
|
+
LogUtils.info(f"[CRUD] get_object: {model_name} #{object_id} non trouvé")
|
|
437
|
+
return None
|
|
438
|
+
except Exception as e:
|
|
439
|
+
LogUtils.error(f"[CRUD] get_object: erreur pour {model_name} #{object_id}: {e}")
|
|
396
440
|
return None
|
|
File without changes
|
|
File without changes
|