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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lucy-assist
3
- Version: 1.0.5
3
+ Version: 1.0.6
4
4
  Summary: Assistant IA intelligent Revolucy pour outil métier
5
5
  Author-email: Revolucy <hello@revolucy.fr>
6
6
  Maintainer-email: Maxence <hello@revolucy.fr>
@@ -1,4 +1,4 @@
1
- lucy_assist/__init__.py,sha256=kJG9Bgp37TXe9Ph2DsiuyDMalCvaJkAXRDPGYqASiyk,335
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=aNPvo8b9pUjqnGpd5p6zUt4QJAYdJwjOw2V7URe1ANE,13230
22
- lucy_assist/services/crud_service.py,sha256=wpFFFpApQfycjY5I2AWYk_sZEXUsMI0HiQ69LKeRz4U,13051
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.5.dist-info/METADATA,sha256=anJPOWh-GwZl2vhMz5er7zGGSSR9SAzw76KFbANystc,5543
43
- django_lucy_assist-1.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
44
- django_lucy_assist-1.0.5.dist-info/top_level.txt,sha256=T-UCiwpn5yF3Oem3234TUpSVnEgbkrM2rGz9Tz5N-QA,12
45
- django_lucy_assist-1.0.5.dist-info/RECORD,,
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.5'
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
- if field.get_internal_type() in ['CharField', 'TextField']:
340
+ field_type = field.get_internal_type()
341
+ if field_type in ['CharField', 'TextField']:
304
342
  search_fields.append(field.name)
305
-
306
- if not search_fields:
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 = Q()
313
- for field_name in search_fields[:5]: # Limiter à 5 champs
314
- q_objects |= Q(**{f'{field_name}__icontains': query})
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
- obj = model.objects.get(pk=object_id)
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
- # Gérer les FK
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