django-spire 0.20.4__py3-none-any.whl → 0.21.1__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 (39) hide show
  1. django_spire/ai/chat/intelligence/decoders/intent_decoder.py +49 -0
  2. django_spire/ai/chat/intelligence/workflows/chat_workflow.py +19 -53
  3. django_spire/ai/chat/message_intel.py +15 -7
  4. django_spire/ai/chat/models.py +6 -5
  5. django_spire/ai/chat/router.py +105 -0
  6. django_spire/ai/chat/templates/django_spire/ai/chat/element/recent_chat_select_element.html +1 -1
  7. django_spire/ai/chat/templates/django_spire/ai/chat/message/message.html +2 -2
  8. django_spire/ai/chat/templates/django_spire/ai/chat/widget/selection_widget.html +1 -1
  9. django_spire/ai/chat/tests/test_router/test_base_chat_router.py +110 -0
  10. django_spire/ai/chat/tests/test_router/test_chat_workflow.py +113 -0
  11. django_spire/ai/chat/tests/test_router/test_integration.py +147 -0
  12. django_spire/ai/chat/tests/test_router/test_intent_decoder.py +141 -0
  13. django_spire/ai/chat/tests/test_router/test_message_intel.py +67 -0
  14. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +92 -0
  15. django_spire/ai/chat/tests/test_urls/test_json_urls.py +1 -1
  16. django_spire/ai/chat/views/message_request_views.py +5 -3
  17. django_spire/ai/chat/views/message_response_views.py +2 -2
  18. django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
  19. django_spire/ai/sms/models.py +1 -1
  20. django_spire/ai/tests/test_ai.py +1 -1
  21. django_spire/consts.py +1 -1
  22. django_spire/core/static/django_spire/css/app-navigation.css +4 -4
  23. django_spire/core/static/django_spire/css/app-side-panel.css +0 -45
  24. django_spire/core/templates/django_spire/navigation/top_navigation.html +42 -38
  25. django_spire/core/templates/django_spire/page/full_page.html +69 -47
  26. django_spire/knowledge/collection/seeding/seeder.py +2 -2
  27. django_spire/knowledge/entry/seeding/seeder.py +10 -5
  28. django_spire/knowledge/intelligence/decoders/entry_decoder.py +4 -1
  29. django_spire/knowledge/intelligence/intel/message_intel.py +1 -1
  30. django_spire/knowledge/intelligence/router.py +26 -0
  31. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -3
  32. django_spire/knowledge/templates/django_spire/knowledge/sub_navigation/item/entry_sub_navigation_item.html +1 -0
  33. django_spire/settings.py +13 -6
  34. {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/METADATA +1 -1
  35. {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/RECORD +38 -30
  36. django_spire/ai/chat/intelligence/decoders/tools.py +0 -34
  37. {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/WHEEL +0 -0
  38. {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/licenses/LICENSE.md +0 -0
  39. {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import Mock, patch
4
+
5
+ from django.contrib.auth.models import Permission, User
6
+ from django.test import RequestFactory, override_settings
7
+
8
+ from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
9
+ from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
10
+ from django_spire.ai.chat.router import BaseChatRouter
11
+ from django_spire.core.tests.test_cases import BaseTestCase
12
+
13
+
14
+ class KnowledgeRouter(BaseChatRouter):
15
+ def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
16
+ return DefaultMessageIntel(text='Knowledge search result')
17
+
18
+
19
+ class SupportRouter(BaseChatRouter):
20
+ def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
21
+ return DefaultMessageIntel(text='Support response')
22
+
23
+
24
+ class TestRouterIntegration(BaseTestCase):
25
+ def setUp(self) -> None:
26
+ super().setUp()
27
+
28
+ self.factory = RequestFactory()
29
+ self.request = self.factory.get('/')
30
+ self.request.user = self.super_user
31
+
32
+ @override_settings(
33
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SPIRE',
34
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
35
+ 'SPIRE': 'django_spire.ai.chat.router.SpireChatRouter',
36
+ 'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
37
+ 'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
38
+ },
39
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
40
+ 'KNOWLEDGE_SEARCH': {
41
+ 'INTENT_DESCRIPTION': 'User asking about documentation',
42
+ 'REQUIRED_PERMISSION': 'auth.view_user',
43
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
44
+ },
45
+ 'SUPPORT': {
46
+ 'INTENT_DESCRIPTION': 'User needs support',
47
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
48
+ }
49
+ }
50
+ )
51
+ def test_intent_routing_with_permission(self) -> None:
52
+ permission = Permission.objects.get(codename='view_user')
53
+ self.super_user.user_permissions.add(permission)
54
+
55
+ with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
56
+ mock_decoder_instance = type('MockDecoder', (), {
57
+ 'process': lambda self, user_input, **kwargs: [
58
+ lambda **kwargs: KnowledgeRouter().workflow(**kwargs)
59
+ ]
60
+ })()
61
+
62
+ mock_decoder.return_value = mock_decoder_instance
63
+
64
+ result = chat_workflow(
65
+ request=self.request,
66
+ user_input='Tell me about the documentation',
67
+ message_history=None
68
+ )
69
+
70
+ assert isinstance(result, BaseMessageIntel)
71
+
72
+ @override_settings(
73
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SUPPORT',
74
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
75
+ 'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
76
+ }
77
+ )
78
+ def test_direct_router_selection(self) -> None:
79
+ result = chat_workflow(
80
+ request=self.request,
81
+ user_input='I need help',
82
+ message_history=None
83
+ )
84
+
85
+ assert isinstance(result, DefaultMessageIntel)
86
+ assert result.text == 'Support response'
87
+
88
+ @override_settings(
89
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
90
+ 'RESTRICTED': {
91
+ 'INTENT_DESCRIPTION': 'Restricted intent',
92
+ 'REQUIRED_PERMISSION': 'auth.delete_user',
93
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
94
+ }
95
+ }
96
+ )
97
+ def test_intent_excluded_without_permission(self) -> None:
98
+ from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
99
+
100
+ regular_user = User.objects.create_user(username='regular', password='test')
101
+
102
+ request = self.factory.get('/')
103
+ request.user = regular_user
104
+
105
+ decoder = generate_intent_decoder(request=request, default_callable=None)
106
+
107
+ assert 'Restricted intent' not in decoder.mapping
108
+
109
+ @override_settings(
110
+ DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='KNOWLEDGE',
111
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={
112
+ 'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
113
+ }
114
+ )
115
+ def test_end_to_end_workflow(self) -> None:
116
+ from dandy.llm.request.message import MessageHistory
117
+
118
+ message_history = MessageHistory()
119
+ message_history.add_message(role='user', content='Previous message')
120
+
121
+ result = chat_workflow(
122
+ request=self.request,
123
+ user_input='Current message',
124
+ message_history=message_history
125
+ )
126
+
127
+ assert isinstance(result, BaseMessageIntel)
128
+ assert isinstance(result, DefaultMessageIntel)
129
+
130
+ @override_settings(
131
+ DJANGO_SPIRE_AI_CHAT_ROUTERS={}
132
+ )
133
+ def test_workflow_handles_empty_routers(self) -> None:
134
+ with patch('django_spire.ai.chat.intelligence.workflows.chat_workflow.get_callable_from_module_string_and_validate_arguments') as mock_get_callable:
135
+ mock_router_class = Mock()
136
+ mock_router_instance = Mock()
137
+ mock_router_instance.process.return_value = DefaultMessageIntel(text='Fallback')
138
+ mock_router_class.return_value = mock_router_instance
139
+ mock_get_callable.return_value = mock_router_class
140
+
141
+ result = chat_workflow(
142
+ request=self.request,
143
+ user_input='Test',
144
+ message_history=None
145
+ )
146
+
147
+ assert isinstance(result, DefaultMessageIntel)
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import Mock
4
+
5
+ from django.contrib.auth.models import Permission, User
6
+ from django.test import RequestFactory, override_settings
7
+
8
+ from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
9
+ from django_spire.ai.chat.message_intel import DefaultMessageIntel
10
+ from django_spire.ai.chat.router import BaseChatRouter
11
+ from django_spire.core.tests.test_cases import BaseTestCase
12
+
13
+
14
+ class TestRouter(BaseChatRouter):
15
+ def workflow(self, request, user_input, message_history=None) -> DefaultMessageIntel:
16
+ return DefaultMessageIntel(text='Test response')
17
+
18
+
19
+ class TestIntentDecoder(BaseTestCase):
20
+ def setUp(self) -> None:
21
+ super().setUp()
22
+ self.factory = RequestFactory()
23
+ self.request = self.factory.get('/')
24
+ self.request.user = self.super_user
25
+
26
+ @override_settings(DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={})
27
+ def test_decoder_with_no_intents(self) -> None:
28
+ decoder = generate_intent_decoder(
29
+ request=self.request,
30
+ default_callable=lambda **kwargs: DefaultMessageIntel(text='Default')
31
+ )
32
+
33
+ assert decoder is not None
34
+ assert len(decoder.mapping) == 1
35
+
36
+ @override_settings(
37
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
38
+ 'TEST_INTENT': {
39
+ 'INTENT_DESCRIPTION': 'Test intent description',
40
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
41
+ }
42
+ }
43
+ )
44
+ def test_decoder_adds_intent_without_permission(self) -> None:
45
+ decoder = generate_intent_decoder(
46
+ request=self.request,
47
+ default_callable=None
48
+ )
49
+
50
+ assert len(decoder.mapping) == 1
51
+ assert 'Test intent description' in decoder.mapping
52
+
53
+ @override_settings(
54
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
55
+ 'TEST_INTENT': {
56
+ 'INTENT_DESCRIPTION': 'Test intent with permission',
57
+ 'REQUIRED_PERMISSION': 'auth.add_user',
58
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
59
+ }
60
+ }
61
+ )
62
+ def test_decoder_checks_required_permission(self) -> None:
63
+ permission = Permission.objects.get(codename='add_user')
64
+ self.super_user.user_permissions.add(permission)
65
+
66
+ decoder = generate_intent_decoder(
67
+ request=self.request,
68
+ default_callable=None
69
+ )
70
+
71
+ assert 'Test intent with permission' in decoder.mapping
72
+
73
+ @override_settings(
74
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
75
+ 'TEST_INTENT': {
76
+ 'INTENT_DESCRIPTION': 'Test intent with permission',
77
+ 'REQUIRED_PERMISSION': 'auth.add_user',
78
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
79
+ }
80
+ }
81
+ )
82
+ def test_decoder_excludes_intent_without_permission(self) -> None:
83
+ regular_user = User.objects.create_user(username='regular', password='test')
84
+
85
+ request = self.factory.get('/')
86
+ request.user = regular_user
87
+
88
+ decoder = generate_intent_decoder(
89
+ request=request,
90
+ default_callable=None
91
+ )
92
+
93
+ assert 'Test intent with permission' not in decoder.mapping
94
+
95
+ @override_settings(
96
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
97
+ 'INVALID_ROUTER': {
98
+ 'INTENT_DESCRIPTION': 'Invalid router',
99
+ 'CHAT_ROUTER': 'non.existent.router.InvalidRouter',
100
+ }
101
+ }
102
+ )
103
+ def test_decoder_handles_import_error(self) -> None:
104
+ decoder = generate_intent_decoder(
105
+ request=self.request,
106
+ default_callable=None
107
+ )
108
+
109
+ assert 'Invalid router' not in decoder.mapping
110
+
111
+ def test_decoder_adds_default_callable(self) -> None:
112
+ default_callable = Mock()
113
+
114
+ decoder = generate_intent_decoder(
115
+ request=self.request,
116
+ default_callable=default_callable
117
+ )
118
+
119
+ assert "None of the above choices match the user's intent" in decoder.mapping
120
+
121
+ @override_settings(
122
+ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
123
+ 'INTENT_1': {
124
+ 'INTENT_DESCRIPTION': 'First intent',
125
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
126
+ },
127
+ 'INTENT_2': {
128
+ 'INTENT_DESCRIPTION': 'Second intent',
129
+ 'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
130
+ }
131
+ }
132
+ )
133
+ def test_decoder_adds_multiple_intents(self) -> None:
134
+ decoder = generate_intent_decoder(
135
+ request=self.request,
136
+ default_callable=None
137
+ )
138
+
139
+ assert 'First intent' in decoder.mapping
140
+ assert 'Second intent' in decoder.mapping
141
+ assert len(decoder.mapping) == 2
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
6
+ from django_spire.core.tests.test_cases import BaseTestCase
7
+
8
+
9
+ class CustomMessageIntel(BaseMessageIntel):
10
+ _template: str = 'django_spire/ai/chat/message/default_message.html'
11
+ text: str
12
+ extra_data: str = 'Extra'
13
+
14
+ def render_to_str(self) -> str:
15
+ return f'{self.text} - {self.extra_data}'
16
+
17
+
18
+ class TestMessageIntel(BaseTestCase):
19
+ def test_default_message_intel_has_template(self) -> None:
20
+ intel = DefaultMessageIntel(text='Test')
21
+ assert intel.template == 'django_spire/ai/chat/message/default_message.html'
22
+
23
+ def test_default_message_intel_render_to_str(self) -> None:
24
+ intel = DefaultMessageIntel(text='Hello World')
25
+ result = intel.render_to_str()
26
+
27
+ assert result == 'Hello World'
28
+
29
+ def test_custom_message_intel_render_to_str(self) -> None:
30
+ intel = CustomMessageIntel(text='Test', extra_data='Custom')
31
+ result = intel.render_to_str()
32
+
33
+ assert result == 'Test - Custom'
34
+
35
+ def test_render_template_to_str_renders_django_template(self) -> None:
36
+ intel = DefaultMessageIntel(text='Hello')
37
+ result = intel.render_template_to_str()
38
+
39
+ assert isinstance(result, str)
40
+ assert len(result) > 0
41
+
42
+ def test_render_template_to_str_with_context(self) -> None:
43
+ intel = DefaultMessageIntel(text='Test')
44
+ result = intel.render_template_to_str(context_data={'extra': 'data'})
45
+
46
+ assert isinstance(result, str)
47
+
48
+ def test_template_property(self) -> None:
49
+ intel = DefaultMessageIntel(text='Test')
50
+
51
+ assert intel.template == intel._template
52
+
53
+ def test_message_intel_raises_without_template(self) -> None:
54
+ with pytest.raises(ValueError):
55
+ class NoTemplateIntel(BaseMessageIntel):
56
+ _template = None
57
+
58
+ def render_to_str(self) -> str:
59
+ return 'test'
60
+
61
+ def test_message_intel_raises_with_empty_template(self) -> None:
62
+ with pytest.raises(ValueError):
63
+ class EmptyTemplateIntel(BaseMessageIntel):
64
+ _template = ''
65
+
66
+ def render_to_str(self) -> str:
67
+ return 'test'
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import Mock, patch
4
+
5
+ from django.test import RequestFactory, override_settings
6
+
7
+ from django_spire.ai.chat.message_intel import DefaultMessageIntel
8
+ from django_spire.ai.chat.router import SpireChatRouter
9
+ from django_spire.core.tests.test_cases import BaseTestCase
10
+
11
+
12
+ class TestSpireChatRouter(BaseTestCase):
13
+ def setUp(self) -> None:
14
+ super().setUp()
15
+ self.factory = RequestFactory()
16
+ self.request = self.factory.get('/')
17
+ self.request.user = self.super_user
18
+
19
+ def test_router_can_be_instantiated(self) -> None:
20
+ router = SpireChatRouter()
21
+ assert isinstance(router, SpireChatRouter)
22
+
23
+ def test_default_chat_callable_returns_message_intel(self) -> None:
24
+ router = SpireChatRouter()
25
+
26
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
27
+ mock_bot_instance = Mock()
28
+ MockBot.return_value = mock_bot_instance
29
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
30
+
31
+ result = router._default_chat_callable(
32
+ request=self.request,
33
+ user_input='Hello',
34
+ message_history=None
35
+ )
36
+
37
+ assert isinstance(result, DefaultMessageIntel)
38
+ mock_bot_instance.llm.prompt_to_intel.assert_called_once()
39
+
40
+ @override_settings(DJANGO_SPIRE_AI_PERSONA_NAME='Test Bot')
41
+ def test_default_callable_uses_persona_name(self) -> None:
42
+ router = SpireChatRouter()
43
+
44
+ with patch('django_spire.ai.chat.router.Bot') as MockBot:
45
+ mock_bot_instance = Mock()
46
+ MockBot.return_value = mock_bot_instance
47
+ mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
48
+
49
+ router._default_chat_callable(
50
+ request=self.request,
51
+ user_input='Hello',
52
+ message_history=None
53
+ )
54
+
55
+ assert MockBot.called
56
+
57
+ def test_workflow_uses_intent_decoder(self) -> None:
58
+ router = SpireChatRouter()
59
+
60
+ with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
61
+ mock_decoder_instance = Mock()
62
+ mock_decoder.return_value = mock_decoder_instance
63
+ mock_decoder_instance.process.return_value = [lambda **kwargs: DefaultMessageIntel(text='Response')]
64
+
65
+ result = router.workflow(
66
+ request=self.request,
67
+ user_input='Hello',
68
+ message_history=None
69
+ )
70
+
71
+ mock_decoder.assert_called_once()
72
+ assert isinstance(result, DefaultMessageIntel)
73
+
74
+ @override_settings(DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={})
75
+ def test_workflow_with_no_intent_routers(self) -> None:
76
+ router = SpireChatRouter()
77
+
78
+ with patch.object(router, '_default_chat_callable') as mock_default:
79
+ mock_default.return_value = DefaultMessageIntel(text='Default response')
80
+
81
+ with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
82
+ mock_decoder_instance = Mock()
83
+ mock_decoder.return_value = mock_decoder_instance
84
+ mock_decoder_instance.process.return_value = [mock_default]
85
+
86
+ result = router.workflow(
87
+ request=self.request,
88
+ user_input='Hello',
89
+ message_history=None
90
+ )
91
+
92
+ assert isinstance(result, DefaultMessageIntel)
@@ -20,4 +20,4 @@ class ChatJsonUrlTests(BaseTestCase):
20
20
  content_type='application/json'
