django-spire 0.18.3__py3-none-any.whl → 0.19.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 (234) hide show
  1. django_spire/ai/admin.py +1 -0
  2. django_spire/ai/chat/apps.py +1 -8
  3. django_spire/ai/chat/intelligence/decoders/tools.py +29 -0
  4. django_spire/ai/chat/intelligence/prompts.py +13 -7
  5. django_spire/ai/chat/intelligence/workflows/chat_workflow.py +40 -52
  6. django_spire/ai/chat/models.py +2 -1
  7. django_spire/ai/chat/querysets.py +1 -3
  8. django_spire/ai/chat/responses.py +5 -0
  9. django_spire/ai/chat/templates/django_spire/ai/chat/card/chat_card.html +2 -2
  10. django_spire/ai/chat/templates/django_spire/ai/chat/dropdown/ellipsis_dropdown.html +1 -1
  11. django_spire/ai/chat/templates/django_spire/ai/chat/element/recent_chat_select_element.html +82 -44
  12. django_spire/ai/chat/templates/django_spire/ai/chat/message/loading_response_message.html +8 -3
  13. django_spire/ai/chat/templates/django_spire/ai/chat/message/message.html +16 -7
  14. django_spire/ai/chat/templates/django_spire/ai/chat/message/request_message.html +5 -8
  15. django_spire/ai/chat/templates/django_spire/ai/chat/message/response_message.html +14 -10
  16. django_spire/ai/chat/templates/django_spire/ai/chat/page/chat_page.html +35 -0
  17. django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +72 -23
  18. django_spire/ai/chat/templates/django_spire/ai/chat/widget/selection_widget.html +42 -0
  19. django_spire/ai/chat/tests/test_urls/test_json_urls.py +4 -2
  20. django_spire/ai/chat/urls/__init__.py +1 -1
  21. django_spire/ai/chat/urls/json_urls.py +1 -1
  22. django_spire/ai/chat/urls/page_urls.py +1 -1
  23. django_spire/ai/chat/urls/{template_urls.py → template/template_urls.py} +1 -1
  24. django_spire/ai/chat/views/json_views.py +4 -3
  25. django_spire/ai/chat/views/message_request_views.py +32 -14
  26. django_spire/ai/chat/views/message_response_views.py +14 -11
  27. django_spire/ai/chat/views/message_views.py +7 -1
  28. django_spire/ai/chat/views/page_views.py +2 -2
  29. django_spire/ai/chat/views/{template_views.py → template/template_views.py} +1 -7
  30. django_spire/ai/context/__init__.py +0 -0
  31. django_spire/ai/context/admin.py +15 -0
  32. django_spire/ai/context/apps.py +16 -0
  33. django_spire/ai/context/choices.py +11 -0
  34. django_spire/ai/context/intelligence/__init__.py +0 -0
  35. django_spire/ai/context/intelligence/prompts/__init__.py +0 -0
  36. django_spire/ai/context/intelligence/prompts/organization_prompts.py +19 -0
  37. django_spire/ai/context/migrations/0001_initial.py +67 -0
  38. django_spire/ai/context/migrations/__init__.py +0 -0
  39. django_spire/ai/context/models.py +67 -0
  40. django_spire/ai/context/querysets.py +15 -0
  41. django_spire/ai/context/seeding/__init__.py +0 -0
  42. django_spire/ai/context/seeding/seed.py +24 -0
  43. django_spire/ai/prompt/system/bots.py +3 -5
  44. django_spire/ai/prompt/tuning/bots.py +5 -10
  45. django_spire/ai/sms/admin.py +2 -0
  46. django_spire/ai/sms/apps.py +2 -7
  47. django_spire/ai/sms/decorators.py +2 -2
  48. django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +18 -18
  49. django_spire/ai/sms/tests/test_webhook.py +5 -4
  50. django_spire/ai/sms/urls.py +3 -0
  51. django_spire/ai/sms/views.py +3 -5
  52. django_spire/ai/urls.py +2 -0
  53. django_spire/auth/templates/django_spire/auth/form/login_form.html +25 -0
  54. django_spire/auth/templates/django_spire/auth/page/auth_page.html +2 -3
  55. django_spire/auth/templates/django_spire/auth/page/login_page.html +1 -25
  56. django_spire/conf.py +1 -3
  57. django_spire/consts.py +1 -7
  58. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +3 -2
  59. django_spire/contrib/utils.py +2 -0
  60. django_spire/core/static/django_spire/css/app-button.css +11 -1
  61. django_spire/core/static/django_spire/css/app-import.css +2 -0
  62. django_spire/core/static/django_spire/css/app-layout.css +15 -0
  63. django_spire/core/static/django_spire/css/app-navigation.css +24 -2
  64. django_spire/core/static/django_spire/css/app-page.css +8 -0
  65. django_spire/core/static/django_spire/css/app-side-panel.css +102 -0
  66. django_spire/core/static/django_spire/css/app-text.css +19 -1
  67. django_spire/core/static/django_spire/css/bootstrap-extension.css +128 -7
  68. django_spire/core/static/django_spire/css/bootstrap-override.css +161 -111
  69. django_spire/core/static/django_spire/css/themes/ayu/app-dark.css +0 -4
  70. django_spire/core/static/django_spire/css/themes/ayu/app-light.css +0 -4
  71. django_spire/core/static/django_spire/css/themes/catppuccin/app-dark.css +0 -4
  72. django_spire/core/static/django_spire/css/themes/catppuccin/app-light.css +0 -4
  73. django_spire/core/static/django_spire/css/themes/default/app-dark.css +0 -4
  74. django_spire/core/static/django_spire/css/themes/default/app-light.css +0 -4
  75. django_spire/core/static/django_spire/css/themes/dracula/app-dark.css +0 -4
  76. django_spire/core/static/django_spire/css/themes/dracula/app-light.css +0 -4
  77. django_spire/core/static/django_spire/css/themes/gruvbox/app-dark.css +0 -4
  78. django_spire/core/static/django_spire/css/themes/gruvbox/app-light.css +0 -4
  79. django_spire/core/static/django_spire/css/themes/material/app-dark.css +0 -4
  80. django_spire/core/static/django_spire/css/themes/material/app-light.css +0 -4
  81. django_spire/core/static/django_spire/css/themes/nord/app-dark.css +0 -4
  82. django_spire/core/static/django_spire/css/themes/nord/app-light.css +0 -4
  83. django_spire/core/static/django_spire/css/themes/oceanic-next/app-dark.css +0 -4
  84. django_spire/core/static/django_spire/css/themes/oceanic-next/app-light.css +0 -4
  85. django_spire/core/static/django_spire/css/themes/one-dark/app-dark.css +0 -4
  86. django_spire/core/static/django_spire/css/themes/one-dark/app-light.css +0 -4
  87. django_spire/core/static/django_spire/css/themes/palenight/app-dark.css +0 -4
  88. django_spire/core/static/django_spire/css/themes/palenight/app-light.css +0 -4
  89. django_spire/core/static/django_spire/css/themes/rose-pine/app-dark.css +0 -4
  90. django_spire/core/static/django_spire/css/themes/rose-pine/app-light.css +0 -4
  91. django_spire/core/static/django_spire/css/themes/synthwave/app-dark.css +0 -4
  92. django_spire/core/static/django_spire/css/themes/synthwave/app-light.css +0 -4
  93. django_spire/core/static/django_spire/css/themes/tokyo-night/app-dark.css +0 -4
  94. django_spire/core/static/django_spire/css/themes/tokyo-night/app-light.css +0 -4
  95. django_spire/core/static/django_spire/font/Poppins-Black.ttf +0 -0
  96. django_spire/core/static/django_spire/font/Poppins-BlackItalic.ttf +0 -0
  97. django_spire/core/static/django_spire/font/Poppins-Bold.ttf +0 -0
  98. django_spire/core/static/django_spire/font/Poppins-BoldItalic.ttf +0 -0
  99. django_spire/core/static/django_spire/font/Poppins-ExtraBold.ttf +0 -0
  100. django_spire/core/static/django_spire/font/Poppins-ExtraBoldItalic.ttf +0 -0
  101. django_spire/core/static/django_spire/font/Poppins-ExtraLight.ttf +0 -0
  102. django_spire/core/static/django_spire/font/Poppins-ExtraLightItalic.ttf +0 -0
  103. django_spire/core/static/django_spire/font/Poppins-Italic.ttf +0 -0
  104. django_spire/core/static/django_spire/font/Poppins-Light.ttf +0 -0
  105. django_spire/core/static/django_spire/font/Poppins-LightItalic.ttf +0 -0
  106. django_spire/core/static/django_spire/font/Poppins-Medium.ttf +0 -0
  107. django_spire/core/static/django_spire/font/Poppins-MediumItalic.ttf +0 -0
  108. django_spire/core/static/django_spire/font/Poppins-SemiBold.ttf +0 -0
  109. django_spire/core/static/django_spire/font/Poppins-SemiBoldItalic.ttf +0 -0
  110. django_spire/core/static/django_spire/font/Poppins-Thin.ttf +0 -0
  111. django_spire/core/static/django_spire/font/Poppins-ThinItalic.ttf +0 -0
  112. django_spire/core/static/django_spire/js/ui.js +12 -0
  113. django_spire/core/templates/django_spire/button/base_button.html +1 -1
  114. django_spire/core/templates/django_spire/button/primary_dark_outlined_button.html +3 -0
  115. django_spire/core/templates/django_spire/dropdown/element/dropdown_link_element.html +12 -8
  116. django_spire/core/templates/django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html +2 -1
  117. django_spire/core/templates/django_spire/dropdown/ellipsis_dropdown.html +2 -1
  118. django_spire/core/templates/django_spire/navigation/accordion/nav_accordion.html +1 -1
  119. django_spire/core/templates/django_spire/navigation/elements/nav_link.html +1 -1
  120. django_spire/core/templates/django_spire/navigation/elements/nav_title_divider.html +1 -1
  121. django_spire/core/templates/django_spire/navigation/side_navigation.html +52 -8
  122. django_spire/core/templates/django_spire/navigation/top_navigation.html +8 -2
  123. django_spire/core/templates/django_spire/page/full_page.html +218 -11
  124. django_spire/core/utils.py +26 -2
  125. django_spire/knowledge/collection/models.py +24 -0
  126. django_spire/knowledge/collection/services/transformation_service.py +2 -1
  127. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +44 -42
  128. django_spire/knowledge/collection/urls/form_urls.py +1 -0
  129. django_spire/knowledge/collection/urls/page_urls.py +1 -0
  130. django_spire/knowledge/collection/views/form_views.py +13 -2
  131. django_spire/knowledge/collection/views/page_views.py +36 -4
  132. django_spire/knowledge/entry/models.py +32 -0
  133. django_spire/knowledge/entry/services/transformation_services.py +10 -10
  134. django_spire/knowledge/entry/version/block/data/data.py +2 -1
  135. django_spire/knowledge/entry/version/block/data/heading_data.py +2 -2
  136. django_spire/knowledge/entry/version/block/data/list/data.py +3 -4
  137. django_spire/knowledge/entry/version/block/data/maps.py +3 -5
  138. django_spire/knowledge/entry/version/block/data/text_data.py +2 -2
  139. django_spire/knowledge/entry/version/block/models.py +10 -7
  140. django_spire/knowledge/entry/version/block/services/factory_service.py +11 -10
  141. django_spire/knowledge/entry/version/block/tests/factories.py +6 -5
  142. django_spire/knowledge/entry/version/converters/docx_converter.py +1 -1
  143. django_spire/knowledge/entry/version/intelligence/bots/markdown_format_llm_bot.py +14 -12
  144. django_spire/knowledge/entry/version/seeding/seeder.py +3 -3
  145. django_spire/knowledge/entry/version/services/processor_service.py +36 -10
  146. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +1 -1
  147. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +1 -1
  148. django_spire/knowledge/entry/version/urls/page_urls.py +1 -1
  149. django_spire/knowledge/entry/version/views/json_views.py +5 -3
  150. django_spire/knowledge/entry/version/views/page_views.py +11 -12
  151. django_spire/knowledge/entry/version/views/redirect_views.py +1 -1
  152. django_spire/knowledge/entry/views/form_views.py +1 -1
  153. django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +33 -11
  154. django_spire/knowledge/intelligence/decoders/__init__.py +0 -0
  155. django_spire/knowledge/intelligence/{maps/collection_map.py → decoders/collection_decoder.py} +5 -5
  156. django_spire/knowledge/intelligence/{maps/entry_map.py → decoders/entry_decoder.py} +3 -3
  157. django_spire/knowledge/intelligence/intel/collection_intel.py +1 -0
  158. django_spire/knowledge/intelligence/intel/entry_intel.py +5 -2
  159. django_spire/knowledge/intelligence/intel/message_intel.py +2 -0
  160. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +43 -51
  161. django_spire/knowledge/migrations/0007_alter_collection_options.py +17 -0
  162. django_spire/knowledge/static/django_spire/knowledge/entry/version/js/editor.js +24 -5
  163. django_spire/knowledge/templates/django_spire/knowledge/collection/card/top_level_list_card.html +21 -0
  164. django_spire/knowledge/templates/django_spire/knowledge/collection/component/x_collection_navigation.html +15 -6
  165. django_spire/knowledge/templates/django_spire/knowledge/collection/element/ellipsis_dropdown.html +22 -0
  166. django_spire/knowledge/templates/django_spire/knowledge/collection/form/form.html +21 -9
  167. django_spire/knowledge/templates/django_spire/knowledge/collection/item/collection_item.html +19 -0
  168. django_spire/knowledge/templates/django_spire/knowledge/collection/page/display_page.html +49 -0
  169. django_spire/knowledge/templates/django_spire/knowledge/collection/page/form_page.html +1 -1
  170. django_spire/knowledge/templates/django_spire/knowledge/container/container.html +33 -0
  171. django_spire/knowledge/templates/django_spire/knowledge/entry/file/page/list_page.html +1 -1
  172. django_spire/knowledge/templates/django_spire/knowledge/entry/item/list_item.html +2 -2
  173. django_spire/knowledge/templates/django_spire/knowledge/entry/page/form_page.html +1 -1
  174. django_spire/knowledge/templates/django_spire/knowledge/entry/page/import_form_page.html +1 -1
  175. django_spire/knowledge/templates/django_spire/knowledge/entry/version/container/detail_container.html +51 -45
  176. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/editor_page.html +54 -0
  177. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/form_page.html +1 -1
  178. django_spire/knowledge/templates/django_spire/knowledge/message/knowledge_message_intel.html +18 -1
  179. django_spire/knowledge/templates/django_spire/knowledge/page/full_page.html +37 -0
  180. django_spire/knowledge/templates/django_spire/knowledge/page/home_page.html +2 -2
  181. django_spire/knowledge/templates/django_spire/knowledge/sub_navigation/item/collection_sub_navigation_item.html +29 -0
  182. django_spire/knowledge/templates/django_spire/knowledge/sub_navigation/item/entry_sub_navigation_item.html +20 -0
  183. django_spire/knowledge/templates/django_spire/knowledge/{navigation/content/navigation_content.html → sub_navigation/widget/collection_entry_sub_navigation_widget.html} +42 -19
  184. django_spire/knowledge/views/page_views.py +4 -4
  185. django_spire/settings.py +3 -3
  186. django_spire/theme/templates/django_spire/theme/card/badges_preview_card.html +0 -1
  187. django_spire/theme/templates/django_spire/theme/card/base_preview_card.html +1 -0
  188. django_spire/theme/templates/django_spire/theme/card/typography_preview_card.html +1 -1
  189. django_spire/theme/templates/django_spire/theme/example/form/example_form.html +2 -0
  190. django_spire/theme/templates/django_spire/theme/example/form/example_form_card.html +5 -0
  191. django_spire/theme/templates/django_spire/theme/page/badges_page.html +7 -9
  192. django_spire/theme/templates/django_spire/theme/page/borders_page.html +6 -9
  193. django_spire/theme/templates/django_spire/theme/page/buttons_page.html +6 -9
  194. django_spire/theme/templates/django_spire/theme/page/colors_page.html +6 -6
  195. django_spire/theme/templates/django_spire/theme/page/dashboard_page.html +57 -0
  196. django_spire/theme/templates/django_spire/theme/page/django_glue_page.html +7 -9
  197. django_spire/theme/templates/django_spire/theme/page/theme_page.html +24 -0
  198. django_spire/theme/templates/django_spire/theme/page/typography_page.html +6 -9
  199. django_spire/theme/templates/django_spire/theme/section/badge_section_card.html +5 -0
  200. django_spire/theme/templates/django_spire/theme/section/border_section_card.html +5 -0
  201. django_spire/theme/templates/django_spire/theme/section/button_section.html +1 -0
  202. django_spire/theme/templates/django_spire/theme/section/button_section_card.html +5 -0
  203. django_spire/theme/templates/django_spire/theme/section/color_section.html +10 -10
  204. django_spire/theme/templates/django_spire/theme/section/color_section_card.html +5 -0
  205. django_spire/theme/templates/django_spire/theme/section/typography_section.html +41 -1
  206. django_spire/theme/templates/django_spire/theme/section/typography_section_card.html +5 -0
  207. {django_spire-0.18.3.dist-info → django_spire-0.19.0.dist-info}/METADATA +3 -3
  208. {django_spire-0.18.3.dist-info → django_spire-0.19.0.dist-info}/RECORD +217 -178
  209. django_spire/ai/chat/intelligence/bots/chat_bot.py +0 -8
  210. django_spire/ai/chat/intelligence/maps/intent_llm_map.py +0 -10
  211. django_spire/ai/chat/templates/django_spire/ai/chat/page/home_page.html +0 -9
  212. django_spire/ai/chat/templates/django_spire/ai/chat/widget/chat_widget.html +0 -64
  213. django_spire/ai/chat/templates/django_spire/ai/chat/widget/search_chat_results_widget.html +0 -25
  214. django_spire/ai/chat/templates/django_spire/ai/chat/widget/search_chat_widget.html +0 -39
  215. django_spire/ai/chat/templates/django_spire/ai/chat/widget/select_chat_widget.html +0 -24
  216. django_spire/ai/chat/tools.py +0 -57
  217. django_spire/ai/sms/tools.py +0 -57
  218. django_spire/core/static/django_spire/font/Karla-Bold.ttf +0 -0
  219. django_spire/core/static/django_spire/font/Merriweather.ttf +0 -0
  220. django_spire/knowledge/context_processors.py +0 -18
  221. django_spire/knowledge/templates/django_spire/knowledge/entry/version/page/detail_page.html +0 -25
  222. django_spire/knowledge/templates/django_spire/knowledge/navigation/card/navigation_card.html +0 -9
  223. django_spire/knowledge/templates/django_spire/knowledge/navigation/item/collection/collection_item.html +0 -29
  224. django_spire/knowledge/templates/django_spire/knowledge/navigation/item/entry/entry_item.html +0 -18
  225. django_spire/knowledge/templates/django_spire/knowledge/navigation/page/full_page.html +0 -90
  226. /django_spire/ai/chat/intelligence/{bots → decoders}/__init__.py +0 -0
  227. /django_spire/ai/chat/{intelligence/maps → urls/template}/__init__.py +0 -0
  228. /django_spire/{knowledge/intelligence/maps → ai/chat/views/template}/__init__.py +0 -0
  229. /django_spire/knowledge/entry/version/{constants.py → consts.py} +0 -0
  230. /django_spire/knowledge/templates/django_spire/knowledge/{navigation/item/collection/dropdown/navigation_ellipsis_dropdown.html → sub_navigation/element/collection_sub_navigation_ellipsis_dropdown.html} +0 -0
  231. /django_spire/knowledge/templates/django_spire/knowledge/{navigation/item/entry/dropdown/navigation_ellipsis_dropdown.html → sub_navigation/element/entry_sub_navigation_ellipsis_dropdown.html} +0 -0
  232. {django_spire-0.18.3.dist-info → django_spire-0.19.0.dist-info}/WHEEL +0 -0
  233. {django_spire-0.18.3.dist-info → django_spire-0.19.0.dist-info}/licenses/LICENSE.md +0 -0
  234. {django_spire-0.18.3.dist-info → django_spire-0.19.0.dist-info}/top_level.txt +0 -0
