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.
Files changed (44) hide show
  1. django_lucy_assist-0.1.0.dist-info/METADATA +206 -0
  2. django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
  3. django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
  4. django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
  5. lucy_assist/__init__.py +11 -0
  6. lucy_assist/admin.py +22 -0
  7. lucy_assist/apps.py +10 -0
  8. lucy_assist/conf.py +103 -0
  9. lucy_assist/constantes.py +120 -0
  10. lucy_assist/context_processors.py +65 -0
  11. lucy_assist/migrations/0001_initial.py +92 -0
  12. lucy_assist/migrations/__init__.py +0 -0
  13. lucy_assist/models/__init__.py +14 -0
  14. lucy_assist/models/base.py +54 -0
  15. lucy_assist/models/configuration.py +175 -0
  16. lucy_assist/models/conversation.py +54 -0
  17. lucy_assist/models/message.py +45 -0
  18. lucy_assist/models/project_context_cache.py +213 -0
  19. lucy_assist/services/__init__.py +21 -0
  20. lucy_assist/services/bug_notification_service.py +183 -0
  21. lucy_assist/services/claude_service.py +417 -0
  22. lucy_assist/services/context_service.py +350 -0
  23. lucy_assist/services/crud_service.py +364 -0
  24. lucy_assist/services/gitlab_service.py +248 -0
  25. lucy_assist/services/project_context_service.py +412 -0
  26. lucy_assist/services/tool_executor_service.py +343 -0
  27. lucy_assist/services/tools_definition.py +229 -0
  28. lucy_assist/signals.py +25 -0
  29. lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
  30. lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
  31. lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
  32. lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
  33. lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
  34. lucy_assist/tests/__init__.py +0 -0
  35. lucy_assist/tests/factories/__init__.py +15 -0
  36. lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
  37. lucy_assist/tests/test_lucy_assist.py +186 -0
  38. lucy_assist/urls.py +36 -0
  39. lucy_assist/utils/__init__.py +7 -0
  40. lucy_assist/utils/log_utils.py +59 -0
  41. lucy_assist/utils/message_utils.py +130 -0
  42. lucy_assist/utils/token_utils.py +87 -0
  43. lucy_assist/views/__init__.py +13 -0
  44. lucy_assist/views/api_views.py +595 -0
