mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 19:27:23 +08:00
Merge branch 'main' into deploy/dev
This commit is contained in:
commit
fef5d88f59
@ -4,7 +4,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
|
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -52,11 +52,23 @@ class ChatRequestPayload(BaseModel):
|
|||||||
query: str
|
query: str
|
||||||
files: list[dict[str, Any]] | None = None
|
files: list[dict[str, Any]] | None = None
|
||||||
response_mode: Literal["blocking", "streaming"] | None = None
|
response_mode: Literal["blocking", "streaming"] | None = None
|
||||||
conversation_id: UUID | None = None
|
conversation_id: str | None = Field(default=None, description="Conversation UUID")
|
||||||
retriever_from: str = Field(default="dev")
|
retriever_from: str = Field(default="dev")
|
||||||
auto_generate_name: bool = Field(default=True, description="Auto generate conversation name")
|
auto_generate_name: bool = Field(default=True, description="Auto generate conversation name")
|
||||||
workflow_id: str | None = Field(default=None, description="Workflow ID for advanced chat")
|
workflow_id: str | None = Field(default=None, description="Workflow ID for advanced chat")
|
||||||
|
|
||||||
|
@field_validator("conversation_id", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def normalize_conversation_id(cls, value: str | UUID | None) -> str | None:
|
||||||
|
"""Allow missing or blank conversation IDs; enforce UUID format when provided."""
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return helper.uuid_value(value)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("conversation_id must be a valid UUID") from exc
|
||||||
|
|
||||||
|
|
||||||
register_schema_models(service_api_ns, CompletionRequestPayload, ChatRequestPayload)
|
register_schema_models(service_api_ns, CompletionRequestPayload, ChatRequestPayload)
|
||||||
|
|
||||||
|
|||||||
@ -13,5 +13,5 @@ def remove_leading_symbols(text: str) -> str:
|
|||||||
"""
|
"""
|
||||||
# Match Unicode ranges for punctuation and symbols
|
# Match Unicode ranges for punctuation and symbols
|
||||||
# FIXME this pattern is confused quick fix for #11868 maybe refactor it later
|
# FIXME this pattern is confused quick fix for #11868 maybe refactor it later
|
||||||
pattern = r"^[\u2000-\u206F\u2E00-\u2E7F\u3000-\u303F\"#$%&'()*+,./:;<=>?@^_`~]+"
|
pattern = r'^[\[\]\u2000-\u2025\u2027-\u206F\u2E00-\u2E7F\u3000-\u300F\u3011-\u303F"#$%&\'()*+,./:;<=>?@^_`~]+'
|
||||||
return re.sub(pattern, "", text)
|
return re.sub(pattern, "", text)
|
||||||
|
|||||||
@ -107,7 +107,7 @@ def email(email):
|
|||||||
EmailStr = Annotated[str, AfterValidator(email)]
|
EmailStr = Annotated[str, AfterValidator(email)]
|
||||||
|
|
||||||
|
|
||||||
def uuid_value(value):
|
def uuid_value(value: Any) -> str:
|
||||||
if value == "":
|
if value == "":
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|||||||
@ -151,7 +151,7 @@ dev = [
|
|||||||
"types-pywin32~=310.0.0",
|
"types-pywin32~=310.0.0",
|
||||||
"types-pyyaml~=6.0.12",
|
"types-pyyaml~=6.0.12",
|
||||||
"types-regex~=2024.11.6",
|
"types-regex~=2024.11.6",
|
||||||
"types-shapely~=2.0.0",
|
"types-shapely~=2.1.0",
|
||||||
"types-simplejson>=3.20.0",
|
"types-simplejson>=3.20.0",
|
||||||
"types-six>=1.17.0",
|
"types-six>=1.17.0",
|
||||||
"types-tensorflow>=2.18.0",
|
"types-tensorflow>=2.18.0",
|
||||||
|
|||||||
@ -70,9 +70,28 @@ class ModelProviderService:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
provider_config = provider_configuration.custom_configuration.provider
|
provider_config = provider_configuration.custom_configuration.provider
|
||||||
model_config = provider_configuration.custom_configuration.models
|
models = provider_configuration.custom_configuration.models
|
||||||
can_added_models = provider_configuration.custom_configuration.can_added_models
|
can_added_models = provider_configuration.custom_configuration.can_added_models
|
||||||
|
|
||||||
|
# IMPORTANT: Never expose decrypted credentials in the provider list API.
|
||||||
|
# Sanitize custom model configurations by dropping the credentials payload.
|
||||||
|
sanitized_model_config = []
|
||||||
|
if models:
|
||||||
|
from core.entities.provider_entities import CustomModelConfiguration # local import to avoid cycles
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
sanitized_model_config.append(
|
||||||
|
CustomModelConfiguration(
|
||||||
|
model=model.model,
|
||||||
|
model_type=model.model_type,
|
||||||
|
credentials=None, # strip secrets from list view
|
||||||
|
current_credential_id=model.current_credential_id,
|
||||||
|
current_credential_name=model.current_credential_name,
|
||||||
|
available_model_credentials=model.available_model_credentials,
|
||||||
|
unadded_to_model_list=model.unadded_to_model_list,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
provider_response = ProviderResponse(
|
provider_response = ProviderResponse(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
provider=provider_configuration.provider.provider,
|
provider=provider_configuration.provider.provider,
|
||||||
@ -95,7 +114,7 @@ class ModelProviderService:
|
|||||||
current_credential_id=getattr(provider_config, "current_credential_id", None),
|
current_credential_id=getattr(provider_config, "current_credential_id", None),
|
||||||
current_credential_name=getattr(provider_config, "current_credential_name", None),
|
current_credential_name=getattr(provider_config, "current_credential_name", None),
|
||||||
available_credentials=getattr(provider_config, "available_credentials", []),
|
available_credentials=getattr(provider_config, "available_credentials", []),
|
||||||
custom_models=model_config,
|
custom_models=sanitized_model_config,
|
||||||
can_added_models=can_added_models,
|
can_added_models=can_added_models,
|
||||||
),
|
),
|
||||||
system_configuration=SystemConfigurationResponse(
|
system_configuration=SystemConfigurationResponse(
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from controllers.service_api.app.completion import ChatRequestPayload
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_request_payload_accepts_blank_conversation_id():
|
||||||
|
payload = ChatRequestPayload.model_validate({"inputs": {}, "query": "hello", "conversation_id": ""})
|
||||||
|
|
||||||
|
assert payload.conversation_id is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_request_payload_validates_uuid():
|
||||||
|
conversation_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
payload = ChatRequestPayload.model_validate({"inputs": {}, "query": "hello", "conversation_id": conversation_id})
|
||||||
|
|
||||||
|
assert payload.conversation_id == conversation_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_request_payload_rejects_invalid_uuid():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
ChatRequestPayload.model_validate({"inputs": {}, "query": "hello", "conversation_id": "invalid"})
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import types
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.entities.provider_entities import CredentialConfiguration, CustomModelConfiguration
|
||||||
|
from core.model_runtime.entities.common_entities import I18nObject
|
||||||
|
from core.model_runtime.entities.model_entities import ModelType
|
||||||
|
from core.model_runtime.entities.provider_entities import ConfigurateMethod
|
||||||
|
from models.provider import ProviderType
|
||||||
|
from services.model_provider_service import ModelProviderService
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeConfigurations:
|
||||||
|
def __init__(self, provider_configuration: types.SimpleNamespace) -> None:
|
||||||
|
self._provider_configuration = provider_configuration
|
||||||
|
|
||||||
|
def values(self) -> list[types.SimpleNamespace]:
|
||||||
|
return [self._provider_configuration]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def service_with_fake_configurations():
|
||||||
|
# Build a fake provider schema with minimal fields used by ProviderResponse
|
||||||
|
fake_provider = types.SimpleNamespace(
|
||||||
|
provider="langgenius/openai_api_compatible/openai_api_compatible",
|
||||||
|
label=I18nObject(en_US="OpenAI API Compatible", zh_Hans="OpenAI API Compatible"),
|
||||||
|
description=None,
|
||||||
|
icon_small=None,
|
||||||
|
icon_small_dark=None,
|
||||||
|
icon_large=None,
|
||||||
|
background=None,
|
||||||
|
help=None,
|
||||||
|
supported_model_types=[ModelType.LLM],
|
||||||
|
configurate_methods=[ConfigurateMethod.CUSTOMIZABLE_MODEL],
|
||||||
|
provider_credential_schema=None,
|
||||||
|
model_credential_schema=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include decrypted credentials to simulate the leak source
|
||||||
|
custom_model = CustomModelConfiguration(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
model_type=ModelType.LLM,
|
||||||
|
credentials={"api_key": "sk-plain-text", "endpoint": "https://example.com"},
|
||||||
|
current_credential_id="cred-1",
|
||||||
|
current_credential_name="API KEY 1",
|
||||||
|
available_model_credentials=[],
|
||||||
|
unadded_to_model_list=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_custom_provider = types.SimpleNamespace(
|
||||||
|
current_credential_id="cred-1",
|
||||||
|
current_credential_name="API KEY 1",
|
||||||
|
available_credentials=[CredentialConfiguration(credential_id="cred-1", credential_name="API KEY 1")],
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_custom_configuration = types.SimpleNamespace(
|
||||||
|
provider=fake_custom_provider, models=[custom_model], can_added_models=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_system_configuration = types.SimpleNamespace(enabled=False, current_quota_type=None, quota_configurations=[])
|
||||||
|
|
||||||
|
fake_provider_configuration = types.SimpleNamespace(
|
||||||
|
provider=fake_provider,
|
||||||
|
preferred_provider_type=ProviderType.CUSTOM,
|
||||||
|
custom_configuration=fake_custom_configuration,
|
||||||
|
system_configuration=fake_system_configuration,
|
||||||
|
is_custom_configuration_available=lambda: True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class _FakeProviderManager:
|
||||||
|
def get_configurations(self, tenant_id: str) -> _FakeConfigurations:
|
||||||
|
return _FakeConfigurations(fake_provider_configuration)
|
||||||
|
|
||||||
|
svc = ModelProviderService()
|
||||||
|
svc.provider_manager = _FakeProviderManager()
|
||||||
|
return svc
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_provider_list_strips_credentials(service_with_fake_configurations: ModelProviderService):
|
||||||
|
providers = service_with_fake_configurations.get_provider_list(tenant_id="tenant-1", model_type=None)
|
||||||
|
|
||||||
|
assert len(providers) == 1
|
||||||
|
custom_models = providers[0].custom_configuration.custom_models
|
||||||
|
|
||||||
|
assert custom_models is not None
|
||||||
|
assert len(custom_models) == 1
|
||||||
|
# The sanitizer should drop credentials in list response
|
||||||
|
assert custom_models[0].credentials is None
|
||||||
@ -14,6 +14,7 @@ from core.tools.utils.text_processing_utils import remove_leading_symbols
|
|||||||
("Hello, World!", "Hello, World!"),
|
("Hello, World!", "Hello, World!"),
|
||||||
("", ""),
|
("", ""),
|
||||||
(" ", " "),
|
(" ", " "),
|
||||||
|
("【测试】", "【测试】"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_remove_leading_symbols(input_text, expected_output):
|
def test_remove_leading_symbols(input_text, expected_output):
|
||||||
|
|||||||
8
api/uv.lock
generated
8
api/uv.lock
generated
@ -1681,7 +1681,7 @@ dev = [
|
|||||||
{ name = "types-redis", specifier = ">=4.6.0.20241004" },
|
{ name = "types-redis", specifier = ">=4.6.0.20241004" },
|
||||||
{ name = "types-regex", specifier = "~=2024.11.6" },
|
{ name = "types-regex", specifier = "~=2024.11.6" },
|
||||||
{ name = "types-setuptools", specifier = ">=80.9.0" },
|
{ name = "types-setuptools", specifier = ">=80.9.0" },
|
||||||
{ name = "types-shapely", specifier = "~=2.0.0" },
|
{ name = "types-shapely", specifier = "~=2.1.0" },
|
||||||
{ name = "types-simplejson", specifier = ">=3.20.0" },
|
{ name = "types-simplejson", specifier = ">=3.20.0" },
|
||||||
{ name = "types-six", specifier = ">=1.17.0" },
|
{ name = "types-six", specifier = ">=1.17.0" },
|
||||||
{ name = "types-tensorflow", specifier = ">=2.18.0" },
|
{ name = "types-tensorflow", specifier = ">=2.18.0" },
|
||||||
@ -6557,14 +6557,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-shapely"
|
name = "types-shapely"
|
||||||
version = "2.0.0.20250404"
|
version = "2.1.0.20250917"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/55/c71a25fd3fc9200df4d0b5fd2f6d74712a82f9a8bbdd90cefb9e6aee39dd/types_shapely-2.0.0.20250404.tar.gz", hash = "sha256:863f540b47fa626c33ae64eae06df171f9ab0347025d4458d2df496537296b4f", size = 25066, upload-time = "2025-04-04T02:54:30.592Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/19/7f28b10994433d43b9caa66f3b9bd6a0a9192b7ce8b5a7fc41534e54b821/types_shapely-2.1.0.20250917.tar.gz", hash = "sha256:5c56670742105aebe40c16414390d35fcaa55d6f774d328c1a18273ab0e2134a", size = 26363, upload-time = "2025-09-17T02:47:44.604Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/ff/7f4d414eb81534ba2476f3d54f06f1463c2ebf5d663fd10cff16ba607dd6/types_shapely-2.0.0.20250404-py3-none-any.whl", hash = "sha256:170fb92f5c168a120db39b3287697fdec5c93ef3e1ad15e52552c36b25318821", size = 36350, upload-time = "2025-04-04T02:54:29.506Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/a9/554ac40810e530263b6163b30a2b623bc16aae3fb64416f5d2b3657d0729/types_shapely-2.1.0.20250917-py3-none-any.whl", hash = "sha256:9334a79339504d39b040426be4938d422cec419168414dc74972aa746a8bf3a1", size = 37813, upload-time = "2025-09-17T02:47:43.788Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "dify-web",
|
"name": "dify-web",
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
|
"packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=v22.11.0"
|
"node": ">=v22.11.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user