]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement (beta): add direct LLM language setting (#12906)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 3 Jun 2026 15:53:22 +0000 (08:53 -0700)
committerGitHub <noreply@github.com>
Wed, 3 Jun 2026 15:53:22 +0000 (15:53 +0000)
docs/configuration.md
src-ui/src/app/data/paperless-config.ts
src/documents/tests/test_api_app_config.py
src/documents/tests/test_views.py
src/documents/views.py
src/paperless/config.py
src/paperless/migrations/0012_applicationconfiguration_llm_output_language.py [new file with mode: 0644]
src/paperless/models.py
src/paperless/serialisers.py
src/paperless/settings/__init__.py

index 66470792d061dfb71bc3d3bdfcc1b5ceefa5ed10..2b2fd3b9f3f4d767b82b042568e3771515af2129 100644 (file)
@@ -2108,6 +2108,12 @@ used with the OpenAI-compatible backend to target a custom provider or local gat
 
     Defaults to None.
 
+### [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE=<str>`](#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE) {#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE}
+
+: The language to use for AI suggestions (results may vary by LLM model). If not supplied, defaults to the user's UI language setting or None.
+
+    Defaults to None.
+
 #### [`PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS=<bool>`](#PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS) {#PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS}
 
 : If set to false, Paperless blocks AI endpoint URLs that resolve to non-public addresses (e.g., localhost, etc).
index f7b654bf43c4984c6a7f999f1e6ccef058950c8c..1c74d8c08296f2ecd393c186ed187954f2d96c4a 100644 (file)
@@ -352,6 +352,14 @@ export const PaperlessConfigOptions: ConfigOption[] = [
     config_key: 'PAPERLESS_AI_LLM_ENDPOINT',
     category: ConfigCategory.AI,
   },
+  {
+    key: 'llm_output_language',
+    title: $localize`LLM Output Language`,
+    type: ConfigOptionType.String,
+    config_key: 'PAPERLESS_AI_LLM_OUTPUT_LANGUAGE',
+    category: ConfigCategory.AI,
+    note: $localize`Language to use for generated AI suggestions. When unset, AI suggestions use the user's display language if explicitly set.`,
+  },
 ]
 
 export interface PaperlessConfig extends ObjectWithId {
@@ -392,4 +400,5 @@ export interface PaperlessConfig extends ObjectWithId {
   llm_model: string
   llm_api_key: string
   llm_endpoint: string
+  llm_output_language: string
 }
index e0441f17c375460b33ee1cda941aa0365c1010f2..2418236bd01a4382f403ab7e163451ce9cb0e54b 100644 (file)
@@ -81,6 +81,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
                 "llm_model": None,
                 "llm_api_key": None,
                 "llm_endpoint": None,
+                "llm_output_language": None,
             },
         )
 
index 90736849f32f158dfdd266fd943a73e8e214fceb..a67590b819411bb0dbb3ebc7e82f701a62e6cf81 100644 (file)
@@ -408,6 +408,45 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
             "KI Title",
         )
 
