django-lucy-assist 1.0.5__tar.gz → 1.0.7__tar.gz
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 → django_lucy_assist-1.0.7}/PKG-INFO +2 -2
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/README.md +1 -1
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/PKG-INFO +2 -2
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/SOURCES.txt +2 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/__init__.py +1 -1
- django_lucy_assist-1.0.7/lucy_assist/admin.py +71 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/conf.py +8 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/constantes.py +21 -1
- django_lucy_assist-1.0.7/lucy_assist/migrations/0003_configurationlucyassist_crud_views_mapping.py +18 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/configuration.py +54 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/context_service.py +94 -10
- django_lucy_assist-1.0.7/lucy_assist/services/crud_service.py +1113 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/tool_executor_service.py +41 -1
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/tools_definition.py +36 -6
- django_lucy_assist-1.0.7/lucy_assist/services/view_discovery_service.py +339 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/pyproject.toml +1 -1
- django_lucy_assist-1.0.5/lucy_assist/admin.py +0 -22
- django_lucy_assist-1.0.5/lucy_assist/services/crud_service.py +0 -396
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/MANIFEST.in +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/dependency_links.txt +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/requires.txt +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/top_level.txt +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/apps.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/context_processors.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/migrations/0001_initial.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/migrations/0002_configurationlucyassist_prompt_complementaire.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/migrations/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/base.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/conversation.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/message.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/models/project_context_cache.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/bug_notification_service.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/claude_service.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/gitlab_service.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/project_context_service.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/signals.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/static/lucy_assist/css/lucy-assist.css +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/static/lucy_assist/js/lucy-assist.js +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/templates/lucy_assist/chatbot_sidebar.html +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/templates/lucy_assist/partials/documentation_content.html +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/tests/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/tests/factories/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/tests/factories/lucy_assist_factories.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/tests/test_lucy_assist.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/urls.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/utils/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/utils/log_utils.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/utils/message_utils.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/utils/token_utils.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/views/__init__.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/views/api_views.py +0 -0
- {django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lucy-assist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
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>
|
|
@@ -176,7 +176,7 @@ Lucy Assist expose plusieurs endpoints API :
|
|
|
176
176
|
- `POST /lucy-assist/api/conversations/` - Créer une conversation
|
|
177
177
|
- `GET /lucy-assist/api/conversations/<id>/` - Détail d'une conversation
|
|
178
178
|
- `POST /lucy-assist/api/conversations/<id>/messages/` - Ajouter un message
|
|
179
|
-
- `POST /lucy-assist/api/
|
|
179
|
+
- `POST /lucy-assist/api/convers2ations/<id>/completion/` - Générer une réponse (streaming)
|
|
180
180
|
- `GET /lucy-assist/api/tokens/status/` - Statut des tokens
|
|
181
181
|
|
|
182
182
|
## Licence
|
|
@@ -133,7 +133,7 @@ Lucy Assist expose plusieurs endpoints API :
|
|
|
133
133
|
- `POST /lucy-assist/api/conversations/` - Créer une conversation
|
|
134
134
|
- `GET /lucy-assist/api/conversations/<id>/` - Détail d'une conversation
|
|
135
135
|
- `POST /lucy-assist/api/conversations/<id>/messages/` - Ajouter un message
|
|
136
|
-
- `POST /lucy-assist/api/
|
|
136
|
+
- `POST /lucy-assist/api/convers2ations/<id>/completion/` - Générer une réponse (streaming)
|
|
137
137
|
- `GET /lucy-assist/api/tokens/status/` - Statut des tokens
|
|
138
138
|
|
|
139
139
|
## Licence
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lucy-assist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
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>
|
|
@@ -176,7 +176,7 @@ Lucy Assist expose plusieurs endpoints API :
|
|
|
176
176
|
- `POST /lucy-assist/api/conversations/` - Créer une conversation
|
|
177
177
|
- `GET /lucy-assist/api/conversations/<id>/` - Détail d'une conversation
|
|
178
178
|
- `POST /lucy-assist/api/conversations/<id>/messages/` - Ajouter un message
|
|
179
|
-
- `POST /lucy-assist/api/
|
|
179
|
+
- `POST /lucy-assist/api/convers2ations/<id>/completion/` - Générer une réponse (streaming)
|
|
180
180
|
- `GET /lucy-assist/api/tokens/status/` - Statut des tokens
|
|
181
181
|
|
|
182
182
|
## Licence
|
{django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/django_lucy_assist.egg-info/SOURCES.txt
RENAMED
|
@@ -16,6 +16,7 @@ lucy_assist/signals.py
|
|
|
16
16
|
lucy_assist/urls.py
|
|
17
17
|
lucy_assist/migrations/0001_initial.py
|
|
18
18
|
lucy_assist/migrations/0002_configurationlucyassist_prompt_complementaire.py
|
|
19
|
+
lucy_assist/migrations/0003_configurationlucyassist_crud_views_mapping.py
|
|
19
20
|
lucy_assist/migrations/__init__.py
|
|
20
21
|
lucy_assist/models/__init__.py
|
|
21
22
|
lucy_assist/models/base.py
|
|
@@ -32,6 +33,7 @@ lucy_assist/services/gitlab_service.py
|
|
|
32
33
|
lucy_assist/services/project_context_service.py
|
|
33
34
|
lucy_assist/services/tool_executor_service.py
|
|
34
35
|
lucy_assist/services/tools_definition.py
|
|
36
|
+
lucy_assist/services/view_discovery_service.py
|
|
35
37
|
lucy_assist/static/lucy_assist/css/lucy-assist.css
|
|
36
38
|
lucy_assist/static/lucy_assist/image/icon-lucy.png
|
|
37
39
|
lucy_assist/static/lucy_assist/js/lucy-assist.js
|
|
@@ -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.7'
|
|
9
9
|
__author__ = 'Revolucy'
|
|
10
10
|
|
|
11
11
|
default_app_config = 'lucy_assist.apps.LucyAssistConfig'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from lucy_assist.models import Conversation, Message, ConfigurationLucyAssist
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BlockMessage(admin.StackedInline):
|
|
7
|
+
model = Message
|
|
8
|
+
extra = 1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(Conversation)
|
|
12
|
+
class ConversationAdmin(admin.ModelAdmin):
|
|
13
|
+
list_display = ('id', 'utilisateur', 'titre', 'created_date', 'is_active')
|
|
14
|
+
list_filter = ('is_active', 'created_date')
|
|
15
|
+
search_fields = ('utilisateur__email', 'titre')
|
|
16
|
+
ordering = ('-created_date',)
|
|
17
|
+
inlines = [BlockMessage]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@admin.register(ConfigurationLucyAssist)
|
|
21
|
+
class ConfigurationLucyAssistAdmin(admin.ModelAdmin):
|
|
22
|
+
list_display = ('id', 'tokens_disponibles', 'prix_par_million_tokens', 'nb_vues_crud', 'updated_date')
|
|
23
|
+
readonly_fields = ('crud_views_mapping_display', 'model_app_mapping_display')
|
|
24
|
+
fieldsets = (
|
|
25
|
+
('Configuration Tokens', {
|
|
26
|
+
'fields': ('tokens_disponibles', 'prix_par_million_tokens', 'actif')
|
|
27
|
+
}),
|
|
28
|
+
('Personnalisation', {
|
|
29
|
+
'fields': ('avatar', 'questions_frequentes', 'prompt_complementaire')
|
|
30
|
+
}),
|
|
31
|
+
('Mapping Automatique (lecture seule)', {
|
|
32
|
+
'fields': ('crud_views_mapping_display', 'model_app_mapping_display'),
|
|
33
|
+
'classes': ('collapse',)
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
actions = ['refresh_crud_views']
|
|
37
|
+
|
|
38
|
+
def nb_vues_crud(self, obj):
|
|
39
|
+
"""Affiche le nombre de modèles avec des vues CRUD découvertes."""
|
|
40
|
+
if obj.crud_views_mapping:
|
|
41
|
+
return len(obj.crud_views_mapping)
|
|
42
|
+
return 0
|
|
43
|
+
nb_vues_crud.short_description = "Modèles CRUD"
|
|
44
|
+
|
|
45
|
+
def crud_views_mapping_display(self, obj):
|
|
46
|
+
"""Affiche le mapping des vues CRUD de manière lisible."""
|
|
47
|
+
import json
|
|
48
|
+
if obj.crud_views_mapping:
|
|
49
|
+
return json.dumps(obj.crud_views_mapping, indent=2, ensure_ascii=False)
|
|
50
|
+
return "Aucune vue découverte. Utilisez l'action 'Rafraîchir les vues CRUD'."
|
|
51
|
+
crud_views_mapping_display.short_description = "Vues CRUD découvertes"
|
|
52
|
+
|
|
53
|
+
def model_app_mapping_display(self, obj):
|
|
54
|
+
"""Affiche le mapping modèle -> app de manière lisible."""
|
|
55
|
+
import json
|
|
56
|
+
if obj.model_app_mapping:
|
|
57
|
+
return json.dumps(obj.model_app_mapping, indent=2, ensure_ascii=False)
|
|
58
|
+
return "Aucun mapping. Sera construit automatiquement."
|
|
59
|
+
model_app_mapping_display.short_description = "Mapping Modèle -> App"
|
|
60
|
+
|
|
61
|
+
@admin.action(description="Rafraîchir les vues CRUD découvertes")
|
|
62
|
+
def refresh_crud_views(self, request, queryset):
|
|
63
|
+
"""Action admin pour rafraîchir le mapping des vues CRUD."""
|
|
64
|
+
for config in queryset:
|
|
65
|
+
mapping = config.refresh_crud_views_mapping()
|
|
66
|
+
nb_models = len(mapping)
|
|
67
|
+
nb_views = sum(len(actions) for actions in mapping.values())
|
|
68
|
+
self.message_user(
|
|
69
|
+
request,
|
|
70
|
+
f"Mapping rafraîchi: {nb_models} modèles, {nb_views} vues découvertes."
|
|
71
|
+
)
|
|
@@ -55,6 +55,14 @@ class LucyAssistSettings:
|
|
|
55
55
|
"Comment modifier mon profil ?",
|
|
56
56
|
"Où trouver la liste des réservations ?",
|
|
57
57
|
],
|
|
58
|
+
|
|
59
|
+
# Chemin vers le module contenant set_current_user pour le ThreadLocal
|
|
60
|
+
# Ex: 'alyse.middleware.middleware' pour le projet Alyse
|
|
61
|
+
'THREAD_LOCAL_MODULE': None,
|
|
62
|
+
|
|
63
|
+
# Attributs de l'utilisateur à copier vers la requête
|
|
64
|
+
# Ex: ['franchise', 'tenant', 'organization']
|
|
65
|
+
'REQUEST_USER_ATTRS': [],
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
def __init__(self):
|
|
@@ -79,7 +79,27 @@ Utilisateur : "Modifie l'adresse du membre 42"
|
|
|
79
79
|
|
|
80
80
|
- Si l'utilisateur demande explicitement "comment faire" ou "explique-moi"
|
|
81
81
|
- Si tu n'as pas assez d'informations (dans ce cas, demande les infos manquantes)
|
|
82
|
-
|
|
82
|
+
|
|
83
|
+
## IMPORTANT - Procédure de suppression
|
|
84
|
+
|
|
85
|
+
Pour TOUTE demande de suppression, tu DOIS suivre cette procédure :
|
|
86
|
+
|
|
87
|
+
1. D'abord, utilise le tool `get_deletion_impact` pour analyser les conséquences
|
|
88
|
+
2. Affiche à l'utilisateur le résultat complet :
|
|
89
|
+
- L'objet qui sera supprimé
|
|
90
|
+
- TOUS les objets supprimés en cascade (avec leur type et nombre)
|
|
91
|
+
- Les champs qui seront mis à NULL
|
|
92
|
+
- Les éventuels blocages (objets protégés)
|
|
93
|
+
3. Demande une confirmation EXPLICITE à l'utilisateur ("Confirmez-vous la suppression ?")
|
|
94
|
+
4. SEULEMENT si l'utilisateur confirme explicitement (oui, ok, confirme, etc.), utilise `delete_object` avec `confirmed: true`
|
|
95
|
+
|
|
96
|
+
Exemple de réponse après get_deletion_impact :
|
|
97
|
+
"Voici les conséquences de la suppression du Client #42 :
|
|
98
|
+
- 5 Réservations seront supprimées en cascade
|
|
99
|
+
- 3 Paiements seront supprimés en cascade
|
|
100
|
+
- 2 Documents auront leur champ 'client' mis à NULL
|
|
101
|
+
|
|
102
|
+
**Confirmez-vous vouloir supprimer ce client et tous les éléments associés ?**"
|
|
83
103
|
|
|
84
104
|
## Contexte de la page actuelle
|
|
85
105
|
{page_context}
|
django_lucy_assist-1.0.7/lucy_assist/migrations/0003_configurationlucyassist_crud_views_mapping.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('lucy_assist', '0002_configurationlucyassist_prompt_complementaire'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='configurationlucyassist',
|
|
15
|
+
name='crud_views_mapping',
|
|
16
|
+
field=models.JSONField(blank=True, default=dict),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -26,6 +26,17 @@ class ConfigurationLucyAssist(LucyAssistBaseModel):
|
|
|
26
26
|
# Mapping des modèles vers leurs applications Django
|
|
27
27
|
model_app_mapping = models.JSONField(blank=True, default=dict)
|
|
28
28
|
|
|
29
|
+
# Mapping des vues CRUD découvertes automatiquement
|
|
30
|
+
# Structure: {
|
|
31
|
+
# "model_name": {
|
|
32
|
+
# "list": {"url_name": "app:model-list", "url": "/app/model/"},
|
|
33
|
+
# "create": {"url_name": "app:model-formulaire", "url": "/app/model/formulaire/"},
|
|
34
|
+
# "detail": {"url_name": "app:model-detail", "url": "/app/model/detail/<pk>/"},
|
|
35
|
+
# "delete": {"url_name": "app:model-suppression", "url": "/app/model/suppression/<pk>/"}
|
|
36
|
+
# }
|
|
37
|
+
# }
|
|
38
|
+
crud_views_mapping = models.JSONField(blank=True, default=dict)
|
|
39
|
+
|
|
29
40
|
class Meta:
|
|
30
41
|
verbose_name = "Configuration Lucy Assist"
|
|
31
42
|
verbose_name_plural = "Configuration Lucy Assist"
|
|
@@ -51,10 +62,53 @@ class ConfigurationLucyAssist(LucyAssistBaseModel):
|
|
|
51
62
|
def save(self, *args, **kwargs):
|
|
52
63
|
# Forcer l'ID à 1 pour le singleton
|
|
53
64
|
self.pk = 1
|
|
65
|
+
|
|
66
|
+
# Auto-découverte des vues CRUD si le mapping est vide
|
|
67
|
+
if not self.crud_views_mapping:
|
|
68
|
+
self.crud_views_mapping = self._discover_crud_views()
|
|
69
|
+
|
|
54
70
|
super().save(*args, **kwargs)
|
|
55
71
|
# Invalider le cache
|
|
56
72
|
cache.delete('lucy_assist_config')
|
|
57
73
|
|
|
74
|
+
def refresh_crud_views_mapping(self):
|
|
75
|
+
"""
|
|
76
|
+
Force la redécouverte des vues CRUD.
|
|
77
|
+
Utile après l'ajout de nouvelles vues dans le projet.
|
|
78
|
+
"""
|
|
79
|
+
self.crud_views_mapping = self._discover_crud_views()
|
|
80
|
+
self.save(update_fields=['crud_views_mapping'])
|
|
81
|
+
cache.delete('lucy_assist_config')
|
|
82
|
+
return self.crud_views_mapping
|
|
83
|
+
|
|
84
|
+
def _discover_crud_views(self) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
Découvre automatiquement les vues CRUD du projet
|
|
87
|
+
en parcourant les URL patterns.
|
|
88
|
+
"""
|
|
89
|
+
from lucy_assist.services.view_discovery_service import ViewDiscoveryService
|
|
90
|
+
service = ViewDiscoveryService()
|
|
91
|
+
return service.discover_crud_views()
|
|
92
|
+
|
|
93
|
+
def get_crud_view_for_model(self, model_name: str, action: str) -> dict:
|
|
94
|
+
"""
|
|
95
|
+
Retourne les infos de la vue CRUD pour un modèle et une action donnés.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
model_name: Nom du modèle (ex: 'Client', 'Reservation')
|
|
99
|
+
action: Type d'action ('list', 'create', 'detail', 'update', 'delete')
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict avec 'url_name', 'url', 'method' ou None si non trouvé
|
|
103
|
+
"""
|
|
104
|
+
if not self.crud_views_mapping:
|
|
105
|
+
self.crud_views_mapping = self._discover_crud_views()
|
|
106
|
+
self.save(update_fields=['crud_views_mapping'])
|
|
107
|
+
|
|
108
|
+
model_lower = model_name.lower()
|
|
109
|
+
model_views = self.crud_views_mapping.get(model_lower, {})
|
|
110
|
+
return model_views.get(action)
|
|
111
|
+
|
|
58
112
|
@property
|
|
59
113
|
def tokens_restants_en_euros(self):
|
|
60
114
|
"""Retourne la valeur en euros des tokens restants."""
|
{django_lucy_assist-1.0.5 → django_lucy_assist-1.0.7}/lucy_assist/services/context_service.py
RENAMED
|
@@ -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,80 @@ 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 = [] # DateField (comparaison directe)
|
|
338
|
+
datetime_fields = [] # DateTimeField (utilise __date)
|
|
301
339
|
for field in model._meta.get_fields():
|
|
302
340
|
if hasattr(field, 'get_internal_type'):
|
|
303
|
-
|
|
341
|
+
field_type = field.get_internal_type()
|
|
342
|
+
if field_type in ['CharField', 'TextField']:
|
|
304
343
|
search_fields.append(field.name)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
344
|
+
elif field_type == 'DateField':
|
|
345
|
+
date_fields.append(field.name)
|
|
346
|
+
elif field_type == 'DateTimeField':
|
|
347
|
+
datetime_fields.append(field.name)
|
|
309
348
|
|
|
310
349
|
# Construire la requête
|
|
311
350
|
from django.db.models import Q
|
|
312
|
-
q_objects =
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
q_objects = None
|
|
352
|
+
has_criteria = False
|
|
353
|
+
|
|
354
|
+
# Si on a une date, chercher dans les champs date
|
|
355
|
+
if search_date and (date_fields or datetime_fields):
|
|
356
|
+
date_q = Q()
|
|
357
|
+
search_date_only = search_date.date()
|
|
358
|
+
|
|
359
|
+
# DateField: comparaison directe
|
|
360
|
+
for field_name in date_fields:
|
|
361
|
+
date_q |= Q(**{field_name: search_date_only})
|
|
362
|
+
|
|
363
|
+
# DateTimeField: utiliser __date
|
|
364
|
+
for field_name in datetime_fields:
|
|
365
|
+
date_q |= Q(**{f'{field_name}__date': search_date_only})
|
|
366
|
+
|
|
367
|
+
q_objects = date_q
|
|
368
|
+
has_criteria = True
|
|
369
|
+
LogUtils.info(f"[search_objects] {model.__name__}: recherche date dans DateField={date_fields}, DateTimeField={datetime_fields}")
|
|
370
|
+
|
|
371
|
+
# Si on a du texte, chercher dans les champs texte
|
|
372
|
+
if text_query.strip() and search_fields:
|
|
373
|
+
query_words = text_query.strip().split()
|
|
374
|
+
text_q = Q()
|
|
375
|
+
|
|
376
|
+
if len(query_words) > 1:
|
|
377
|
+
# Recherche multi-mots: chaque mot doit matcher au moins un champ
|
|
378
|
+
for word in query_words:
|
|
379
|
+
word_q = Q()
|
|
380
|
+
for field_name in search_fields[:5]:
|
|
381
|
+
word_q |= Q(**{f'{field_name}__icontains': word})
|
|
382
|
+
text_q &= word_q
|
|
383
|
+
else:
|
|
384
|
+
# Recherche simple
|
|
385
|
+
for field_name in search_fields[:5]:
|
|
386
|
+
text_q |= Q(**{f'{field_name}__icontains': text_query})
|
|
387
|
+
|
|
388
|
+
if q_objects is not None:
|
|
389
|
+
q_objects &= text_q
|
|
390
|
+
else:
|
|
391
|
+
q_objects = text_q
|
|
392
|
+
has_criteria = True
|
|
393
|
+
|
|
394
|
+
# Si pas de critère de recherche valide, skip
|
|
395
|
+
if not has_criteria:
|
|
396
|
+
if not search_fields and not date_fields and not datetime_fields:
|
|
397
|
+
LogUtils.info(f"[search_objects] {model.__name__}: aucun champ recherchable")
|
|
398
|
+
continue
|
|
315
399
|
|
|
316
400
|
# Filtrer par permissions si possible
|
|
317
401
|
# Note: certains modèles utilisent des managers customs qui
|