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.
- django_spire/ai/chat/intelligence/decoders/intent_decoder.py +49 -0
- django_spire/ai/chat/intelligence/workflows/chat_workflow.py +19 -53
- django_spire/ai/chat/message_intel.py +15 -7
- django_spire/ai/chat/models.py +6 -5
- django_spire/ai/chat/router.py +105 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/element/recent_chat_select_element.html +1 -1
- django_spire/ai/chat/templates/django_spire/ai/chat/message/message.html +2 -2
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/selection_widget.html +1 -1
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +113 -0
- django_spire/ai/chat/tests/test_router/test_integration.py +147 -0
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +141 -0
- django_spire/ai/chat/tests/test_router/test_message_intel.py +67 -0
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +92 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +1 -1
- django_spire/ai/chat/views/message_request_views.py +5 -3
- django_spire/ai/chat/views/message_response_views.py +2 -2
- django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
- django_spire/ai/sms/models.py +1 -1
- django_spire/ai/tests/test_ai.py +1 -1
- django_spire/consts.py +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +4 -4
- django_spire/core/static/django_spire/css/app-side-panel.css +0 -45
- django_spire/core/templates/django_spire/navigation/top_navigation.html +42 -38
- django_spire/core/templates/django_spire/page/full_page.html +69 -47
- django_spire/knowledge/collection/seeding/seeder.py +2 -2
- django_spire/knowledge/entry/seeding/seeder.py +10 -5
- django_spire/knowledge/intelligence/decoders/entry_decoder.py +4 -1
- django_spire/knowledge/intelligence/intel/message_intel.py +1 -1
- django_spire/knowledge/intelligence/router.py +26 -0
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -3
- django_spire/knowledge/templates/django_spire/knowledge/sub_navigation/item/entry_sub_navigation_item.html +1 -0
- django_spire/settings.py +13 -6
- {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/METADATA +1 -1
- {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/RECORD +38 -30
- django_spire/ai/chat/intelligence/decoders/tools.py +0 -34
- {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/WHEEL +0 -0
- {django_spire-0.20.4.dist-info → django_spire-0.21.1.dist-info}/licenses/LICENSE.md +0 -0
- {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)
|
|
@@ -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=
|
|
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":
|
|
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
|
|
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=
|
|
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
|
)
|
django_spire/ai/sms/models.py
CHANGED
|
@@ -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
|
|
8
|
+
from django_spire.ai.sms.querysets import SmsConversationQuerySet, SmsMessageQuerySet
|
|
9
9
|
from django_spire.history.mixins import HistoryModelMixin
|
|
10
10
|
|
|
11
11
|
|
django_spire/ai/tests/test_ai.py
CHANGED
django_spire/consts.py
CHANGED
|
@@ -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%,
|
|
86
|
-
height:
|
|
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%,
|
|
92
|
-
height:
|
|
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);
|