mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-06-05 13:19:46 +00:00
Enhancement (beta): add direct LLM language setting (#12906)
This commit is contained in:
@@ -2108,6 +2108,12 @@ used with the OpenAI-compatible backend to target a custom provider or local gat
|
|||||||
|
|
||||||
Defaults to None.
|
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}
|
#### [`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).
|
: If set to false, Paperless blocks AI endpoint URLs that resolve to non-public addresses (e.g., localhost, etc).
|
||||||
|
|||||||
@@ -352,6 +352,14 @@ export const PaperlessConfigOptions: ConfigOption[] = [
|
|||||||
config_key: 'PAPERLESS_AI_LLM_ENDPOINT',
|
config_key: 'PAPERLESS_AI_LLM_ENDPOINT',
|
||||||
category: ConfigCategory.AI,
|
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 {
|
export interface PaperlessConfig extends ObjectWithId {
|
||||||
@@ -392,4 +400,5 @@ export interface PaperlessConfig extends ObjectWithId {
|
|||||||
llm_model: string
|
llm_model: string
|
||||||
llm_api_key: string
|
llm_api_key: string
|
||||||
llm_endpoint: string
|
llm_endpoint: string
|
||||||
|
llm_output_language: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
"llm_model": None,
|
"llm_model": None,
|
||||||
"llm_api_key": None,
|
"llm_api_key": None,
|
||||||
"llm_endpoint": None,
|
"llm_endpoint": None,
|
||||||
|
"llm_output_language": None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -408,6 +408,45 @@ class TestAISuggestions(DirectoriesMixin, TestCase):
|
|||||||
"KI Title",
|
"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")
|
@patch("documents.views.get_ai_document_classification")
|
||||||
@override_settings(
|
@override_settings(
|
||||||
AI_ENABLED=True,
|
AI_ENABLED=True,
|
||||||
|
|||||||
@@ -1469,10 +1469,14 @@ class DocumentViewSet(
|
|||||||
if not ai_config.ai_enabled:
|
if not ai_config.ai_enabled:
|
||||||
return HttpResponseBadRequest("AI is required for this feature")
|
return HttpResponseBadRequest("AI is required for this feature")
|
||||||
|
|
||||||
output_language = None
|
output_language = ai_config.llm_output_language
|
||||||
if hasattr(request.user, "ui_settings") and isinstance(
|
if (
|
||||||
request.user.ui_settings.settings,
|
not output_language
|
||||||
dict,
|
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
|
output_language = request.user.ui_settings.settings.get("language") or None
|
||||||
llm_cache_backend = (
|
llm_cache_backend = (
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ class AIConfig(BaseConfig):
|
|||||||
llm_model: str = dataclasses.field(init=False)
|
llm_model: str = dataclasses.field(init=False)
|
||||||
llm_api_key: str = dataclasses.field(init=False)
|
llm_api_key: str = dataclasses.field(init=False)
|
||||||
llm_endpoint: 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)
|
llm_allow_internal_endpoints: bool = dataclasses.field(init=False)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
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_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_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_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
|
self.llm_allow_internal_endpoints = settings.LLM_ALLOW_INTERNAL_ENDPOINTS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -359,6 +359,13 @@ class ApplicationConfiguration(AbstractSingletonModel):
|
|||||||
max_length=256,
|
max_length=256,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
llm_output_language = models.CharField(
|
||||||
|
verbose_name=_("Sets the LLM output language"),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
max_length=32,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("paperless application settings")
|
verbose_name = _("paperless application settings")
|
||||||
permissions = [
|
permissions = [
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ class ApplicationConfigurationSerializer(
|
|||||||
data["barcode_tag_mapping"] = None
|
data["barcode_tag_mapping"] = None
|
||||||
if "language" in data and data["language"] == "":
|
if "language" in data and data["language"] == "":
|
||||||
data["language"] = None
|
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 "llm_api_key" in data and data["llm_api_key"] is not None:
|
||||||
if data["llm_api_key"] == "":
|
if data["llm_api_key"] == "":
|
||||||
data["llm_api_key"] = None
|
data["llm_api_key"] = None
|
||||||
|
|||||||
@@ -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_MODEL = os.getenv("PAPERLESS_AI_LLM_MODEL")
|
||||||
LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY")
|
LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY")
|
||||||
LLM_ENDPOINT = os.getenv("PAPERLESS_AI_LLM_ENDPOINT")
|
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(
|
LLM_ALLOW_INTERNAL_ENDPOINTS = get_bool_from_env(
|
||||||
"PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS",
|
"PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS",
|
||||||
"true",
|
"true",
|
||||||
|
|||||||
Reference in New Issue
Block a user