mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 00:18:03 +08:00
Merge main HEAD (segment 5) into sandboxed-agent-rebase
Resolve 83 conflicts: 10 backend, 62 frontend, 11 config/lock files. Preserve sandbox/agent/collaboration features while adopting main's UI refactorings (Dialog/AlertDialog/Popover), model provider updates, and enterprise features. Made-with: Cursor
This commit is contained in:
@ -12,6 +12,7 @@ import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.dataset import DatasetCollectionBinding
|
||||
from models.enums import CollectionBindingType
|
||||
from services.dataset_service import DatasetCollectionBindingService
|
||||
|
||||
|
||||
@ -32,7 +33,7 @@ class DatasetCollectionBindingTestDataFactory:
|
||||
provider_name: str = "openai",
|
||||
model_name: str = "text-embedding-ada-002",
|
||||
collection_name: str = "collection-abc",
|
||||
collection_type: str = "dataset",
|
||||
collection_type: str = CollectionBindingType.DATASET,
|
||||
) -> DatasetCollectionBinding:
|
||||
"""
|
||||
Create a DatasetCollectionBinding with specified attributes.
|
||||
@ -41,7 +42,7 @@ class DatasetCollectionBindingTestDataFactory:
|
||||
provider_name: Name of the embedding model provider (e.g., "openai", "cohere")
|
||||
model_name: Name of the embedding model (e.g., "text-embedding-ada-002")
|
||||
collection_name: Name of the vector database collection
|
||||
collection_type: Type of collection (default: "dataset")
|
||||
collection_type: Type of collection (default: CollectionBindingType.DATASET)
|
||||
|
||||
Returns:
|
||||
DatasetCollectionBinding instance
|
||||
@ -76,7 +77,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
|
||||
# Arrange
|
||||
provider_name = "openai"
|
||||
model_name = "text-embedding-ada-002"
|
||||
collection_type = "dataset"
|
||||
collection_type = CollectionBindingType.DATASET
|
||||
existing_binding = DatasetCollectionBindingTestDataFactory.create_collection_binding(
|
||||
db_session_with_containers,
|
||||
provider_name=provider_name,
|
||||
@ -104,7 +105,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
|
||||
# Arrange
|
||||
provider_name = f"provider-{uuid4()}"
|
||||
model_name = f"model-{uuid4()}"
|
||||
collection_type = "dataset"
|
||||
collection_type = CollectionBindingType.DATASET
|
||||
|
||||
# Act
|
||||
result = DatasetCollectionBindingService.get_dataset_collection_binding(
|
||||
@ -145,7 +146,7 @@ class TestDatasetCollectionBindingServiceGetBinding:
|
||||
result = DatasetCollectionBindingService.get_dataset_collection_binding(provider_name, model_name)
|
||||
|
||||
# Assert
|
||||
assert result.type == "dataset"
|
||||
assert result.type == CollectionBindingType.DATASET
|
||||
assert result.provider_name == provider_name
|
||||
assert result.model_name == model_name
|
||||
|
||||
@ -186,18 +187,20 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
|
||||
provider_name="openai",
|
||||
model_name="text-embedding-ada-002",
|
||||
collection_name="test-collection",
|
||||
collection_type="dataset",
|
||||
collection_type=CollectionBindingType.DATASET,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(binding.id, "dataset")
|
||||
result = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||
binding.id, CollectionBindingType.DATASET
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.id == binding.id
|
||||
assert result.provider_name == "openai"
|
||||
assert result.model_name == "text-embedding-ada-002"
|
||||
assert result.collection_name == "test-collection"
|
||||
assert result.type == "dataset"
|
||||
assert result.type == CollectionBindingType.DATASET
|
||||
|
||||
def test_get_dataset_collection_binding_by_id_and_type_not_found_error(self, db_session_with_containers: Session):
|
||||
"""Test error handling when collection binding is not found by ID and type."""
|
||||
@ -206,7 +209,9 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError, match="Dataset collection binding not found"):
|
||||
DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(non_existent_id, "dataset")
|
||||
DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type(
|
||||
non_existent_id, CollectionBindingType.DATASET
|
||||
)
|
||||
|
||||
def test_get_dataset_collection_binding_by_id_and_type_different_collection_type(
|
||||
self, db_session_with_containers: Session
|
||||
@ -240,7 +245,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
|
||||
provider_name="openai",
|
||||
model_name="text-embedding-ada-002",
|
||||
collection_name="test-collection",
|
||||
collection_type="dataset",
|
||||
collection_type=CollectionBindingType.DATASET,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -248,7 +253,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
|
||||
|
||||
# Assert
|
||||
assert result.id == binding.id
|
||||
assert result.type == "dataset"
|
||||
assert result.type == CollectionBindingType.DATASET
|
||||
|
||||
def test_get_dataset_collection_binding_by_id_and_type_wrong_type_error(self, db_session_with_containers: Session):
|
||||
"""Test error when binding exists but with wrong collection type."""
|
||||
@ -258,7 +263,7 @@ class TestDatasetCollectionBindingServiceGetBindingByIdAndType:
|
||||
provider_name="openai",
|
||||
model_name="text-embedding-ada-002",
|
||||
collection_name="test-collection",
|
||||
collection_type="dataset",
|
||||
collection_type=CollectionBindingType.DATASET,
|
||||
)
|
||||
|
||||
# Act & Assert
|
||||
|
||||
@ -15,6 +15,7 @@ from werkzeug.exceptions import NotFound
|
||||
|
||||
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import AppDatasetJoin, Dataset, DatasetPermissionEnum
|
||||
from models.enums import DataSourceType
|
||||
from models.model import App
|
||||
from services.dataset_service import DatasetService
|
||||
from services.errors.account import NoPermissionError
|
||||
@ -72,7 +73,7 @@ class DatasetUpdateDeleteTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description="Test description",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique="high_quality",
|
||||
created_by=created_by,
|
||||
permission=permission,
|
||||
|
||||
@ -13,9 +13,10 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from extensions.storage.storage_type import StorageType
|
||||
from models import Account
|
||||
from models.dataset import Dataset, Document
|
||||
from models.enums import CreatorUserRole
|
||||
from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||
from models.model import UploadFile
|
||||
from services.dataset_service import DocumentService
|
||||
from services.errors.document import DocumentIndexingError
|
||||
@ -88,7 +89,7 @@ class DocumentStatusTestDataFactory:
|
||||
data_source_info=json.dumps(data_source_info or {}),
|
||||
batch=f"batch-{uuid4()}",
|
||||
name=name,
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=created_by,
|
||||
doc_form="text_model",
|
||||
)
|
||||
@ -100,7 +101,7 @@ class DocumentStatusTestDataFactory:
|
||||
document.paused_by = paused_by
|
||||
document.paused_at = paused_at
|
||||
document.doc_metadata = doc_metadata or {}
|
||||
if indexing_status == "completed" and "completed_at" not in kwargs:
|
||||
if indexing_status == IndexingStatus.COMPLETED and "completed_at" not in kwargs:
|
||||
document.completed_at = FIXED_TIME
|
||||
|
||||
for key, value in kwargs.items():
|
||||
@ -139,7 +140,7 @@ class DocumentStatusTestDataFactory:
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=created_by,
|
||||
)
|
||||
dataset.id = dataset_id
|
||||
@ -198,7 +199,7 @@ class DocumentStatusTestDataFactory:
|
||||
"""
|
||||
upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
storage_type=StorageType.LOCAL,
|
||||
key=f"uploads/{uuid4()}",
|
||||
name=name,
|
||||
size=128,
|
||||
@ -291,7 +292,7 @@ class TestDocumentServicePauseDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="waiting",
|
||||
indexing_status=IndexingStatus.WAITING,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -326,7 +327,7 @@ class TestDocumentServicePauseDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="indexing",
|
||||
indexing_status=IndexingStatus.INDEXING,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -354,7 +355,7 @@ class TestDocumentServicePauseDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="parsing",
|
||||
indexing_status=IndexingStatus.PARSING,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -383,7 +384,7 @@ class TestDocumentServicePauseDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -412,7 +413,7 @@ class TestDocumentServicePauseDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -487,7 +488,7 @@ class TestDocumentServiceRecoverDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="indexing",
|
||||
indexing_status=IndexingStatus.INDEXING,
|
||||
is_paused=True,
|
||||
paused_by=str(uuid4()),
|
||||
paused_at=paused_time,
|
||||
@ -526,7 +527,7 @@ class TestDocumentServiceRecoverDocument:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="indexing",
|
||||
indexing_status=IndexingStatus.INDEXING,
|
||||
is_paused=False,
|
||||
)
|
||||
|
||||
@ -609,7 +610,7 @@ class TestDocumentServiceRetryDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
)
|
||||
|
||||
mock_document_service_dependencies["redis_client"].get.return_value = None
|
||||
@ -619,7 +620,7 @@ class TestDocumentServiceRetryDocument:
|
||||
|
||||
# Assert
|
||||
db_session_with_containers.refresh(document)
|
||||
assert document.indexing_status == "waiting"
|
||||
assert document.indexing_status == IndexingStatus.WAITING
|
||||
|
||||
expected_cache_key = f"document_{document.id}_is_retried"
|
||||
mock_document_service_dependencies["redis_client"].setex.assert_called_once_with(expected_cache_key, 600, 1)
|
||||
@ -646,14 +647,14 @@ class TestDocumentServiceRetryDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
)
|
||||
document2 = DocumentStatusTestDataFactory.create_document(
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
position=2,
|
||||
)
|
||||
|
||||
@ -665,8 +666,8 @@ class TestDocumentServiceRetryDocument:
|
||||
# Assert
|
||||
db_session_with_containers.refresh(document1)
|
||||
db_session_with_containers.refresh(document2)
|
||||
assert document1.indexing_status == "waiting"
|
||||
assert document2.indexing_status == "waiting"
|
||||
assert document1.indexing_status == IndexingStatus.WAITING
|
||||
assert document2.indexing_status == IndexingStatus.WAITING
|
||||
|
||||
mock_document_service_dependencies["retry_task"].delay.assert_called_once_with(
|
||||
dataset.id, [document1.id, document2.id], mock_document_service_dependencies["user_id"]
|
||||
@ -693,7 +694,7 @@ class TestDocumentServiceRetryDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
)
|
||||
|
||||
mock_document_service_dependencies["redis_client"].get.return_value = "1"
|
||||
@ -703,7 +704,7 @@ class TestDocumentServiceRetryDocument:
|
||||
DocumentService.retry_document(dataset.id, [document])
|
||||
|
||||
db_session_with_containers.refresh(document)
|
||||
assert document.indexing_status == "error"
|
||||
assert document.indexing_status == IndexingStatus.ERROR
|
||||
|
||||
def test_retry_document_missing_current_user_error(
|
||||
self, db_session_with_containers, mock_document_service_dependencies
|
||||
@ -726,7 +727,7 @@ class TestDocumentServiceRetryDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="error",
|
||||
indexing_status=IndexingStatus.ERROR,
|
||||
)
|
||||
|
||||
mock_document_service_dependencies["redis_client"].get.return_value = None
|
||||
@ -816,7 +817,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
enabled=False,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
document2 = DocumentStatusTestDataFactory.create_document(
|
||||
db_session_with_containers,
|
||||
@ -824,7 +825,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
enabled=False,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
position=2,
|
||||
)
|
||||
document_ids = [document1.id, document2.id]
|
||||
@ -866,7 +867,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
enabled=True,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
completed_at=FIXED_TIME,
|
||||
)
|
||||
document_ids = [document.id]
|
||||
@ -909,7 +910,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
document_id=str(uuid4()),
|
||||
archived=False,
|
||||
enabled=True,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
document_ids = [document.id]
|
||||
|
||||
@ -951,7 +952,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
document_id=str(uuid4()),
|
||||
archived=True,
|
||||
enabled=True,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
document_ids = [document.id]
|
||||
|
||||
@ -1015,7 +1016,7 @@ class TestDocumentServiceBatchUpdateDocumentStatus:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
document_id=str(uuid4()),
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
document_ids = [document.id]
|
||||
|
||||
@ -1098,7 +1099,7 @@ class TestDocumentServiceRenameDocument:
|
||||
document_id=document_id,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -1139,7 +1140,7 @@ class TestDocumentServiceRenameDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=tenant_id,
|
||||
doc_metadata={"existing_key": "existing_value"},
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -1187,7 +1188,7 @@ class TestDocumentServiceRenameDocument:
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=tenant_id,
|
||||
data_source_info={"upload_file_id": upload_file.id},
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -1277,7 +1278,7 @@ class TestDocumentServiceRenameDocument:
|
||||
document_id=document_id,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=str(uuid4()),
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
|
||||
# Act & Assert
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
from models import Account
|
||||
from models.enums import ConversationFromSource, MessageFileBelongsTo
|
||||
from models.model import AppModelConfig, Conversation, EndUser, Message, MessageAgentThought
|
||||
from services.account_service import AccountService, TenantService
|
||||
from services.agent_service import AgentService
|
||||
@ -164,7 +165,7 @@ class TestAgentService:
|
||||
inputs={},
|
||||
status="normal",
|
||||
mode="chat",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
db_session_with_containers.commit()
|
||||
@ -203,7 +204,7 @@ class TestAgentService:
|
||||
answer_unit_price=0.001,
|
||||
provider_response_latency=1.5,
|
||||
currency="USD",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(message)
|
||||
db_session_with_containers.commit()
|
||||
@ -405,7 +406,7 @@ class TestAgentService:
|
||||
inputs={},
|
||||
status="normal",
|
||||
mode="chat",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
db_session_with_containers.commit()
|
||||
@ -444,7 +445,7 @@ class TestAgentService:
|
||||
answer_unit_price=0.001,
|
||||
provider_response_latency=1.5,
|
||||
currency="USD",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(message)
|
||||
db_session_with_containers.commit()
|
||||
@ -477,7 +478,7 @@ class TestAgentService:
|
||||
inputs={},
|
||||
status="normal",
|
||||
mode="chat",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
db_session_with_containers.commit()
|
||||
@ -516,7 +517,7 @@ class TestAgentService:
|
||||
answer_unit_price=0.001,
|
||||
provider_response_latency=1.5,
|
||||
currency="USD",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(message)
|
||||
db_session_with_containers.commit()
|
||||
@ -623,7 +624,7 @@ class TestAgentService:
|
||||
inputs={},
|
||||
status="normal",
|
||||
mode="chat",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
app_model_config_id=None, # Explicitly set to None
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
@ -646,7 +647,7 @@ class TestAgentService:
|
||||
answer_unit_price=0.001,
|
||||
provider_response_latency=1.5,
|
||||
currency="USD",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
)
|
||||
db_session_with_containers.add(message)
|
||||
db_session_with_containers.commit()
|
||||
@ -852,7 +853,7 @@ class TestAgentService:
|
||||
type=FileType.IMAGE,
|
||||
transfer_method=FileTransferMethod.REMOTE_URL,
|
||||
url="http://example.com/file1.jpg",
|
||||
belongs_to="user",
|
||||
belongs_to=MessageFileBelongsTo.USER,
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=message.from_account_id,
|
||||
)
|
||||
@ -861,7 +862,7 @@ class TestAgentService:
|
||||
type=FileType.IMAGE,
|
||||
transfer_method=FileTransferMethod.REMOTE_URL,
|
||||
url="http://example.com/file2.png",
|
||||
belongs_to="user",
|
||||
belongs_to=MessageFileBelongsTo.USER,
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by=message.from_account_id,
|
||||
)
|
||||
|
||||
@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from models import Account
|
||||
from models.enums import ConversationFromSource, InvokeFrom
|
||||
from models.model import MessageAnnotation
|
||||
from services.annotation_service import AppAnnotationService
|
||||
from services.app_service import AppService
|
||||
@ -136,8 +137,8 @@ class TestAnnotationService:
|
||||
system_instruction="",
|
||||
system_instruction_tokens=0,
|
||||
status="normal",
|
||||
invoke_from="console",
|
||||
from_source="console",
|
||||
invoke_from=InvokeFrom.EXPLORE,
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=None,
|
||||
from_account_id=account.id,
|
||||
)
|
||||
@ -174,8 +175,8 @@ class TestAnnotationService:
|
||||
provider_response_latency=0,
|
||||
total_price=0,
|
||||
currency="USD",
|
||||
invoke_from="console",
|
||||
from_source="console",
|
||||
invoke_from=InvokeFrom.EXPLORE,
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=None,
|
||||
from_account_id=account.id,
|
||||
)
|
||||
@ -721,7 +722,7 @@ class TestAnnotationService:
|
||||
query=f"Query {i}: {fake.sentence()}",
|
||||
user_id=account.id,
|
||||
message_id=fake.uuid4(),
|
||||
from_source="console",
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
score=0.8 + (i * 0.1),
|
||||
)
|
||||
|
||||
@ -772,7 +773,7 @@ class TestAnnotationService:
|
||||
query=query,
|
||||
user_id=account.id,
|
||||
message_id=message_id,
|
||||
from_source="console",
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
score=score,
|
||||
)
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ from sqlalchemy import select
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.enums import ConversationFromSource
|
||||
from models.model import App, Conversation, EndUser, Message, MessageAnnotation
|
||||
from services.annotation_service import AppAnnotationService
|
||||
from services.conversation_service import ConversationService
|
||||
@ -107,7 +108,7 @@ class ConversationServiceIntegrationTestDataFactory:
|
||||
system_instruction_tokens=0,
|
||||
status="normal",
|
||||
invoke_from=invoke_from.value,
|
||||
from_source="api" if isinstance(user, EndUser) else "console",
|
||||
from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=user.id if isinstance(user, EndUser) else None,
|
||||
from_account_id=user.id if isinstance(user, Account) else None,
|
||||
dialogue_count=0,
|
||||
@ -154,7 +155,7 @@ class ConversationServiceIntegrationTestDataFactory:
|
||||
currency="USD",
|
||||
status="normal",
|
||||
invoke_from=InvokeFrom.WEB_APP.value,
|
||||
from_source="api" if isinstance(user, EndUser) else "console",
|
||||
from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=user.id if isinstance(user, EndUser) else None,
|
||||
from_account_id=user.id if isinstance(user, Account) else None,
|
||||
)
|
||||
|
||||
@ -16,6 +16,7 @@ from models.dataset import (
|
||||
DatasetPermission,
|
||||
DatasetPermissionEnum,
|
||||
)
|
||||
from models.enums import DataSourceType
|
||||
from services.dataset_service import DatasetPermissionService, DatasetService
|
||||
from services.errors.account import NoPermissionError
|
||||
|
||||
@ -67,7 +68,7 @@ class DatasetPermissionTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description="desc",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique="high_quality",
|
||||
created_by=created_by,
|
||||
permission=permission,
|
||||
|
||||
@ -15,6 +15,7 @@ from core.rag.retrieval.retrieval_methods import RetrievalMethod
|
||||
from dify_graph.model_runtime.entities.model_entities import ModelType
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset, DatasetPermissionEnum, Document, ExternalKnowledgeBindings, Pipeline
|
||||
from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||
from services.dataset_service import DatasetService
|
||||
from services.entities.knowledge_entities.knowledge_entities import RerankingModel, RetrievalModel
|
||||
from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, RagPipelineDatasetCreateEntity
|
||||
@ -74,7 +75,7 @@ class DatasetServiceIntegrationDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description=description,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=indexing_technique,
|
||||
created_by=created_by,
|
||||
provider=provider,
|
||||
@ -98,13 +99,13 @@ class DatasetServiceIntegrationDataFactory:
|
||||
tenant_id=dataset.tenant_id,
|
||||
dataset_id=dataset.id,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
data_source_info='{"upload_file_id": "upload-file-id"}',
|
||||
batch=str(uuid4()),
|
||||
name=name,
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=created_by,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
doc_form="text_model",
|
||||
)
|
||||
db_session_with_containers.add(document)
|
||||
@ -437,7 +438,7 @@ class TestDatasetServiceCreateRagPipelineDataset:
|
||||
created_pipeline = db_session_with_containers.get(Pipeline, result.pipeline_id)
|
||||
assert created_dataset is not None
|
||||
assert created_dataset.name == entity.name
|
||||
assert created_dataset.runtime_mode == "rag_pipeline"
|
||||
assert created_dataset.runtime_mode == DatasetRuntimeMode.RAG_PIPELINE
|
||||
assert created_dataset.created_by == account.id
|
||||
assert created_dataset.permission == DatasetPermissionEnum.ONLY_ME
|
||||
assert created_pipeline is not None
|
||||
|
||||
@ -14,6 +14,7 @@ import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.dataset import Dataset, Document
|
||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||
from services.dataset_service import DocumentService
|
||||
from services.errors.document import DocumentIndexingError
|
||||
|
||||
@ -42,7 +43,7 @@ class DocumentBatchUpdateIntegrationDataFactory:
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant_id or str(uuid4()),
|
||||
name=name,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=created_by or str(uuid4()),
|
||||
)
|
||||
if dataset_id:
|
||||
@ -72,11 +73,11 @@ class DocumentBatchUpdateIntegrationDataFactory:
|
||||
tenant_id=dataset.tenant_id,
|
||||
dataset_id=dataset.id,
|
||||
position=position,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
data_source_info=json.dumps({"upload_file_id": str(uuid4())}),
|
||||
batch=f"batch-{uuid4()}",
|
||||
name=name,
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=created_by or str(uuid4()),
|
||||
doc_form="text_model",
|
||||
)
|
||||
@ -85,7 +86,9 @@ class DocumentBatchUpdateIntegrationDataFactory:
|
||||
document.archived = archived
|
||||
document.indexing_status = indexing_status
|
||||
document.completed_at = (
|
||||
completed_at if completed_at is not None else (FIXED_TIME if indexing_status == "completed" else None)
|
||||
completed_at
|
||||
if completed_at is not None
|
||||
else (FIXED_TIME if indexing_status == IndexingStatus.COMPLETED else None)
|
||||
)
|
||||
|
||||
for key, value in kwargs.items():
|
||||
@ -243,7 +246,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
|
||||
dataset=dataset,
|
||||
document_ids=document_ids,
|
||||
enabled=True,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -277,7 +280,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
|
||||
db_session_with_containers,
|
||||
dataset=dataset,
|
||||
enabled=False,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
completed_at=FIXED_TIME,
|
||||
)
|
||||
|
||||
@ -306,7 +309,7 @@ class TestDatasetServiceBatchUpdateDocumentStatus:
|
||||
db_session_with_containers,
|
||||
dataset=dataset,
|
||||
enabled=True,
|
||||
indexing_status="indexing",
|
||||
indexing_status=IndexingStatus.INDEXING,
|
||||
completed_at=None,
|
||||
)
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset, Document
|
||||
from models.enums import DataSourceType, DocumentCreatedFrom
|
||||
from services.dataset_service import DatasetService
|
||||
|
||||
|
||||
@ -58,7 +59,7 @@ class DatasetDeleteIntegrationDataFactory:
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant_id,
|
||||
name=f"dataset-{uuid4()}",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=indexing_technique,
|
||||
index_struct=index_struct,
|
||||
created_by=created_by,
|
||||
@ -84,10 +85,10 @@ class DatasetDeleteIntegrationDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=dataset_id,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
batch=f"batch-{uuid4()}",
|
||||
name="Document",
|
||||
created_from="upload_file",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=created_by,
|
||||
doc_form=doc_form,
|
||||
)
|
||||
|
||||
@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset, DatasetPermissionEnum, Document, DocumentSegment
|
||||
from models.enums import DataSourceType, DocumentCreatedFrom
|
||||
from services.dataset_service import SegmentService
|
||||
|
||||
|
||||
@ -62,7 +63,7 @@ class SegmentServiceTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=f"Test Dataset {uuid4()}",
|
||||
description="Test description",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique="high_quality",
|
||||
created_by=created_by,
|
||||
permission=DatasetPermissionEnum.ONLY_ME,
|
||||
@ -82,10 +83,10 @@ class SegmentServiceTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=dataset_id,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
batch=f"batch-{uuid4()}",
|
||||
name=f"test-doc-{uuid4()}.txt",
|
||||
created_from="api",
|
||||
created_from=DocumentCreatedFrom.API,
|
||||
created_by=created_by,
|
||||
)
|
||||
db_session_with_containers.add(document)
|
||||
|
||||
@ -24,6 +24,7 @@ from models.dataset import (
|
||||
DatasetProcessRule,
|
||||
DatasetQuery,
|
||||
)
|
||||
from models.enums import DatasetQuerySource, DataSourceType, ProcessRuleMode
|
||||
from models.model import Tag, TagBinding
|
||||
from services.dataset_service import DatasetService, DocumentService
|
||||
|
||||
@ -100,7 +101,7 @@ class DatasetRetrievalTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description="desc",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique="high_quality",
|
||||
created_by=created_by,
|
||||
permission=permission,
|
||||
@ -149,7 +150,7 @@ class DatasetRetrievalTestDataFactory:
|
||||
dataset_query = DatasetQuery(
|
||||
dataset_id=dataset_id,
|
||||
content=content,
|
||||
source="web",
|
||||
source=DatasetQuerySource.APP,
|
||||
source_app_id=None,
|
||||
created_by_role="account",
|
||||
created_by=created_by,
|
||||
@ -601,7 +602,7 @@ class TestDatasetServiceGetProcessRules:
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
created_by=account.id,
|
||||
mode="custom",
|
||||
mode=ProcessRuleMode.CUSTOM,
|
||||
rules=rules_data,
|
||||
)
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||
from dify_graph.model_runtime.entities.model_entities import ModelType
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset, ExternalKnowledgeBindings
|
||||
from models.enums import DataSourceType
|
||||
from services.dataset_service import DatasetService
|
||||
from services.errors.account import NoPermissionError
|
||||
|
||||
@ -64,7 +65,7 @@ class DatasetUpdateTestDataFactory:
|
||||
tenant_id=tenant_id,
|
||||
name=name,
|
||||
description=description,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique=indexing_technique,
|
||||
created_by=created_by,
|
||||
provider=provider,
|
||||
|
||||
@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
from sqlalchemy import select
|
||||
|
||||
from models.dataset import Dataset, Document
|
||||
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
|
||||
from services.dataset_service import DocumentService
|
||||
|
||||
|
||||
@ -11,7 +12,7 @@ def _create_dataset(db_session_with_containers) -> Dataset:
|
||||
dataset = Dataset(
|
||||
tenant_id=str(uuid4()),
|
||||
name=f"dataset-{uuid4()}",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
dataset.id = str(uuid4())
|
||||
@ -35,11 +36,11 @@ def _create_document(
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=dataset_id,
|
||||
position=position,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
data_source_info="{}",
|
||||
batch=f"batch-{uuid4()}",
|
||||
name=f"doc-{uuid4()}",
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=str(uuid4()),
|
||||
doc_form="text_model",
|
||||
)
|
||||
@ -48,7 +49,7 @@ def _create_document(
|
||||
document.enabled = enabled
|
||||
document.archived = archived
|
||||
document.is_paused = is_paused
|
||||
if indexing_status == "completed":
|
||||
if indexing_status == IndexingStatus.COMPLETED:
|
||||
document.completed_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
||||
|
||||
db_session_with_containers.add(document)
|
||||
@ -62,7 +63,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
enabled=True,
|
||||
archived=False,
|
||||
position=1,
|
||||
@ -71,7 +72,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
enabled=False,
|
||||
archived=False,
|
||||
position=2,
|
||||
@ -80,7 +81,7 @@ def test_build_display_status_filters_available(db_session_with_containers):
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
enabled=True,
|
||||
archived=True,
|
||||
position=3,
|
||||
@ -101,14 +102,14 @@ def test_apply_display_status_filter_applies_when_status_present(db_session_with
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="waiting",
|
||||
indexing_status=IndexingStatus.WAITING,
|
||||
position=1,
|
||||
)
|
||||
_create_document(
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
position=2,
|
||||
)
|
||||
|
||||
@ -125,14 +126,14 @@ def test_apply_display_status_filter_returns_same_when_invalid(db_session_with_c
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="waiting",
|
||||
indexing_status=IndexingStatus.WAITING,
|
||||
position=1,
|
||||
)
|
||||
doc2 = _create_document(
|
||||
db_session_with_containers,
|
||||
dataset_id=dataset.id,
|
||||
tenant_id=dataset.tenant_id,
|
||||
indexing_status="completed",
|
||||
indexing_status=IndexingStatus.COMPLETED,
|
||||
position=2,
|
||||
)
|
||||
|
||||
|
||||
@ -7,9 +7,10 @@ from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from extensions.storage.storage_type import StorageType
|
||||
from models import Account
|
||||
from models.dataset import Dataset, Document
|
||||
from models.enums import CreatorUserRole
|
||||
from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom
|
||||
from models.model import UploadFile
|
||||
from services.dataset_service import DocumentService
|
||||
|
||||
@ -33,7 +34,7 @@ def make_dataset(db_session_with_containers, dataset_id=None, tenant_id=None, bu
|
||||
dataset = Dataset(
|
||||
tenant_id=tenant_id,
|
||||
name=f"dataset-{uuid4()}",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=str(uuid4()),
|
||||
)
|
||||
dataset.id = dataset_id
|
||||
@ -62,11 +63,11 @@ def make_document(
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=dataset_id,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
data_source_info=json.dumps(data_source_info or {}),
|
||||
batch=f"batch-{uuid4()}",
|
||||
name=name,
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=str(uuid4()),
|
||||
doc_form="text_model",
|
||||
)
|
||||
@ -83,7 +84,7 @@ def make_upload_file(db_session_with_containers, tenant_id: str, file_id: str, n
|
||||
"""Persist an upload file row referenced by document.data_source_info."""
|
||||
upload_file = UploadFile(
|
||||
tenant_id=tenant_id,
|
||||
storage_type="local",
|
||||
storage_type=StorageType.LOCAL,
|
||||
key=f"uploads/{uuid4()}",
|
||||
name=name,
|
||||
size=128,
|
||||
|
||||
@ -360,10 +360,9 @@ class TestFeatureService:
|
||||
assert result is not None
|
||||
assert isinstance(result, SystemFeatureModel)
|
||||
|
||||
# --- 1. Verify Response Payload Optimization (Data Minimization) ---
|
||||
# Ensure only essential UI flags are returned to unauthenticated clients
|
||||
# to keep the payload lightweight and adhere to architectural boundaries.
|
||||
assert result.license.status == LicenseStatus.NONE
|
||||
# --- 1. Verify only license *status* is exposed to unauthenticated clients ---
|
||||
# Detailed license info (expiry, workspaces) remains auth-gated.
|
||||
assert result.license.status == LicenseStatus.ACTIVE
|
||||
assert result.license.expired_at == ""
|
||||
assert result.license.workspaces.enabled is False
|
||||
assert result.license.workspaces.limit == 0
|
||||
|
||||
@ -8,6 +8,7 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from extensions.ext_database import db
|
||||
from models.enums import FeedbackFromSource, FeedbackRating
|
||||
from models.model import App, Conversation, Message
|
||||
from services.feedback_service import FeedbackService
|
||||
|
||||
@ -47,8 +48,8 @@ class TestFeedbackService:
|
||||
app_id=app_id,
|
||||
conversation_id="test-conversation-id",
|
||||
message_id="test-message-id",
|
||||
rating="like",
|
||||
from_source="user",
|
||||
rating=FeedbackRating.LIKE,
|
||||
from_source=FeedbackFromSource.USER,
|
||||
content="Great answer!",
|
||||
from_end_user_id="user-123",
|
||||
from_account_id=None,
|
||||
@ -61,8 +62,8 @@ class TestFeedbackService:
|
||||
app_id=app_id,
|
||||
conversation_id="test-conversation-id",
|
||||
message_id="test-message-id",
|
||||
rating="dislike",
|
||||
from_source="admin",
|
||||
rating=FeedbackRating.DISLIKE,
|
||||
from_source=FeedbackFromSource.ADMIN,
|
||||
content="Could be more detailed",
|
||||
from_end_user_id=None,
|
||||
from_account_id="admin-456",
|
||||
@ -179,8 +180,8 @@ class TestFeedbackService:
|
||||
# Test with filters
|
||||
result = FeedbackService.export_feedbacks(
|
||||
app_id=sample_data["app"].id,
|
||||
from_source="admin",
|
||||
rating="dislike",
|
||||
from_source=FeedbackFromSource.ADMIN,
|
||||
rating=FeedbackRating.DISLIKE,
|
||||
has_comment=True,
|
||||
start_date="2024-01-01",
|
||||
end_date="2024-12-31",
|
||||
@ -293,8 +294,8 @@ class TestFeedbackService:
|
||||
app_id=sample_data["app"].id,
|
||||
conversation_id="test-conversation-id",
|
||||
message_id="test-message-id",
|
||||
rating="dislike",
|
||||
from_source="user",
|
||||
rating=FeedbackRating.DISLIKE,
|
||||
from_source=FeedbackFromSource.USER,
|
||||
content="回答不够详细,需要更多信息",
|
||||
from_end_user_id="user-123",
|
||||
from_account_id=None,
|
||||
|
||||
@ -9,6 +9,7 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.storage.storage_type import StorageType
|
||||
from models import Account, Tenant
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import EndUser, UploadFile
|
||||
@ -140,7 +141,7 @@ class TestFileService:
|
||||
|
||||
upload_file = UploadFile(
|
||||
tenant_id=account.current_tenant_id if hasattr(account, "current_tenant_id") else str(fake.uuid4()),
|
||||
storage_type="local",
|
||||
storage_type=StorageType.LOCAL,
|
||||
key=f"upload_files/test/{fake.uuid4()}.txt",
|
||||
name="test_file.txt",
|
||||
size=1024,
|
||||
|
||||
@ -7,6 +7,7 @@ import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import ConversationFromSource, FeedbackFromSource, FeedbackRating
|
||||
from models.model import (
|
||||
App,
|
||||
AppAnnotationHitHistory,
|
||||
@ -93,7 +94,7 @@ class TestAppMessageExportServiceIntegration:
|
||||
name="conv",
|
||||
inputs={"seed": 1},
|
||||
status="normal",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
from_end_user_id=str(uuid.uuid4()),
|
||||
)
|
||||
session.add(conversation)
|
||||
@ -128,7 +129,7 @@ class TestAppMessageExportServiceIntegration:
|
||||
total_price=Decimal("0.003"),
|
||||
currency="USD",
|
||||
message_metadata=message_metadata,
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
from_end_user_id=conversation.from_end_user_id,
|
||||
created_at=created_at,
|
||||
)
|
||||
@ -172,8 +173,8 @@ class TestAppMessageExportServiceIntegration:
|
||||
app_id=app.id,
|
||||
conversation_id=conversation.id,
|
||||
message_id=first_message.id,
|
||||
rating="like",
|
||||
from_source="user",
|
||||
rating=FeedbackRating.LIKE,
|
||||
from_source=FeedbackFromSource.USER,
|
||||
content="first",
|
||||
from_end_user_id=conversation.from_end_user_id,
|
||||
)
|
||||
@ -181,8 +182,8 @@ class TestAppMessageExportServiceIntegration:
|
||||
app_id=app.id,
|
||||
conversation_id=conversation.id,
|
||||
message_id=first_message.id,
|
||||
rating="dislike",
|
||||
from_source="user",
|
||||
rating=FeedbackRating.DISLIKE,
|
||||
from_source=FeedbackFromSource.USER,
|
||||
content="second",
|
||||
from_end_user_id=conversation.from_end_user_id,
|
||||
)
|
||||
@ -190,8 +191,8 @@ class TestAppMessageExportServiceIntegration:
|
||||
app_id=app.id,
|
||||
conversation_id=conversation.id,
|
||||
message_id=first_message.id,
|
||||
rating="like",
|
||||
from_source="admin",
|
||||
rating=FeedbackRating.LIKE,
|
||||
from_source=FeedbackFromSource.ADMIN,
|
||||
content="should-be-filtered",
|
||||
from_account_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ import pytest
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.enums import ConversationFromSource, FeedbackRating, InvokeFrom
|
||||
from models.model import MessageFeedback
|
||||
from services.app_service import AppService
|
||||
from services.errors.message import (
|
||||
@ -148,8 +149,8 @@ class TestMessageService:
|
||||
system_instruction="",
|
||||
system_instruction_tokens=0,
|
||||
status="normal",
|
||||
invoke_from="console",
|
||||
from_source="console",
|
||||
invoke_from=InvokeFrom.EXPLORE,
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=None,
|
||||
from_account_id=account.id,
|
||||
)
|
||||
@ -186,8 +187,8 @@ class TestMessageService:
|
||||
provider_response_latency=0,
|
||||
total_price=0,
|
||||
currency="USD",
|
||||
invoke_from="console",
|
||||
from_source="console",
|
||||
invoke_from=InvokeFrom.EXPLORE,
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_end_user_id=None,
|
||||
from_account_id=account.id,
|
||||
)
|
||||
@ -405,7 +406,7 @@ class TestMessageService:
|
||||
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
|
||||
|
||||
# Create feedback
|
||||
rating = "like"
|
||||
rating = FeedbackRating.LIKE
|
||||
content = fake.text(max_nb_chars=100)
|
||||
feedback = MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=account, rating=rating, content=content
|
||||
@ -435,7 +436,11 @@ class TestMessageService:
|
||||
# Test creating feedback with no user
|
||||
with pytest.raises(ValueError, match="user cannot be None"):
|
||||
MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=None, rating="like", content=fake.text(max_nb_chars=100)
|
||||
app_model=app,
|
||||
message_id=message.id,
|
||||
user=None,
|
||||
rating=FeedbackRating.LIKE,
|
||||
content=fake.text(max_nb_chars=100),
|
||||
)
|
||||
|
||||
def test_create_feedback_update_existing(
|
||||
@ -452,14 +457,14 @@ class TestMessageService:
|
||||
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
|
||||
|
||||
# Create initial feedback
|
||||
initial_rating = "like"
|
||||
initial_rating = FeedbackRating.LIKE
|
||||
initial_content = fake.text(max_nb_chars=100)
|
||||
feedback = MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=account, rating=initial_rating, content=initial_content
|
||||
)
|
||||
|
||||
# Update feedback
|
||||
updated_rating = "dislike"
|
||||
updated_rating = FeedbackRating.DISLIKE
|
||||
updated_content = fake.text(max_nb_chars=100)
|
||||
updated_feedback = MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=account, rating=updated_rating, content=updated_content
|
||||
@ -487,7 +492,11 @@ class TestMessageService:
|
||||
|
||||
# Create initial feedback
|
||||
feedback = MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=account, rating="like", content=fake.text(max_nb_chars=100)
|
||||
app_model=app,
|
||||
message_id=message.id,
|
||||
user=account,
|
||||
rating=FeedbackRating.LIKE,
|
||||
content=fake.text(max_nb_chars=100),
|
||||
)
|
||||
|
||||
# Delete feedback by setting rating to None
|
||||
@ -538,7 +547,7 @@ class TestMessageService:
|
||||
app_model=app,
|
||||
message_id=message.id,
|
||||
user=account,
|
||||
rating="like" if i % 2 == 0 else "dislike",
|
||||
rating=FeedbackRating.LIKE if i % 2 == 0 else FeedbackRating.DISLIKE,
|
||||
content=f"Feedback {i}: {fake.text(max_nb_chars=50)}",
|
||||
)
|
||||
feedbacks.append(feedback)
|
||||
@ -568,7 +577,11 @@ class TestMessageService:
|
||||
message = self._create_test_message(db_session_with_containers, app, conversation, account, fake)
|
||||
|
||||
MessageService.create_feedback(
|
||||
app_model=app, message_id=message.id, user=account, rating="like", content=f"Feedback {i}"
|
||||
app_model=app,
|
||||
message_id=message.id,
|
||||
user=account,
|
||||
rating=FeedbackRating.LIKE,
|
||||
content=f"Feedback {i}",
|
||||
)
|
||||
|
||||
# Get feedbacks with pagination
|
||||
|
||||
@ -4,6 +4,7 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from models.enums import ConversationFromSource
|
||||
from models.model import Message
|
||||
from services import message_service
|
||||
from tests.test_containers_integration_tests.helpers.execution_extra_content import (
|
||||
@ -36,7 +37,7 @@ def test_attach_message_extra_contents_assigns_serialized_payload(db_session_wit
|
||||
total_price=Decimal(0),
|
||||
currency="USD",
|
||||
status="normal",
|
||||
from_source="console",
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_account_id=fixture.account.id,
|
||||
)
|
||||
db_session_with_containers.add(message_without_extra_content)
|
||||
|
||||
@ -11,6 +11,14 @@ from sqlalchemy.orm import Session
|
||||
from enums.cloud_plan import CloudPlan
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import (
|
||||
ConversationFromSource,
|
||||
DataSourceType,
|
||||
FeedbackFromSource,
|
||||
FeedbackRating,
|
||||
MessageChainType,
|
||||
MessageFileBelongsTo,
|
||||
)
|
||||
from models.model import (
|
||||
App,
|
||||
AppAnnotationHitHistory,
|
||||
@ -165,7 +173,7 @@ class TestMessagesCleanServiceIntegration:
|
||||
name="Test conversation",
|
||||
inputs={},
|
||||
status="normal",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
from_end_user_id=str(uuid.uuid4()),
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
@ -195,7 +203,7 @@ class TestMessagesCleanServiceIntegration:
|
||||
answer_unit_price=Decimal("0.002"),
|
||||
total_price=Decimal("0.003"),
|
||||
currency="USD",
|
||||
from_source="api",
|
||||
from_source=ConversationFromSource.API,
|
||||
from_account_id=conversation.from_end_user_id,
|
||||
created_at=created_at,
|
||||
)
|
||||
@ -215,8 +223,8 @@ class TestMessagesCleanServiceIntegration:
|
||||
app_id=message.app_id,
|
||||
conversation_id=message.conversation_id,
|
||||
message_id=message.id,
|
||||
rating="like",
|
||||
from_source="api",
|
||||
rating=FeedbackRating.LIKE,
|
||||
from_source=FeedbackFromSource.USER,
|
||||
from_end_user_id=str(uuid.uuid4()),
|
||||
)
|
||||
db_session_with_containers.add(feedback)
|
||||
@ -235,7 +243,7 @@ class TestMessagesCleanServiceIntegration:
|
||||
# MessageChain
|
||||
chain = MessageChain(
|
||||
message_id=message.id,
|
||||
type="system",
|
||||
type=MessageChainType.SYSTEM,
|
||||
input=json.dumps({"test": "input"}),
|
||||
output=json.dumps({"test": "output"}),
|
||||
)
|
||||
@ -248,7 +256,7 @@ class TestMessagesCleanServiceIntegration:
|
||||
type="image",
|
||||
transfer_method="local_file",
|
||||
url="http://example.com/test.jpg",
|
||||
belongs_to="user",
|
||||
belongs_to=MessageFileBelongsTo.USER,
|
||||
created_by_role="end_user",
|
||||
created_by=str(uuid.uuid4()),
|
||||
)
|
||||
@ -287,7 +295,7 @@ class TestMessagesCleanServiceIntegration:
|
||||
dataset_name="Test dataset",
|
||||
document_id=str(uuid.uuid4()),
|
||||
document_name="Test document",
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
segment_id=str(uuid.uuid4()),
|
||||
score=0.9,
|
||||
content="Test content",
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||
from core.rag.index_processor.constant.built_in_field import BuiltInField
|
||||
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document
|
||||
from models.enums import DatasetMetadataType, DataSourceType, DocumentCreatedFrom
|
||||
from services.entities.knowledge_entities.knowledge_entities import MetadataArgs
|
||||
from services.metadata_service import MetadataService
|
||||
|
||||
@ -101,7 +102,7 @@ class TestMetadataService:
|
||||
tenant_id=tenant.id,
|
||||
name=fake.company(),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
created_by=account.id,
|
||||
built_in_field_enabled=False,
|
||||
)
|
||||
@ -132,11 +133,11 @@ class TestMetadataService:
|
||||
tenant_id=dataset.tenant_id,
|
||||
dataset_id=dataset.id,
|
||||
position=1,
|
||||
data_source_type="upload_file",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
data_source_info="{}",
|
||||
batch="test-batch",
|
||||
name=fake.file_name(),
|
||||
created_from="web",
|
||||
created_from=DocumentCreatedFrom.WEB,
|
||||
created_by=account.id,
|
||||
doc_form="text",
|
||||
doc_language="en",
|
||||
@ -163,7 +164,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
@ -201,7 +202,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
long_name = "a" * 256 # 256 characters, exceeding 255 limit
|
||||
metadata_args = MetadataArgs(type="string", name=long_name)
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=long_name)
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."):
|
||||
@ -226,11 +227,11 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create first metadata
|
||||
first_metadata_args = MetadataArgs(type="string", name="duplicate_name")
|
||||
first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="duplicate_name")
|
||||
MetadataService.create_metadata(dataset.id, first_metadata_args)
|
||||
|
||||
# Try to create second metadata with same name
|
||||
second_metadata_args = MetadataArgs(type="number", name="duplicate_name")
|
||||
second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="duplicate_name")
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name already exists."):
|
||||
@ -256,7 +257,7 @@ class TestMetadataService:
|
||||
|
||||
# Try to create metadata with built-in field name
|
||||
built_in_field_name = BuiltInField.document_name
|
||||
metadata_args = MetadataArgs(type="string", name=built_in_field_name)
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=built_in_field_name)
|
||||
|
||||
# Act & Assert: Verify proper error handling
|
||||
with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."):
|
||||
@ -281,7 +282,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -318,7 +319,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Try to update with too long name
|
||||
@ -347,10 +348,10 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create two metadata entries
|
||||
first_metadata_args = MetadataArgs(type="string", name="first_metadata")
|
||||
first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="first_metadata")
|
||||
first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args)
|
||||
|
||||
second_metadata_args = MetadataArgs(type="number", name="second_metadata")
|
||||
second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="second_metadata")
|
||||
second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args)
|
||||
|
||||
# Try to update first metadata with second metadata's name
|
||||
@ -376,7 +377,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="old_name")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Try to update with built-in field name
|
||||
@ -432,7 +433,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata first
|
||||
metadata_args = MetadataArgs(type="string", name="to_be_deleted")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="to_be_deleted")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -496,7 +497,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Create metadata binding
|
||||
@ -798,7 +799,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Mock DocumentService.get_document
|
||||
@ -866,7 +867,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Mock DocumentService.get_document
|
||||
@ -917,7 +918,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Create metadata operation data
|
||||
@ -1038,7 +1039,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Create document and metadata binding
|
||||
@ -1101,7 +1102,7 @@ class TestMetadataService:
|
||||
mock_external_service_dependencies["current_user"].id = account.id
|
||||
|
||||
# Create metadata
|
||||
metadata_args = MetadataArgs(type="string", name="test_metadata")
|
||||
metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata")
|
||||
metadata = MetadataService.create_metadata(dataset.id, metadata_args)
|
||||
|
||||
# Act: Execute the method under test
|
||||
|
||||
@ -4,6 +4,7 @@ import pytest
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.enums import ConversationFromSource
|
||||
from models.model import EndUser, Message
|
||||
from models.web import SavedMessage
|
||||
from services.app_service import AppService
|
||||
@ -132,11 +133,14 @@ class TestSavedMessageService:
|
||||
# Create a simple conversation first
|
||||
from models.model import Conversation
|
||||
|
||||
is_account = hasattr(user, "current_tenant")
|
||||
from_source = ConversationFromSource.CONSOLE if is_account else ConversationFromSource.API
|
||||
|
||||
conversation = Conversation(
|
||||
app_id=app.id,
|
||||
from_source="account" if hasattr(user, "current_tenant") else "end_user",
|
||||
from_end_user_id=user.id if not hasattr(user, "current_tenant") else None,
|
||||
from_account_id=user.id if hasattr(user, "current_tenant") else None,
|
||||
from_source=from_source,
|
||||
from_end_user_id=user.id if not is_account else None,
|
||||
from_account_id=user.id if is_account else None,
|
||||
name=fake.sentence(nb_words=3),
|
||||
inputs={},
|
||||
status="normal",
|
||||
@ -150,9 +154,9 @@ class TestSavedMessageService:
|
||||
message = Message(
|
||||
app_id=app.id,
|
||||
conversation_id=conversation.id,
|
||||
from_source="account" if hasattr(user, "current_tenant") else "end_user",
|
||||
from_end_user_id=user.id if not hasattr(user, "current_tenant") else None,
|
||||
from_account_id=user.id if hasattr(user, "current_tenant") else None,
|
||||
from_source=from_source,
|
||||
from_end_user_id=user.id if not is_account else None,
|
||||
from_account_id=user.id if is_account else None,
|
||||
inputs={},
|
||||
query=fake.sentence(nb_words=5),
|
||||
message=fake.text(max_nb_chars=100),
|
||||
|
||||
@ -9,6 +9,7 @@ from werkzeug.exceptions import NotFound
|
||||
|
||||
from models import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.dataset import Dataset
|
||||
from models.enums import DataSourceType
|
||||
from models.model import App, Tag, TagBinding
|
||||
from services.tag_service import TagService
|
||||
|
||||
@ -100,7 +101,7 @@ class TestTagService:
|
||||
description=fake.text(max_nb_chars=100),
|
||||
provider="vendor",
|
||||
permission="only_me",
|
||||
data_source_type="upload",
|
||||
data_source_type=DataSourceType.UPLOAD_FILE,
|
||||
indexing_technique="high_quality",
|
||||
tenant_id=tenant_id,
|
||||
created_by=mock_external_service_dependencies["current_user"].id,
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from models import Account
|
||||
from models.enums import ConversationFromSource
|
||||
from models.model import Conversation, EndUser
|
||||
from models.web import PinnedConversation
|
||||
from services.account_service import AccountService, TenantService
|
||||
@ -145,7 +146,7 @@ class TestWebConversationService:
|
||||
system_instruction_tokens=50,
|
||||
status="normal",
|
||||
invoke_from=InvokeFrom.WEB_APP,
|
||||
from_source="console" if isinstance(user, Account) else "api",
|
||||
from_source=ConversationFromSource.CONSOLE if isinstance(user, Account) else ConversationFromSource.API,
|
||||
from_end_user_id=user.id if isinstance(user, EndUser) else None,
|
||||
from_account_id=user.id if isinstance(user, Account) else None,
|
||||
dialogue_count=0,
|
||||
|
||||
@ -122,6 +122,7 @@ class TestWorkflowDraftVariableService:
|
||||
name,
|
||||
value,
|
||||
variable_type: DraftVariableType = DraftVariableType.CONVERSATION,
|
||||
user_id: str | None = None,
|
||||
fake=None,
|
||||
):
|
||||
"""
|
||||
@ -144,10 +145,15 @@ class TestWorkflowDraftVariableService:
|
||||
WorkflowDraftVariable: Created test variable instance with proper type configuration
|
||||
"""
|
||||
fake = fake or Faker()
|
||||
if user_id is None:
|
||||
app = db_session_with_containers.query(App).filter_by(id=app_id).first()
|
||||
assert app is not None
|
||||
user_id = app.created_by
|
||||
if variable_type == "conversation":
|
||||
# Create conversation variable using the appropriate factory method
|
||||
variable = WorkflowDraftVariable.new_conversation_variable(
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
value=value,
|
||||
description=fake.text(max_nb_chars=20),
|
||||
@ -156,6 +162,7 @@ class TestWorkflowDraftVariableService:
|
||||
# Create system variable with editable flag and execution context
|
||||
variable = WorkflowDraftVariable.new_sys_variable(
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
value=value,
|
||||
node_execution_id=fake.uuid4(),
|
||||
@ -165,6 +172,7 @@ class TestWorkflowDraftVariableService:
|
||||
# Create node variable with visibility and editability settings
|
||||
variable = WorkflowDraftVariable.new_node_variable(
|
||||
app_id=app_id,
|
||||
user_id=user_id,
|
||||
node_id=node_id,
|
||||
name=name,
|
||||
value=value,
|
||||
@ -189,7 +197,13 @@ class TestWorkflowDraftVariableService:
|
||||
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
|
||||
test_value = StringSegment(value=fake.word())
|
||||
variable = self._create_test_variable(
|
||||
db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "test_var", test_value, fake=fake
|
||||
db_session_with_containers,
|
||||
app.id,
|
||||
CONVERSATION_VARIABLE_NODE_ID,
|
||||
"test_var",
|
||||
test_value,
|
||||
user_id=app.created_by,
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_variable = service.get_variable(variable.id)
|
||||
@ -250,7 +264,7 @@ class TestWorkflowDraftVariableService:
|
||||
["test_node_1", "var3"],
|
||||
]
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_variables = service.get_draft_variables_by_selectors(app.id, selectors)
|
||||
retrieved_variables = service.get_draft_variables_by_selectors(app.id, selectors, user_id=app.created_by)
|
||||
assert len(retrieved_variables) == 3
|
||||
var_names = [var.name for var in retrieved_variables]
|
||||
assert "var1" in var_names
|
||||
@ -288,7 +302,7 @@ class TestWorkflowDraftVariableService:
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
result = service.list_variables_without_values(app.id, page=1, limit=3)
|
||||
result = service.list_variables_without_values(app.id, page=1, limit=3, user_id=app.created_by)
|
||||
assert result.total == 5
|
||||
assert len(result.variables) == 3
|
||||
assert result.variables[0].created_at >= result.variables[1].created_at
|
||||
@ -339,7 +353,7 @@ class TestWorkflowDraftVariableService:
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
result = service.list_node_variables(app.id, node_id)
|
||||
result = service.list_node_variables(app.id, node_id, user_id=app.created_by)
|
||||
assert len(result.variables) == 2
|
||||
for var in result.variables:
|
||||
assert var.node_id == node_id
|
||||
@ -381,7 +395,7 @@ class TestWorkflowDraftVariableService:
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
result = service.list_conversation_variables(app.id)
|
||||
result = service.list_conversation_variables(app.id, user_id=app.created_by)
|
||||
assert len(result.variables) == 2
|
||||
for var in result.variables:
|
||||
assert var.node_id == CONVERSATION_VARIABLE_NODE_ID
|
||||
@ -559,7 +573,7 @@ class TestWorkflowDraftVariableService:
|
||||
assert len(app_variables) == 3
|
||||
assert len(other_app_variables) == 1
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
service.delete_workflow_variables(app.id)
|
||||
service.delete_user_workflow_variables(app.id, user_id=app.created_by)
|
||||
app_variables_after = db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id).all()
|
||||
other_app_variables_after = (
|
||||
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=other_app.id).all()
|
||||
@ -567,6 +581,69 @@ class TestWorkflowDraftVariableService:
|
||||
assert len(app_variables_after) == 0
|
||||
assert len(other_app_variables_after) == 1
|
||||
|
||||
def test_draft_variables_are_isolated_between_users(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test draft variable isolation for different users in the same app.
|
||||
|
||||
This test verifies that:
|
||||
1. Query APIs return only variables owned by the target user.
|
||||
2. User-scoped deletion only removes variables for that user and keeps
|
||||
other users' variables in the same app untouched.
|
||||
"""
|
||||
fake = Faker()
|
||||
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
|
||||
user_a = app.created_by
|
||||
user_b = fake.uuid4()
|
||||
|
||||
# Use identical variable names on purpose to verify uniqueness scope includes user_id.
|
||||
self._create_test_variable(
|
||||
db_session_with_containers,
|
||||
app.id,
|
||||
CONVERSATION_VARIABLE_NODE_ID,
|
||||
"shared_name",
|
||||
StringSegment(value="value_a"),
|
||||
user_id=user_a,
|
||||
fake=fake,
|
||||
)
|
||||
self._create_test_variable(
|
||||
db_session_with_containers,
|
||||
app.id,
|
||||
CONVERSATION_VARIABLE_NODE_ID,
|
||||
"shared_name",
|
||||
StringSegment(value="value_b"),
|
||||
user_id=user_b,
|
||||
fake=fake,
|
||||
)
|
||||
self._create_test_variable(
|
||||
db_session_with_containers,
|
||||
app.id,
|
||||
CONVERSATION_VARIABLE_NODE_ID,
|
||||
"only_a",
|
||||
StringSegment(value="only_a"),
|
||||
user_id=user_a,
|
||||
fake=fake,
|
||||
)
|
||||
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
|
||||
user_a_vars = service.list_conversation_variables(app.id, user_id=user_a)
|
||||
user_b_vars = service.list_conversation_variables(app.id, user_id=user_b)
|
||||
assert {v.name for v in user_a_vars.variables} == {"shared_name", "only_a"}
|
||||
assert {v.name for v in user_b_vars.variables} == {"shared_name"}
|
||||
|
||||
service.delete_user_workflow_variables(app.id, user_id=user_a)
|
||||
|
||||
user_a_remaining = (
|
||||
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, user_id=user_a).count()
|
||||
)
|
||||
user_b_remaining = (
|
||||
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, user_id=user_b).count()
|
||||
)
|
||||
assert user_a_remaining == 0
|
||||
assert user_b_remaining == 1
|
||||
|
||||
def test_delete_node_variables_success(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
@ -627,7 +704,7 @@ class TestWorkflowDraftVariableService:
|
||||
assert len(other_node_variables) == 1
|
||||
assert len(conv_variables) == 1
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
service.delete_node_variables(app.id, node_id)
|
||||
service.delete_node_variables(app.id, node_id, user_id=app.created_by)
|
||||
target_node_variables_after = (
|
||||
db_session_with_containers.query(WorkflowDraftVariable).filter_by(app_id=app.id, node_id=node_id).all()
|
||||
)
|
||||
@ -675,7 +752,7 @@ class TestWorkflowDraftVariableService:
|
||||
|
||||
db_session_with_containers.commit()
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
service.prefill_conversation_variable_default_values(workflow)
|
||||
service.prefill_conversation_variable_default_values(workflow, user_id="00000000-0000-0000-0000-000000000001")
|
||||
draft_variables = (
|
||||
db_session_with_containers.query(WorkflowDraftVariable)
|
||||
.filter_by(app_id=app.id, node_id=CONVERSATION_VARIABLE_NODE_ID)
|
||||
@ -715,7 +792,7 @@ class TestWorkflowDraftVariableService:
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id)
|
||||
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id, app.created_by)
|
||||
assert retrieved_conv_id == conversation_id
|
||||
|
||||
def test_get_conversation_id_from_draft_variable_not_found(
|
||||
@ -731,7 +808,7 @@ class TestWorkflowDraftVariableService:
|
||||
fake = Faker()
|
||||
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id)
|
||||
retrieved_conv_id = service._get_conversation_id_from_draft_variable(app.id, app.created_by)
|
||||
assert retrieved_conv_id is None
|
||||
|
||||
def test_list_system_variables_success(
|
||||
@ -772,7 +849,7 @@ class TestWorkflowDraftVariableService:
|
||||
db_session_with_containers, app.id, CONVERSATION_VARIABLE_NODE_ID, "conv_var", conv_var_value, fake=fake
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
result = service.list_system_variables(app.id)
|
||||
result = service.list_system_variables(app.id, user_id=app.created_by)
|
||||
assert len(result.variables) == 2
|
||||
for var in result.variables:
|
||||
assert var.node_id == SYSTEM_VARIABLE_NODE_ID
|
||||
@ -819,15 +896,15 @@ class TestWorkflowDraftVariableService:
|
||||
fake=fake,
|
||||
)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_conv_var = service.get_conversation_variable(app.id, "test_conv_var")
|
||||
retrieved_conv_var = service.get_conversation_variable(app.id, "test_conv_var", user_id=app.created_by)
|
||||
assert retrieved_conv_var is not None
|
||||
assert retrieved_conv_var.name == "test_conv_var"
|
||||
assert retrieved_conv_var.node_id == CONVERSATION_VARIABLE_NODE_ID
|
||||
retrieved_sys_var = service.get_system_variable(app.id, "test_sys_var")
|
||||
retrieved_sys_var = service.get_system_variable(app.id, "test_sys_var", user_id=app.created_by)
|
||||
assert retrieved_sys_var is not None
|
||||
assert retrieved_sys_var.name == "test_sys_var"
|
||||
assert retrieved_sys_var.node_id == SYSTEM_VARIABLE_NODE_ID
|
||||
retrieved_node_var = service.get_node_variable(app.id, "test_node", "test_node_var")
|
||||
retrieved_node_var = service.get_node_variable(app.id, "test_node", "test_node_var", user_id=app.created_by)
|
||||
assert retrieved_node_var is not None
|
||||
assert retrieved_node_var.name == "test_node_var"
|
||||
assert retrieved_node_var.node_id == "test_node"
|
||||
@ -845,9 +922,14 @@ class TestWorkflowDraftVariableService:
|
||||
fake = Faker()
|
||||
app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, fake=fake)
|
||||
service = WorkflowDraftVariableService(db_session_with_containers)
|
||||
retrieved_conv_var = service.get_conversation_variable(app.id, "non_existent_conv_var")
|
||||
retrieved_conv_var = service.get_conversation_variable(app.id, "non_existent_conv_var", user_id=app.created_by)
|
||||
assert retrieved_conv_var is None
|
||||
retrieved_sys_var = service.get_system_variable(app.id, "non_existent_sys_var")
|
||||
retrieved_sys_var = service.get_system_variable(app.id, "non_existent_sys_var", user_id=app.created_by)
|
||||
assert retrieved_sys_var is None
|
||||
retrieved_node_var = service.get_node_variable(app.id, "test_node", "non_existent_node_var")
|
||||
retrieved_node_var = service.get_node_variable(
|
||||
app.id,
|
||||
"test_node",
|
||||
"non_existent_node_var",
|
||||
user_id=app.created_by,
|
||||
)
|
||||
assert retrieved_node_var is None
|
||||
|
||||
@ -7,7 +7,7 @@ import pytest
|
||||
from faker import Faker
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.enums import CreatorUserRole
|
||||
from models.enums import ConversationFromSource, CreatorUserRole
|
||||
from models.model import (
|
||||
Message,
|
||||
)
|
||||
@ -165,7 +165,7 @@ class TestWorkflowRunService:
|
||||
inputs={},
|
||||
status="normal",
|
||||
mode="chat",
|
||||
from_source=CreatorUserRole.ACCOUNT,
|
||||
from_source=ConversationFromSource.CONSOLE,
|
||||
from_account_id=account.id,
|
||||
)
|
||||
db_session_with_containers.add(conversation)
|
||||
@ -186,7 +186,7 @@ class TestWorkflowRunService:
|
||||
message.answer_price_unit = 0.001
|
||||
message.currency = "USD"
|
||||
message.status = "normal"
|
||||
message.from_source = CreatorUserRole.ACCOUNT
|
||||
message.from_source = ConversationFromSource.CONSOLE
|
||||
message.from_account_id = account.id
|
||||
message.workflow_run_id = workflow_run.id
|
||||
message.inputs = {"input": "test input"}
|
||||
|
||||
@ -802,6 +802,81 @@ class TestWorkflowService:
|
||||
with pytest.raises(ValueError, match="No valid workflow found"):
|
||||
workflow_service.publish_workflow(session=db_session_with_containers, app_model=app, account=account)
|
||||
|
||||
def test_restore_published_workflow_to_draft_does_not_persist_normalized_source_features(
|
||||
self, db_session_with_containers: Session
|
||||
):
|
||||
"""Restore copies legacy feature JSON into draft without rewriting the source row."""
|
||||
fake = Faker()
|
||||
account = self._create_test_account(db_session_with_containers, fake)
|
||||
app = self._create_test_app(db_session_with_containers, fake)
|
||||
app.mode = AppMode.ADVANCED_CHAT
|
||||
|
||||
legacy_features = {
|
||||
"file_upload": {
|
||||
"image": {
|
||||
"enabled": True,
|
||||
"number_limits": 6,
|
||||
"transfer_methods": ["remote_url", "local_file"],
|
||||
}
|
||||
},
|
||||
"opening_statement": "",
|
||||
"retriever_resource": {"enabled": True},
|
||||
"sensitive_word_avoidance": {"enabled": False},
|
||||
"speech_to_text": {"enabled": False},
|
||||
"suggested_questions": [],
|
||||
"suggested_questions_after_answer": {"enabled": False},
|
||||
"text_to_speech": {"enabled": False, "language": "", "voice": ""},
|
||||
}
|
||||
published_workflow = Workflow(
|
||||
id=fake.uuid4(),
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type=WorkflowType.WORKFLOW,
|
||||
version="2026.03.19.001",
|
||||
graph=json.dumps({"nodes": [], "edges": []}),
|
||||
features=json.dumps(legacy_features),
|
||||
created_by=account.id,
|
||||
updated_by=account.id,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
)
|
||||
draft_workflow = Workflow(
|
||||
id=fake.uuid4(),
|
||||
tenant_id=app.tenant_id,
|
||||
app_id=app.id,
|
||||
type=WorkflowType.WORKFLOW,
|
||||
version=Workflow.VERSION_DRAFT,
|
||||
graph=json.dumps({"nodes": [], "edges": []}),
|
||||
features=json.dumps({}),
|
||||
created_by=account.id,
|
||||
updated_by=account.id,
|
||||
environment_variables=[],
|
||||
conversation_variables=[],
|
||||
)
|
||||
db_session_with_containers.add(published_workflow)
|
||||
db_session_with_containers.add(draft_workflow)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
restored_workflow = workflow_service.restore_published_workflow_to_draft(
|
||||
app_model=app,
|
||||
workflow_id=published_workflow.id,
|
||||
account=account,
|
||||
)
|
||||
|
||||
db_session_with_containers.expire_all()
|
||||
refreshed_published_workflow = (
|
||||
db_session_with_containers.query(Workflow).filter_by(id=published_workflow.id).first()
|
||||
)
|
||||
refreshed_draft_workflow = db_session_with_containers.query(Workflow).filter_by(id=draft_workflow.id).first()
|
||||
|
||||
assert restored_workflow.id == draft_workflow.id
|
||||
assert refreshed_published_workflow is not None
|
||||
assert refreshed_draft_workflow is not None
|
||||
assert refreshed_published_workflow.serialized_features == json.dumps(legacy_features)
|
||||
assert refreshed_draft_workflow.serialized_features == json.dumps(legacy_features)
|
||||
|
||||
def test_get_default_block_configs(self, db_session_with_containers: Session):
|
||||
"""
|
||||
Test retrieval of default block configurations for all node types.
|
||||
|
||||
@ -48,41 +48,42 @@ class TestToolTransformService:
|
||||
name=fake.company(),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
icon='{"background": "#FF6B6B", "content": "🔧"}',
|
||||
icon_dark='{"background": "#252525", "content": "🔧"}',
|
||||
tenant_id="test_tenant_id",
|
||||
user_id="test_user_id",
|
||||
credentials={"auth_type": "api_key_header", "api_key": "test_key"},
|
||||
provider_type="api",
|
||||
credentials_str='{"auth_type": "api_key_header", "api_key": "test_key"}',
|
||||
schema="{}",
|
||||
schema_type_str="openapi",
|
||||
tools_str="[]",
|
||||
)
|
||||
elif provider_type == "builtin":
|
||||
provider = BuiltinToolProvider(
|
||||
name=fake.company(),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
icon="🔧",
|
||||
icon_dark="🔧",
|
||||
tenant_id="test_tenant_id",
|
||||
user_id="test_user_id",
|
||||
provider="test_provider",
|
||||
credential_type="api_key",
|
||||
credentials={"api_key": "test_key"},
|
||||
encrypted_credentials='{"api_key": "test_key"}',
|
||||
)
|
||||
elif provider_type == "workflow":
|
||||
provider = WorkflowToolProvider(
|
||||
name=fake.company(),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
icon='{"background": "#FF6B6B", "content": "🔧"}',
|
||||
icon_dark='{"background": "#252525", "content": "🔧"}',
|
||||
tenant_id="test_tenant_id",
|
||||
user_id="test_user_id",
|
||||
workflow_id="test_workflow_id",
|
||||
app_id="test_workflow_id",
|
||||
label="Test Workflow",
|
||||
version="1.0.0",
|
||||
parameter_configuration="[]",
|
||||
)
|
||||
elif provider_type == "mcp":
|
||||
provider = MCPToolProvider(
|
||||
name=fake.company(),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
provider_icon='{"background": "#FF6B6B", "content": "🔧"}',
|
||||
icon='{"background": "#FF6B6B", "content": "🔧"}',
|
||||
tenant_id="test_tenant_id",
|
||||
user_id="test_user_id",
|
||||
server_url="https://mcp.example.com",
|
||||
server_url_hash="test_server_url_hash",
|
||||
server_identifier="test_server",
|
||||
tools='[{"name": "test_tool", "description": "Test tool"}]',
|
||||
authed=True,
|
||||
|
||||
@ -510,7 +510,7 @@ class TestWorkflowConverter:
|
||||
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.MULTIPLE,
|
||||
top_k=10,
|
||||
score_threshold=0.8,
|
||||
reranking_model={"provider": "cohere", "model": "rerank-v2"},
|
||||
reranking_model={"reranking_provider_name": "cohere", "reranking_model_name": "rerank-v2"},
|
||||
reranking_enabled=True,
|
||||
),
|
||||
)
|
||||
@ -543,8 +543,8 @@ class TestWorkflowConverter:
|
||||
multiple_config = node["data"]["multiple_retrieval_config"]
|
||||
assert multiple_config["top_k"] == 10
|
||||
assert multiple_config["score_threshold"] == 0.8
|
||||
assert multiple_config["reranking_model"]["provider"] == "cohere"
|
||||
assert multiple_config["reranking_model"]["model"] == "rerank-v2"
|
||||
assert multiple_config["reranking_model"]["reranking_provider_name"] == "cohere"
|
||||
assert multiple_config["reranking_model"]["reranking_model_name"] == "rerank-v2"
|
||||
|
||||
# Verify single retrieval config is None for multiple strategy
|
||||
assert node["data"]["single_retrieval_config"] is None
|
||||
|
||||
Reference in New Issue
Block a user