21
21
  )
22
22
 
23
- self.assertEqual(response.status_code, 200)
23
+ assert response.status_code == 200
@@ -4,7 +4,6 @@ import json
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from django.conf import settings
8
7
  from django.http import HttpResponse
9
8
  from django.utils.timezone import now
10
9
 
@@ -12,6 +11,7 @@ from django_spire.ai.chat.choices import MessageResponseType
12
11
  from django_spire.ai.chat.message_intel import DefaultMessageIntel
13
12
  from django_spire.ai.chat.models import Chat
14
13
  from django_spire.ai.chat.responses import MessageResponse, MessageResponseGroup
14
+ from django_spire.conf import settings
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from django.core.handlers.wsgi import WSGIRequest
@@ -59,10 +59,12 @@ def request_message_render_view(request: WSGIRequest) -> HttpResponse:
59
59
 
60
60
  chat.add_message_response(user_message_response)
61
61
 
62
+ persona_name = getattr(settings, 'DJANGO_SPIRE_AI_PERSONA_NAME', 'AI Assistant')
63
+
62
64
  message_response_group.add_message_response(
63
65
  MessageResponse(
64
66
  type=MessageResponseType.LOADING_RESPONSE,
65
- sender='Spire',
67
+ sender=persona_name,
66
68
  message_intel=DefaultMessageIntel(
67
69
  text=body_data['message_body']
68
70
  ),
@@ -74,7 +76,7 @@ def request_message_render_view(request: WSGIRequest) -> HttpResponse:
74
76
  message_response_group.render_to_html_string(
75
77
  context_data={
76
78
  "chat_id": chat.id,
77
- "chat_workflow_name": settings.AI_PERSONA_NAME,
79
+ "chat_workflow_name": persona_name,
78
80
  }
79
81
  )
80
82
  )
@@ -8,10 +8,10 @@ from django.conf import settings
8
8
  from django.http import HttpResponse
9
9
  from django.utils.timezone import now
10
10
 
11
+ from django_spire.ai.chat.choices import MessageResponseType
11
12
  from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
12
13
  from django_spire.ai.chat.models import Chat
13
14
  from django_spire.ai.chat.responses import MessageResponse
14
- from django_spire.ai.chat.choices import MessageResponseType
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from django.core.handlers.wsgi import WSGIRequest
@@ -33,7 +33,7 @@ def response_message_render_view(request: WSGIRequest) -> HttpResponse:
33
33
 
34
34
  response_message = MessageResponse(
35
35
  type=MessageResponseType.RESPONSE,
36
- sender=settings.AI_PERSONA_NAME,
36
+ sender=getattr(settings, 'DJANGO_SPIRE_AI_PERSONA_NAME', 'AI Assistant'),
37
37
  message_intel=message_intel,
38
38
  synthesis_speech=body_data.get('synthesis_speech', False),
39
39
  message_timestamp=formatted_timestamp
@@ -8,20 +8,20 @@ from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workf
8
8
  from django_spire.ai.sms.intel import SmsIntel
9
9
 
10
10
  if TYPE_CHECKING:
11
- from django.core.handlers.wsgi import WSGIRequest
12
11
  from dandy.llm.request.message import MessageHistory
12
+ from django.core.handlers.wsgi import WSGIRequest
13
13
 
14
14
 
15
15
  @recorder_to_html_file('spire_ai_sms_conversation_workflow')
16
16
  def sms_conversation_workflow(
17
17
  request: WSGIRequest, user_input: str, message_history: MessageHistory | None = None, actor: str | None = None
18
18
  ) -> SmsIntel:
19
+ message_intel = chat_workflow(
20
+ request,
21
+ user_input=user_input,
22
+ message_history=message_history,
23
+ )
24
+
19
25
  return SmsIntel(
20
- body=str(
21
- chat_workflow(
22
- request,
23
- user_input=user_input,
24
- message_history=message_history,
25
- )
26
- )
26
+ body=message_intel.render_to_str()
27
27
  )
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
5
5
  from django.db import models
6
6
  from django.utils.timezone import now
7
7
 
8
- from django_spire.ai.sms.querysets import SmsMessageQuerySet, SmsConversationQuerySet
8
+ from django_spire.ai.sms.querysets import SmsConversationQuerySet, SmsMessageQuerySet
9
9
  from django_spire.history.mixins import HistoryModelMixin
10
10
 
11
11
 
@@ -25,4 +25,4 @@ class AiTestCase(BaseTestCase):
25
25
 
26
26
  horse_intel = generate_horse_intel('Make me a magical horse that grants wishes!')
27
27
 
28
- self.assertNotEqual(horse_intel.first_name, '')
28
+ assert horse_intel.first_name != ''
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.20.4'
1
+ __VERSION__ = '0.21.1'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -82,14 +82,14 @@
82
82
  }
83
83
 
84
84
  .side-navigation-fade-bottom {
85
- background: linear-gradient(to top, var(--app-side-navigation-bg-color) 0%, var(--app-side-navigation-bg-color) 50%, transparent 100%);
86
- height: 100px;
85
+ background: linear-gradient(to top, var(--app-side-navigation-bg-color) 0%, transparent 100%);
86
+ height: 60px;
87
87
  pointer-events: none;
88
88
  }
89
89
 
90
90
  .side-navigation-fade-top {
91
- background: linear-gradient(to bottom, var(--app-side-navigation-bg-color) 0%, var(--app-side-navigation-bg-color) 50%, transparent 100%);
92
- height: 75px;
91
+ background: linear-gradient(to bottom, var(--app-side-navigation-bg-color) 0%, transparent 100%);
92
+ height: 60px;
93
93
  pointer-events: none;
94
94
  }
95
95
 
@@ -5,25 +5,6 @@
5
5
  top: var(--app-top-navigation-height);
6
6
  }