django_spire/ai/admin.py CHANGED
@@ -35,6 +35,7 @@ class AiUsageAdmin(AiUsageAdminMixin):
35
35
  was_successful: bool | None = None,
36
36
  ) -> str:
37
37
  was_successful_filter = '' if was_successful is None else f'&was_successful__exact={int(was_successful)}'
38
+
38
39
  url = (
39
40
  reverse("admin:django_spire_ai_aiinteraction_changelist")
40
41
  + "?"
@@ -1,9 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from django.apps import AppConfig
4
- from django.conf import settings
5
-
6
- from django_spire.consts import AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME
7
4
  from django_spire.utils import check_required_apps
8
5
 
9
6
 
@@ -20,11 +17,7 @@ class AiChatConfig(AppConfig):
20
17
  },
21
18
  )
22
19
 
23
- REQUIRED_APPS = ('django_spire_ai',)
20
+ REQUIRED_APPS = ('django_spire_ai', 'django_spire_ai_context')
24
21
 
25
22
  def ready(self) -> None:
26
- if not isinstance(getattr(settings, AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME), str):
27
- message = f'"{AI_CHAT_WORKFLOW_CLASS_SETTINGS_NAME}" must be set in the django settings when using "{self.label}".'
28
- raise TypeError(message)
29
-
30
23
  check_required_apps(self.label)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TYPE_CHECKING
