django-lucy-assist 0.1.0__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-0.1.0.dist-info/METADATA +206 -0
- django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
- django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
- django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
- lucy_assist/__init__.py +11 -0
- lucy_assist/admin.py +22 -0
- lucy_assist/apps.py +10 -0
- lucy_assist/conf.py +103 -0
- lucy_assist/constantes.py +120 -0
- lucy_assist/context_processors.py +65 -0
- lucy_assist/migrations/0001_initial.py +92 -0
- lucy_assist/migrations/__init__.py +0 -0
- lucy_assist/models/__init__.py +14 -0
- lucy_assist/models/base.py +54 -0
- lucy_assist/models/configuration.py +175 -0
- lucy_assist/models/conversation.py +54 -0
- lucy_assist/models/message.py +45 -0
- lucy_assist/models/project_context_cache.py +213 -0
- lucy_assist/services/__init__.py +21 -0
- lucy_assist/services/bug_notification_service.py +183 -0
- lucy_assist/services/claude_service.py +417 -0
- lucy_assist/services/context_service.py +350 -0
- lucy_assist/services/crud_service.py +364 -0
- lucy_assist/services/gitlab_service.py +248 -0
- lucy_assist/services/project_context_service.py +412 -0
- lucy_assist/services/tool_executor_service.py +343 -0
- lucy_assist/services/tools_definition.py +229 -0
- lucy_assist/signals.py +25 -0
- lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
- lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
- lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
- lucy_assist/tests/__init__.py +0 -0
- lucy_assist/tests/factories/__init__.py +15 -0
- lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
- lucy_assist/tests/test_lucy_assist.py +186 -0
- lucy_assist/urls.py +36 -0
- lucy_assist/utils/__init__.py +7 -0
- lucy_assist/utils/log_utils.py +59 -0
- lucy_assist/utils/message_utils.py +130 -0
- lucy_assist/utils/token_utils.py +87 -0
- lucy_assist/views/__init__.py +13 -0
- lucy_assist/views/api_views.py +595 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Generated migration for lucy_assist
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
operations = [
|
|
17
|
+
migrations.CreateModel(
|
|
18
|
+
name='ConfigurationLucyAssist',
|
|
19
|
+
fields=[
|
|
20
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
21
|
+
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
22
|
+
('updated_date', models.DateTimeField(auto_now=True, null=True, blank=True)),
|
|
23
|
+
('is_active', models.BooleanField(default=True)),
|
|
24
|
+
('tokens_disponibles', models.BigIntegerField(default=0)),
|
|
25
|
+
('prix_par_million_tokens', models.DecimalField(decimal_places=2, default=10.0, max_digits=10)),
|
|
26
|
+
('questions_frequentes', models.JSONField(blank=True, default=list)),
|
|
27
|
+
('actif', models.BooleanField(default=True)),
|
|
28
|
+
('avatar', models.ImageField(blank=True, null=True, upload_to='')),
|
|
29
|
+
('model_app_mapping', models.JSONField(blank=True, default=dict)),
|
|
30
|
+
],
|
|
31
|
+
options={
|
|
32
|
+
'verbose_name': 'Configuration Lucy Assist',
|
|
33
|
+
'verbose_name_plural': 'Configuration Lucy Assist',
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
migrations.CreateModel(
|
|
37
|
+
name='Conversation',
|
|
38
|
+
fields=[
|
|
39
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
40
|
+
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
41
|
+
('updated_date', models.DateTimeField(auto_now=True, null=True, blank=True)),
|
|
42
|
+
('is_active', models.BooleanField(default=True)),
|
|
43
|
+
('titre', models.CharField(blank=True, max_length=255, null=True)),
|
|
44
|
+
('page_contexte', models.CharField(blank=True, max_length=500, null=True)),
|
|
45
|
+
('utilisateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lucy_assist_conversations', to=settings.AUTH_USER_MODEL)),
|
|
46
|
+
],
|
|
47
|
+
options={
|
|
48
|
+
'verbose_name': 'Conversation Lucy Assist',
|
|
49
|
+
'verbose_name_plural': 'Conversations Lucy Assist',
|
|
50
|
+
'ordering': ['-created_date'],
|
|
51
|
+
},
|
|
52
|
+
),
|
|
53
|
+
migrations.CreateModel(
|
|
54
|
+
name='Message',
|
|
55
|
+
fields=[
|
|
56
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
57
|
+
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
58
|
+
('updated_date', models.DateTimeField(auto_now=True, null=True, blank=True)),
|
|
59
|
+
('is_active', models.BooleanField(default=True)),
|
|
60
|
+
('repondant', models.CharField(choices=[('UTILISATEUR', 'Utilisateur'), ('CHATBOT', 'Lucy Assist')], max_length=20)),
|
|
61
|
+
('contenu', models.TextField()),
|
|
62
|
+
('tokens_utilises', models.IntegerField(default=0)),
|
|
63
|
+
('type_action', models.CharField(blank=True, choices=[('AIDE_NAVIGATION', 'Aide à la navigation'), ('RECHERCHE', "Recherche d'objets"), ('CRUD', "Création/Modification/Suppresion d'objets"), ('ANALYSE_BUG', 'Analyse de bug'), ('EXPLICATION', 'Explication de fonctionnalité')], max_length=50, null=True)),
|
|
64
|
+
('metadata', models.JSONField(blank=True, default=dict)),
|
|
65
|
+
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='lucy_assist.conversation')),
|
|
66
|
+
],
|
|
67
|
+
options={
|
|
68
|
+
'verbose_name': 'Message Lucy Assist',
|
|
69
|
+
'verbose_name_plural': 'Messages Lucy Assist',
|
|
70
|
+
'ordering': ['created_date'],
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
migrations.CreateModel(
|
|
74
|
+
name='ProjectContextCache',
|
|
75
|
+
fields=[
|
|
76
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
77
|
+
('created_date', models.DateTimeField(auto_now_add=True)),
|
|
78
|
+
('updated_date', models.DateTimeField(auto_now=True, null=True, blank=True)),
|
|
79
|
+
('is_active', models.BooleanField(default=True)),
|
|
80
|
+
('cache_key', models.CharField(db_index=True, max_length=255, unique=True)),
|
|
81
|
+
('contenu', models.JSONField(default=dict)),
|
|
82
|
+
('content_hash', models.CharField(blank=True, max_length=64)),
|
|
83
|
+
('expire_at', models.DateTimeField()),
|
|
84
|
+
('tokens_economises', models.BigIntegerField(default=0)),
|
|
85
|
+
('hit_count', models.IntegerField(default=0)),
|
|
86
|
+
],
|
|
87
|
+
options={
|
|
88
|
+
'verbose_name': 'Cache contexte projet',
|
|
89
|
+
'verbose_name_plural': 'Caches contexte projet',
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .base import LucyAssistBaseModel
|
|
2
|
+
from .conversation import Conversation
|
|
3
|
+
from .message import Message
|
|
4
|
+
from .configuration import ConfigurationLucyAssist, get_default_model_app_mapping
|
|
5
|
+
from .project_context_cache import ProjectContextCache
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'LucyAssistBaseModel',
|
|
9
|
+
'Conversation',
|
|
10
|
+
'Message',
|
|
11
|
+
'ConfigurationLucyAssist',
|
|
12
|
+
'get_default_model_app_mapping',
|
|
13
|
+
'ProjectContextCache',
|
|
14
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modèle de base pour Lucy Assist.
|
|
3
|
+
Utilise le modèle de base configuré dans les settings ou un modèle par défaut.
|
|
4
|
+
"""
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
from lucy_assist.conf import lucy_assist_settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_base_model():
|
|
11
|
+
"""
|
|
12
|
+
Retourne le modèle de base à utiliser pour les modèles Lucy Assist.
|
|
13
|
+
Permet de personnaliser le modèle de base via les settings Django.
|
|
14
|
+
"""
|
|
15
|
+
base_model = lucy_assist_settings.BASE_MODEL
|
|
16
|
+
|
|
17
|
+
if base_model is None:
|
|
18
|
+
# Utiliser un modèle par défaut simple
|
|
19
|
+
return LucyAssistBaseModel
|
|
20
|
+
|
|
21
|
+
if isinstance(base_model, str):
|
|
22
|
+
# Importer le modèle depuis le chemin string
|
|
23
|
+
from django.utils.module_loading import import_string
|
|
24
|
+
return import_string(base_model)
|
|
25
|
+
|
|
26
|
+
return base_model
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LucyAssistBaseModel(models.Model):
|
|
30
|
+
"""
|
|
31
|
+
Modèle de base par défaut pour Lucy Assist.
|
|
32
|
+
Fournit les champs minimaux nécessaires pour le tracking.
|
|
33
|
+
"""
|
|
34
|
+
created_date = models.DateTimeField(auto_now_add=True)
|
|
35
|
+
updated_date = models.DateTimeField(auto_now=True, null=True, blank=True)
|
|
36
|
+
is_active = models.BooleanField(default=True)
|
|
37
|
+
|
|
38
|
+
class Meta:
|
|
39
|
+
abstract = True
|
|
40
|
+
ordering = ["-created_date"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Cache pour le modèle de base résolu
|
|
44
|
+
_resolved_base_model = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_lucy_assist_model_base():
|
|
48
|
+
"""
|
|
49
|
+
Retourne le modèle de base résolu (avec cache).
|
|
50
|
+
"""
|
|
51
|
+
global _resolved_base_model
|
|
52
|
+
if _resolved_base_model is None:
|
|
53
|
+
_resolved_base_model = get_base_model()
|
|
54
|
+
return _resolved_base_model
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.core.cache import cache
|
|
3
|
+
|
|
4
|
+
from lucy_assist.models.base import LucyAssistBaseModel
|
|
5
|
+
from lucy_assist.conf import lucy_assist_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigurationLucyAssist(LucyAssistBaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Configuration singleton pour Lucy Assist.
|
|
11
|
+
Gère les tokens disponibles et les paramètres globaux.
|
|
12
|
+
"""
|
|
13
|
+
tokens_disponibles = models.BigIntegerField(default=0)
|
|
14
|
+
prix_par_million_tokens = models.DecimalField(
|
|
15
|
+
max_digits=10,
|
|
16
|
+
decimal_places=2,
|
|
17
|
+
default=lucy_assist_settings.PRIX_PAR_MILLION_TOKENS
|
|
18
|
+
)
|
|
19
|
+
questions_frequentes = models.JSONField(default=list, blank=True)
|
|
20
|
+
actif = models.BooleanField(default=True)
|
|
21
|
+
avatar = models.ImageField(null=True, blank=True)
|
|
22
|
+
|
|
23
|
+
# Mapping des modèles vers leurs applications Django
|
|
24
|
+
model_app_mapping = models.JSONField(blank=True, default=dict)
|
|
25
|
+
|
|
26
|
+
class Meta:
|
|
27
|
+
verbose_name = "Configuration Lucy Assist"
|
|
28
|
+
verbose_name_plural = "Configuration Lucy Assist"
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return f"Configuration Lucy Assist - {self.tokens_disponibles:,} tokens"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_config(cls):
|
|
35
|
+
"""
|
|
36
|
+
Retourne la configuration singleton.
|
|
37
|
+
Utilise le cache pour optimiser les performances.
|
|
38
|
+
"""
|
|
39
|
+
cache_key = 'lucy_assist_config'
|
|
40
|
+
config = cache.get(cache_key)
|
|
41
|
+
|
|
42
|
+
if config is None:
|
|
43
|
+
config, _ = cls.objects.get_or_create(pk=1)
|
|
44
|
+
cache.set(cache_key, config, timeout=300) # Cache 5 minutes
|
|
45
|
+
|
|
46
|
+
return config
|
|
47
|
+
|
|
48
|
+
def save(self, *args, **kwargs):
|
|
49
|
+
# Forcer l'ID à 1 pour le singleton
|
|
50
|
+
self.pk = 1
|
|
51
|
+
super().save(*args, **kwargs)
|
|
52
|
+
# Invalider le cache
|
|
53
|
+
cache.delete('lucy_assist_config')
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def tokens_restants_en_euros(self):
|
|
57
|
+
"""Retourne la valeur en euros des tokens restants."""
|
|
58
|
+
return (self.tokens_disponibles / 1_000_000) * float(self.prix_par_million_tokens)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def conversations_estimees(self):
|
|
62
|
+
"""Estime le nombre de conversations possibles avec les tokens restants."""
|
|
63
|
+
return int(self.tokens_disponibles / lucy_assist_settings.TOKENS_MOYENS_PAR_CONVERSATION)
|
|
64
|
+
|
|
65
|
+
def ajouter_tokens(self, montant_euros):
|
|
66
|
+
"""
|
|
67
|
+
Ajoute des tokens en fonction d'un montant en euros.
|
|
68
|
+
Retourne le nombre de tokens ajoutés.
|
|
69
|
+
"""
|
|
70
|
+
tokens_a_ajouter = int((montant_euros / float(self.prix_par_million_tokens)) * 1_000_000)
|
|
71
|
+
self.tokens_disponibles += tokens_a_ajouter
|
|
72
|
+
self.save(update_fields=['tokens_disponibles'])
|
|
73
|
+
return tokens_a_ajouter
|
|
74
|
+
|
|
75
|
+
def a_suffisamment_tokens(self, tokens_requis=2000):
|
|
76
|
+
"""Vérifie si suffisamment de tokens sont disponibles."""
|
|
77
|
+
return self.tokens_disponibles >= tokens_requis
|
|
78
|
+
|
|
79
|
+
def get_questions_frequentes(self):
|
|
80
|
+
"""
|
|
81
|
+
Retourne les questions fréquentes configurées.
|
|
82
|
+
Si aucune n'est configurée, retourne les questions par défaut.
|
|
83
|
+
"""
|
|
84
|
+
if self.questions_frequentes:
|
|
85
|
+
return self.questions_frequentes
|
|
86
|
+
|
|
87
|
+
# Questions par défaut si aucune n'est configurée
|
|
88
|
+
return lucy_assist_settings.QUESTIONS_FREQUENTES_DEFAULT
|
|
89
|
+
|
|
90
|
+
def get_app_for_model(self, model_name: str) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Retourne le nom de l'application Django pour un modèle donné.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
model_name: Nom du modèle (insensible à la casse)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Nom de l'application Django, ou le nom du modèle en minuscules si non trouvé
|
|
99
|
+
"""
|
|
100
|
+
# Si le mapping est vide, le construire dynamiquement depuis les modèles Django
|
|
101
|
+
if not self.model_app_mapping:
|
|
102
|
+
self.model_app_mapping = self._build_model_app_mapping()
|
|
103
|
+
self.save(update_fields=['model_app_mapping'])
|
|
104
|
+
# Invalider le cache pour que les prochains appels utilisent la nouvelle valeur
|
|
105
|
+
cache.delete('lucy_assist_config')
|
|
106
|
+
|
|
107
|
+
model_lower = model_name.lower()
|
|
108
|
+
return self.model_app_mapping.get(model_lower, model_lower)
|
|
109
|
+
|
|
110
|
+
def _build_model_app_mapping(self) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Construit dynamiquement le mapping modèle -> application
|
|
113
|
+
en parcourant tous les modèles Django enregistrés.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dict avec les noms de modèles en minuscules comme clés
|
|
117
|
+
et les noms d'applications comme valeurs.
|
|
118
|
+
"""
|
|
119
|
+
from django.apps import apps
|
|
120
|
+
|
|
121
|
+
mapping = {}
|
|
122
|
+
apps_prefix = lucy_assist_settings.PROJECT_APPS_PREFIX
|
|
123
|
+
|
|
124
|
+
for app_config in apps.get_app_configs():
|
|
125
|
+
app_label = app_config.label
|
|
126
|
+
|
|
127
|
+
# Filtrer les apps si un préfixe est configuré
|
|
128
|
+
if apps_prefix and not app_config.name.startswith(apps_prefix):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Utiliser le label de l'app
|
|
132
|
+
app_name = app_label
|
|
133
|
+
|
|
134
|
+
for model in app_config.get_models():
|
|
135
|
+
model_name_lower = model.__name__.lower()
|
|
136
|
+
# Ne pas écraser si déjà présent (priorité au premier trouvé)
|
|
137
|
+
if model_name_lower not in mapping:
|
|
138
|
+
mapping[model_name_lower] = app_name
|
|
139
|
+
|
|
140
|
+
return mapping
|
|
141
|
+
|
|
142
|
+
def update_model_mapping(self, model_name: str, app_name: str) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Met à jour le mapping pour un modèle spécifique.
|
|
145
|
+
Utilisé quand le chatbot découvre un nouveau mapping.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
model_name: Nom du modèle
|
|
149
|
+
app_name: Nom de l'application Django
|
|
150
|
+
"""
|
|
151
|
+
if not self.model_app_mapping:
|
|
152
|
+
self.model_app_mapping = {}
|
|
153
|
+
|
|
154
|
+
model_lower = model_name.lower()
|
|
155
|
+
self.model_app_mapping[model_lower] = app_name
|
|
156
|
+
self.save(update_fields=['model_app_mapping'])
|
|
157
|
+
cache.delete('lucy_assist_config')
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def get_app_for_model_static(cls, model_name: str) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Version statique pour récupérer l'app d'un modèle.
|
|
163
|
+
Utilise la config en cache.
|
|
164
|
+
"""
|
|
165
|
+
config = cls.get_config()
|
|
166
|
+
return config.get_app_for_model(model_name)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_default_model_app_mapping() -> dict:
|
|
170
|
+
"""
|
|
171
|
+
Fonction utilisée par la migration pour la valeur par défaut.
|
|
172
|
+
Retourne un dict vide car le mapping est maintenant construit
|
|
173
|
+
dynamiquement lors du premier accès.
|
|
174
|
+
"""
|
|
175
|
+
return {}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
from lucy_assist.models.base import LucyAssistBaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Conversation(LucyAssistBaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Modèle représentant une conversation entre un utilisateur et Lucy Assist.
|
|
10
|
+
"""
|
|
11
|
+
titre = models.CharField(max_length=255, blank=True, null=True)
|
|
12
|
+
page_contexte = models.CharField(max_length=500, blank=True, null=True)
|
|
13
|
+
|
|
14
|
+
# ForeignKey
|
|
15
|
+
utilisateur = models.ForeignKey(
|
|
16
|
+
settings.AUTH_USER_MODEL,
|
|
17
|
+
on_delete=models.CASCADE,
|
|
18
|
+
related_name='lucy_assist_conversations'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Meta:
|
|
22
|
+
verbose_name = "Conversation Lucy Assist"
|
|
23
|
+
verbose_name_plural = "Conversations Lucy Assist"
|
|
24
|
+
ordering = ['-created_date']
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
user_str = getattr(self.utilisateur, 'email', str(self.utilisateur))
|
|
28
|
+
return f"Conversation #{self.pk} - {user_str}"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def total_tokens(self):
|
|
32
|
+
"""Retourne le nombre total de tokens utilisés dans cette conversation."""
|
|
33
|
+
return self.messages.aggregate(
|
|
34
|
+
total=models.Sum('tokens_utilises')
|
|
35
|
+
)['total'] or 0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def dernier_message(self):
|
|
39
|
+
"""Retourne le dernier message de la conversation."""
|
|
40
|
+
return self.messages.order_by('-created_date').first()
|
|
41
|
+
|
|
42
|
+
def generer_titre(self):
|
|
43
|
+
"""Génère un titre automatique basé sur le premier message utilisateur."""
|
|
44
|
+
premier_message = self.messages.filter(
|
|
45
|
+
repondant='UTILISATEUR'
|
|
46
|
+
).order_by('created_date').first()
|
|
47
|
+
|
|
48
|
+
if premier_message:
|
|
49
|
+
# Tronquer le message pour créer un titre
|
|
50
|
+
contenu = premier_message.contenu[:50]
|
|
51
|
+
if len(premier_message.contenu) > 50:
|
|
52
|
+
contenu += "..."
|
|
53
|
+
self.titre = contenu
|
|
54
|
+
self.save(update_fields=['titre'])
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from lucy_assist.models.base import LucyAssistBaseModel
|
|
4
|
+
from lucy_assist.constantes import LucyAssistConstantes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Message(LucyAssistBaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Modèle représentant un message dans une conversation Lucy Assist.
|
|
10
|
+
"""
|
|
11
|
+
repondant = models.CharField(max_length=20, choices=LucyAssistConstantes.Repondant.tuples)
|
|
12
|
+
contenu = models.TextField()
|
|
13
|
+
tokens_utilises = models.IntegerField(default=0)
|
|
14
|
+
type_action = models.CharField(
|
|
15
|
+
max_length=50,
|
|
16
|
+
choices=LucyAssistConstantes.TypeAction.tuples,
|
|
17
|
+
blank=True,
|
|
18
|
+
null=True
|
|
19
|
+
)
|
|
20
|
+
metadata = models.JSONField(default=dict, blank=True) # Données supplémentaires (contexte, erreurs, etc.)
|
|
21
|
+
|
|
22
|
+
# ForeignKey
|
|
23
|
+
conversation = models.ForeignKey(
|
|
24
|
+
'lucy_assist.Conversation',
|
|
25
|
+
on_delete=models.CASCADE,
|
|
26
|
+
related_name='messages'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class Meta:
|
|
30
|
+
verbose_name = "Message Lucy Assist"
|
|
31
|
+
verbose_name_plural = "Messages Lucy Assist"
|
|
32
|
+
ordering = ['created_date']
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
return f"Message #{self.pk} - {self.get_repondant_display()}"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def est_chatbot(self):
|
|
39
|
+
"""Retourne True si le message vient du chatbot."""
|
|
40
|
+
return self.repondant == LucyAssistConstantes.Repondant.CHATBOT
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def est_utilisateur(self):
|
|
44
|
+
"""Retourne True si le message vient de l'utilisateur."""
|
|
45
|
+
return self.repondant == LucyAssistConstantes.Repondant.UTILISATEUR
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache du contexte projet pour optimiser les tokens Claude.
|
|
3
|
+
"""
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.core.cache import cache
|
|
11
|
+
from django.utils import timezone
|
|
12
|
+
|
|
13
|
+
from lucy_assist.models.base import LucyAssistBaseModel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProjectContextCache(LucyAssistBaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Cache du contexte projet analysé.
|
|
19
|
+
|
|
20
|
+
Stocke les informations sur la structure du projet pour éviter
|
|
21
|
+
de re-analyser GitLab à chaque requête Claude.
|
|
22
|
+
"""
|
|
23
|
+
cache_key = models.CharField(
|
|
24
|
+
max_length=255,
|
|
25
|
+
unique=True,
|
|
26
|
+
db_index=True
|
|
27
|
+
) # Clé unique pour identifier le type de contexte
|
|
28
|
+
contenu = models.JSONField(default=dict) # Contenu mis en cache (JSON)
|
|
29
|
+
content_hash = models.CharField(max_length=64, blank=True) # Hash du contenu source pour détecter les changements
|
|
30
|
+
expire_at = models.DateTimeField()
|
|
31
|
+
tokens_economises = models.BigIntegerField(default=0) # Nombre de tokens économisés grâce au cache
|
|
32
|
+
hit_count = models.IntegerField(default=0) # Nombre d'utilisations
|
|
33
|
+
|
|
34
|
+
class Meta:
|
|
35
|
+
verbose_name = "Cache contexte projet"
|
|
36
|
+
verbose_name_plural = "Caches contexte projet"
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return f"Cache: {self.cache_key}"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def is_expired(self) -> bool:
|
|
43
|
+
"""Vérifie si le cache est expiré."""
|
|
44
|
+
return timezone.now() > self.expire_at
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def is_valid(self) -> bool:
|
|
48
|
+
"""Vérifie si le cache est valide (non expiré)."""
|
|
49
|
+
return not self.is_expired
|
|
50
|
+
|
|
51
|
+
def increment_hit(self, tokens_saved: int = 0):
|
|
52
|
+
"""Incrémente le compteur d'utilisation."""
|
|
53
|
+
self.hit_count += 1
|
|
54
|
+
self.tokens_economises += tokens_saved
|
|
55
|
+
self.save(update_fields=['hit_count', 'tokens_economises'])
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_or_create_cache(
|
|
59
|
+
cls,
|
|
60
|
+
cache_key: str,
|
|
61
|
+
ttl_hours: int = 24
|
|
62
|
+
) -> 'ProjectContextCache':
|
|
63
|
+
"""
|
|
64
|
+
Récupère ou crée un cache.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
cache_key: Clé unique du cache
|
|
68
|
+
ttl_hours: Durée de vie en heures
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Instance de ProjectContextCache
|
|
72
|
+
"""
|
|
73
|
+
expire_at = timezone.now() + timedelta(hours=ttl_hours)
|
|
74
|
+
|
|
75
|
+
cache_obj, created = cls.objects.get_or_create(
|
|
76
|
+
cache_key=cache_key,
|
|
77
|
+
defaults={
|
|
78
|
+
'expire_at': expire_at,
|
|
79
|
+
'contenu': {}
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Renouveler si expiré
|
|
84
|
+
if cache_obj.is_expired:
|
|
85
|
+
cache_obj.expire_at = expire_at
|
|
86
|
+
cache_obj.contenu = {}
|
|
87
|
+
cache_obj.content_hash = ''
|
|
88
|
+
cache_obj.save()
|
|
89
|
+
|
|
90
|
+
return cache_obj
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_cached_content(
|
|
94
|
+
cls,
|
|
95
|
+
cache_key: str,
|
|
96
|
+
content_hash: Optional[str] = None
|
|
97
|
+
) -> Optional[Dict]:
|
|
98
|
+
"""
|
|
99
|
+
Récupère le contenu mis en cache si valide.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
cache_key: Clé du cache
|
|
103
|
+
content_hash: Hash pour vérifier si le contenu a changé
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Contenu caché ou None si invalide/expiré
|
|
107
|
+
"""
|
|
108
|
+
# D'abord vérifier le cache Django (plus rapide)
|
|
109
|
+
django_cache_key = f"lucy_assist_context_{cache_key}"
|
|
110
|
+
cached = cache.get(django_cache_key)
|
|
111
|
+
if cached:
|
|
112
|
+
return cached
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
cache_obj = cls.objects.get(cache_key=cache_key)
|
|
116
|
+
|
|
117
|
+
# Vérifier l'expiration
|
|
118
|
+
if cache_obj.is_expired:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
# Vérifier le hash si fourni
|
|
122
|
+
if content_hash and cache_obj.content_hash != content_hash:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Mettre en cache Django pour accès rapide
|
|
126
|
+
cache.set(django_cache_key, cache_obj.contenu, timeout=3600)
|
|
127
|
+
|
|
128
|
+
return cache_obj.contenu
|
|
129
|
+
|
|
130
|
+
except cls.DoesNotExist:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def set_cached_content(
|
|
135
|
+
cls,
|
|
136
|
+
cache_key: str,
|
|
137
|
+
contenu: Dict,
|
|
138
|
+
content_hash: str = '',
|
|
139
|
+
ttl_hours: int = 24
|
|
140
|
+
) -> 'ProjectContextCache':
|
|
141
|
+
"""
|
|
142
|
+
Stocke le contenu en cache.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
cache_key: Clé du cache
|
|
146
|
+
contenu: Contenu à cacher
|
|
147
|
+
content_hash: Hash du contenu source
|
|
148
|
+
ttl_hours: Durée de vie en heures
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Instance de ProjectContextCache
|
|
152
|
+
"""
|
|
153
|
+
expire_at = timezone.now() + timedelta(hours=ttl_hours)
|
|
154
|
+
|
|
155
|
+
cache_obj, _ = cls.objects.update_or_create(
|
|
156
|
+
cache_key=cache_key,
|
|
157
|
+
defaults={
|
|
158
|
+
'contenu': contenu,
|
|
159
|
+
'content_hash': content_hash,
|
|
160
|
+
'expire_at': expire_at
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Mettre aussi en cache Django
|
|
165
|
+
django_cache_key = f"lucy_assist_context_{cache_key}"
|
|
166
|
+
cache.set(django_cache_key, contenu, timeout=ttl_hours * 3600)
|
|
167
|
+
|
|
168
|
+
return cache_obj
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def compute_hash(content: Any) -> str:
|
|
172
|
+
"""Calcule le hash d'un contenu."""
|
|
173
|
+
if isinstance(content, dict) or isinstance(content, list):
|
|
174
|
+
content = json.dumps(content, sort_keys=True)
|
|
175
|
+
return hashlib.sha256(str(content).encode()).hexdigest()[:16]
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def invalidate_cache(cls, cache_key: str):
|
|
179
|
+
"""Invalide un cache spécifique."""
|
|
180
|
+
# Supprimer du cache Django
|
|
181
|
+
django_cache_key = f"lucy_assist_context_{cache_key}"
|
|
182
|
+
cache.delete(django_cache_key)
|
|
183
|
+
|
|
184
|
+
# Supprimer de la BDD
|
|
185
|
+
cls.objects.filter(cache_key=cache_key).delete()
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def invalidate_all(cls):
|
|
189
|
+
"""Invalide tous les caches Lucy Assist."""
|
|
190
|
+
# Supprimer tous les caches (la suppression du cache Django pattern n'est pas disponible)
|
|
191
|
+
cls.objects.all().delete()
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def get_stats(cls) -> Dict:
|
|
195
|
+
"""Retourne les statistiques des caches."""
|
|
196
|
+
from django.db.models import Sum, Count
|
|
197
|
+
|
|
198
|
+
stats = cls.objects.aggregate(
|
|
199
|
+
total_caches=Count('id'),
|
|
200
|
+
total_hits=Sum('hit_count'),
|
|
201
|
+
total_tokens_saved=Sum('tokens_economises')
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
active_caches = cls.objects.filter(
|
|
205
|
+
expire_at__gt=timezone.now()
|
|
206
|
+
).count()
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
'total_caches': stats['total_caches'] or 0,
|
|
210
|
+
'active_caches': active_caches,
|
|
211
|
+
'total_hits': stats['total_hits'] or 0,
|
|
212
|
+
'total_tokens_saved': stats['total_tokens_saved'] or 0
|
|
213
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .claude_service import ClaudeService
|
|
2
|
+
from .gitlab_service import GitLabService
|
|
3
|
+
from .context_service import ContextService
|
|
4
|
+
from .crud_service import CRUDService
|
|
5
|
+
from .project_context_service import ProjectContextService
|
|
6
|
+
from .tool_executor_service import ToolExecutorService, create_tool_executor
|
|
7
|
+
from .tools_definition import LUCY_ASSIST_TOOLS, get_app_for_model
|
|
8
|
+
from .bug_notification_service import BugNotificationService
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'ClaudeService',
|
|
12
|
+
'GitLabService',
|
|
13
|
+
'ContextService',
|
|
14
|
+
'CRUDService',
|
|
15
|
+
'ProjectContextService',
|
|
16
|
+
'ToolExecutorService',
|
|
17
|
+
'create_tool_executor',
|
|
18
|
+
'LUCY_ASSIST_TOOLS',
|
|
19
|
+
'get_app_for_model',
|
|
20
|
+
'BugNotificationService',
|
|
21
|
+
]
|