Compare commits

...
9 changed files with 114 additions and 16 deletions
+1 -1
View File
@@ -66,7 +66,7 @@ dependencies = [
"python-ipware~=3.0.0", "python-ipware~=3.0.0",
"python-magic~=0.4.27", "python-magic~=0.4.27",
"rapidfuzz~=3.14.5", "rapidfuzz~=3.14.5",
"redis[hiredis]~=5.2.1", "redis[hiredis]~=6.4.0",
"regex>=2026.7.19", "regex>=2026.7.19",
"scikit-learn~=1.9.0", "scikit-learn~=1.9.0",
"sentence-transformers>=5.6.1", "sentence-transformers>=5.6.1",
@@ -22,7 +22,7 @@
} }
::ng-deep .pngx-pdf-viewer-container { ::ng-deep .pngx-pdf-viewer-container {
overflow: hidden; overflow: hidden !important;
} }
.hover-actions { .hover-actions {
+21 -1
View File
@@ -76,7 +76,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
"remote_ocr_api_key": None, "remote_ocr_api_key": None,
"remote_ocr_endpoint": None, "remote_ocr_endpoint": None,
"remote_ocr_mode": None, "remote_ocr_mode": None,
"ai_enabled": False, "ai_enabled": None,
"llm_embedding_backend": None, "llm_embedding_backend": None,
"llm_embedding_model": None, "llm_embedding_model": None,
"llm_embedding_endpoint": None, "llm_embedding_endpoint": None,
@@ -949,6 +949,26 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
) )
mock_update.assert_called_once() mock_update.assert_called_once()
@override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND=None)
def test_external_ai_setting_triggers_index_update(self) -> None:
config = ApplicationConfiguration.objects.first()
assert config is not None
config.ai_enabled = None
config.llm_embedding_backend = None
config.save()
with (
patch("documents.tasks.llmindex_index.apply_async") as mock_update,
patch("paperless.views.llm_index_exists", return_value=False),
):
self.client.patch(
f"{self.ENDPOINT}1/",
json.dumps({"llm_embedding_backend": "openai-like"}),
content_type="application/json",
)
mock_update.assert_called_once()
def test_update_llm_embedding_chunk_size_triggers_rebuild(self) -> None: def test_update_llm_embedding_chunk_size_triggers_rebuild(self) -> None:
config = ApplicationConfiguration.objects.first() config = ApplicationConfiguration.objects.first()
assert config is not None assert config is not None
+21 -7
View File
@@ -133,21 +133,27 @@ class BarcodeConfig(BaseConfig):
app_config = self._get_config_instance() app_config = self._get_config_instance()
self.barcodes_enabled = ( self.barcodes_enabled = (
app_config.barcodes_enabled or settings.CONSUMER_ENABLE_BARCODES app_config.barcodes_enabled
if app_config.barcodes_enabled is not None
else settings.CONSUMER_ENABLE_BARCODES
) )
self.barcode_enable_tiff_support = ( self.barcode_enable_tiff_support = (
app_config.barcode_enable_tiff_support app_config.barcode_enable_tiff_support
or settings.CONSUMER_BARCODE_TIFF_SUPPORT if app_config.barcode_enable_tiff_support is not None
else settings.CONSUMER_BARCODE_TIFF_SUPPORT
) )
self.barcode_string = ( self.barcode_string = (
app_config.barcode_string or settings.CONSUMER_BARCODE_STRING app_config.barcode_string or settings.CONSUMER_BARCODE_STRING
) )
self.barcode_retain_split_pages = ( self.barcode_retain_split_pages = (
app_config.barcode_retain_split_pages app_config.barcode_retain_split_pages
or settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES if app_config.barcode_retain_split_pages is not None
else settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES
) )
self.barcode_enable_asn = ( self.barcode_enable_asn = (
app_config.barcode_enable_asn or settings.CONSUMER_ENABLE_ASN_BARCODE app_config.barcode_enable_asn
if app_config.barcode_enable_asn is not None
else settings.CONSUMER_ENABLE_ASN_BARCODE
) )
self.barcode_asn_prefix = ( self.barcode_asn_prefix = (
app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX
@@ -160,13 +166,17 @@ class BarcodeConfig(BaseConfig):
app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES
) )
self.barcode_enable_tag = ( self.barcode_enable_tag = (
app_config.barcode_enable_tag or settings.CONSUMER_ENABLE_TAG_BARCODE app_config.barcode_enable_tag
if app_config.barcode_enable_tag is not None
else settings.CONSUMER_ENABLE_TAG_BARCODE
) )
self.barcode_tag_mapping = ( self.barcode_tag_mapping = (
app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING
) )
self.barcode_tag_split = ( self.barcode_tag_split = (
app_config.barcode_tag_split or settings.CONSUMER_TAG_BARCODE_SPLIT app_config.barcode_tag_split
if app_config.barcode_tag_split is not None
else settings.CONSUMER_TAG_BARCODE_SPLIT
) )
@@ -248,7 +258,11 @@ class AIConfig(BaseConfig):
def __post_init__(self) -> None: def __post_init__(self) -> None:
app_config = self._get_config_instance() app_config = self._get_config_instance()
self.ai_enabled = app_config.ai_enabled or settings.AI_ENABLED self.ai_enabled = (
app_config.ai_enabled
if app_config.ai_enabled is not None
else settings.AI_ENABLED
)
self.llm_embedding_backend = ( self.llm_embedding_backend = (
app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND
) )
@@ -0,0 +1,28 @@
from django.db import migrations
from django.db import models
def normalize_ai_enabled(apps, schema_editor):
application_configuration = apps.get_model(
"paperless",
"ApplicationConfiguration",
)
application_configuration.objects.filter(ai_enabled=False).update(ai_enabled=None)
class Migration(migrations.Migration):
dependencies = [
("paperless", "0015_applicationconfiguration_remote_ocr_mode"),
]
operations = [
migrations.AlterField(
model_name="applicationconfiguration",
name="ai_enabled",
field=models.BooleanField(
null=True,
verbose_name="Enables AI features",
),
),
migrations.RunPython(normalize_ai_enabled, migrations.RunPython.noop),
]
-1
View File
@@ -348,7 +348,6 @@ class ApplicationConfiguration(AbstractSingletonModel):
ai_enabled = models.BooleanField( ai_enabled = models.BooleanField(
verbose_name=_("Enables AI features"), verbose_name=_("Enables AI features"),
null=True, null=True,
default=False,
) )
llm_embedding_backend = models.CharField( llm_embedding_backend = models.CharField(
@@ -0,0 +1,32 @@
from django.test import TestCase
from django.test import override_settings
from paperless.config import AIConfig
from paperless.config import BarcodeConfig
from paperless.models import ApplicationConfiguration
class TestBooleanConfigPrecedence(TestCase):
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_database_false_overrides_barcode_environment_setting(self) -> None:
config, _ = ApplicationConfiguration.objects.get_or_create()
config.barcodes_enabled = False
config.save()
self.assertFalse(BarcodeConfig().barcodes_enabled)
@override_settings(AI_ENABLED=True)
def test_database_false_overrides_ai_environment_setting(self) -> None:
config, _ = ApplicationConfiguration.objects.get_or_create()
config.ai_enabled = False
config.save()
self.assertFalse(AIConfig().ai_enabled)
@override_settings(AI_ENABLED=True)
def test_null_ai_setting_uses_environment_setting(self) -> None:
config, _ = ApplicationConfiguration.objects.get_or_create()
config.ai_enabled = None
config.save()
self.assertTrue(AIConfig().ai_enabled)
+6 -1
View File
@@ -443,8 +443,13 @@ class ApplicationConfigurationViewSet(ModelViewSet[ApplicationConfiguration]):
new_llm_embedding_backend = ( new_llm_embedding_backend = (
new_instance.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND new_instance.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND
) )
new_ai_enabled = (
new_instance.ai_enabled
if new_instance.ai_enabled is not None
else settings.AI_ENABLED
)
new_ai_index_enabled = bool( new_ai_index_enabled = bool(
new_instance.ai_enabled and new_llm_embedding_backend, new_ai_enabled and new_llm_embedding_backend,
) )
new_llm_embedding_chunk_size = ( new_llm_embedding_chunk_size = (
new_instance.llm_embedding_chunk_size or settings.LLM_EMBEDDING_CHUNK_SIZE new_instance.llm_embedding_chunk_size or settings.LLM_EMBEDDING_CHUNK_SIZE
Generated
+4 -4
View File
@@ -3070,7 +3070,7 @@ requires-dist = [
{ name = "python-ipware", specifier = "~=3.0.0" }, { name = "python-ipware", specifier = "~=3.0.0" },
{ name = "python-magic", specifier = "~=0.4.27" }, { name = "python-magic", specifier = "~=0.4.27" },
{ name = "rapidfuzz", specifier = "~=3.14.5" }, { name = "rapidfuzz", specifier = "~=3.14.5" },
{ name = "redis", extras = ["hiredis"], specifier = "~=5.2.1" }, { name = "redis", extras = ["hiredis"], specifier = "~=6.4.0" },
{ name = "regex", specifier = ">=2026.7.19" }, { name = "regex", specifier = ">=2026.7.19" },
{ name = "scikit-learn", specifier = "~=1.9.0" }, { name = "scikit-learn", specifier = "~=1.9.0" },
{ name = "sentence-transformers", specifier = ">=5.6.1" }, { name = "sentence-transformers", specifier = ">=5.6.1" },
@@ -4150,14 +4150,14 @@ wheels = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "5.2.1" version = "6.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" }, { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355, upload-time = "2024-12-06T09:50:41.956Z" } sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload-time = "2024-12-06T09:50:39.656Z" }, { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]