+    @patch("documents.views.get_ai_document_classification")
+    @override_settings(
+        AI_ENABLED=True,
+        LLM_BACKEND="mock_backend",
+        LLM_OUTPUT_LANGUAGE="fr-fr",
+    )
+    def test_ai_suggestions_configured_language_takes_precedence(
+        self,
+        mock_get_ai_classification,
+    ) -> None:
+        UiSettings.objects.create(user=self.user, settings={"language": "de-de"})
+        mock_get_ai_classification.return_value = {
+            "title": "Titre IA",
+            "tags": [],
+            "correspondents": [],
+            "document_types": [],
+            "storage_paths": [],
+            "dates": [],
+        }
+
+        self.client.force_login(user=self.user)
+        response = self.client.get(
+            f"/api/documents/{self.document.pk}/ai_suggestions/",
+        )
+
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        mock_get_ai_classification.assert_called_once_with(
+            self.document,
+            self.user,
+            "fr-fr",
+        )
+        self.assertEqual(
+            get_llm_suggestion_cache(
+                self.document.pk,
+                backend="mock_backend:fr-fr",
+            ).suggestions["title"],
+            "Titre IA",
+        )
+
     @patch("documents.views.get_ai_document_classification")
     @override_settings(
         AI_ENABLED=True,
index e92ca2d1d607b86e5d492ae2975561cf8a122d04..51142912921acf87612b6d839082dbcc157f8e21 100644 (file)
@@ -1469,10 +1469,14 @@ class DocumentViewSet(
         if not ai_config.ai_enabled:
             return HttpResponseBadRequest("AI is required for this feature")
 
-        output_language = None
-        if hasattr(request.user, "ui_settings") and isinstance(
-            request.user.ui_settings.settings,
-            dict,
+        output_language = ai_config.llm_output_language
+        if (
+            not output_language
+            and hasattr(request.user, "ui_settings")
+            and isinstance(
+                request.user.ui_settings.settings,
+                dict,
+            )
         ):
             output_language = request.user.ui_settings.settings.get("language") or None
         llm_cache_backend = (
index 8c9c7b3ca696a8cf82acf119bf9cecb2b9523e83..40341b92efdd5654406bbba4c0b500ba9a0ea5df 100644 (file)
@@ -201,6 +201,7 @@ class AIConfig(BaseConfig):
     llm_model: str = dataclasses.field(init=False)
     llm_api_key: str = dataclasses.field(init=False)
     llm_endpoint: str = dataclasses.field(init=False)
+    llm_output_language: str = dataclasses.field(init=False)
     llm_allow_internal_endpoints: bool = dataclasses.field(init=False)
 
     def __post_init__(self) -> None:
@@ -224,6 +225,9 @@ class AIConfig(BaseConfig):
         self.llm_model = app_config.llm_model or settings.LLM_MODEL
         self.llm_api_key = app_config.llm_api_key or settings.LLM_API_KEY
         self.llm_endpoint = app_config.llm_endpoint or settings.LLM_ENDPOINT
+        self.llm_output_language = (
+            app_config.llm_output_language or settings.LLM_OUTPUT_LANGUAGE
+        )
         self.llm_allow_internal_endpoints = settings.LLM_ALLOW_INTERNAL_ENDPOINTS
 
     @property
diff --git a/src/paperless/migrations/0012_applicationconfiguration_llm_output_language.py b/src/paperless/migrations/0012_applicationconfiguration_llm_output_language.py
new file mode 100644 (file)
index 0000000..3dcce37
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 5.2.6 on 2026-06-02
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("paperless", "0011_applicationconfiguration_llm_embedding_chunk_size"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="applicationconfiguration",
+            name="llm_output_language",
+            field=models.CharField(
+                blank=True,
+                max_length=32,
+                null=True,
+                verbose_name="Sets the LLM output language",
+            ),
+        ),
+    ]
index 7c562b8116322633ed7c27ea8f2fe7f5758495a3..d246a546f2aa07aa7069b4e3a4ea72822dbb0ea5 100644 (file)
@@ -359,6 +359,13 @@ class ApplicationConfiguration(AbstractSingletonModel):
         max_length=256,
     )
 
+    llm_output_language = models.CharField(
+        verbose_name=_("Sets the LLM output language"),
+        blank=True,
+        null=True,
+        max_length=32,
+    )
+
     class Meta:
         verbose_name = _("paperless application settings")
         permissions = [
index d1597ab139e7d685c99dad7133308700254df622..58702b726f7ebec9e073e88468d606826e31c1ff 100644 (file)
@@ -227,6 +227,8 @@ class ApplicationConfigurationSerializer(
             data["barcode_tag_mapping"] = None
         if "language" in data and data["language"] == "":
             data["language"] = None
+        if "llm_output_language" in data and data["llm_output_language"] == "":
+            data["llm_output_language"] = None
         if "llm_api_key" in data and data["llm_api_key"] is not None:
             if data["llm_api_key"] == "":
                 data["llm_api_key"] = None
index 8e47611c7faae13611622aae97dd2dd8d00dad9c..062926d3881ac83a3776f9ff53c448af507e1c6d 100644 (file)
@@ -1202,6 +1202,7 @@ LLM_BACKEND = os.getenv("PAPERLESS_AI_LLM_BACKEND")  # "ollama" or "openai-like"
 LLM_MODEL = os.getenv("PAPERLESS_AI_LLM_MODEL")
 LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY")
 LLM_ENDPOINT = os.getenv("PAPERLESS_AI_LLM_ENDPOINT")
+LLM_OUTPUT_LANGUAGE = os.getenv("PAPERLESS_AI_LLM_OUTPUT_LANGUAGE")
 LLM_ALLOW_INTERNAL_ENDPOINTS = get_bool_from_env(
     "PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS",
     "true",