django-spire 0.23.7__py3-none-any.whl → 0.23.8__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 +11 -11
- django_spire/ai/chat/apps.py +1 -0
- django_spire/ai/chat/templates/django_spire/ai/chat/widget/dialog_widget.html +1 -1
- django_spire/ai/chat/tests/factories.py +15 -0
- django_spire/ai/chat/tests/test_controller.py +45 -0
- django_spire/ai/chat/tests/test_models.py +301 -0
- django_spire/ai/chat/tests/test_prompts.py +48 -0
- django_spire/ai/chat/tests/test_responses.py +208 -0
- django_spire/ai/chat/tests/test_router/test_base_chat_router.py +66 -6
- django_spire/ai/chat/tests/test_router/test_chat_workflow.py +73 -3
- django_spire/ai/chat/tests/test_router/test_integration.py +86 -6
- django_spire/ai/chat/tests/test_router/test_intent_decoder.py +93 -1
- django_spire/ai/chat/tests/test_router/test_message_intel.py +60 -1
- django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +110 -0
- django_spire/ai/chat/tests/test_urls/test_json_urls.py +202 -1
- django_spire/ai/context/tests/__init__.py +0 -0
- django_spire/ai/context/tests/test_context.py +188 -0
- django_spire/ai/decorators.py +7 -6
- django_spire/ai/prompt/tests/test_bots.py +100 -10
- django_spire/ai/prompt/tests/test_prompt_intel.py +83 -0
- django_spire/ai/prompt/tests/test_prompt_tuning.py +126 -0
- django_spire/ai/sms/decorators.py +8 -2
- django_spire/ai/sms/tests/test_sms.py +240 -16
- django_spire/ai/sms/tests/test_sms_intel.py +42 -0
- django_spire/ai/sms/tests/test_webhook.py +155 -7
- django_spire/ai/sms/views.py +23 -24
- django_spire/ai/tests/test_ai.py +131 -7
- django_spire/auth/apps.py +4 -2
- django_spire/auth/controller/controller.py +36 -23
- django_spire/auth/controller/exceptions.py +9 -0
- django_spire/auth/group/admin.py +1 -0
- django_spire/auth/group/apps.py +2 -0
- django_spire/auth/group/factories.py +17 -8
- django_spire/auth/group/forms.py +7 -0
- django_spire/auth/group/tests/test_factories.py +146 -0
- django_spire/auth/group/tests/test_forms.py +282 -0
- django_spire/auth/group/tests/test_models.py +192 -0
- django_spire/auth/group/tests/test_querysets.py +98 -0
- django_spire/auth/group/tests/test_utils.py +341 -0
- django_spire/auth/group/tests/test_views.py +377 -0
- django_spire/auth/group/urls/__init__.py +3 -1
- django_spire/auth/group/urls/form_urls.py +2 -0
- django_spire/auth/group/urls/json_urls.py +3 -0
- django_spire/auth/group/urls/page_urls.py +2 -0
- django_spire/auth/group/utils.py +6 -2
- django_spire/auth/group/views/form_views.py +6 -3
- django_spire/auth/group/views/json_views.py +6 -2
- django_spire/auth/mfa/admin.py +2 -0
- django_spire/auth/mfa/apps.py +2 -0
- django_spire/auth/mfa/forms.py +1 -0
- django_spire/auth/mfa/querysets.py +9 -2
- django_spire/auth/mfa/tests/test_models.py +233 -0
- django_spire/auth/mfa/tests/test_utils.py +106 -0
- django_spire/auth/mfa/urls/__init__.py +2 -0
- django_spire/auth/mfa/urls/page_urls.py +2 -0
- django_spire/auth/mfa/urls/redirect_urls.py +2 -0
- django_spire/auth/mfa/views/page_views.py +2 -1
- django_spire/auth/permissions/consts.py +2 -2
- django_spire/auth/permissions/decorators.py +8 -8
- django_spire/auth/permissions/permissions.py +28 -35
- django_spire/auth/permissions/tests/test_decorators.py +333 -0
- django_spire/auth/permissions/tests/test_permissions.py +337 -0
- django_spire/auth/permissions/tests/test_tools.py +305 -0
- django_spire/auth/permissions/tools.py +21 -15
- django_spire/auth/seeding/seed.py +3 -0
- django_spire/auth/seeding/seeder.py +2 -0
- django_spire/auth/tests/test_controller.py +323 -0
- django_spire/auth/tests/test_url_endpoints.py +9 -9
- django_spire/auth/tests/test_views.py +406 -0
- django_spire/auth/urls/admin_urls.py +2 -0
- django_spire/auth/urls/redirect_urls.py +2 -0
- django_spire/auth/user/apps.py +2 -0
- django_spire/auth/user/forms.py +9 -0
- django_spire/auth/user/models.py +1 -1
- django_spire/auth/user/services/services.py +1 -0
- django_spire/auth/user/tests/factories.py +14 -13
- django_spire/auth/user/tests/test_factories.py +166 -2
- django_spire/auth/user/tests/test_forms.py +573 -0
- django_spire/auth/user/tests/test_models.py +257 -0
- django_spire/auth/user/tests/test_services.py +200 -0
- django_spire/auth/user/tests/test_tools.py +153 -0
- django_spire/auth/user/tests/test_user_factories.py +139 -0
- django_spire/auth/user/tests/test_views.py +363 -0
- django_spire/auth/user/tools.py +7 -1
- django_spire/auth/user/urls/form_urls.py +3 -0
- django_spire/auth/user/urls/page_urls.py +3 -0
- django_spire/auth/user/views/form_views.py +19 -10
- django_spire/auth/user/views/page_views.py +8 -2
- django_spire/auth/views/redirect_views.py +14 -9
- django_spire/comment/admin.py +2 -0
- django_spire/comment/apps.py +2 -0
- django_spire/comment/templatetags/comment_tags.py +1 -0
- django_spire/comment/tests/test_forms.py +27 -0
- django_spire/comment/tests/test_models.py +215 -0
- django_spire/comment/tests/test_querysets.py +101 -0
- django_spire/comment/tests/test_utils.py +90 -0
- django_spire/comment/urls.py +2 -0
- django_spire/comment/utils.py +22 -13
- django_spire/comment/views.py +1 -1
- django_spire/conf.py +8 -6
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/apps.py +2 -0
- django_spire/contrib/breadcrumb/breadcrumbs.py +18 -18
- django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py +198 -0
- django_spire/contrib/constructor/__init__.py +3 -3
- django_spire/contrib/constructor/constructor.py +15 -15
- django_spire/contrib/constructor/django_model_constructor.py +5 -4
- django_spire/contrib/constructor/exceptions.py +5 -3
- django_spire/contrib/constructor/tests/__init__.py +0 -0
- django_spire/contrib/constructor/tests/test_constructor.py +193 -0
- django_spire/contrib/form/tests/__init__.py +0 -0
- django_spire/contrib/form/tests/test_forms.py +203 -0
- django_spire/contrib/generic_views/modal_views.py +2 -1
- django_spire/contrib/generic_views/portal_views.py +20 -19
- django_spire/contrib/generic_views/tests/__init__.py +0 -0
- django_spire/contrib/generic_views/tests/test_views.py +459 -0
- django_spire/contrib/help/apps.py +2 -0
- django_spire/contrib/help/templatetags/help.py +1 -0
- django_spire/contrib/help/tests/__init__.py +0 -0
- django_spire/contrib/help/tests/test_templatetags.py +100 -0
- django_spire/contrib/options/mixins.py +6 -5
- django_spire/contrib/options/tests/factories.py +5 -1
- django_spire/contrib/options/tests/test_options.py +234 -0
- django_spire/contrib/ordering/exceptions.py +7 -3
- django_spire/contrib/ordering/mixins.py +2 -0
- django_spire/contrib/ordering/querysets.py +3 -1
- django_spire/contrib/ordering/services/processor_service.py +8 -4
- django_spire/contrib/ordering/services/service.py +1 -2
- django_spire/contrib/ordering/tests/__init__.py +0 -0
- django_spire/contrib/ordering/tests/test_ordering.py +165 -0
- django_spire/contrib/ordering/validators.py +6 -6
- django_spire/contrib/pagination/templatetags/pagination_tags.py +12 -5
- django_spire/contrib/pagination/tests/__init__.py +0 -0
- django_spire/contrib/pagination/tests/test_pagination.py +179 -0
- django_spire/contrib/performance/decorators.py +16 -6
- django_spire/contrib/performance/tests/__init__.py +0 -0
- django_spire/contrib/performance/tests/test_performance.py +107 -0
- django_spire/contrib/queryset/enums.py +3 -1
- django_spire/contrib/queryset/filter_tools.py +10 -5
- django_spire/contrib/queryset/mixins.py +16 -16
- django_spire/contrib/queryset/tests/__init__.py +0 -0
- django_spire/contrib/queryset/tests/test_queryset.py +137 -0
- django_spire/contrib/seeding/field/base.py +13 -7
- django_spire/contrib/seeding/field/callable.py +8 -1
- django_spire/contrib/seeding/field/cleaners.py +5 -5
- django_spire/contrib/seeding/field/custom.py +20 -10
- django_spire/contrib/seeding/field/django/seeder.py +8 -6
- django_spire/contrib/seeding/field/enums.py +7 -5
- django_spire/contrib/seeding/field/override.py +16 -6
- django_spire/contrib/seeding/field/static.py +9 -2
- django_spire/contrib/seeding/field/tests/test_base.py +18 -14
- django_spire/contrib/seeding/field/tests/test_callable.py +13 -9
- django_spire/contrib/seeding/field/tests/test_cleaners.py +51 -38
- django_spire/contrib/seeding/field/tests/test_static.py +13 -9
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +2 -0
- django_spire/contrib/seeding/intelligence/intel.py +5 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +6 -1
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +7 -1
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +6 -2
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +8 -2
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +2 -0
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +6 -1
- django_spire/contrib/seeding/management/commands/seeding.py +9 -3
- django_spire/contrib/seeding/management/example.py +2 -0
- django_spire/contrib/seeding/model/base.py +16 -7
- django_spire/contrib/seeding/model/config.py +31 -15
- django_spire/contrib/seeding/model/django/config.py +13 -13
- django_spire/contrib/seeding/model/django/seeder.py +4 -4
- django_spire/contrib/seeding/model/django/tests/test_seeder.py +34 -23
- django_spire/contrib/seeding/model/enums.py +2 -0
- django_spire/contrib/seeding/tests/test_config.py +71 -0
- django_spire/contrib/seeding/tests/test_custom.py +35 -0
- django_spire/contrib/seeding/tests/test_enums.py +40 -0
- django_spire/contrib/seeding/tests/test_intel.py +32 -0
- django_spire/contrib/seeding/tests/test_override.py +63 -0
- django_spire/contrib/service/__init__.py +2 -2
- django_spire/contrib/service/django_model_service.py +16 -15
- django_spire/contrib/service/exceptions.py +5 -3
- django_spire/contrib/service/tests/__init__.py +0 -0
- django_spire/contrib/service/tests/test_service.py +153 -0
- django_spire/contrib/session/apps.py +2 -0
- django_spire/contrib/session/controller.py +48 -42
- django_spire/contrib/session/templatetags/session_tags.py +11 -2
- django_spire/contrib/session/tests/test_session_controller.py +117 -53
- django_spire/contrib/tests/__init__.py +0 -0
- django_spire/contrib/tests/test_utils.py +37 -0
- django_spire/contrib/utils.py +4 -1
- django_spire/core/apps.py +2 -0
- django_spire/core/converters/tests/test_to_data.py +353 -0
- django_spire/core/converters/tests/test_to_enums.py +61 -41
- django_spire/core/converters/tests/test_to_pydantic.py +138 -109
- django_spire/core/converters/to_data.py +29 -10
- django_spire/core/converters/to_enums.py +4 -2
- django_spire/core/converters/to_pydantic.py +22 -22
- django_spire/core/decorators.py +19 -6
- django_spire/core/forms/widgets.py +4 -0
- django_spire/core/maps.py +3 -1
- django_spire/core/middleware/maintenance.py +3 -3
- django_spire/core/middleware.py +8 -6
- django_spire/core/redirect/__init__.py +5 -0
- django_spire/core/redirect/generic_redirect.py +1 -2
- django_spire/core/redirect/tests/__init__.py +0 -0
- django_spire/core/redirect/tests/test_generic_redirect.py +34 -0
- django_spire/core/{tests/tests_redirect.py → redirect/tests/test_safe_redirect.py} +55 -81
- django_spire/core/shortcuts.py +3 -3
- django_spire/core/static/django_spire/css/app-layout.css +1 -1
- django_spire/core/static/django_spire/css/app-navigation.css +3 -3
- django_spire/core/static/django_spire/css/bootstrap-override.css +4 -0
- django_spire/core/tag/admin.py +12 -0
- django_spire/core/tag/intelligence/tag_set_bot.py +2 -0
- django_spire/core/tag/mixins.py +2 -0
- django_spire/core/tag/models.py +2 -0
- django_spire/core/tag/querysets.py +2 -0
- django_spire/core/tag/service/tag_service.py +6 -3
- django_spire/core/tag/tests/test_intelligence.py +9 -9
- django_spire/core/tag/tests/test_tags.py +44 -54
- django_spire/core/tag/tests/test_tools.py +191 -0
- django_spire/core/tag/tools.py +3 -0
- django_spire/core/templates/django_spire/card/card.html +5 -2
- django_spire/core/templates/django_spire/card/title_card.html +9 -4
- django_spire/core/templates/django_spire/infinite_scroll/base.html +1 -0
- django_spire/core/templates/django_spire/navigation/side_navigation.html +19 -24
- django_spire/core/templates/django_spire/page/full_page.html +46 -16
- django_spire/core/templates/django_spire/table/base.html +4 -2
- django_spire/core/templatetags/json.py +6 -2
- django_spire/core/templatetags/message.py +13 -8
- django_spire/core/templatetags/string_formating.py +8 -5
- django_spire/core/templatetags/tests/__init__.py +0 -0
- django_spire/core/templatetags/tests/test_templatetags.py +427 -0
- django_spire/core/templatetags/variable_types.py +17 -9
- django_spire/core/tests/test_cases.py +1 -1
- django_spire/core/tests/test_conf.py +43 -0
- django_spire/core/tests/test_consts.py +28 -0
- django_spire/core/tests/test_context_processors.py +93 -0
- django_spire/core/tests/test_decorators.py +95 -0
- django_spire/core/tests/test_django_spire_utils.py +56 -0
- django_spire/core/tests/test_exceptions.py +37 -0
- django_spire/core/tests/test_models.py +54 -0
- django_spire/core/tests/test_settings.py +45 -0
- django_spire/core/tests/test_shortcuts.py +74 -0
- django_spire/core/tests/test_urls.py +16 -0
- django_spire/core/tests/test_utils.py +58 -0
- django_spire/core/urls.py +4 -1
- django_spire/core/utils.py +12 -8
- django_spire/exceptions.py +16 -1
- django_spire/file/admin.py +4 -2
- django_spire/file/apps.py +8 -10
- django_spire/file/fields.py +7 -7
- django_spire/file/forms.py +1 -1
- django_spire/file/interfaces.py +15 -15
- django_spire/file/mixins.py +1 -4
- django_spire/file/models.py +3 -5
- django_spire/file/tests/factories.py +59 -0
- django_spire/file/tests/test_admin.py +69 -0
- django_spire/file/tests/test_apps.py +24 -0
- django_spire/file/tests/test_fields.py +114 -0
- django_spire/file/tests/test_forms.py +20 -0
- django_spire/file/tests/test_interfaces.py +183 -0
- django_spire/file/tests/test_models.py +82 -0
- django_spire/file/tests/test_querysets.py +102 -0
- django_spire/file/tests/test_utils.py +32 -0
- django_spire/file/tests/test_views.py +145 -0
- django_spire/file/tests/test_widgets.py +82 -0
- django_spire/file/tools.py +8 -2
- django_spire/file/views.py +7 -3
- django_spire/file/widgets.py +12 -12
- django_spire/help_desk/admin.py +15 -0
- django_spire/help_desk/apps.py +2 -0
- django_spire/help_desk/auth/controller.py +2 -0
- django_spire/help_desk/choices.py +2 -0
- django_spire/help_desk/enums.py +2 -0
- django_spire/help_desk/exceptions.py +31 -3
- django_spire/help_desk/forms.py +2 -0
- django_spire/help_desk/models.py +2 -0
- django_spire/help_desk/querysets.py +4 -1
- django_spire/help_desk/services/notification_service.py +26 -27
- django_spire/help_desk/services/service.py +2 -3
- django_spire/help_desk/tests/factories.py +8 -3
- django_spire/help_desk/tests/test_admin.py +41 -0
- django_spire/help_desk/tests/test_apps.py +41 -0
- django_spire/help_desk/tests/test_choices.py +50 -0
- django_spire/help_desk/tests/test_controller.py +87 -0
- django_spire/help_desk/tests/test_enums.py +18 -0
- django_spire/help_desk/tests/test_exceptions.py +37 -0
- django_spire/help_desk/tests/test_forms.py +89 -0
- django_spire/help_desk/tests/test_models.py +59 -0
- django_spire/help_desk/tests/test_querysets.py +38 -0
- django_spire/help_desk/tests/test_services/test_notification_service.py +15 -8
- django_spire/help_desk/tests/test_services/test_service.py +92 -0
- django_spire/help_desk/tests/test_urls/test_form_urls.py +6 -6
- django_spire/help_desk/tests/test_urls/test_page_urls.py +8 -9
- django_spire/help_desk/tests/test_views/test_form_views.py +46 -19
- django_spire/help_desk/tests/test_views/test_page_views.py +32 -9
- django_spire/help_desk/urls/__init__.py +4 -1
- django_spire/help_desk/urls/form_urls.py +3 -0
- django_spire/help_desk/urls/page_urls.py +3 -0
- django_spire/help_desk/views/form_views.py +13 -5
- django_spire/help_desk/views/page_views.py +11 -3
- django_spire/history/activity/admin.py +2 -0
- django_spire/history/activity/apps.py +3 -1
- django_spire/history/activity/mixins.py +13 -7
- django_spire/history/activity/models.py +6 -5
- django_spire/history/activity/querysets.py +2 -0
- django_spire/history/activity/tests/__init__.py +0 -0
- django_spire/history/activity/tests/test_activity.py +176 -0
- django_spire/history/admin.py +9 -2
- django_spire/history/choices.py +3 -0
- django_spire/history/models.py +5 -5
- django_spire/history/tests/test_admin.py +93 -0
- django_spire/history/tests/test_history.py +101 -0
- django_spire/history/tests/test_mixins.py +84 -0
- django_spire/history/viewed/admin.py +3 -1
- django_spire/history/viewed/apps.py +3 -1
- django_spire/history/viewed/models.py +2 -0
- django_spire/history/viewed/tests/__init__.py +0 -0
- django_spire/history/viewed/tests/test_viewed.py +46 -0
- django_spire/knowledge/auth/tests/__init__.py +0 -0
- django_spire/knowledge/auth/tests/test_controller.py +116 -0
- django_spire/knowledge/collection/admin.py +5 -1
- django_spire/knowledge/collection/models.py +3 -1
- django_spire/knowledge/collection/seeding/seed.py +1 -0
- django_spire/knowledge/collection/services/factory_service.py +10 -11
- django_spire/knowledge/collection/services/ordering_service.py +1 -2
- django_spire/knowledge/collection/services/service.py +5 -10
- django_spire/knowledge/collection/services/tag_service.py +5 -2
- django_spire/knowledge/collection/tests/factories.py +28 -1
- django_spire/knowledge/collection/tests/test_models.py +48 -0
- django_spire/knowledge/collection/tests/test_querysets.py +93 -0
- django_spire/knowledge/collection/tests/test_services/test_factory_service.py +100 -0
- django_spire/knowledge/collection/tests/test_services/test_services.py +160 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +21 -3
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +39 -1
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +12 -4
- django_spire/knowledge/collection/urls/__init__.py +3 -0
- django_spire/knowledge/collection/urls/form_urls.py +2 -0
- django_spire/knowledge/collection/urls/json_urls.py +2 -0
- django_spire/knowledge/collection/urls/page_urls.py +2 -0
- django_spire/knowledge/collection/views/form_views.py +4 -4
- django_spire/knowledge/collection/views/json_views.py +5 -1
- django_spire/knowledge/collection/views/page_views.py +5 -2
- django_spire/knowledge/entry/admin.py +7 -1
- django_spire/knowledge/entry/forms.py +2 -0
- django_spire/knowledge/entry/models.py +2 -0
- django_spire/knowledge/entry/seeding/seed.py +3 -0
- django_spire/knowledge/entry/services/automation_service.py +5 -4
- django_spire/knowledge/entry/services/factory_service.py +7 -5
- django_spire/knowledge/entry/services/service.py +4 -7
- django_spire/knowledge/entry/services/tag_service.py +0 -1
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +1 -5
- django_spire/knowledge/entry/tests/factories.py +1 -2
- django_spire/knowledge/entry/tests/test_factory_service.py +20 -0
- django_spire/knowledge/entry/tests/test_models.py +41 -0
- django_spire/knowledge/entry/tests/test_querysets.py +71 -0
- django_spire/knowledge/entry/tests/test_services.py +94 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +9 -14
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +48 -5
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +6 -8
- django_spire/knowledge/entry/tests/test_urls/test_template_urls.py +40 -0
- django_spire/knowledge/entry/urls/form_urls.py +2 -0
- django_spire/knowledge/entry/urls/json_urls.py +2 -0
- django_spire/knowledge/entry/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/urls/template_urls.py +2 -0
- django_spire/knowledge/entry/version/block/choices.py +2 -0
- django_spire/knowledge/entry/version/block/data/data.py +1 -0
- django_spire/knowledge/entry/version/block/data/list/data.py +8 -13
- django_spire/knowledge/entry/version/block/data/list/maps.py +3 -0
- django_spire/knowledge/entry/version/block/data/list/meta.py +1 -2
- django_spire/knowledge/entry/version/block/data/list/tests/__init__.py +0 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_maps.py +32 -0
- django_spire/knowledge/entry/version/block/data/list/tests/test_meta.py +58 -0
- django_spire/knowledge/entry/version/block/data/maps.py +3 -6
- django_spire/knowledge/entry/version/block/models.py +7 -5
- django_spire/knowledge/entry/version/block/seeding/constants.py +5 -4
- django_spire/knowledge/entry/version/block/services/service.py +2 -3
- django_spire/knowledge/entry/version/block/tests/factories.py +4 -10
- django_spire/knowledge/entry/version/block/tests/test_choices.py +56 -0
- django_spire/knowledge/entry/version/block/tests/test_data.py +90 -0
- django_spire/knowledge/entry/version/block/tests/test_maps.py +37 -0
- django_spire/knowledge/entry/version/block/tests/test_models.py +55 -0
- django_spire/knowledge/entry/version/block/tests/test_querysets.py +35 -0
- django_spire/knowledge/entry/version/block/tests/test_services.py +65 -0
- django_spire/knowledge/entry/version/choices.py +2 -0
- django_spire/knowledge/entry/version/converters/converter.py +1 -1
- django_spire/knowledge/entry/version/converters/docx_converter.py +4 -7
- django_spire/knowledge/entry/version/converters/markdown_converter.py +20 -20
- django_spire/knowledge/entry/version/maps.py +4 -5
- django_spire/knowledge/entry/version/querysets.py +1 -1
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -2
- django_spire/knowledge/entry/version/services/processor_service.py +5 -4
- django_spire/knowledge/entry/version/services/service.py +1 -2
- django_spire/knowledge/entry/version/tests/factories.py +2 -2
- django_spire/knowledge/entry/version/tests/test_choices.py +18 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +56 -8
- django_spire/knowledge/entry/version/tests/test_converters/test_markdown_converter.py +78 -0
- django_spire/knowledge/entry/version/tests/test_maps.py +58 -0
- django_spire/knowledge/entry/version/tests/test_models.py +23 -0
- django_spire/knowledge/entry/version/tests/test_querysets.py +26 -0
- django_spire/knowledge/entry/version/tests/test_services.py +62 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +27 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +15 -8
- django_spire/knowledge/entry/version/tests/test_urls/test_redirect_urls.py +38 -0
- django_spire/knowledge/entry/version/urls/__init__.py +3 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +2 -1
- django_spire/knowledge/entry/version/urls/page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +5 -1
- django_spire/knowledge/entry/version/views/page_views.py +10 -3
- django_spire/knowledge/entry/version/views/redirect_views.py +5 -1
- django_spire/knowledge/entry/views/form_views.py +16 -8
- django_spire/knowledge/entry/views/json_views.py +3 -1
- django_spire/knowledge/entry/views/page_views.py +8 -2
- django_spire/knowledge/entry/views/template_views.py +7 -1
- django_spire/knowledge/exceptions.py +2 -1
- django_spire/knowledge/intelligence/intel/answer_intel.py +2 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +0 -1
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +4 -5
- django_spire/knowledge/models.py +1 -2
- django_spire/knowledge/tests/__init__.py +0 -0
- django_spire/knowledge/tests/test_templatetags.py +40 -0
- django_spire/knowledge/tests/test_urls/__init__.py +0 -0
- django_spire/knowledge/tests/test_urls/test_page_urls.py +24 -0
- django_spire/knowledge/urls/__init__.py +2 -0
- django_spire/knowledge/urls/page_urls.py +2 -0
- django_spire/knowledge/views/page_views.py +8 -3
- django_spire/notification/admin.py +3 -1
- django_spire/notification/app/admin.py +2 -0
- django_spire/notification/app/apps.py +3 -1
- django_spire/notification/app/exceptions.py +9 -2
- django_spire/notification/app/models.py +8 -4
- django_spire/notification/app/processor.py +22 -26
- django_spire/notification/app/querysets.py +2 -0
- django_spire/notification/app/tests/__init__.py +0 -0
- django_spire/notification/app/tests/factories.py +34 -0
- django_spire/notification/app/tests/test_apps.py +24 -0
- django_spire/notification/app/tests/test_models.py +72 -0
- django_spire/notification/app/tests/test_processor.py +111 -0
- django_spire/notification/app/tests/test_querysets.py +90 -0
- django_spire/notification/app/tests/test_views/__init__.py +0 -0
- django_spire/notification/app/tests/test_views/test_json_views.py +48 -0
- django_spire/notification/app/tests/test_views/test_page_views.py +19 -0
- django_spire/notification/app/urls/__init__.py +3 -1
- django_spire/notification/app/urls/json_urls.py +6 -4
- django_spire/notification/app/urls/page_urls.py +4 -3
- django_spire/notification/app/urls/template_urls.py +4 -2
- django_spire/notification/apps.py +4 -1
- django_spire/notification/email/admin.py +5 -1
- django_spire/notification/email/apps.py +3 -1
- django_spire/notification/email/exceptions.py +4 -2
- django_spire/notification/email/helper.py +5 -3
- django_spire/notification/email/models.py +4 -0
- django_spire/notification/email/processor.py +19 -15
- django_spire/notification/email/querysets.py +3 -0
- django_spire/notification/email/tests/__init__.py +0 -0
- django_spire/notification/email/tests/factories.py +35 -0
- django_spire/notification/email/tests/test_apps.py +24 -0
- django_spire/notification/email/tests/test_models.py +52 -0
- django_spire/notification/email/tests/test_processor.py +92 -0
- django_spire/notification/email/tests/test_querysets.py +43 -0
- django_spire/notification/exceptions.py +17 -2
- django_spire/notification/managers.py +7 -1
- django_spire/notification/maps.py +4 -1
- django_spire/notification/mixins.py +2 -0
- django_spire/notification/models.py +3 -1
- django_spire/notification/processors/notification.py +12 -5
- django_spire/notification/processors/processor.py +2 -0
- django_spire/notification/processors/tests/__init__.py +0 -0
- django_spire/notification/processors/tests/test_notification.py +106 -0
- django_spire/notification/push/admin.py +10 -1
- django_spire/notification/push/apps.py +3 -1
- django_spire/notification/push/models.py +2 -3
- django_spire/notification/push/tests/__init__.py +0 -0
- django_spire/notification/push/tests/test_apps.py +24 -0
- django_spire/notification/push/tests/test_models.py +28 -0
- django_spire/notification/querysets.py +7 -1
- django_spire/notification/sms/admin.py +2 -0
- django_spire/notification/sms/apps.py +4 -1
- django_spire/notification/sms/automations.py +2 -0
- django_spire/notification/sms/choices.py +2 -0
- django_spire/notification/sms/exceptions.py +19 -5
- django_spire/notification/sms/helper.py +33 -23
- django_spire/notification/sms/models.py +5 -1
- django_spire/notification/sms/processor.py +20 -20
- django_spire/notification/sms/querysets.py +2 -0
- django_spire/notification/sms/tests/factories.py +33 -0
- django_spire/notification/sms/tests/test_apps.py +24 -0
- django_spire/notification/sms/tests/test_automation.py +38 -0
- django_spire/notification/sms/tests/test_choices.py +15 -0
- django_spire/notification/sms/tests/test_consts.py +17 -0
- django_spire/notification/sms/tests/test_exceptions.py +27 -0
- django_spire/notification/sms/tests/test_helper.py +50 -0
- django_spire/notification/sms/tests/test_models.py +81 -0
- django_spire/notification/sms/tests/test_processor.py +107 -0
- django_spire/notification/sms/tests/test_tools.py +25 -11
- django_spire/notification/sms/tools.py +16 -5
- django_spire/notification/sms/urls/__init__.py +3 -1
- django_spire/notification/sms/urls/media_urls.py +2 -0
- django_spire/notification/sms/views/media_views.py +14 -4
- django_spire/notification/tests/__init__.py +0 -0
- django_spire/notification/tests/factories.py +26 -0
- django_spire/notification/tests/test_admin.py +55 -0
- django_spire/notification/tests/test_apps.py +30 -0
- django_spire/notification/tests/test_automation.py +18 -0
- django_spire/notification/tests/test_choices.py +59 -0
- django_spire/notification/tests/test_exceptions.py +58 -0
- django_spire/notification/tests/test_managers.py +100 -0
- django_spire/notification/tests/test_maps.py +31 -0
- django_spire/notification/tests/test_models.py +76 -0
- django_spire/notification/tests/test_querysets.py +184 -0
- django_spire/notification/tests/test_utils.py +23 -0
- django_spire/notification/urls.py +3 -1
- django_spire/notification/utils.py +3 -1
- django_spire/settings.py +3 -0
- django_spire/theme/tests/test_context_processor.py +15 -13
- django_spire/theme/tests/test_enums.py +2 -2
- django_spire/theme/tests/test_filesystem.py +2 -5
- django_spire/theme/tests/test_integration.py +12 -12
- django_spire/theme/tests/test_model.py +40 -38
- django_spire/theme/tests/test_views/test_json_views.py +33 -33
- django_spire/theme/urls/json_urls.py +3 -0
- django_spire/theme/urls/page_urls.py +3 -0
- django_spire/urls.py +19 -15
- django_spire/utils.py +13 -4
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/METADATA +1 -1
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/RECORD +530 -358
- django_spire/contrib/options/tests/test_unit.py +0 -148
- django_spire/contrib/seeding/tests/test_seeding.py +0 -25
- django_spire/core/tests/test_templatetags.py +0 -117
- django_spire/core/tests/tests_shortcuts.py +0 -73
- django_spire/history/activity/tests.py +0 -3
- django_spire/history/activity/views.py +0 -3
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +0 -71
- django_spire/notification/app/tests.py +0 -3
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/WHEEL +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.7.dist-info → django_spire-0.23.8.dist-info}/top_level.txt +0 -0
|
@@ -4,6 +4,7 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
+
from dandy.llm.request.message import MessageHistory
|
|
7
8
|
from django.test import RequestFactory
|
|
8
9
|
|
|
9
10
|
from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
|
|
@@ -11,7 +12,6 @@ from django_spire.ai.chat.router import BaseChatRouter
|
|
|
11
12
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
|
-
from dandy.llm.request.message import MessageHistory
|
|
15
15
|
from django.core.handlers.wsgi import WSGIRequest
|
|
16
16
|
|
|
17
17
|
|
|
@@ -53,7 +53,12 @@ class TestBaseChatRouter(BaseTestCase):
|
|
|
53
53
|
|
|
54
54
|
def test_process_validates_workflow_return_type(self) -> None:
|
|
55
55
|
class InvalidRouter(BaseChatRouter):
|
|
56
|
-
def workflow(
|
|
56
|
+
def workflow(
|
|
57
|
+
self,
|
|
58
|
+
request: WSGIRequest,
|
|
59
|
+
user_input: str,
|
|
60
|
+
message_history: MessageHistory | None = None
|
|
61
|
+
) -> str:
|
|
57
62
|
return 'Invalid return type'
|
|
58
63
|
|
|
59
64
|
router = InvalidRouter()
|
|
@@ -69,7 +74,12 @@ class TestBaseChatRouter(BaseTestCase):
|
|
|
69
74
|
|
|
70
75
|
def test_process_handles_none_return(self) -> None:
|
|
71
76
|
class NoneRouter(BaseChatRouter):
|
|
72
|
-
def workflow(
|
|
77
|
+
def workflow(
|
|
78
|
+
self,
|
|
79
|
+
request: WSGIRequest,
|
|
80
|
+
user_input: str,
|
|
81
|
+
message_history: MessageHistory | None = None
|
|
82
|
+
) -> None:
|
|
73
83
|
return None
|
|
74
84
|
|
|
75
85
|
router = NoneRouter()
|
|
@@ -83,12 +93,15 @@ class TestBaseChatRouter(BaseTestCase):
|
|
|
83
93
|
assert result.text == 'I apologize, but I was unable to process your request.'
|
|
84
94
|
|
|
85
95
|
def test_process_accepts_all_parameters(self) -> None:
|
|
86
|
-
from dandy.llm.request.message import MessageHistory
|
|
87
|
-
|
|
88
96
|
class ParamTestRouter(BaseChatRouter):
|
|
89
97
|
received_params = {}
|
|
90
98
|
|
|
91
|
-
def workflow(
|
|
99
|
+
def workflow(
|
|
100
|
+
self,
|
|
101
|
+
request: WSGIRequest,
|
|
102
|
+
user_input: str,
|
|
103
|
+
message_history: MessageHistory | None = None
|
|
104
|
+
) -> DefaultMessageIntel:
|
|
92
105
|
self.received_params = {
|
|
93
106
|
'request': request,
|
|
94
107
|
'user_input': user_input,
|
|
@@ -108,3 +121,50 @@ class TestBaseChatRouter(BaseTestCase):
|
|
|
108
121
|
assert router.received_params['request'] == self.request
|
|
109
122
|
assert router.received_params['user_input'] == 'Test input'
|
|
110
123
|
assert router.received_params['message_history'] == message_history
|
|
124
|
+
|
|
125
|
+
def test_process_returns_base_message_intel_subclass(self) -> None:
|
|
126
|
+
class CustomMessageIntel(BaseMessageIntel):
|
|
127
|
+
_template: str = 'django_spire/ai/chat/message/default_message.html'
|
|
128
|
+
custom_field: str
|
|
129
|
+
|
|
130
|
+
def render_to_str(self) -> str:
|
|
131
|
+
return self.custom_field
|
|
132
|
+
|
|
133
|
+
class CustomRouter(BaseChatRouter):
|
|
134
|
+
def workflow(
|
|
135
|
+
self,
|
|
136
|
+
request: WSGIRequest,
|
|
137
|
+
user_input: str,
|
|
138
|
+
message_history: MessageHistory | None = None
|
|
139
|
+
) -> CustomMessageIntel:
|
|
140
|
+
return CustomMessageIntel(custom_field='Custom value')
|
|
141
|
+
|
|
142
|
+
router = CustomRouter()
|
|
143
|
+
result = router.process(
|
|
144
|
+
request=self.request,
|
|
145
|
+
user_input='Hello',
|
|
146
|
+
message_history=None
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
assert isinstance(result, CustomMessageIntel)
|
|
150
|
+
assert result.custom_field == 'Custom value'
|
|
151
|
+
|
|
152
|
+
def test_process_with_empty_user_input(self) -> None:
|
|
153
|
+
class EmptyInputRouter(BaseChatRouter):
|
|
154
|
+
def workflow(
|
|
155
|
+
self,
|
|
156
|
+
request: WSGIRequest,
|
|
157
|
+
user_input: str,
|
|
158
|
+
message_history: MessageHistory | None = None
|
|
159
|
+
) -> DefaultMessageIntel:
|
|
160
|
+
return DefaultMessageIntel(text=f'Received: {user_input}')
|
|
161
|
+
|
|
162
|
+
router = EmptyInputRouter()
|
|
163
|
+
result = router.process(
|
|
164
|
+
request=self.request,
|
|
165
|
+
user_input='',
|
|
166
|
+
message_history=None
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert isinstance(result, DefaultMessageIntel)
|
|
170
|
+
assert result.text == 'Received: '
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
from unittest.mock import Mock, patch
|
|
4
5
|
|
|
6
|
+
from dandy.llm.request.message import MessageHistory
|
|
5
7
|
from django.test import RequestFactory, override_settings
|
|
6
8
|
|
|
7
9
|
from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
|
|
@@ -9,9 +11,17 @@ from django_spire.ai.chat.message_intel import DefaultMessageIntel
|
|
|
9
11
|
from django_spire.ai.chat.router import BaseChatRouter
|
|
10
12
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
11
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
class MockRouter(BaseChatRouter):
|
|
14
|
-
def workflow(
|
|
19
|
+
def workflow(
|
|
20
|
+
self,
|
|
21
|
+
request: WSGIRequest,
|
|
22
|
+
user_input: str,
|
|
23
|
+
message_history: MessageHistory | None = None
|
|
24
|
+
) -> DefaultMessageIntel:
|
|
15
25
|
return DefaultMessageIntel(text='Mock response')
|
|
16
26
|
|
|
17
27
|
|
|
@@ -95,8 +105,6 @@ class TestChatWorkflow(BaseTestCase):
|
|
|
95
105
|
}
|
|
96
106
|
)
|
|
97
107
|
def test_workflow_passes_message_history(self) -> None:
|
|
98
|
-
from dandy.llm.request.message import MessageHistory
|
|
99
|
-
|
|
100
108
|
message_history = MessageHistory()
|
|
101
109
|
|
|
102
110
|
with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
|
|
@@ -111,3 +119,65 @@ class TestChatWorkflow(BaseTestCase):
|
|
|
111
119
|
mock_process.assert_called_once()
|
|
112
120
|
call_kwargs = mock_process.call_args[1]
|
|
113
121
|
assert call_kwargs['message_history'] == message_history
|
|
122
|
+
|
|
123
|
+
@override_settings(
|
|
124
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='TEST',
|
|
125
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
126
|
+
'TEST': 'django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter'
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
def test_workflow_passes_request(self) -> None:
|
|
130
|
+
with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
|
|
131
|
+
mock_process.return_value = DefaultMessageIntel(text='Response')
|
|
132
|
+
|
|
133
|
+
chat_workflow(
|
|
134
|
+
request=self.request,
|
|
135
|
+
user_input='Hello',
|
|
136
|
+
message_history=None
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
mock_process.assert_called_once()
|
|
140
|
+
call_kwargs = mock_process.call_args[1]
|
|
141
|
+
assert call_kwargs['request'] == self.request
|
|
142
|
+
|
|
143
|
+
@override_settings(
|
|
144
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='TEST',
|
|
145
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
146
|
+
'TEST': 'django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter'
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
def test_workflow_passes_user_input(self) -> None:
|
|
150
|
+
with patch('django_spire.ai.chat.tests.test_router.test_chat_workflow.MockRouter.process') as mock_process:
|
|
151
|
+
mock_process.return_value = DefaultMessageIntel(text='Response')
|
|
152
|
+
|
|
153
|
+
chat_workflow(
|
|
154
|
+
request=self.request,
|
|
155
|
+
user_input='Test user input',
|
|
156
|
+
message_history=None
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
mock_process.assert_called_once()
|
|
160
|
+
call_kwargs = mock_process.call_args[1]
|
|
161
|
+
assert call_kwargs['user_input'] == 'Test user input'
|
|
162
|
+
|
|
163
|
+
@override_settings(
|
|
164
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={}
|
|
165
|
+
)
|
|
166
|
+
def test_workflow_uses_fallback_router_path(self) -> None:
|
|
167
|
+
with patch('django_spire.ai.chat.intelligence.workflows.chat_workflow.get_callable_from_module_string_and_validate_arguments') as mock_get_callable:
|
|
168
|
+
mock_router_class = Mock()
|
|
169
|
+
mock_router_instance = Mock()
|
|
170
|
+
mock_router_instance.process.return_value = DefaultMessageIntel(text='Fallback')
|
|
171
|
+
mock_router_class.return_value = mock_router_instance
|
|
172
|
+
mock_get_callable.return_value = mock_router_class
|
|
173
|
+
|
|
174
|
+
chat_workflow(
|
|
175
|
+
request=self.request,
|
|
176
|
+
user_input='Hello',
|
|
177
|
+
message_history=None
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
mock_get_callable.assert_called_once_with(
|
|
181
|
+
'django_spire.ai.chat.router.SpireChatRouter',
|
|
182
|
+
[]
|
|
183
|
+
)
|
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
from unittest.mock import Mock, patch
|
|
4
5
|
|
|
6
|
+
from dandy.llm.request.message import MessageHistory
|
|
5
7
|
from django.contrib.auth.models import Permission, User
|
|
6
8
|
from django.test import RequestFactory, override_settings
|
|
7
9
|
|
|
10
|
+
from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
|
|
8
11
|
from django_spire.ai.chat.intelligence.workflows.chat_workflow import chat_workflow
|
|
9
12
|
from django_spire.ai.chat.message_intel import BaseMessageIntel, DefaultMessageIntel
|
|
10
13
|
from django_spire.ai.chat.router import BaseChatRouter
|
|
11
14
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
12
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
class KnowledgeRouter(BaseChatRouter):
|
|
15
|
-
def workflow(
|
|
21
|
+
def workflow(
|
|
22
|
+
self,
|
|
23
|
+
request: WSGIRequest,
|
|
24
|
+
user_input: str,
|
|
25
|
+
message_history: MessageHistory | None = None
|
|
26
|
+
) -> DefaultMessageIntel:
|
|
16
27
|
return DefaultMessageIntel(text='Knowledge search result')
|
|
17
28
|
|
|
18
29
|
|
|
19
30
|
class SupportRouter(BaseChatRouter):
|
|
20
|
-
def workflow(
|
|
31
|
+
def workflow(
|
|
32
|
+
self,
|
|
33
|
+
request: WSGIRequest,
|
|
34
|
+
user_input: str,
|
|
35
|
+
message_history: MessageHistory | None = None
|
|
36
|
+
) -> DefaultMessageIntel:
|
|
21
37
|
return DefaultMessageIntel(text='Support response')
|
|
22
38
|
|
|
23
39
|
|
|
@@ -95,8 +111,6 @@ class TestRouterIntegration(BaseTestCase):
|
|
|
95
111
|
}
|
|
96
112
|
)
|
|
97
113
|
def test_intent_excluded_without_permission(self) -> None:
|
|
98
|
-
from django_spire.ai.chat.intelligence.decoders.intent_decoder import generate_intent_decoder
|
|
99
|
-
|
|
100
114
|
regular_user = User.objects.create_user(username='regular', password='test')
|
|
101
115
|
|
|
102
116
|
request = self.factory.get('/')
|
|
@@ -113,8 +127,6 @@ class TestRouterIntegration(BaseTestCase):
|
|
|
113
127
|
}
|
|
114
128
|
)
|
|
115
129
|
def test_end_to_end_workflow(self) -> None:
|
|
116
|
-
from dandy.llm.request.message import MessageHistory
|
|
117
|
-
|
|
118
130
|
message_history = MessageHistory()
|
|
119
131
|
message_history.add_message(role='user', content='Previous message')
|
|
120
132
|
|
|
@@ -145,3 +157,71 @@ class TestRouterIntegration(BaseTestCase):
|
|
|
145
157
|
)
|
|
146
158
|
|
|
147
159
|
assert isinstance(result, DefaultMessageIntel)
|
|
160
|
+
|
|
161
|
+
@override_settings(
|
|
162
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='KNOWLEDGE',
|
|
163
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
164
|
+
'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
|
|
165
|
+
'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
def test_router_selection_by_key(self) -> None:
|
|
169
|
+
result = chat_workflow(
|
|
170
|
+
request=self.request,
|
|
171
|
+
user_input='Test',
|
|
172
|
+
message_history=None
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assert isinstance(result, DefaultMessageIntel)
|
|
176
|
+
assert result.text == 'Knowledge search result'
|
|
177
|
+
|
|
178
|
+
@override_settings(
|
|
179
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SUPPORT',
|
|
180
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
181
|
+
'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
def test_workflow_with_none_message_history(self) -> None:
|
|
185
|
+
result = chat_workflow(
|
|
186
|
+
request=self.request,
|
|
187
|
+
user_input='Test input',
|
|
188
|
+
message_history=None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert isinstance(result, DefaultMessageIntel)
|
|
192
|
+
assert result.text == 'Support response'
|
|
193
|
+
|
|
194
|
+
@override_settings(
|
|
195
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='SUPPORT',
|
|
196
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
197
|
+
'SUPPORT': 'django_spire.ai.chat.tests.test_router.test_integration.SupportRouter',
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
def test_workflow_preserves_user_input(self) -> None:
|
|
201
|
+
test_input = 'This is a specific test input'
|
|
202
|
+
|
|
203
|
+
with patch.object(SupportRouter, 'workflow', wraps=SupportRouter().workflow) as mock_workflow:
|
|
204
|
+
mock_workflow.return_value = DefaultMessageIntel(text='Response')
|
|
205
|
+
|
|
206
|
+
chat_workflow(
|
|
207
|
+
request=self.request,
|
|
208
|
+
user_input=test_input,
|
|
209
|
+
message_history=None
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@override_settings(
|
|
213
|
+
DJANGO_SPIRE_AI_DEFAULT_CHAT_ROUTER='KNOWLEDGE',
|
|
214
|
+
DJANGO_SPIRE_AI_CHAT_ROUTERS={
|
|
215
|
+
'KNOWLEDGE': 'django_spire.ai.chat.tests.test_router.test_integration.KnowledgeRouter',
|
|
216
|
+
},
|
|
217
|
+
DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={}
|
|
218
|
+
)
|
|
219
|
+
def test_workflow_without_intent_routers(self) -> None:
|
|
220
|
+
result = chat_workflow(
|
|
221
|
+
request=self.request,
|
|
222
|
+
user_input='Test',
|
|
223
|
+
message_history=None
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert isinstance(result, DefaultMessageIntel)
|
|
227
|
+
assert result.text == 'Knowledge search result'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
3
4
|
from unittest.mock import Mock
|
|
4
5
|
|
|
5
6
|
from django.contrib.auth.models import Permission, User
|
|
@@ -10,15 +11,25 @@ from django_spire.ai.chat.message_intel import DefaultMessageIntel
|
|
|
10
11
|
from django_spire.ai.chat.router import BaseChatRouter
|
|
11
12
|
from django_spire.core.tests.test_cases import BaseTestCase
|
|
12
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from dandy.llm.request.message import MessageHistory
|
|
16
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
class TestRouter(BaseChatRouter):
|
|
15
|
-
def workflow(
|
|
20
|
+
def workflow(
|
|
21
|
+
self,
|
|
22
|
+
request: WSGIRequest,
|
|
23
|
+
user_input: str,
|
|
24
|
+
message_history: MessageHistory | None = None
|
|
25
|
+
) -> DefaultMessageIntel:
|
|
16
26
|
return DefaultMessageIntel(text='Test response')
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
class TestIntentDecoder(BaseTestCase):
|
|
20
30
|
def setUp(self) -> None:
|
|
21
31
|
super().setUp()
|
|
32
|
+
|
|
22
33
|
self.factory = RequestFactory()
|
|
23
34
|
self.request = self.factory.get('/')
|
|
24
35
|
self.request.user = self.super_user
|
|
@@ -139,3 +150,84 @@ class TestIntentDecoder(BaseTestCase):
|
|
|
139
150
|
assert 'First intent' in decoder.mapping
|
|
140
151
|
assert 'Second intent' in decoder.mapping
|
|
141
152
|
assert len(decoder.mapping) == 2
|
|
153
|
+
|
|
154
|
+
@override_settings(
|
|
155
|
+
DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
|
|
156
|
+
'TEST_INTENT': {
|
|
157
|
+
'INTENT_DESCRIPTION': 'Test intent',
|
|
158
|
+
'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
def test_decoder_mapping_keys_description(self) -> None:
|
|
163
|
+
decoder = generate_intent_decoder(
|
|
164
|
+
request=self.request,
|
|
165
|
+
default_callable=None
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert decoder.mapping_keys_description == "Intent of the User's Request"
|
|
169
|
+
|
|
170
|
+
@override_settings(
|
|
171
|
+
DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
|
|
172
|
+
'TEST_INTENT': {
|
|
173
|
+
'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
def test_decoder_with_missing_intent_description(self) -> None:
|
|
178
|
+
decoder = generate_intent_decoder(
|
|
179
|
+
request=self.request,
|
|
180
|
+
default_callable=None
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert '' in decoder.mapping
|
|
184
|
+
|
|
185
|
+
@override_settings(
|
|
186
|
+
DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
|
|
187
|
+
'NO_ROUTER': {
|
|
188
|
+
'INTENT_DESCRIPTION': 'No router intent',
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
def test_decoder_with_missing_chat_router(self) -> None:
|
|
193
|
+
decoder = generate_intent_decoder(
|
|
194
|
+
request=self.request,
|
|
195
|
+
default_callable=None
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert 'No router intent' not in decoder.mapping
|
|
199
|
+
|
|
200
|
+
def test_decoder_with_none_default_callable(self) -> None:
|
|
201
|
+
decoder = generate_intent_decoder(
|
|
202
|
+
request=self.request,
|
|
203
|
+
default_callable=None
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert "None of the above choices match the user's intent" not in decoder.mapping
|
|
207
|
+
|
|
208
|
+
@override_settings(
|
|
209
|
+
DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS={
|
|
210
|
+
'INTENT_A': {
|
|
211
|
+
'INTENT_DESCRIPTION': 'Intent A',
|
|
212
|
+
'REQUIRED_PERMISSION': 'auth.add_user',
|
|
213
|
+
'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
|
|
214
|
+
},
|
|
215
|
+
'INTENT_B': {
|
|
216
|
+
'INTENT_DESCRIPTION': 'Intent B',
|
|
217
|
+
'CHAT_ROUTER': 'django_spire.ai.chat.tests.test_router.test_intent_decoder.TestRouter',
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
def test_decoder_mixed_permission_intents(self) -> None:
|
|
222
|
+
regular_user = User.objects.create_user(username='mixed_user', password='test')
|
|
223
|
+
|
|
224
|
+
request = self.factory.get('/')
|
|
225
|
+
request.user = regular_user
|
|
226
|
+
|
|
227
|
+
decoder = generate_intent_decoder(
|
|
228
|
+
request=request,
|
|
229
|
+
default_callable=None
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
assert 'Intent A' not in decoder.mapping
|
|
233
|
+
assert 'Intent B' in decoder.mapping
|
|
@@ -8,8 +8,8 @@ from django_spire.core.tests.test_cases import BaseTestCase
|
|
|
8
8
|
|
|
9
9
|
class CustomMessageIntel(BaseMessageIntel):
|
|
10
10
|
_template: str = 'django_spire/ai/chat/message/default_message.html'
|
|
11
|
-
text: str
|
|
12
11
|
extra_data: str = 'Extra'
|
|
12
|
+
text: str
|
|
13
13
|
|
|
14
14
|
def render_to_str(self) -> str:
|
|
15
15
|
return f'{self.text} - {self.extra_data}'
|
|
@@ -18,6 +18,7 @@ class CustomMessageIntel(BaseMessageIntel):
|
|
|
18
18
|
class TestMessageIntel(BaseTestCase):
|
|
19
19
|
def test_default_message_intel_has_template(self) -> None:
|
|
20
20
|
intel = DefaultMessageIntel(text='Test')
|
|
21
|
+
|
|
21
22
|
assert intel.template == 'django_spire/ai/chat/message/default_message.html'
|
|
22
23
|
|
|
23
24
|
def test_default_message_intel_render_to_str(self) -> None:
|
|
@@ -65,3 +66,61 @@ class TestMessageIntel(BaseTestCase):
|
|
|
65
66
|
|
|
66
67
|
def render_to_str(self) -> str:
|
|
67
68
|
return 'test'
|
|
69
|
+
|
|
70
|
+
def test_default_message_intel_model_dump(self) -> None:
|
|
71
|
+
intel = DefaultMessageIntel(text='Test text')
|
|
72
|
+
dump = intel.model_dump()
|
|
73
|
+
|
|
74
|
+
assert 'text' in dump
|
|
75
|
+
assert dump['text'] == 'Test text'
|
|
76
|
+
|
|
77
|
+
def test_custom_message_intel_default_extra_data(self) -> None:
|
|
78
|
+
intel = CustomMessageIntel(text='Test')
|
|
79
|
+
|
|
80
|
+
assert intel.extra_data == 'Extra'
|
|
81
|
+
|
|
82
|
+
def test_default_message_intel_empty_text(self) -> None:
|
|
83
|
+
intel = DefaultMessageIntel(text='')
|
|
84
|
+
result = intel.render_to_str()
|
|
85
|
+
|
|
86
|
+
assert result == ''
|
|
87
|
+
|
|
88
|
+
def test_default_message_intel_long_text(self) -> None:
|
|
89
|
+
long_text = 'A' * 10000
|
|
90
|
+
intel = DefaultMessageIntel(text=long_text)
|
|
91
|
+
result = intel.render_to_str()
|
|
92
|
+
|
|
93
|
+
assert result == long_text
|
|
94
|
+
assert len(result) == 10000
|
|
95
|
+
|
|
96
|
+
def test_default_message_intel_special_characters(self) -> None:
|
|
97
|
+
special_text = '<script>alert("xss")</script>'
|
|
98
|
+
intel = DefaultMessageIntel(text=special_text)
|
|
99
|
+
result = intel.render_to_str()
|
|
100
|
+
|
|
101
|
+
assert result == special_text
|
|
102
|
+
|
|
103
|
+
def test_default_message_intel_unicode(self) -> None:
|
|
104
|
+
unicode_text = 'Hello 世界 🌍'
|
|
105
|
+
intel = DefaultMessageIntel(text=unicode_text)
|
|
106
|
+
result = intel.render_to_str()
|
|
107
|
+
|
|
108
|
+
assert result == unicode_text
|
|
109
|
+
|
|
110
|
+
def test_render_template_to_str_with_none_context(self) -> None:
|
|
111
|
+
intel = DefaultMessageIntel(text='Test')
|
|
112
|
+
result = intel.render_template_to_str(context_data=None)
|
|
113
|
+
|
|
114
|
+
assert isinstance(result, str)
|
|
115
|
+
|
|
116
|
+
def test_render_template_to_str_with_empty_context(self) -> None:
|
|
117
|
+
intel = DefaultMessageIntel(text='Test')
|
|
118
|
+
result = intel.render_template_to_str(context_data={})
|
|
119
|
+
|
|
120
|
+
assert isinstance(result, str)
|
|
121
|
+
|
|
122
|
+
def test_custom_message_intel_override_extra_data(self) -> None:
|
|
123
|
+
intel = CustomMessageIntel(text='Test', extra_data='Override')
|
|
124
|
+
|
|
125
|
+
assert intel.extra_data == 'Override'
|
|
126
|
+
assert intel.render_to_str() == 'Test - Override'
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from unittest.mock import Mock, patch
|
|
4
4
|
|
|
5
|
+
from dandy.llm.request.message import MessageHistory
|
|
5
6
|
from django.test import RequestFactory, override_settings
|
|
6
7
|
|
|
7
8
|
from django_spire.ai.chat.message_intel import DefaultMessageIntel
|
|
@@ -12,12 +13,14 @@ from django_spire.core.tests.test_cases import BaseTestCase
|
|
|
12
13
|
class TestSpireChatRouter(BaseTestCase):
|
|
13
14
|
def setUp(self) -> None:
|
|
14
15
|
super().setUp()
|
|
16
|
+
|
|
15
17
|
self.factory = RequestFactory()
|
|
16
18
|
self.request = self.factory.get('/')
|
|
17
19
|
self.request.user = self.super_user
|
|
18
20
|
|
|
19
21
|
def test_router_can_be_instantiated(self) -> None:
|
|
20
22
|
router = SpireChatRouter()
|
|
23
|
+
|
|
21
24
|
assert isinstance(router, SpireChatRouter)
|
|
22
25
|
|
|
23
26
|
def test_default_chat_callable_returns_message_intel(self) -> None:
|
|
@@ -90,3 +93,110 @@ class TestSpireChatRouter(BaseTestCase):
|
|
|
90
93
|
)
|
|
91
94
|
|
|
92
95
|
assert isinstance(result, DefaultMessageIntel)
|
|
96
|
+
|
|
97
|
+
def test_workflow_passes_message_history_to_decoder(self) -> None:
|
|
98
|
+
router = SpireChatRouter()
|
|
99
|
+
message_history = MessageHistory()
|
|
100
|
+
|
|
101
|
+
with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
|
|
102
|
+
mock_decoder_instance = Mock()
|
|
103
|
+
mock_decoder.return_value = mock_decoder_instance
|
|
104
|
+
mock_decoder_instance.process.return_value = [
|
|
105
|
+
lambda **kwargs: DefaultMessageIntel(text='Response')
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
router.workflow(
|
|
109
|
+
request=self.request,
|
|
110
|
+
user_input='Hello',
|
|
111
|
+
message_history=message_history
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
mock_decoder.assert_called_once()
|
|
115
|
+
|
|
116
|
+
def test_workflow_returns_message_intel(self) -> None:
|
|
117
|
+
router = SpireChatRouter()
|
|
118
|
+
|
|
119
|
+
with patch('django_spire.ai.chat.router.generate_intent_decoder') as mock_decoder:
|
|
120
|
+
mock_decoder_instance = Mock()
|
|
121
|
+
mock_decoder.return_value = mock_decoder_instance
|
|
122
|
+
mock_decoder_instance.process.return_value = [
|
|
123
|
+
lambda **kwargs: DefaultMessageIntel(text='Test Response')
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
result = router.workflow(
|
|
127
|
+
request=self.request,
|
|
128
|
+
user_input='Hello',
|
|
129
|
+
message_history=None
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
assert isinstance(result, DefaultMessageIntel)
|
|
133
|
+
assert result.text == 'Test Response'
|
|
134
|
+
|
|
135
|
+
@override_settings(DJANGO_SPIRE_AI_PERSONA_NAME='Custom Persona')
|
|
136
|
+
def test_default_callable_with_custom_persona(self) -> None:
|
|
137
|
+
router = SpireChatRouter()
|
|
138
|
+
|
|
139
|
+
with patch('django_spire.ai.chat.router.Bot') as MockBot:
|
|
140
|
+
mock_bot_instance = Mock()
|
|
141
|
+
MockBot.return_value = mock_bot_instance
|
|
142
|
+
mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
|
|
143
|
+
|
|
144
|
+
router._default_chat_callable(
|
|
145
|
+
request=self.request,
|
|
146
|
+
user_input='Hello',
|
|
147
|
+
message_history=None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert mock_bot_instance.llm_role is not None
|
|
151
|
+
|
|
152
|
+
def test_default_callable_passes_message_history(self) -> None:
|
|
153
|
+
router = SpireChatRouter()
|
|
154
|
+
message_history = MessageHistory()
|
|
155
|
+
|
|
156
|
+
with patch('django_spire.ai.chat.router.Bot') as MockBot:
|
|
157
|
+
mock_bot_instance = Mock()
|
|
158
|
+
MockBot.return_value = mock_bot_instance
|
|
159
|
+
mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
|
|
160
|
+
|
|
161
|
+
router._default_chat_callable(
|
|
162
|
+
request=self.request,
|
|
163
|
+
user_input='Hello',
|
|
164
|
+
message_history=message_history
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
|
|
168
|
+
assert call_kwargs['message_history'] == message_history
|
|
169
|
+
|
|
170
|
+
def test_default_callable_passes_user_input(self) -> None:
|
|
171
|
+
router = SpireChatRouter()
|
|
172
|
+
|
|
173
|
+
with patch('django_spire.ai.chat.router.Bot') as MockBot:
|
|
174
|
+
mock_bot_instance = Mock()
|
|
175
|
+
MockBot.return_value = mock_bot_instance
|
|
176
|
+
mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
|
|
177
|
+
|
|
178
|
+
router._default_chat_callable(
|
|
179
|
+
request=self.request,
|
|
180
|
+
user_input='Test input',
|
|
181
|
+
message_history=None
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
|
|
185
|
+
assert call_kwargs['prompt'] == 'Test input'
|
|
186
|
+
|
|
187
|
+
def test_default_callable_uses_default_message_intel(self) -> None:
|
|
188
|
+
router = SpireChatRouter()
|
|
189
|
+
|
|
190
|
+
with patch('django_spire.ai.chat.router.Bot') as MockBot:
|
|
191
|
+
mock_bot_instance = Mock()
|
|
192
|
+
MockBot.return_value = mock_bot_instance
|
|
193
|
+
mock_bot_instance.llm.prompt_to_intel.return_value = DefaultMessageIntel(text='Response')
|
|
194
|
+
|
|
195
|
+
router._default_chat_callable(
|
|
196
|
+
request=self.request,
|
|
197
|
+
user_input='Hello',
|
|
198
|
+
message_history=None
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
call_kwargs = mock_bot_instance.llm.prompt_to_intel.call_args[1]
|
|
202
|
+
assert call_kwargs['intel_class'] == DefaultMessageIntel
|