7
7
 
8
- .btn-close-panel {
9
- align-items: center;
10
- background: transparent;
11
- border: none;
12
- border-radius: 50%;
13
- color: var(--app-primary);
14
- cursor: pointer;
15
- display: flex;
16
- height: 28px;
17
- justify-content: center;
18
- transition: background 0.2s ease, color 0.2s ease;
19
- width: 28px;
20
- }
21
-
22
- .btn-close-panel:hover {
23
- background: var(--app-primary);
24
- color: var(--app-default-button-text-color);
25
- }
26
-
27
8
  .panel-toggle-container {
28
9
  height: 30px;
29
10
  position: fixed;
@@ -50,32 +31,6 @@
50
31
  width: 30px !important;
51
32
  }
52
33
 
53
- .side-panel-transition-enter {
54
- transition: opacity 0.3s ease;
55
- }
56
-
57
- .side-panel-transition-enter-start-left,
58
- .side-panel-transition-enter-start-right {
59
- opacity: 0;
60
- }
61
-
62
- .side-panel-transition-enter-end {
63
- opacity: 1;
64
- }
65
-
66
- .side-panel-transition-leave {
67
- transition: opacity 0.3s ease;
68
- }
69
-
70
- .side-panel-transition-leave-start {
71
- opacity: 1;
72
- }
73
-
74
- .side-panel-transition-leave-end-left,
75
- .side-panel-transition-leave-end-right {
76
- opacity: 0;
77
- }
78
-
79
34
  @media (min-width: 992px) {
80
35
  .panel-toggle-container-left {
81
36
  left: var(--app-side-navigation-width);