django-spire 0.16.11__py3-none-any.whl → 0.16.13__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/admin.py +3 -1
- django_spire/ai/apps.py +2 -0
- django_spire/ai/chat/admin.py +15 -9
- django_spire/ai/chat/apps.py +4 -1
- django_spire/ai/chat/auth/controller.py +3 -1
- django_spire/ai/chat/choices.py +2 -0
- django_spire/ai/chat/intelligence/maps/intent_llm_map.py +8 -5
- django_spire/ai/chat/intelligence/prompts.py +4 -2
- django_spire/ai/chat/intelligence/workflows/chat_workflow.py +27 -28
- django_spire/ai/chat/message_intel.py +7 -4
- django_spire/ai/chat/models.py +8 -9
- django_spire/ai/chat/querysets.py +3 -1
- django_spire/ai/chat/responses.py +19 -10
- django_spire/ai/chat/tools.py +20 -15
- django_spire/ai/chat/urls/message_urls.py +2 -1
- django_spire/ai/chat/urls/page_urls.py +1 -0
- django_spire/ai/chat/views/message_request_views.py +2 -0
- django_spire/ai/chat/views/message_response_views.py +4 -4
- django_spire/ai/chat/views/message_views.py +2 -0
- django_spire/ai/chat/views/page_views.py +7 -2
- django_spire/ai/chat/views/template_views.py +2 -0
- django_spire/ai/decorators.py +13 -7
- django_spire/ai/mixins.py +4 -2
- django_spire/ai/models.py +7 -2
- django_spire/ai/prompt/bots.py +14 -32
- django_spire/ai/prompt/intel.py +1 -1
- django_spire/ai/prompt/prompts.py +7 -1
- django_spire/ai/prompt/system/bots.py +42 -75
- django_spire/ai/prompt/system/intel.py +5 -4
- django_spire/ai/prompt/system/prompts.py +5 -1
- django_spire/ai/prompt/system/system_prompt_cli.py +15 -9
- django_spire/ai/prompt/tests/test_bots.py +14 -11
- django_spire/ai/prompt/text_to_prompt_cli.py +5 -2
- django_spire/ai/prompt/tuning/bot_tuning_cli.py +14 -13
- django_spire/ai/prompt/tuning/bots.py +68 -116
- django_spire/ai/prompt/tuning/intel.py +1 -1
- django_spire/ai/prompt/tuning/mixins.py +2 -0
- django_spire/ai/prompt/tuning/prompt_tuning_cli.py +8 -8
- django_spire/ai/prompt/tuning/prompts.py +4 -2
- django_spire/ai/sms/admin.py +3 -1
- django_spire/ai/sms/apps.py +2 -0
- django_spire/ai/sms/decorators.py +2 -0
- django_spire/ai/sms/intel.py +4 -2
- django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
- django_spire/ai/sms/models.py +16 -14
- django_spire/ai/sms/querysets.py +4 -1
- django_spire/ai/sms/tools.py +18 -16
- django_spire/ai/sms/urls.py +1 -1
- django_spire/ai/sms/views.py +2 -0
- django_spire/ai/tests/test_ai.py +3 -5
- django_spire/ai/urls.py +1 -0
- django_spire/consts.py +1 -1
- django_spire/contrib/seeding/field/django/seeder.py +6 -4
- django_spire/contrib/seeding/field/llm.py +1 -2
- django_spire/contrib/seeding/intelligence/bots/field_seeding_bots.py +7 -8
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +15 -16
- django_spire/contrib/seeding/intelligence/intel.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +5 -7
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +3 -5
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +1 -2
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +3 -5
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +3 -3
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +2 -4
- django_spire/contrib/seeding/management/commands/seeding.py +5 -2
- django_spire/contrib/seeding/model/base.py +12 -10
- django_spire/contrib/seeding/model/django/seeder.py +13 -10
- django_spire/contrib/seeding/tests/test_seeding.py +1 -1
- django_spire/file/apps.py +13 -0
- django_spire/file/interfaces.py +42 -9
- django_spire/knowledge/admin.py +2 -0
- django_spire/knowledge/apps.py +2 -0
- django_spire/knowledge/auth/controller.py +2 -0
- django_spire/knowledge/collection/admin.py +3 -0
- django_spire/knowledge/collection/forms.py +2 -0
- django_spire/knowledge/collection/models.py +4 -0
- django_spire/knowledge/collection/querysets.py +3 -3
- django_spire/knowledge/collection/seeding/seed.py +2 -0
- django_spire/knowledge/collection/tests/factories.py +2 -0
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/collection/views/json_views.py +2 -0
- django_spire/knowledge/collection/views/page_views.py +2 -0
- django_spire/knowledge/context_processors.py +2 -0
- django_spire/knowledge/entry/services/automation_service.py +2 -4
- django_spire/knowledge/entry/services/factory_service.py +1 -2
- django_spire/knowledge/entry/services/tool_service.py +2 -2
- django_spire/knowledge/entry/services/transformation_services.py +0 -1
- django_spire/knowledge/entry/tests/factories.py +3 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/entry/urls/form_urls.py +1 -0
- django_spire/knowledge/entry/urls/json_urls.py +1 -0
- django_spire/knowledge/entry/urls/page_urls.py +1 -0
- django_spire/knowledge/entry/urls/template_urls.py +1 -0
- django_spire/knowledge/entry/version/admin.py +2 -0
- django_spire/knowledge/entry/version/block/admin.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/heading_block.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/text_block.py +2 -0
- django_spire/knowledge/entry/version/block/maps.py +2 -0
- django_spire/knowledge/entry/version/block/models.py +2 -0
- django_spire/knowledge/entry/version/block/tests/factories.py +2 -0
- django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/version/block/views/json_views.py +2 -0
- django_spire/knowledge/entry/version/converters/docx_converter.py +1 -1
- django_spire/knowledge/entry/version/converters/markdown_converter.py +2 -1
- django_spire/knowledge/entry/version/intelligence/bots/markdown_format_llm_bot.py +12 -9
- django_spire/knowledge/entry/version/maps.py +2 -0
- django_spire/knowledge/entry/version/models.py +2 -0
- django_spire/knowledge/entry/version/querysets.py +2 -0
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -0
- django_spire/knowledge/entry/version/tests/factories.py +2 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +3 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/form_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/page_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +1 -0
- django_spire/knowledge/entry/version/views/form_views.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +2 -0
- django_spire/knowledge/entry/version/views/page_views.py +2 -0
- django_spire/knowledge/entry/version/views/redirect_views.py +2 -0
- django_spire/knowledge/entry/views/form_views.py +4 -2
- django_spire/knowledge/exceptions.py +2 -0
- django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +5 -16
- django_spire/knowledge/intelligence/intel/collection_intel.py +3 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +3 -3
- django_spire/knowledge/intelligence/intel/message_intel.py +2 -0
- django_spire/knowledge/intelligence/maps/collection_map.py +9 -10
- django_spire/knowledge/intelligence/maps/entry_map.py +8 -9
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +8 -6
- django_spire/knowledge/models.py +2 -0
- django_spire/knowledge/seeding/seed.py +2 -0
- django_spire/knowledge/templatetags/spire_knowledge_tags.py +3 -0
- django_spire/knowledge/urls/__init__.py +1 -0
- {django_spire-0.16.11.dist-info → django_spire-0.16.13.dist-info}/METADATA +7 -2
- {django_spire-0.16.11.dist-info → django_spire-0.16.13.dist-info}/RECORD +147 -148
- django_spire/knowledge/entry/tests/constants.py +0 -1
- {django_spire-0.16.11.dist-info → django_spire-0.16.13.dist-info}/WHEEL +0 -0
- {django_spire-0.16.11.dist-info → django_spire-0.16.13.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.16.11.dist-info → django_spire-0.16.13.dist-info}/top_level.txt +0 -0
django_spire/ai/admin.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django import forms
|
|
2
4
|
from django.contrib import admin
|
|
3
5
|
from django.urls import reverse
|
|
@@ -24,7 +26,7 @@ class AiUsageAdmin(AiUsageAdminMixin):
|
|
|
24
26
|
search_fields = ('recorded_date',)
|
|
25
27
|
ordering = ('-recorded_date',)
|
|
26
28
|
|
|
27
|
-
def get_readonly_fields(self, request, obj=None):
|
|
29
|
+
def get_readonly_fields(self, request, obj=None) -> list[str]:
|
|
28
30
|
return [field.name for field in self.model._meta.fields]
|
|
29
31
|
|
|
30
32
|
def view_interactions_link(
|
django_spire/ai/apps.py
CHANGED
django_spire/ai/chat/admin.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
1
5
|
from django.contrib import admin
|
|
2
6
|
from django.shortcuts import reverse
|
|
3
7
|
from django.utils.html import format_html
|
|
@@ -5,23 +9,25 @@ from django.utils.http import urlencode
|
|
|
5
9
|
|
|
6
10
|
from django_spire.ai.chat import models
|
|
7
11
|
|
|
12
|
+
|
|
8
13
|
@admin.register(models.Chat)
|
|
9
14
|
class ChatAdmin(admin.ModelAdmin):
|
|
10
15
|
list_display = ('name', 'user', 'view_chat_messages_link', 'created_datetime')
|
|
11
16
|
search_fields = ('id', 'name')
|
|
12
|
-
ordering = ['-id']
|
|
17
|
+
ordering: ClassVar = ['-id']
|
|
13
18
|
|
|
14
|
-
def get_readonly_fields(self, request, obj=None):
|
|
19
|
+
def get_readonly_fields(self, request, obj=None) -> list[str]:
|
|
15
20
|
return [field.name for field in self.model._meta.fields]
|
|
16
21
|
|
|
17
22
|
def view_chat_messages_link(self, obj):
|
|
18
23
|
count = obj.messages.count()
|
|
19
24
|
url = (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
reverse("admin:django_spire_ai_chat_chatmessage_changelist")
|
|
26
|
+
+ "?"
|
|
27
|
+
+ urlencode({"chat__id": f"{obj.id}"})
|
|
23
28
|
)
|
|
24
|
-
|
|
29
|
+
|
|
30
|
+
return format_html(f'<a href="{url}">{count} Messages</a>')
|
|
25
31
|
|
|
26
32
|
view_chat_messages_link.short_description = "Messages"
|
|
27
33
|
|
|
@@ -33,13 +39,13 @@ class ChatAdmin(admin.ModelAdmin):
|
|
|
33
39
|
class ChatMessageAdmin(admin.ModelAdmin):
|
|
34
40
|
list_display = ('content_body', 'chat', 'chat__user','is_processed', 'is_viewed', 'created_datetime')
|
|
35
41
|
search_fields = ('id', 'content')
|
|
36
|
-
ordering = ['-id']
|
|
42
|
+
ordering: ClassVar = ['-id']
|
|
37
43
|
|
|
38
|
-
def content_body(self, obj):
|
|
44
|
+
def content_body(self, obj) -> str:
|
|
39
45
|
return str(obj)
|
|
40
46
|
|
|
41
47
|
content_body.short_description = 'Body'
|
|
42
48
|
|
|
43
|
-
def get_readonly_fields(self, request, obj=None):
|
|
49
|
+
def get_readonly_fields(self, request, obj=None) -> list[str]:
|
|
44
50
|
return [field.name for field in self.model._meta.fields]
|
|
45
51
|
|
django_spire/ai/chat/apps.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.apps import AppConfig
|
|
2
4
|
from django.conf import settings
|
|
3
5
|
|
|
@@ -22,6 +24,7 @@ class AiChatConfig(AppConfig):
|
|
|
22
24
|
|
|
23
25
|
def ready(self) -> None:
|
|
24
26
|
if not isinstance(getattr(settings, AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME), str):
|
|
25
|
-
|
|
27
|
+
message = f'"{AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME}" must be set in the django settings when using "{self.label}".'
|
|
28
|
+
raise TypeError(message)
|
|
26
29
|
|
|
27
30
|
check_required_apps(self.label)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django_spire.auth.controller.controller import BaseAuthController
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class BaseAiChatAuthController(BaseAuthController):
|
|
5
|
-
def can_delete(self):
|
|
7
|
+
def can_delete(self) -> bool:
|
|
6
8
|
return self.request.user.has_perm('django_spire_ai_chat.delete_chat')
|
django_spire/ai/chat/choices.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from
|
|
2
|
-
from dandy.map import Map
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
3
|
+
from typing import ClassVar
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
from dandy import Decoder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IntentDecoder(Decoder):
|
|
9
|
+
mapping_keys_description = 'The user\'s chat intent'
|
|
10
|
+
mapping: ClassVar = {}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dandy import Prompt
|
|
2
4
|
|
|
3
5
|
from django_spire.conf import settings
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
def organization_prompt():
|
|
8
|
+
def organization_prompt() -> Prompt:
|
|
7
9
|
return (
|
|
8
10
|
Prompt()
|
|
9
11
|
.text(f'You are a chat assistant for a company called "{settings.ORGANIZATION_NAME}".')
|
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from dandy
|
|
5
|
+
from dandy import Prompt, Bot
|
|
6
6
|
from dandy.recorder import recorder_to_html_file
|
|
7
|
-
from dandy.workflow import BaseWorkflow
|
|
8
|
-
from dandy.llm import LlmBot, MessageHistory, Prompt
|
|
9
|
-
from django.core.handlers.wsgi import WSGIRequest
|
|
10
7
|
|
|
11
|
-
from django_spire.ai.chat.intelligence.maps.intent_llm_map import
|
|
8
|
+
from django_spire.ai.chat.intelligence.maps.intent_llm_map import IntentDecoder
|
|
12
9
|
from django_spire.ai.chat.intelligence.prompts import organization_prompt
|
|
13
10
|
from django_spire.ai.chat.message_intel import DefaultMessageIntel
|
|
14
11
|
from django_spire.auth.controller.controller import AppAuthController
|
|
15
|
-
from django_spire.knowledge.intelligence.workflows.knowledge_workflow import \
|
|
16
|
-
KnowledgeWorkflow
|
|
17
|
-
|
|
18
12
|
|
|
19
13
|
if TYPE_CHECKING:
|
|
14
|
+
from dandy.llm.request.message import MessageHistory
|
|
15
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
16
|
+
|
|
20
17
|
from django_spire.ai.chat.message_intel import BaseMessageIntel
|
|
21
18
|
|
|
22
19
|
|
|
23
|
-
class SpireChatWorkflow
|
|
24
|
-
@
|
|
25
|
-
def
|
|
20
|
+
class SpireChatWorkflow:
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _generate_intent_decoder(request: WSGIRequest) -> IntentDecoder:
|
|
23
|
+
from django_spire.knowledge.intelligence.workflows.knowledge_workflow import KnowledgeWorkflow
|
|
24
|
+
|
|
26
25
|
intent_dict = {}
|
|
27
26
|
|
|
28
27
|
if AppAuthController(app_name='knowledge', request=request).can_view():
|
|
29
28
|
intent_message = (
|
|
30
29
|
'The user is looking for information or knowledge on something.'
|
|
31
30
|
)
|
|
31
|
+
|
|
32
32
|
intent_dict[intent_message] = KnowledgeWorkflow
|
|
33
33
|
|
|
34
34
|
intent_dict['None of the above choices match the user\'s intent'] = None
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
36
|
+
decoder = IntentDecoder()
|
|
37
|
+
decoder.mapping = intent_dict
|
|
38
|
+
return decoder
|
|
39
39
|
|
|
40
|
-
@
|
|
40
|
+
@staticmethod
|
|
41
41
|
@recorder_to_html_file('spire_chat_workflow')
|
|
42
42
|
def process(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
message_history: MessageHistory | None = None
|
|
43
|
+
request: WSGIRequest,
|
|
44
|
+
user_input: str,
|
|
45
|
+
message_history: MessageHistory | None = None
|
|
47
46
|
) -> BaseMessageIntel:
|
|
48
|
-
|
|
49
|
-
intents =
|
|
47
|
+
intent_decoder = SpireChatWorkflow._generate_intent_decoder(request)
|
|
48
|
+
intents = intent_decoder.process(user_input, max_return_values=1)
|
|
49
|
+
|
|
50
|
+
bot = Bot()
|
|
50
51
|
|
|
51
52
|
if intents[0] is None:
|
|
52
|
-
|
|
53
|
+
return bot.llm.prompt_to_intel(
|
|
53
54
|
prompt=user_input,
|
|
55
|
+
intel_class=DefaultMessageIntel,
|
|
54
56
|
message_history=message_history,
|
|
55
57
|
postfix_system_prompt=organization_prompt()
|
|
56
58
|
)
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
response = LlmBot.process(
|
|
60
|
+
return bot.llm.prompt_to_intel(
|
|
61
61
|
prompt=(
|
|
62
62
|
Prompt()
|
|
63
63
|
.text(f'User Input: {user_input}')
|
|
@@ -69,8 +69,7 @@ class SpireChatWorkflow(BaseWorkflow):
|
|
|
69
69
|
.line_break()
|
|
70
70
|
.text(intents[0].process(user_input))
|
|
71
71
|
),
|
|
72
|
+
intel_class=DefaultMessageIntel,
|
|
72
73
|
message_history=message_history,
|
|
73
74
|
postfix_system_prompt=organization_prompt()
|
|
74
75
|
)
|
|
75
|
-
|
|
76
|
-
return DefaultMessageIntel(text=response.text)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from abc import abstractmethod, ABC
|
|
2
4
|
|
|
3
|
-
from dandy
|
|
5
|
+
from dandy import BaseIntel
|
|
4
6
|
from django.template.loader import render_to_string
|
|
5
7
|
|
|
6
8
|
|
|
@@ -11,7 +13,8 @@ class BaseMessageIntel(BaseIntel, ABC):
|
|
|
11
13
|
super().__init_subclass__()
|
|
12
14
|
|
|
13
15
|
if cls._template is None or cls._template == '':
|
|
14
|
-
|
|
16
|
+
message = f'{cls.__module__}.{cls.__qualname__}._template must be set'
|
|
17
|
+
raise ValueError(message)
|
|
15
18
|
|
|
16
19
|
@abstractmethod
|
|
17
20
|
def content_to_str(self) -> str:
|
|
@@ -32,5 +35,5 @@ class DefaultMessageIntel(BaseMessageIntel):
|
|
|
32
35
|
_template: str = 'django_spire/ai/chat/message/default_message.html'
|
|
33
36
|
text: str
|
|
34
37
|
|
|
35
|
-
def content_to_str(self):
|
|
36
|
-
return self.text
|
|
38
|
+
def content_to_str(self) -> str:
|
|
39
|
+
return self.text
|
django_spire/ai/chat/models.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Type
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
4
|
-
from dandy.llm import MessageHistory
|
|
5
|
-
from dandy.llm.service.request.message import RoleLiteralStr
|
|
3
|
+
from dandy.llm.request.message import MessageHistory, RoleLiteralStr
|
|
6
4
|
from django.contrib.auth.models import User
|
|
7
5
|
from django.db import models
|
|
8
6
|
from django.utils.timezone import now
|
|
@@ -117,11 +115,11 @@ class ChatMessage(HistoryModelMixin):
|
|
|
117
115
|
@property
|
|
118
116
|
def intel(self):
|
|
119
117
|
try:
|
|
120
|
-
intel_class:
|
|
118
|
+
intel_class: type[BaseMessageIntel] = get_class_from_string(self._intel_class_name)
|
|
121
119
|
return intel_class.model_validate(self._intel_data)
|
|
122
120
|
|
|
123
121
|
except ImportError:
|
|
124
|
-
intel_class:
|
|
122
|
+
intel_class: type[BaseMessageIntel] = DefaultMessageIntel
|
|
125
123
|
return intel_class.model_validate(
|
|
126
124
|
{'text': str(self._intel_data)}
|
|
127
125
|
)
|
|
@@ -135,10 +133,11 @@ class ChatMessage(HistoryModelMixin):
|
|
|
135
133
|
def role(self) -> RoleLiteralStr:
|
|
136
134
|
if self.response_type == 'request':
|
|
137
135
|
return 'user'
|
|
138
|
-
|
|
136
|
+
|
|
137
|
+
if self.response_type == 'response':
|
|
139
138
|
return 'assistant'
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
|
|
140
|
+
return 'system'
|
|
142
141
|
|
|
143
142
|
def to_message_response(self) -> MessageResponse:
|
|
144
143
|
return MessageResponse(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.db.models import Q, Count
|
|
2
4
|
|
|
3
5
|
from django_spire.history.querysets import HistoryQuerySet
|
|
@@ -31,4 +33,4 @@ class ChatMessageQuerySet(HistoryQuerySet):
|
|
|
31
33
|
return self.order_by('-created_datetime')[:count]
|
|
32
34
|
|
|
33
35
|
def newest_by_count_reversed(self, count: int = 20):
|
|
34
|
-
return self.order_by('-created_datetime')[:count][::-1]
|
|
36
|
+
return self.order_by('-created_datetime')[:count][::-1]
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
4
6
|
from django.template.loader import render_to_string
|
|
5
7
|
|
|
6
8
|
from django_spire.ai.chat.choices import MessageResponseType
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from django_spire.ai.chat.message_intel import BaseMessageIntel
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
@dataclass
|
|
@@ -14,7 +20,7 @@ class MessageResponse:
|
|
|
14
20
|
message_intel: BaseMessageIntel
|
|
15
21
|
synthesis_speech: bool = False
|
|
16
22
|
|
|
17
|
-
def _render_template_to_html_string(self, template: str, context_data: dict = None) -> str:
|
|
23
|
+
def _render_template_to_html_string(self, template: str, context_data: dict[str, Any] | None = None) -> str:
|
|
18
24
|
return render_to_string(
|
|
19
25
|
template_name=template,
|
|
20
26
|
context={
|
|
@@ -25,35 +31,38 @@ class MessageResponse:
|
|
|
25
31
|
},
|
|
26
32
|
)
|
|
27
33
|
|
|
28
|
-
def render_to_html_string(self, context_data: dict = None) -> str:
|
|
34
|
+
def render_to_html_string(self, context_data: dict[str, Any] | None = None) -> str:
|
|
29
35
|
if self.type == MessageResponseType.REQUEST:
|
|
30
36
|
return self._render_template_to_html_string(
|
|
31
37
|
'django_spire/ai/chat/message/request_message.html',
|
|
32
38
|
context_data
|
|
33
39
|
)
|
|
34
|
-
|
|
40
|
+
|
|
41
|
+
if self.type == MessageResponseType.RESPONSE:
|
|
35
42
|
return self._render_template_to_html_string(
|
|
36
43
|
'django_spire/ai/chat/message/response_message.html',
|
|
37
44
|
context_data
|
|
38
45
|
)
|
|
39
|
-
|
|
46
|
+
|
|
47
|
+
if self.type == MessageResponseType.LOADING_RESPONSE:
|
|
40
48
|
return self._render_template_to_html_string(
|
|
41
49
|
'django_spire/ai/chat/message/loading_response_message.html',
|
|
42
50
|
context_data
|
|
43
51
|
)
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
|
|
53
|
+
return ''
|
|
46
54
|
|
|
47
55
|
|
|
48
56
|
@dataclass
|
|
49
57
|
class MessageResponseGroup:
|
|
50
|
-
message_responses:
|
|
58
|
+
message_responses: list[MessageResponse] = field(default_factory=list)
|
|
51
59
|
|
|
52
60
|
def add_message_response(self, message_response: MessageResponse) -> None:
|
|
53
61
|
self.message_responses.append(message_response)
|
|
54
62
|
|
|
55
|
-
def render_to_html_string(self, context_data: dict = None) -> str:
|
|
63
|
+
def render_to_html_string(self, context_data: dict[str, Any] | None = None) -> str:
|
|
56
64
|
html_string = ''
|
|
65
|
+
|
|
57
66
|
for message_response in self.message_responses:
|
|
58
67
|
html_string += message_response.render_to_html_string(context_data)
|
|
59
68
|
|
django_spire/ai/chat/tools.py
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
|
-
from dandy.llm import MessageHistory
|
|
4
|
-
from dandy.workflow import BaseWorkflow
|
|
5
6
|
from django.conf import settings
|
|
6
|
-
from django.core.handlers.wsgi import WSGIRequest
|
|
7
7
|
|
|
8
8
|
from django_spire.ai.chat.message_intel import BaseMessageIntel
|
|
9
9
|
from django_spire.ai.decorators import log_ai_interaction_from_recorder
|
|
10
10
|
from django_spire.consts import AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from dandy.llm.request.message import MessageHistory
|
|
14
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
def chat_workflow_process(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
request: WSGIRequest,
|
|
19
|
+
user_input: str | None = None,
|
|
20
|
+
message_history: MessageHistory | None = None
|
|
17
21
|
) -> BaseMessageIntel:
|
|
18
|
-
|
|
19
22
|
if user_input is None:
|
|
20
|
-
|
|
23
|
+
message = 'user_input is required'
|
|
24
|
+
raise ValueError(message)
|
|
21
25
|
|
|
22
26
|
chat_workflow_class = getattr(settings, AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME)
|
|
23
|
-
|
|
27
|
+
|
|
24
28
|
if chat_workflow_class is None:
|
|
25
|
-
|
|
29
|
+
message = f'"{AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME}" must be set in the django settings.'
|
|
30
|
+
raise ValueError(message)
|
|
26
31
|
|
|
27
32
|
module_name = '.'.join(chat_workflow_class.split('.')[:-1])
|
|
28
33
|
object_name = chat_workflow_class.split('.')[-1]
|
|
@@ -30,9 +35,10 @@ def chat_workflow_process(
|
|
|
30
35
|
try:
|
|
31
36
|
workflow_module = import_module(module_name)
|
|
32
37
|
except ImportError:
|
|
33
|
-
|
|
38
|
+
message = f'Could not import workflow module: {module_name}'
|
|
39
|
+
raise ImportError(message) from None
|
|
34
40
|
|
|
35
|
-
ChatWorkFlow
|
|
41
|
+
ChatWorkFlow = getattr(workflow_module, object_name)
|
|
36
42
|
|
|
37
43
|
@log_ai_interaction_from_recorder(request.user)
|
|
38
44
|
def run_workflow_process() -> BaseMessageIntel:
|
|
@@ -45,8 +51,7 @@ def chat_workflow_process(
|
|
|
45
51
|
output_intel = run_workflow_process()
|
|
46
52
|
|
|
47
53
|
if not issubclass(output_intel.__class__, BaseMessageIntel):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
54
|
+
message = f'{ChatWorkFlow.__class__.__module__}.{ChatWorkFlow.__class__.__qualname__}.process must return an instance of a {BaseMessageIntel.__name__} sub class.'
|
|
55
|
+
raise TypeError(message)
|
|
51
56
|
|
|
52
57
|
return output_intel
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
|
|
3
5
|
from django.conf import settings
|
|
4
6
|
from django.http import HttpResponse
|
|
5
7
|
|
|
6
|
-
from django_spire.ai.chat.message_intel import DefaultMessageIntel
|
|
7
8
|
from django_spire.ai.chat.models import Chat
|
|
8
9
|
from django_spire.ai.chat.responses import MessageResponse
|
|
9
10
|
from django_spire.ai.chat.choices import MessageResponseType
|
|
@@ -17,9 +18,8 @@ def response_message_render_view(request):
|
|
|
17
18
|
chat_workflow_name = getattr(settings, AI_CHAT_WORKFLOW_SENDER_SETTINGS_NAME)
|
|
18
19
|
|
|
19
20
|
if chat_workflow_name is None:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
21
|
+
message = f'"{AI_CHAT_WORKFLOW_SENDER_SETTINGS_NAME}" must be set in the django settings.'
|
|
22
|
+
raise ValueError(message)
|
|
23
23
|
|
|
24
24
|
chat = Chat.objects.by_user(request.user).get(id=body_data['chat_id'])
|
|
25
25
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
|
|
4
5
|
from django_spire.auth.controller.controller import AppAuthController
|
|
5
6
|
from django_spire.contrib import Breadcrumbs
|
|
6
7
|
from django_spire.contrib.generic_views import portal_views
|
|
7
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
11
|
+
from django.template.response import TemplateResponse
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
@AppAuthController('ai_chat').permission_required('can_delete')
|
|
10
15
|
def home_view(request: WSGIRequest) -> TemplateResponse:
|
django_spire/ai/decorators.py
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import traceback
|
|
3
5
|
import uuid
|
|
4
6
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from dandy import Recorder
|
|
7
10
|
from django.utils.timezone import now
|
|
8
|
-
from typing_extensions import Any
|
|
9
11
|
|
|
10
12
|
from django_spire.ai.models import AiInteraction, AiUsage
|
|
11
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.contrib.auth.models import AbstractBaseUser
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
def log_ai_interaction_from_recorder(
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
user: AbstractBaseUser | None = None,
|
|
20
|
+
actor: str | None = None
|
|
16
21
|
):
|
|
17
22
|
if user is None and actor is None:
|
|
18
|
-
|
|
23
|
+
message = 'user or actor must be provided'
|
|
24
|
+
raise ValueError(message)
|
|
19
25
|
|
|
20
26
|
def decorator(func):
|
|
21
27
|
def wrapper(*args, **kwargs):
|
|
@@ -48,7 +54,7 @@ def log_ai_interaction_from_recorder(
|
|
|
48
54
|
|
|
49
55
|
ai_interaction.stack_trace = stack_trace
|
|
50
56
|
|
|
51
|
-
raise
|
|
57
|
+
raise
|
|
52
58
|
|
|
53
59
|
finally:
|
|
54
60
|
Recorder.stop_recording(recording_uuid)
|
django_spire/ai/mixins.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from django.db import models
|
|
2
4
|
|
|
3
5
|
from django.contrib import admin
|
|
@@ -14,7 +16,7 @@ class AiUsageMixin(models.Model):
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class AiUsageAdminMixin(admin.ModelAdmin):
|
|
17
|
-
def run_time_seconds_formatted(self, obj):
|
|
19
|
+
def run_time_seconds_formatted(self, obj) -> str:
|
|
18
20
|
return f"{obj.run_time_seconds:.3f}s"
|
|
19
21
|
|
|
20
|
-
run_time_seconds_formatted.short_description = 'Run Time'
|
|
22
|
+
run_time_seconds_formatted.short_description = 'Run Time'
|
django_spire/ai/models.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth.models import User
|
|
2
4
|
from django.db import models
|
|
3
5
|
from django.utils.formats import localize
|
|
4
6
|
from django.utils.timezone import now
|
|
@@ -53,14 +55,17 @@ class AiInteraction(AiUsageMixin):
|
|
|
53
55
|
def __str__(self):
|
|
54
56
|
return f'"{self.actor}" interaction on "{localize(self.created_datetime)}"'
|
|
55
57
|
|
|
56
|
-
def save(self, *args, **kwargs):
|
|
58
|
+
def save(self, *args, **kwargs) -> None:
|
|
57
59
|
if self.user:
|
|
58
60
|
if self.actor is None:
|
|
59
61
|
self.actor = self.user.get_full_name()
|
|
62
|
+
|
|
60
63
|
if self.user_email is None:
|
|
61
64
|
self.user_email = self.user.email
|
|
65
|
+
|
|
62
66
|
if self.user_first_name is None:
|
|
63
67
|
self.user_first_name = self.user.first_name
|
|
68
|
+
|
|
64
69
|
if self.user_last_name is None:
|
|
65
70
|
self.user_last_name = self.user.last_name
|
|
66
71
|
|