4
+
5
+ from dandy import Decoder
6
+
7
+ from django_spire.auth.controller.controller import AppAuthController
8
+ from django_spire.knowledge.intelligence.workflows.knowledge_workflow import knowledge_search_workflow
9
+
10
+ if TYPE_CHECKING:
11
+ from django.core.handlers.wsgi import WSGIRequest
12
+
13
+
14
+ def generate_intent_decoder(
15
+ request: WSGIRequest,
16
+ default_callable: Callable | None = None
17
+ ) -> Decoder:
18
+ intent_dict = {}
19
+
20
+ if AppAuthController(app_name='knowledge', request=request).can_view():
21
+ intent_dict['The user is looking for information or knowledge on something.'] = knowledge_search_workflow
22
+
23
+ if default_callable is not None:
24
+ intent_dict['None of the above choices match the user\'s intent'] = default_callable
25
+
26
+ return Decoder(
27
+ mapping_keys_description='Intent of the User\'s Request',
28
+ mapping=intent_dict
29
+ )
@@ -2,18 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  from dandy import Prompt
4
4
 
5
- from django_spire.conf import settings
5
+ from django_spire.ai.context.intelligence.prompts.organization_prompts import (
6
+ organization_info_prompt
7
+ )
6
8
 
7
9
 
8
- def organization_prompt() -> Prompt:
10
+ def chat_system_prompt() -> Prompt:
9
11
  return (
10
12
  Prompt()
11
- .text(f'You are a chat assistant for a company called "{settings.ORGANIZATION_NAME}".')
13
+ .text(
14
+ 'You are a chat assistant, please use the organization information below for answering questions.'
15
+ )
12
16
  .line_break()
13
- .text(f'Organization Description: "{settings.ORGANIZATION_DESCRIPTION}"')
17
+ .prompt(organization_info_prompt())
14
18
  .line_break()
15
19
  .text('Make sure to follow the rules below')
16
- .list([
17
- 'Do not talk about being an AI at all to maintain the illusion of being a human assistant.',
18
- ])
20
+ .list(
21
+ [
22
+ 'Do not talk about being an AI at all to maintain the illusion of being a human assistant.',
23
+ ]
24
+ )
19
25
  )
