diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index f617e646a..b06c6f67d 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -1070,7 +1070,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): with ( patch("documents.tasks.llmindex_index.apply_async") as mock_update, - patch("paperless.views.llm_index_exists") as mock_exists, + patch("paperless_ai.indexing.llm_index_exists") as mock_exists, ): mock_exists.return_value = False self.client.patch( @@ -1095,7 +1095,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): with ( patch("documents.tasks.llmindex_index.apply_async") as mock_update, - patch("paperless.views.llm_index_exists", return_value=False), + patch("paperless_ai.indexing.llm_index_exists", return_value=False), ): self.client.patch( f"{self.ENDPOINT}1/", @@ -1115,7 +1115,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): with ( patch("documents.tasks.llmindex_index.apply_async") as mock_update, - patch("paperless.views.llm_index_exists") as mock_exists, + patch("paperless_ai.indexing.llm_index_exists") as mock_exists, ): mock_exists.return_value = True self.client.patch( @@ -1136,7 +1136,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): with ( patch("documents.tasks.llmindex_index.apply_async") as mock_update, - patch("paperless.views.llm_index_exists") as mock_exists, + patch("paperless_ai.indexing.llm_index_exists") as mock_exists, ): mock_exists.return_value = True self.client.patch( @@ -1174,7 +1174,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): with ( patch("documents.tasks.llmindex_index.apply_async") as mock_update, - patch("paperless.views.llm_index_exists") as mock_exists, + patch("paperless_ai.indexing.llm_index_exists") as mock_exists, ): mock_exists.return_value = True self.client.patch( diff --git a/src/documents/tests/test_api_chat.py b/src/documents/tests/test_api_chat.py index f19c4aa43..11703dfd9 100644 --- a/src/documents/tests/test_api_chat.py +++ b/src/documents/tests/test_api_chat.py @@ -61,7 +61,7 @@ class TestChatStreamingViewInputValidation(APITestCase): return_value=self._mock_ai_enabled(), ), mock.patch( - "documents.views.stream_chat_with_documents", + "paperless_ai.chat.stream_chat_with_documents", return_value=iter(chunks), ), ): @@ -104,7 +104,7 @@ class TestChatStreamingViewUnrestrictedFlag: """ mocker.patch("documents.views.AIConfig").return_value.ai_enabled = True return mocker.patch( - "documents.views.stream_chat_with_documents", + "paperless_ai.chat.stream_chat_with_documents", return_value=iter(()), ) diff --git a/src/documents/tests/test_api_document_versions.py b/src/documents/tests/test_api_document_versions.py index b1eb3c737..ba99a7b39 100644 --- a/src/documents/tests/test_api_document_versions.py +++ b/src/documents/tests/test_api_document_versions.py @@ -1110,7 +1110,7 @@ class TestVersionActionPermissions(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, 403) with ( mock.patch("documents.views.AIConfig") as ai_config, - mock.patch("documents.views.stream_chat_with_documents") as chat, + mock.patch("paperless_ai.chat.stream_chat_with_documents") as chat, ): ai_config.return_value.ai_enabled = True response = self.client.post( diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index eeb1ab59a..9a502b2cb 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -2686,7 +2686,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase): response = self.client.get("/api/documents/34676/suggestions/") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - @mock.patch("documents.views.get_ai_document_classification") + @mock.patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings(AI_ENABLED=True) def test_suggestions_still_uses_classifier_when_ai_enabled( self, diff --git a/src/documents/tests/test_permission_filtering_security.py b/src/documents/tests/test_permission_filtering_security.py index 3c2ff3bff..8b1b0169a 100644 --- a/src/documents/tests/test_permission_filtering_security.py +++ b/src/documents/tests/test_permission_filtering_security.py @@ -572,7 +572,7 @@ class TestAiChatAllDocumentsPermissionBoundary: ENDPOINT = "/api/documents/chat/" @override_settings(AI_ENABLED=True) - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") def test_chat_all_documents_excludes_unshared_document(self, mock_stream_chat): mock_stream_chat.return_value = iter([b"data"]) diff --git a/src/documents/tests/test_views.py b/src/documents/tests/test_views.py index d91ced30a..d6672f5b1 100644 --- a/src/documents/tests/test_views.py +++ b/src/documents/tests/test_views.py @@ -474,7 +474,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): self.assertEqual(response.json()["tags"], []) self.assertEqual(response.json()["suggested_tags"], []) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -524,7 +524,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): None, ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -562,7 +562,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): "KI Title", ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -601,7 +601,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): "Titre IA", ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -637,7 +637,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): ), ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -709,7 +709,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): ), ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="openai-like", @@ -738,7 +738,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): get_llm_suggestion_cache(self.document.pk, backend="openai-like"), ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="openai-like", @@ -765,7 +765,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): get_llm_suggestion_cache(self.document.pk, backend="openai-like"), ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="openai-like", @@ -797,7 +797,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): get_llm_suggestion_cache(self.document.pk, backend="openai-like"), ) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -835,7 +835,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): self.assertEqual(response.json()["tags"], [self.tag1.pk]) self.assertEqual(response.json()["suggested_tags"], ["Follow-up"]) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -874,7 +874,7 @@ class TestAISuggestions(DirectoriesMixin, TestCase): self.assertEqual(response.json()["tags"], [self.tag1.pk]) self.assertEqual(response.json()["suggested_tags"], []) - @patch("documents.views.get_ai_document_classification") + @patch("paperless_ai.ai_classifier.get_ai_document_classification") @override_settings( AI_ENABLED=True, LLM_BACKEND="mock_backend", @@ -996,7 +996,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): self.assertEqual(response.status_code, 400) self.assertIn(b"AI is required for this feature", response.content) - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") @patch("documents.views.permitted_document_ids") @override_settings(AI_ENABLED=True) def test_post_no_document_id(self, mock_permitted_ids, mock_stream_chat) -> None: @@ -1016,7 +1016,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): self.assertEqual(list(call_kwargs["documents"]), [self.document]) self.assertIsNone(call_kwargs["output_language"]) - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") @patch("documents.views.permitted_document_ids") @override_settings(AI_ENABLED=True) def test_post_uses_user_display_language( @@ -1042,7 +1042,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): self.assertEqual(list(call_kwargs["documents"]), [self.document]) self.assertEqual(call_kwargs["output_language"], "de-de") - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") @override_settings(AI_ENABLED=True) def test_post_with_document_id(self, mock_stream_chat) -> None: self.grant_view_document_permission() @@ -1079,7 +1079,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): self.assertEqual(response.status_code, 403) self.assertIn(b"Insufficient permissions", response.content) - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") @override_settings(AI_ENABLED=True) def test_post_no_document_id_requires_view_document_permission( self, @@ -1093,7 +1093,7 @@ class TestAIChatStreamingView(DirectoriesMixin, TestCase): self.assertEqual(response.status_code, 403) mock_stream_chat.assert_not_called() - @patch("documents.views.stream_chat_with_documents") + @patch("paperless_ai.chat.stream_chat_with_documents") @override_settings(AI_ENABLED=True) def test_post_with_document_id_requires_view_document_permission( self, diff --git a/src/documents/views.py b/src/documents/views.py index 5e17c67cb..5a4a78296 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -250,20 +250,8 @@ from paperless.parsers.remote import RemoteEngineConfig from paperless.serialisers import GroupSerializer from paperless.serialisers import UserSerializer from paperless.views import StandardPagination -from paperless_ai.ai_classifier import get_ai_document_classification -from paperless_ai.ai_classifier import get_llm_output_language -from paperless_ai.chat import stream_chat_with_documents from paperless_ai.exceptions import LLMProviderError from paperless_ai.exceptions import LLMTimeoutError -from paperless_ai.matching import extract_unmatched_names -from paperless_ai.matching import match_correspondents_by_name -from paperless_ai.matching import match_document_types_by_name -from paperless_ai.matching import match_storage_paths_by_name -from paperless_ai.matching import match_tags_by_name -from paperless_ai.matching import resolve_correspondent_ids -from paperless_ai.matching import resolve_document_type_ids -from paperless_ai.matching import resolve_storage_path_ids -from paperless_ai.matching import resolve_tag_ids from paperless_mail.models import MailAccount from paperless_mail.models import MailRule from paperless_mail.oauth import PaperlessMailOAuth2Manager @@ -1613,6 +1601,18 @@ class DocumentViewSet( if not ai_config.ai_enabled: return HttpResponseBadRequest("AI is required for this feature") + from paperless_ai.ai_classifier import get_ai_document_classification + from paperless_ai.ai_classifier import get_llm_output_language + from paperless_ai.matching import extract_unmatched_names + from paperless_ai.matching import match_correspondents_by_name + from paperless_ai.matching import match_document_types_by_name + from paperless_ai.matching import match_storage_paths_by_name + from paperless_ai.matching import match_tags_by_name + from paperless_ai.matching import resolve_correspondent_ids + from paperless_ai.matching import resolve_document_type_ids + from paperless_ai.matching import resolve_storage_path_ids + from paperless_ai.matching import resolve_tag_ids + output_language = get_llm_output_language( ai_config=ai_config, user=request.user, @@ -2446,6 +2446,9 @@ class ChatStreamingView(GenericAPIView[Any]): ) unrestricted = user_is_unrestricted(request.user) + from paperless_ai.ai_classifier import get_llm_output_language + from paperless_ai.chat import stream_chat_with_documents + output_language = get_llm_output_language( ai_config=ai_config, user=request.user, diff --git a/src/paperless/views.py b/src/paperless/views.py index ba1e6738d..62b624294 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -49,7 +49,6 @@ from paperless.serialisers import GroupSerializer from paperless.serialisers import PaperlessAuthTokenSerializer from paperless.serialisers import ProfileSerializer from paperless.serialisers import UserSerializer -from paperless_ai.indexing import llm_index_exists class PaperlessObtainAuthTokenView(ObtainAuthToken): @@ -471,6 +470,8 @@ class ApplicationConfigurationViewSet(ModelViewSet[ApplicationConfiguration]): or old_llm_embedding_model != new_llm_embedding_model or old_llm_context_size != new_llm_context_size ) + from paperless_ai.indexing import llm_index_exists + rebuild_needed = new_ai_index_enabled and ( not llm_index_exists() or embedding_config_changed )