@@ -0,0 +1,186 @@
1
+ """
2
+ Tests pour Lucy Assist.
3
+ """
4
+ from django.test import TestCase
5
+ from django.urls import reverse
6
+
7
+ from lucy_assist.models import Conversation, Message, ConfigurationLucyAssist
8
+ from lucy_assist.tests.factories import (
9
+ ConfigurationLucyAssistFactory,
10
+ ConversationFactory,
11
+ MessageUtilisateurFactory,
12
+ MessageChatbotFactory,
13
+ )
14
+ from apps.utilisateur.tests.factories import UtilisateurAdminUnafFactory
15
+
16
+
17
+ class TestConfigurationLucyAssist(TestCase):
18
+ """Tests pour le modèle ConfigurationLucyAssist."""
19
+
20
+ def setUp(self):
21
+ self.user = UtilisateurAdminUnafFactory()
22
+ self.client.force_login(self.user)
23
+
24
+ # -----------------------------------------------------------------
25
+ # CONFIGURATION SINGLETON
26
+ # -----------------------------------------------------------------
27
+ def test_configuration_singleton_ok(self):
28
+ """Vérifie que la configuration est un singleton."""
29
+ config1 = ConfigurationLucyAssist.get_config()
30
+ config2 = ConfigurationLucyAssist.get_config()
31
+
32
+ self.assertEqual(config1.pk, config2.pk)
33
+ self.assertEqual(config1.pk, 1)
34
+
35
+ def test_ajouter_tokens_ok(self):
36
+ """Vérifie l'ajout de tokens."""
37
+ config = ConfigurationLucyAssist.get_config()
38
+ initial_tokens = config.tokens_disponibles
39
+
40
+ tokens_ajoutes = config.ajouter_tokens(10) # 10€
41
+
42
+ self.assertEqual(tokens_ajoutes, 1_000_000)
43
+ self.assertEqual(config.tokens_disponibles, initial_tokens + 1_000_000)
44
+
45
+ def test_get_questions_frequentes_default_ok(self):
46
+ """Vérifie les questions fréquentes par défaut."""
47
+ config = ConfigurationLucyAssist.get_config()
48
+ config.questions_frequentes = []
49
+ config.save()
50
+
51
+ questions = config.get_questions_frequentes()
52
+
53
+ self.assertIsInstance(questions, list)
54
+ self.assertGreater(len(questions), 0)
55
+
56
+
57
+ class TestConversation(TestCase):
58
+ """Tests pour le modèle Conversation."""
59
+
60
+ def setUp(self):
61
+ self.user = UtilisateurAdminUnafFactory()
62
+ self.client.force_login(self.user)
63
+ ConfigurationLucyAssistFactory()
64
+
65
+ # -----------------------------------------------------------------
66
+ # CRÉATION
67
+ # -----------------------------------------------------------------
68
+ def test_create_conversation_ok(self):
69
+ """Vérifie la création d'une conversation."""
70
+ conversation = ConversationFactory(utilisateur=self.user)
71
+
72
+ self.assertIsNotNone(conversation.pk)
73
+ self.assertEqual(conversation.utilisateur, self.user)
74
+ self.assertTrue(conversation.is_active)
75
+
76
+ def test_generer_titre_ok(self):
77
+ """Vérifie la génération automatique du titre."""
78
+ conversation = ConversationFactory(utilisateur=self.user, titre=None)
79
+ MessageUtilisateurFactory(
80
+ conversation=conversation,
81
+ contenu="Comment créer un membre ?"
82
+ )
83
+
84
+ conversation.generer_titre()
85
+
86
+ self.assertIsNotNone(conversation.titre)
87
+ self.assertIn("Comment créer", conversation.titre)
88
+
89
+
90
+ class TestMessage(TestCase):
91
+ """Tests pour le modèle Message."""
92
+
93
+ def setUp(self):
94
+ self.user = UtilisateurAdminUnafFactory()
95
+ self.client.force_login(self.user)
96
+ ConfigurationLucyAssistFactory()
97
+
98
+ # -----------------------------------------------------------------
99
+ # CRÉATION
100
+ # -----------------------------------------------------------------
101
+ def test_create_message_utilisateur_ok(self):
102
+ """Vérifie la création d'un message utilisateur."""
103
+ conversation = ConversationFactory(utilisateur=self.user)
104
+ message = MessageUtilisateurFactory(conversation=conversation)
105
+
106
+ self.assertIsNotNone(message.pk)
107
+ self.assertTrue(message.est_utilisateur)
108
+ self.assertFalse(message.est_chatbot)
109
+
110
+ def test_create_message_chatbot_ok(self):
111
+ """Vérifie la création d'un message chatbot."""
112
+ conversation = ConversationFactory(utilisateur=self.user)
113
+ message = MessageChatbotFactory(conversation=conversation)
114
+
115
+ self.assertIsNotNone(message.pk)
116
+ self.assertTrue(message.est_chatbot)
117
+ self.assertFalse(message.est_utilisateur)
118
+ self.assertGreater(message.tokens_utilises, 0)
119
+
120
+
121
+ class TestAPIViews(TestCase):
122
+ """Tests pour les vues API Lucy Assist."""
123
+
124
+ def setUp(self):
125
+ self.user = UtilisateurAdminUnafFactory()
126
+ self.client.force_login(self.user)
127
+ self.config = ConfigurationLucyAssistFactory(tokens_disponibles=1_000_000)
128
+
129
+ # -----------------------------------------------------------------
130
+ # LISTE CONVERSATIONS
131
+ # -----------------------------------------------------------------
132
+ def test_conversation_list_ok(self):
133
+ """Vérifie que la liste des conversations est accessible."""
134
+ ConversationFactory(utilisateur=self.user)
135
+ ConversationFactory(utilisateur=self.user)
136
+
137
+ url = reverse("lucy_assist:api-conversations")
138
+ response = self.client.get(url)
139
+
140
+ self.assertEqual(response.status_code, 200)
141
+ data = response.json()
142
+ self.assertIn('conversations', data)
143
+ self.assertEqual(len(data['conversations']), 2)
144
+
145
+ # -----------------------------------------------------------------
146
+ # CRÉATION CONVERSATION
147
+ # -----------------------------------------------------------------
148
+ def test_conversation_create_ok(self):
149
+ """Vérifie la création d'une conversation via API."""
150
+ initial_count = Conversation.objects.count()
151
+
152
+ url = reverse("lucy_assist:api-conversations")
153
+ response = self.client.post(
154
+ url,
155
+ data={'page_contexte': '/membre/list'},
156
+ content_type='application/json'
157
+ )
158
+
159
+ self.assertEqual(response.status_code, 201)
160
+ self.assertEqual(Conversation.objects.count(), initial_count + 1)
161
+
162
+ # -----------------------------------------------------------------
163
+ # STATUT TOKENS
164
+ # -----------------------------------------------------------------
165
+ def test_token_status_ok(self):
166
+ """Vérifie le statut des tokens."""
167
+ url = reverse("lucy_assist:api-token-status")
168
+ response = self.client.get(url)
169
+
170
+ self.assertEqual(response.status_code, 200)
171
+ data = response.json()
172
+ self.assertIn('tokens_disponibles', data)
173
+ self.assertEqual(data['tokens_disponibles'], 1_000_000)
174
+
175
+ # -----------------------------------------------------------------
176
+ # SUGGESTIONS
177
+ # -----------------------------------------------------------------
178
+ def test_suggestions_ok(self):
179
+ """Vérifie les suggestions de questions."""
180
+ url = reverse("lucy_assist:api-suggestions")
181
+ response = self.client.get(url)
182
+
183
+ self.assertEqual(response.status_code, 200)
184
+ data = response.json()
185
+ self.assertIn('suggestions', data)
186
+ self.assertIsInstance(data['suggestions'], list)
lucy_assist/urls.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ URLs pour Lucy Assist.
3
+
4
+ Ce module définit toutes les routes de l'application Lucy Assist,
5
+ incluant les API REST et les vues de documentation.
6
+ """
7
+ from django.urls import path
8
+
9
+ from lucy_assist import views
10
+
11
+ app_name = "lucy_assist"
12
+
13
+ urlpatterns = [
14
+ # API - Conversations
15
+ path("api/conversations", views.ConversationListCreateView.as_view(), name="api-conversations"),
16
+ path("api/conversations/<int:pk>", views.ConversationDetailView.as_view(), name="api-conversation-detail"),
17
+
18
+ # API - Messages
19
+ path("api/conversations/<int:conversation_id>/messages", views.MessageCreateView.as_view(), name="api-messages"),
20
+ path("api/conversations/<int:conversation_id>/chat", views.ChatCompletionView.as_view(), name="api-chat"),
21
+
22
+ # API - Tokens
23
+ path("api/tokens/status", views.TokenStatusView.as_view(), name="api-token-status"),
24
+ path("api/tokens/buy", views.AcheterTokensView.as_view(), name="api-token-buy"),
25
+
26
+ # API - Utilitaires
27
+ path("api/suggestions", views.SuggestionsView.as_view(), name="api-suggestions"),
28
+ path("api/context", views.PageContextView.as_view(), name="api-context"),
29
+
30
+ # API - Cache (admin only)
31
+ path("api/cache/stats", views.CacheStatsView.as_view(), name="api-cache-stats"),
32
+ path("api/cache/invalidate", views.CacheInvalidateView.as_view(), name="api-cache-invalidate"),
33
+
34
+ # API - Feedback
35
+ path("api/feedback", views.FeedbackCreateView.as_view(), name="api-feedback"),
36
+ ]
@@ -0,0 +1,7 @@
1
+ from .token_utils import TokenUtils
2
+ from .message_utils import MessageUtils
3
+
4
+ __all__ = [
5
+ 'TokenUtils',
6
+ 'MessageUtils',
7
+ ]
@@ -0,0 +1,59 @@
1
+ """
2
+ Utilitaire de logging pour Lucy Assist.
3
+ Version simplifiée et autonome.
4
+ """
5
+ import inspect
6
+ import logging
7
+ import os
8
+
9
+ from django.conf import settings
10
+
11
+ logger = logging.getLogger("lucy_assist")
12
+
13
+
14
+ class LogUtils:
15
+ """
16
+ Classe générique de gestion des logs pour Lucy Assist.
17
+ Format: lucy_assist [$className.$methodName] - $message
18
+ """
19
+
20
+ @staticmethod
21
+ def _get_caller_info():
22
+ """Récupère les informations sur l'appelant."""
23
+ try:
24
+ frame = inspect.stack()[2]
25
+ class_name = frame[0].f_locals.get("self", None)
26
+ if class_name:
27
+ class_name = class_name.__class__.__name__
28
+ else:
29
+ # Méthode statique ou fonction
30
+ class_name = frame[0].f_globals.get('__name__', 'unknown').split('.')[-1]
31
+
32
+ method_name = frame[3]
33
+ return f"{class_name}.{method_name}"
34
+ except Exception:
35
+ return "unknown"
36
+
37
+ @staticmethod
38
+ def info(msg=None):
39
+ """Log un message de niveau INFO."""
40
+ caller = LogUtils._get_caller_info()
41
+ logger.info(f"lucy_assist [{caller}] - {msg}")
42
+
43
+ @staticmethod
44
+ def warning(msg=None):
45
+ """Log un message de niveau WARNING."""
46
+ caller = LogUtils._get_caller_info()
47
+ logger.warning(f"lucy_assist [{caller}] - {msg}")
48
+
49
+ @staticmethod
50
+ def debug(msg=None):
51
+ """Log un message de niveau DEBUG."""
52
+ caller = LogUtils._get_caller_info()
53
+ logger.debug(f"lucy_assist [{caller}] - {msg}")
54
+
55
+ @staticmethod
56
+ def error(msg=None):
57
+ """Log un message de niveau ERROR."""
58
+ caller = LogUtils._get_caller_info()
59
+ logger.error(f"lucy_assist [{caller}] - {msg}")
@@ -0,0 +1,130 @@
1
+ """
2
+ Utilitaires pour le formatage des messages Lucy Assist.
3
+ Sans impact sur la base de données.
4
+ """
5
+ import re
6
+ from typing import List, Dict
7
+
8
+
9
+ class MessageUtils:
10
+ """Utilitaires de formatage et transformation des messages."""
11
+
12
+ @staticmethod
13
+ def tronquer_texte(texte: str, max_length: int = 50, suffix: str = "...") -> str:
14
+ """
15
+ Tronque un texte à une longueur maximale.
16
+
17
+ Args:
18
+ texte: Texte à tronquer
19
+ max_length: Longueur maximale
20
+ suffix: Suffixe à ajouter si tronqué
21
+
22
+ Returns:
23
+ Texte tronqué
24
+ """
25
+ if not texte:
26
+ return ""
27
+ if len(texte) <= max_length:
28
+ return texte
29
+ return texte[:max_length - len(suffix)] + suffix
30
+
31
+ @staticmethod
32
+ def formater_messages_pour_claude(messages: List) -> List[Dict]:
33
+ """
34
+ Formate les messages pour l'API Claude.
35
+
36
+ Args:
37
+ messages: Liste de messages (objets Message)
38
+
39
+ Returns:
40
+ Liste de dicts formatés pour Claude
41
+ """
42
+ from lucy_assist.constantes import LucyAssistConstantes
43
+
44
+ formatted = []
45
+ for msg in messages:
46
+ role = "user" if msg.repondant == LucyAssistConstantes.Repondant.UTILISATEUR else "assistant"
47
+ formatted.append({
48
+ "role": role,
49
+ "content": msg.contenu
50
+ })
51
+ return formatted
52
+
53
+ @staticmethod
54
+ def extraire_json_de_reponse(texte: str) -> dict:
55
+ """
56
+ Extrait un objet JSON d'une réponse texte.
57
+
58
+ Args:
59
+ texte: Texte contenant potentiellement du JSON
60
+
61
+ Returns:
62
+ Dict extrait ou dict vide
63
+ """
64
+ import json
65
+
66
+ try:
67
+ # Chercher un bloc JSON dans le texte
68
+ json_match = re.search(r'\{[^{}]*\}', texte, re.DOTALL)
69
+ if json_match:
70
+ return json.loads(json_match.group())
71
+ except (json.JSONDecodeError, AttributeError):
72
+ pass
73
+
74
+ return {}
75
+
76
+ @staticmethod
77
+ def nettoyer_message(message: str) -> str:
78
+ """
79
+ Nettoie un message utilisateur.
80
+
81
+ Args:
82
+ message: Message brut
83
+
84
+ Returns:
85
+ Message nettoyé
86
+ """
87
+ if not message:
88
+ return ""
89
+
90
+ # Supprimer les espaces multiples
91
+ message = re.sub(r'\s+', ' ', message)
92
+
93
+ # Trim
94
+ message = message.strip()
95
+
96
+ return message
97
+
98
+ @staticmethod
99
+ def markdown_to_html_basic(texte: str) -> str:
100
+ """
101
+ Conversion basique de Markdown vers HTML.
102
+
103
+ Args:
104
+ texte: Texte en markdown
105
+
106
+ Returns:
107
+ Texte en HTML
108
+ """
109
+ if not texte:
110
+ return ""
111
+
112
+ # Échapper le HTML
113
+ texte = texte.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
114
+
115
+ # Gras
116
+ texte = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', texte)
117
+
118
+ # Italique
119
+ texte = re.sub(r'\*(.*?)\*', r'<em>\1</em>', texte)
120
+
121
+ # Code inline
122
+ texte = re.sub(r'`(.*?)`', r'<code>\1</code>', texte)
123
+
124
+ # Liens
125
+ texte = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2" target="_blank">\1</a>', texte)
126
+
127
+ # Sauts de ligne
128
+ texte = texte.replace('\n', '<br>')
129
+
130
+ return texte
@@ -0,0 +1,87 @@
1
+ """
2
+ Utilitaires pour la gestion des tokens Lucy Assist.
3
+ Sans impact sur la base de données.
4
+ """
5
+
6
+
7
+ class TokenUtils:
8
+ """Utilitaires de calcul et formatage des tokens."""
9
+
10
+ PRIX_PAR_MILLION = 10.0
11
+ TOKENS_PAR_CONVERSATION = 2000
12
+
13
+ @staticmethod
14
+ def calculer_tokens_pour_montant(montant_euros: float, prix_par_million: float = None) -> int:
15
+ """
16
+ Calcule le nombre de tokens pour un montant en euros.
17
+
18
+ Args:
19
+ montant_euros: Montant en euros
20
+ prix_par_million: Prix par million de tokens (défaut: 10€)
21
+
22
+ Returns:
23
+ Nombre de tokens
24
+ """
25
+ prix = prix_par_million or TokenUtils.PRIX_PAR_MILLION
26
+ return int((montant_euros / prix) * 1_000_000)
27
+
28
+ @staticmethod
29
+ def calculer_montant_pour_tokens(tokens: int, prix_par_million: float = None) -> float:
30
+ """
31
+ Calcule le montant en euros pour un nombre de tokens.
32
+
33
+ Args:
34
+ tokens: Nombre de tokens
35
+ prix_par_million: Prix par million de tokens (défaut: 10€)
36
+
37
+ Returns:
38
+ Montant en euros
39
+ """
40
+ prix = prix_par_million or TokenUtils.PRIX_PAR_MILLION
41
+ return (tokens / 1_000_000) * prix
42
+
43
+ @staticmethod
44
+ def estimer_conversations(tokens: int, tokens_par_conversation: int = None) -> int:
45
+ """
46
+ Estime le nombre de conversations possibles.
47
+
48
+ Args:
49
+ tokens: Nombre de tokens disponibles
50
+ tokens_par_conversation: Tokens moyens par conversation (défaut: 2000)
51
+
52
+ Returns:
53
+ Nombre de conversations estimées
54
+ """
55
+ tpc = tokens_par_conversation or TokenUtils.TOKENS_PAR_CONVERSATION
56
+ return int(tokens / tpc) if tpc > 0 else 0
57
+
58
+ @staticmethod
59
+ def formater_tokens(tokens: int) -> str:
60
+ """
61
+ Formate un nombre de tokens pour l'affichage.
62
+
63
+ Args:
64
+ tokens: Nombre de tokens
65
+
66
+ Returns:
67
+ String formatée (ex: "1.5M", "500K", "1234")
68
+ """
69
+ if tokens >= 1_000_000:
70
+ return f"{tokens / 1_000_000:.1f}M"
71
+ elif tokens >= 1_000:
72
+ return f"{tokens / 1_000:.0f}K"
73
+ return str(tokens)
74
+
75
+ @staticmethod
76
+ def tokens_suffisants(tokens_disponibles: int, tokens_requis: int = 2000) -> bool:
77
+ """
78
+ Vérifie si suffisamment de tokens sont disponibles.
79
+
80
+ Args:
81
+ tokens_disponibles: Tokens actuellement disponibles
82
+ tokens_requis: Tokens minimum requis
83
+
84
+ Returns:
85
+ True si suffisant
86
+ """
87
+ return tokens_disponibles >= tokens_requis
@@ -0,0 +1,13 @@
1
+ from .api_views import (
2
+ ConversationListCreateView,
3
+ ConversationDetailView,
4
+ MessageCreateView,
5
+ ChatCompletionView,
6
+ TokenStatusView,
7
+ AcheterTokensView,
8
+ SuggestionsView,
9
+ PageContextView,
10
+ CacheStatsView,
11
+ CacheInvalidateView,
12
+ FeedbackCreateView,
13
+ )