@@ -1,71 +1,59 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Callable
4
4
 
5
- from dandy import Prompt, Bot
6
5
  from dandy.recorder import recorder_to_html_file
7
6
 
8
- from django_spire.ai.chat.intelligence.bots.chat_bot import ChatBot
9
- from django_spire.ai.chat.intelligence.maps.intent_llm_map import IntentDecoder
10
- from django_spire.ai.chat.intelligence.prompts import organization_prompt
11
- from django_spire.ai.chat.message_intel import DefaultMessageIntel
12
- from django_spire.auth.controller.controller import AppAuthController
7
+ from django_spire.ai.chat.intelligence.decoders.tools import generate_intent_decoder
8
+ from django_spire.ai.decorators import log_ai_interaction_from_recorder
9
+ from django_spire.conf import settings
10
+ from django_spire.core.utils import (
11
+ get_callable_from_module_string_and_validate_arguments,
12
+ )
13
+ from django_spire.ai.chat.message_intel import BaseMessageIntel
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from dandy.llm.request.message import MessageHistory
16
17
  from django.core.handlers.wsgi import WSGIRequest
17
18
 
18
- from django_spire.ai.chat.message_intel import BaseMessageIntel
19
19
 
20
-
21
- class SpireChatWorkflow:
22
- @staticmethod
23
- def _generate_intent_decoder(request: WSGIRequest) -> IntentDecoder:
24
- from django_spire.knowledge.intelligence.workflows.knowledge_workflow import KnowledgeWorkflow
25
-
26
- intent_dict = {}
27
-
28
- if AppAuthController(app_name='knowledge', request=request).can_view():
29
- intent_message = (
30
- 'The user is looking for information or knowledge on something.'
20
+ @recorder_to_html_file('spire_ai_chat_workflow')
21
+ def chat_workflow(
22
+ request: WSGIRequest,
23
+ user_input: str,
24
+ message_history: MessageHistory | None = None
25
+ ) -> BaseMessageIntel:
26
+ if settings.AI_CHAT_DEFAULT_CALLABLE is not None:
27
+ default_process = get_callable_from_module_string_and_validate_arguments(
28
+ module_string=settings.AI_CHAT_DEFAULT_CALLABLE,
29
+ valid_args=('request', 'user_input', 'message_history'),
31
30
  )
31
+ else:
32
+ default_process = None
32
33
 
33
- intent_dict[intent_message] = KnowledgeWorkflow
34
+ intent_decoder = generate_intent_decoder(
35
+ request=request,
36
+ default_callable=default_process,
37
+ )
34
38
 
35
- intent_dict['None of the above choices match the user\'s intent'] = None
39
+ intent_process = intent_decoder.process(user_input, max_return_values=1)[0]
36
40
 
37
- decoder = IntentDecoder()
38
- decoder.mapping = intent_dict
39
- return decoder
41
+ @log_ai_interaction_from_recorder(request.user)
42
+ def run_workflow_process(callable_: Callable) -> BaseMessageIntel | None:
43
+ return callable_(
44
+ request=request,
45
+ user_input=user_input,
46
+ message_history=message_history,
47
+ )
40
48
 
41
- @staticmethod
42
- @recorder_to_html_file('spire_chat_workflow')
43
- def process(
44
- request: WSGIRequest,
45
- user_input: str,
46
- message_history: MessageHistory | None = None
47
- ) -> BaseMessageIntel:
48
- intent_decoder = SpireChatWorkflow._generate_intent_decoder(request)
49
- intents = intent_decoder.process(user_input, max_return_values=1)
49
+ message_intel = run_workflow_process(intent_process)
50
50
 
51
- chat_bot = ChatBot()
51
+ if message_intel is None and default_process is not None:
52
+ message_intel = run_workflow_process(default_process)
52
53
 
53
- if intents[0] is None:
54
- return chat_bot.llm.prompt_to_intel(
55
- prompt=user_input,
56
- intel_class=DefaultMessageIntel,
57
- message_history=message_history,
58
- postfix_system_prompt=organization_prompt()
59
- )
54
+ if not issubclass(message_intel.__class__, BaseMessageIntel):
55
+ message = f'{intent_process.__qualname__} must return an instance of a {BaseMessageIntel.__name__} sub class.'
56
+ raise TypeError(message)
57
+
58
+ return message_intel
60
59
 
61
- return chat_bot.llm.prompt_to_intel(
62
- prompt=(
63
- Prompt()
64
- .text(f'User Input: {user_input}')
65
- .line_break()
66
- .text(intents[0].process(user_input))
67
- ),
68
- intel_class=DefaultMessageIntel,
69
- message_history=message_history,
70
- postfix_system_prompt=organization_prompt()
71
- )
@@ -143,7 +143,8 @@ class ChatMessage(HistoryModelMixin):
143
143
  return MessageResponse(
144
144
  type=MessageResponseType(self.response_type),
145
145
  sender=self.sender,
146
- message_intel=self.intel
146
+ message_intel=self.intel,
147
+ message_timestamp=self.created_datetime.strftime('%b %d, %Y at %I:%M %p')
147
148
  )
148
149
 
149
150
  class Meta:
@@ -7,9 +7,7 @@ from django_spire.history.querysets import HistoryQuerySet
7
7
 
8
8
  class ChatQuerySet(HistoryQuerySet):
9
9
  def by_user(self, user):
10
- return self.filter(
11
- user=user,
12
- )
10
+ return self.filter(user=user)
13
11
 
14
12
  def get_empty_or_create(self, user):
15
13
  try:
@@ -18,12 +18,14 @@ class MessageResponse:
18
18
  type: MessageResponseType
19
19
  sender: str
20
20
  message_intel: BaseMessageIntel
21
+ message_timestamp: str | None = None
21
22
  synthesis_speech: bool = False
22
23
 
23
24
  def _render_template_to_html_string(self, template: str, context_data: dict[str, Any] | None = None) -> str:
24
25
  return render_to_string(
25
26
  template_name=template,
26
27
  context={
28
+ 'message_timestamp': self.message_timestamp,
27
29
  'sender': self.sender,
28
30
  'message_intel': self.message_intel,
29
31
  'synthesis_speech': self.synthesis_speech,
@@ -63,6 +65,9 @@ class MessageResponseGroup:
63
65
  def render_to_html_string(self, context_data: dict[str, Any] | None = None) -> str:
64
66
  html_string = ''
65
67
 
68
+ context_data = context_data or {}
69
+ context_data['is_loading'] = context_data.get('is_loading', False)
70
+
66
71
  for message_response in self.message_responses:
67
72
  html_string += message_response.render_to_html_string(context_data)
68
73
 
@@ -6,8 +6,8 @@
6
6
 
7
7
  {% block card_content %}
8
8
  <div class="row">
9
- <div class="col-12 vh-minus-nav">
10
- {% include 'django_spire/ai/chat/widget/chat_widget.html' %}
9
+ <div class="col-12 vh-minus-top-nav">
10
+ {% include 'django_spire/ai/chat/widget/selection_widget.html' %}
11
11
  </div>
12
12
  </div>
13
13
  {% endblock %}
@@ -11,7 +11,7 @@
11
11
  {% endblock %}
12
12
 
13
13
  {% block dropdown_content %}
14
- {% include 'django_spire/dropdown/element/dropdown_link_element.html' with x_click="enable_rename_chat()" link_text='Rename' link_css='text-start' %}
14
+ {% include 'django_spire/dropdown/element/dropdown_link_element.html' with x_click="enable_rename_chat(); toggle_dropdown()" link_text='Rename' link_css='text-start' %}
15
15
 
16
16
  {% url 'django_spire:ai:chat:template:confirm_delete' pk=recent_chat.id as delete_url %}
17
17
  {% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=delete_url link_text='Delete' link_css='text-app-danger text-start' %}
@@ -1,84 +1,122 @@
1
1
  <div
2
- class="row mb-2"
2
+ class="mb-1"
3
3
  x-data="{
4
4
  can_rename_chat: false,
5
5
  chat_name: '{{ recent_chat.name|escapejs }}',
6
+ is_active: false,
6
7
  new_chat_name: '{{ recent_chat.name|escapejs }}',
7
8
  show_recent_chat: true,
8
9
 
10
+ cancel_rename_chat() {
11
+ this.new_chat_name = this.chat_name
12
+ this.can_rename_chat = false
13
+ },
14
+
15
+ confirm_rename_chat() {
16
+ this.rename_chat()
17
+ },
18
+
9
19
  disable_rename_chat() {
10
20
  this.can_rename_chat = false
11
21
  },
22
+
12
23
  enable_rename_chat() {
13
24
  this.can_rename_chat = true
25
+
26
+ $nextTick(() => {
27
+ let input = this.$el.querySelector('input')
28
+ if (input) input.focus()
29
+ })
14
30
  },
31
+
15
32
  async rename_chat() {
33
+ if (this.new_chat_name.trim() === '') {
34
+ this.new_chat_name = this.chat_name
35
+ this.can_rename_chat = false
36
+ return
37
+ }
38
+
16
39
  let response = await django_glue_fetch(
17
40
  '{% url "django_spire:ai:chat:json:rename" pk=recent_chat.pk %}',
18
41
  {payload: {new_name: this.new_chat_name}}
19
42
  )
20
43
 
21
44
  let type = response.type
45
+
22
46
  if (type === 'success') {
23
47
  this.chat_name = this.new_chat_name
24
48
  } else {
25
49
  $dispatch('notify', {'type': type, 'message': response.message})
50
+ this.new_chat_name = this.chat_name
26
51
  }
27
52
 
28
53
  this.can_rename_chat = false
29
54
  }
30
55
  }"
31
56
  x-show="show_recent_chat"
57
+ @chat-activated.window="is_active = ($event.detail.chat_id === {{ recent_chat.id }})"
58
+ @click.outside="if (can_rename_chat) cancel_rename_chat()"
32
59
  @deleted_chat.window="if (show_recent_chat) show_recent_chat = $event.detail !== {{ recent_chat.id }}"
33
60
  >
34
- <div class="col-12">
35
- <div class="d-flex flex-wrap justify-content-between btn shadow-sm btn-sm w-100 text-muted">
36
- <div
37
- x-show="!can_rename_chat"
38
- class="col cursor-pointer px-0 text-start"
39
- @click="() => {
40
- load_chat({{ recent_chat.id }})
41
- close_chat_select()
61
+ <div
62
+ :class="is_active ? 'bg-app-primary-soft' : ''"
63
+ class="d-flex align-items-center justify-content-between px-2 py-1 rounded"
64
+ data-chat-id="{{ recent_chat.id }}"
65
+ style="transition: background-color 0.15s ease;"
66
+ >
67
+ <div class="d-flex align-items-center flex-grow-1" style="min-width: 0; width: 0;">
68
+ <i
69
+ :class="{
70
+ 'bi-chat-dots-fill': !can_rename_chat,
71
+ 'bi-chat-dots': can_rename_chat,
72
+ 'text-app-primary': is_active || can_rename_chat,
73
+ 'text-muted': !is_active && !can_rename_chat
42
74
  }"
43
- >
44
- <i class="bi bi-chat-dots-fill"></i>
45
- <span x-text="chat_name.length > 24 ? chat_name.slice(0, 24) + '...' : chat_name"></span>
46
- </div>
47
- <div
48
- x-show="can_rename_chat"
49
- class="col px-0 text-start"
50
- >
51
- <div class="d-flex flex-wrap align-items-center">
52
- <i class="col-auto bi bi-chat-dots-fill"></i>
75
+ class="bi me-2"
76
+ style="flex-shrink: 0;"
77
+ ></i>
78
+
79
+ <template x-if="!can_rename_chat">
80
+ <div @click="$dispatch('load-chat', {chat_id: {{ recent_chat.id }}})" class="cursor-pointer flex-grow-1 text-truncate">
81
+ <span class="px-2 fs-7" x-text="chat_name"></span>
82
+ </div>
83
+ </template>
84
+
85
+ <template x-if="can_rename_chat">
86
+ <div class="d-flex align-items-center position-relative w-100 py-0 px-1" style="flex: 1 1 0; min-width: 0;">
53
87
  <input
54
- x-model="new_chat_name"
55
- class="col form-control d-inline p-0 mx-1 fs-7"
56
- type="text"
57
- style="max-width: 150px;"
58
- @keydown.enter="rename_chat()"
59
- @keydown.esc="disable_rename_chat()"
60
88
  @focus="$el.select()"
89
+ @keydown.enter="confirm_rename_chat()"
90
+ @keydown.esc="cancel_rename_chat()"
91
+ class="form-control ps-1 pe-5 py-0 fs-7"
92
+ type="text"
93
+ x-init="$el.focus()"
94
+ x-model="new_chat_name"
61
95
  />
62
- <i
63
- class="col-auto bi bi-check fs-5 btn-hover"
64
- @click="rename_chat()"
65
- title="Confirm"
66
- ></i>
67
- <i
68
- class="col-auto bi bi-x fs-5 btn-hover"
69
- @click="disable_rename_chat()"
70
- title="Cancel"
71
- ></i>
72
- </div>
73
- </div>
74
- {% if not recent_chat.is_empty %}
75
- <div
76
- x-show="!can_rename_chat"
77
- class="col-auto px-0 text-end"
78
- >
79
- {% include 'django_spire/ai/chat/dropdown/ellipsis_dropdown.html' %}
96
+ <div class="d-flex align-items-center position-absolute g-2" style="pointer-events: none; right: 8px;">
97
+ <i
98
+ @click="confirm_rename_chat()"
99
+ @mouseenter="$el.style.opacity = '0.75'"
100
+ @mouseleave="$el.style.opacity = '1'"
101
+ class="bi bi-check cursor-pointer text-success"
102
+ style="pointer-events: auto; transition: opacity 0.15s ease;"
103
+ title="Confirm"
104
+ ></i>
105
+ <i
106
+ @click="cancel_rename_chat()"
107
+ @mouseenter="$el.style.opacity = '0.75'"
108
+ @mouseleave="$el.style.opacity = '1'"
109
+ class="bi bi-x cursor-pointer text-danger pe-1"
110
+ style="pointer-events: auto; transition: opacity 0.15s ease;"
111
+ title="Cancel"
112
+ ></i>
113
+ </div>
80
114
  </div>
81
- {% endif %}
115
+ </template>
116
+ </div>
117
+
118
+ <div class="ms-2" style="flex-shrink: 0; visibility: hidden;" :style="can_rename_chat ? '' : 'visibility: visible;'">
119
+ {% include 'django_spire/ai/chat/dropdown/ellipsis_dropdown.html' %}
82
120
  </div>
83
121
  </div>
84
122
  </div>
@@ -1,11 +1,13 @@
1
1
  {% load spire_core_tags %}
2
2
 
3
3
  {% generate_id as response_message_intel_id %}
4
+
4
5
  <div
5
6
  x-data="{
6
7
  show_loading: false,
7
8
  show_reading: true,
8
9
  show_typing: false,
10
+
9
11
  init() {
10
12
  this.response_message.render_outer($refs.loading_response_message)
11
13
 
@@ -14,6 +16,7 @@
14
16
  setTimeout(() => {this.show_reading = false}, 4000)
15
17
  setTimeout(() => {this.show_typing = true}, 4000)
16
18
  },
19
+
17
20
  response_message: new ViewGlue(
18
21
  '{% url "django_spire:ai:chat:message:response:new" %}',
19
22
  {
@@ -27,18 +30,20 @@
27
30
  x-cloak
28
31
  >
29
32
  {{ message_intel.text|json_script:response_message_intel_id }}
33
+
30
34
  <div x-ref="loading_response_message" class="row mt-2 pb-3">
31
35
  <div class="col-auto align-self-center">
32
36
  <div class="row align-items-center">
33
37
  <div class="col-12 fs--1">
34
38
  {{ chat_workflow_name }}
35
39
  </div>
40
+
36
41
  <div class="col-12 ps-4 pt-2">
37
- <span class="spinner-border spinner-border-sm" role="status">
42
+ <span class="spinner-border spinner-border-sm text-muted" role="status">
38
43
  <span class="visually-hidden">Loading...</span>
39
44
  </span>
40
- <span x-show="show_reading">Reading...</span>
41
- <span x-show="show_typing">Typing...</span>
45
+ <span class="text-muted" x-show="show_reading">Reading...</span>
46
+ <span class="text-muted" x-show="show_typing">Typing...</span>
42
47
  </div>
43
48
  </div>
44
49
  </div>
@@ -2,29 +2,37 @@
2
2
 
3
3
  {% block message %}
4
4
  {% generate_id as message_intel_id %}
5
+
5
6
  <div
6
7
  class="col-auto mb-1 {{ message_class }}"
7
8
  @mouseover="show_message_menu = true"
8
9
  @mouseleave="show_message_menu = false"
9
10
  x-data="{
10
- show_message_menu: false,
11
11
  message_intel_str: '',
12
+ show_message_menu: false,
13
+
12
14
  init() {
13
15
  this.message_intel_str = JSON.parse(document.getElementById('{{ message_intel_id }}').textContent)
14
- {% if synthesis_speech %}this.start_speaking(this.message_intel_str){% endif %}
15
- $el.scrollIntoView({behavior: 'smooth'})
16
+ {% if synthesis_speech %}$dispatch('speak', {text: this.message_intel_str}){% endif %}
17
+ {% if not is_loading %}$el.scrollIntoView({behavior: 'smooth'}){% endif %}
16
18
  }
17
19
  }"
18
20
  >
19
21
  <div class="container-fluid">
20
22
  {{ message_intel.content_to_str|json_script:message_intel_id }}
23
+
21
24
  <div class="row">
22
- <div class="col fs--1 {{ sender_class }}">
25
+ <div class="col px-2 text-muted {{ sender_class }}" style="font-size: 0.7rem;">
23
26
  {% block message_sender %}
24
- {{ sender }}
27
+ <span class="fw-semibold">{{ sender }}</span>
25
28
  {% endblock %}
29
+
30
+ {% if message_timestamp %}
31
+ <span>- {{ message_timestamp }}</span>
32
+ {% endif %}
26
33
  </div>
27
34
  </div>
35
+
28
36
  <div class="row">
29
37
  <div class="col-12 px-3 py-2 rounded-3 shadow-sm {{ content_class|default:'border' }}">
30
38
  {% block message_content %}
@@ -32,17 +40,18 @@
32
40
  {% endblock %}
33
41
  </div>
34
42
  </div>
43
+
35
44
  <div
36
45
  class="row justify-content-end"
37
46
  :class="show_message_menu ? 'opacity-100' : 'opacity-0'"
38
47
  >
39
48
  <div class="col-auto px-0">
40
- <div @click="start_speaking(message_intel_str)" class="py-1 px-2 cursor-pointer">
49
+ <div @click="$dispatch('speak', {text: message_intel_str})" class="py-1 px-2 cursor-pointer">
41
50
  <i class="bi bi-soundwave"></i>
42
51
  </div>
43
52
  </div>
44
53
  <div class="col-auto pe-0 ps-1">
45
- <div @click="send_chat({message_body: message_intel_str})" class="py-1 px-2 cursor-pointer">
54
+ <div @click="$dispatch('send-message', {message_body: message_intel_str})" class="py-1 px-2 cursor-pointer">
46
55
  <i class="bi bi-repeat"></i>
47
56
  </div>
48
57
  </div>
@@ -1,14 +1,11 @@
1
- <div
2
- class="row justify-content-end ps-4 ps-lg-5"
3
- x-data="{
1
+ <div class="row justify-content-end ps-4 ps-lg-5" data-message-chat-id="{{ chat_id }}" {% if not is_loading %}x-data="{
4
2
  show_message: false,
3
+
5
4
  init() {
6
5
  setTimeout(() => this.show_message = true, 500)
7
6
  setTimeout(() => $el.scrollIntoView({behavior: 'smooth'}), 550)
8
7
  }
9
- }"
10
- x-show="show_message"
11
- x-transition
12
- >
13
- {% include message_intel.template with sender_class="text-end" speech_synthesis_class='justify-content-end' content_class="bg-app-layer-three" %}
8
+ }" x-show="show_message" x-transition{% endif %}>
9
+
10
+ {% include message_intel.template with sender_class="text-end" speech_synthesis_class='justify-content-end' content_class="bg-app-primary-soft" %}
14
11
  </div>
@@ -1,14 +1,18 @@
1
1
  <div
2
- x-data="{
3
- show_message: false,
4
- init() {
5
- setTimeout(() => this.show_message = true, 300)
6
- setTimeout(() => $el.scrollIntoView({behavior: 'smooth'}), 350)
7
- }
8
- }"
9
2
  class="row justify-content-start pe-4 pe-lg-5"
10
- x-show="show_message"
11
- x-transition
3
+ data-message-chat-id="{{ chat_id }}"
4
+ {% if not is_loading %}
5
+ x-data="{
6
+ show_message: false,
7
+
8
+ init() {
9
+ setTimeout(() => this.show_message = true, 300)
10
+ setTimeout(() => $el.scrollIntoView({behavior: 'smooth'}), 350)
11
+ }
12
+ }"
13
+ x-show="show_message"
14
+ x-transition
15
+ {% endif %}
12
16
  >
13
- {% include message_intel.template with content_class="bg-app-alt-layer-three" %}
17
+ {% include message_intel.template with content_class="bg-app-warning-soft" %}
14
18
  </div>
@@ -0,0 +1,35 @@
1
+ {% extends 'django_spire/page/full_page.html' %}
2
+
3
+ {% block full_page_sub_navigation_title %}
4
+ Chat History
5
+ {% endblock %}
6
+
7
+ {% block full_page_sub_navigation %}
8
+ {% include 'django_spire/ai/chat/widget/selection_widget.html' %}
9
+ {% endblock %}
10
+
11
+ {% block full_page_content %}
12
+ <div
13
+ class="row"
14
+ x-data="{
15
+ chat_load_ajax_view: new ViewGlue('{% url "django_spire:ai:chat:template:load" %}'),
16
+
17
+ init() {
18
+ this.load_chat(0)
19
+ },
20
+
21
+ async load_chat(chat_id) {
22
+ await this.chat_load_ajax_view.render_inner(this.$refs.chat_dialog, {
23
+ chat_id: chat_id
24
+ }
25
+ )
26
+
27
+ $dispatch('chat-loaded')
28
+ }
29
+ }"
30
+ @deleted_chat.window="load_chat(0)"
31
+ @load-chat.window="load_chat($event.detail.chat_id)"
32
+ >
33
+ <div class="col" x-ref="chat_dialog"></div>
34
+ </div>
35
+ {% endblock %}