Compare commits

...

8 Commits

Author SHA1 Message Date
yyh
eb5946aac8 refactor(web): remove human input preview React.FC 2026-08-09 17:33:26 +08:00
yyh
20d0a2f57e fix(web): name human input preview close 2026-08-09 17:32:33 +08:00
chelsealong
7522ae14b2 fix(agent): preserve plugin-declared model parameters through Agent Soul (#40163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:48:01 +00:00
Likalikali
e09eee8fd1 refactor: replace manual model_validate with @model_validate in datasets controllers (#40236)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:25:27 +00:00
Likalikali
b692ddd80c refactor: replace manual model_validate with @model_validate in workspace remaining controllers (#40239)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:23:51 +00:00
Likalikali
5ebb389d26 refactor: replace manual model_validate with @model_validate in rag_pipeline controllers (#40237) 2026-08-09 08:22:50 +00:00
Likalikali
fff6f7cf2f refactor: replace manual model_validate with @model_validate in app workflow controllers (#40235) 2026-08-09 08:20:10 +00:00
Likalikali
516fd12b8d refactor: replace manual model_validate with @model_validate in app core controllers (#40234)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:18:02 +00:00
80 changed files with 1724 additions and 930 deletions

View File

@@ -159,8 +159,12 @@ class AgentBackendModelConfig(BaseModel):
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
# only receives the keys the runtime contract accepts.
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
# config only receives the keys the runtime contract accepts directly; anything
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
# provider-specific parameters (see
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"temperature",
"top_p",
@@ -168,6 +172,7 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"frequency_penalty",
"max_tokens",
)
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
@@ -177,6 +182,15 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
stop = settings.get("stop")
if isinstance(stop, list) and stop:
sanitized["stop_sequences"] = stop
extra_body: dict[str, JsonValue] = {
key: value
for key, value in settings.items()
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
}
if extra_body:
sanitized["extra_body"] = extra_body
return sanitized or None

View File

@@ -1,6 +1,5 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -10,7 +9,7 @@ from controllers.common.schema import (
register_response_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, setup_required
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
@@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(AdvancedPromptTemplateQuery)
def get(self, req_data: AdvancedPromptTemplateQuery):
prompt_args: AdvancedPromptTemplateArgs = {
"app_mode": args.app_mode,
"model_mode": args.model_mode,
"model_name": args.model_name,
"has_context": args.has_context,
"app_mode": req_data.app_mode,
"model_mode": req_data.model_mode,
"model_name": req_data.model_name,
"has_context": req_data.has_context,
}
return AdvancedPromptTemplateService.get_prompt(prompt_args)

View File

@@ -21,6 +21,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(session, app_model, node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = session.scalar(
select(UploadFile).where(
@@ -341,11 +342,11 @@ class AgentLogApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT])
def get(self, session: Session, app_model: App):
@model_validate(AgentLogQuery)
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
"""Get agent logs"""
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")

View File

@@ -25,6 +25,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentAppFeaturesPayload)
def post(
self,
req_data: AgentAppFeaturesPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
new_app_model_config = AgentAppFeatureConfigService.update_features(
app_model=app_model,
account=current_user,
config=args.model_dump(exclude_none=True),
config=req_data.model_dump(exclude_none=True),
session=session,
)

View File

@@ -4,7 +4,6 @@ from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
from sqlalchemy import select
@@ -33,6 +32,7 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -697,16 +697,16 @@ class AppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
@model_validate(CreateAppPayload)
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
"""Create app"""
args = CreateAppPayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=args.name,
description=args.description,
mode=args.mode,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
mode=req_data.mode,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
app_service = AppService()
@@ -908,20 +908,20 @@ class AppApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def put(self, session: Session, app_model: App):
@model_validate(UpdateAppPayload)
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
"""Update app"""
args = UpdateAppPayload.model_validate(console_ns.payload)
app_service = AppService()
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
}
app_model = app_service.update_app(app_model, args_dict, session=session)
return AppDetailWithSite.model_validate(
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model(mode=None)
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(CopyAppPayload)
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
"""Copy app"""
# The role of the current user in the ta table must be admin, owner, or editor
args = CopyAppPayload.model_validate(console_ns.payload or {})
with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session)
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
account=current_user,
import_mode=ImportMode.YAML_CONTENT,
yaml_content=yaml_content,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
@agent_manage_required_for_agent_app
@get_app_model
def get(self, app_model: App):
@model_validate(AppExportQuery)
def get(self, req_data: AppExportQuery, app_model: App):
"""Export app"""
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
response = AppExportResponse(
data=AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=args.include_secret,
workflow_id=args.workflow_id,
include_secret=req_data.include_secret,
workflow_id=req_data.workflow_id,
)
)
return response.model_dump(mode="json")
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppNamePayload.model_validate(console_ns.payload)
@model_validate(AppNamePayload)
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_name(app_model, args.name, session=session)
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppIconPayload.model_validate(console_ns.payload or {})
@model_validate(AppIconPayload)
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_icon(
app_model,
args.icon or "",
args.icon_background or "",
args.icon_type,
req_data.icon or "",
req_data.icon_background or "",
req_data.icon_type,
session=session,
)
return AppDetail.model_validate(
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppSiteStatusPayload.model_validate(console_ns.payload)
@model_validate(AppSiteStatusPayload)
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppApiStatusPayload.model_validate(console_ns.payload)
@model_validate(AppApiStatusPayload)
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def post(self, app_model: App):
@model_validate(AppTracePayload)
def post(self, req_data: AppTracePayload, app_model: App):
# add app trace
args = AppTracePayload.model_validate(console_ns.payload)
OpsTraceManager.update_app_tracing_config(
app_id=app_model.id,
enabled=args.enabled,
tracing_provider=args.tracing_provider,
enabled=req_data.enabled,
tracing_provider=req_data.tracing_provider,
)
return SimpleResultResponse(result="success").model_dump(mode="json")

View File

@@ -12,6 +12,7 @@ from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -83,8 +84,8 @@ class AppImportApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
@with_current_user
def post(self, current_user: Account | None = None):
args = AppImportPayload.model_validate(console_ns.payload)
@model_validate(AppImportPayload)
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
# AppDslService performs internal commits for some creation paths, so use a plain
@@ -96,15 +97,15 @@ class AppImportApi(Resource):
try:
result = import_service.import_app(
account=account,
import_mode=args.mode,
yaml_content=args.yaml_content,
yaml_url=args.yaml_url,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
app_id=args.app_id,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
app_id=req_data.app_id,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -113,7 +114,7 @@ class AppImportApi(Resource):
else:
session.commit()
is_created_app = args.app_id is None and result.status in {
is_created_app = req_data.app_id is None and result.status in {
ImportStatus.COMPLETED,
ImportStatus.COMPLETED_WITH_WARNINGS,
}

View File

@@ -31,6 +31,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -276,15 +277,15 @@ class ChatMessageTextApi(Resource):
@login_required
@account_initialization_required
@get_app_model
def post(self, app_model: App):
@model_validate(TextToSpeechPayload)
def post(self, req_data: TextToSpeechPayload, app_model: App):
try:
payload = TextToSpeechPayload.model_validate(console_ns.payload)
message_ref = None
if payload.message_id:
if req_data.message_id:
app_ref = AppRefService.create_app_ref(app_model)
message_ref = AppRefService.create_message_ref(
app_ref,
payload.message_id,
req_data.message_id,
account_id=current_user.id,
)
@@ -292,8 +293,8 @@ class ChatMessageTextApi(Resource):
return AudioService.transcript_tts(
app_model=app_model,
session=db.session(),
text=payload.text,
voice=payload.voice,
text=req_data.text,
voice=req_data.voice,
message_ref=message_ref,
is_draft=True,
)
@@ -339,13 +340,12 @@ class TextModesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
@model_validate(TextToSpeechVoiceQuery)
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
try:
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
response = AudioService.transcript_tts_voices(
tenant_id=app_model.tenant_id,
language=args.language,
language=req_data.language,
)
return dump_response(TextToSpeechVoiceListResponse, response)

View File

@@ -29,6 +29,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,11 +161,11 @@ class CompletionMessageApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
@with_session
@get_app_model(mode=AppMode.COMPLETION)
def post(self, session: Session, current_user: Account, app_model: App):
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
@model_validate(CompletionMessagePayload)
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
args = req_data.model_dump(exclude_none=True, by_alias=True)
streaming = args_model.response_mode != "blocking"
streaming = req_data.response_mode != "blocking"
args["auto_generate_name"] = False
try:

View File

@@ -2,7 +2,7 @@ from typing import Literal
from uuid import UUID
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, or_
@@ -18,6 +18,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -108,17 +109,17 @@ class CompletionConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, session: Session, current_user: Account, app_model: App):
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(CompletionConversationQuery)
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
query = sa.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
query = query.join(Message, Message.conversation_id == Conversation.id).where(
or_(
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
@@ -130,7 +131,7 @@ class CompletionConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -142,7 +143,7 @@ class CompletionConversationApi(Resource):
query = query.where(Conversation.created_at < end_datetime_utc)
# FIXME, the type ignore in this file
if args.annotation_status == "annotated":
if req_data.annotation_status == "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
.join( # type: ignore
@@ -150,7 +151,7 @@ class CompletionConversationApi(Resource):
)
.group_by(Conversation.id)
)
elif args.annotation_status == "not_annotated":
elif req_data.annotation_status == "not_annotated":
query = (
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
.group_by(Conversation.id)
@@ -159,7 +160,7 @@ class CompletionConversationApi(Resource):
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationPaginationResponse,
@@ -238,8 +239,8 @@ class ChatConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, session: Session, current_user: Account, app_model: App):
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ChatConversationQuery)
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
subquery = (
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
@@ -249,10 +250,10 @@ class ChatConversationApi(Resource):
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
keyword_filter = f"%{escaped_keyword}%"
query = (
query.join(
@@ -276,12 +277,12 @@ class ChatConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
if start_datetime_utc:
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at >= start_datetime_utc)
case "created_at" | "-created_at" | _:
@@ -289,13 +290,13 @@ class ChatConversationApi(Resource):
if end_datetime_utc:
end_datetime_utc = end_datetime_utc.replace(second=59)
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at <= end_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at <= end_datetime_utc)
match args.annotation_status:
match req_data.annotation_status:
case "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
@@ -316,7 +317,7 @@ class ChatConversationApi(Resource):
if app_model.mode == AppMode.ADVANCED_CHAT:
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
match args.sort_by:
match req_data.sort_by:
case "created_at":
query = query.order_by(Conversation.created_at.asc())
case "-created_at":
@@ -328,7 +329,7 @@ class ChatConversationApi(Resource):
case _:
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationWithSummaryPaginationResponse,

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import datetime
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -16,6 +15,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def get(self, app_model: App):
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ConversationVariablesQuery)
def get(self, req_data: ConversationVariablesQuery, app_model: App):
stmt = (
select(ConversationVariable)
.where(ConversationVariable.app_id == app_model.id)
.order_by(ConversationVariable.created_at)
)
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
# NOTE: This is a temporary solution to avoid performance issues.
page = 1

View File

@@ -17,7 +17,12 @@ from controllers.console.app.error import (
ProviderQuotaExceededError,
)
from controllers.console.app.wraps import with_session
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
)
from core.app.app_config.entities import ModelConfig
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.helper.code_executor.code_node_provider import CodeNodeProvider
@@ -238,11 +243,11 @@ class RuleGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleGeneratePayload)
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
try:
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
@@ -267,13 +272,13 @@ class RuleCodeGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleCodeGeneratePayload)
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
try:
code_result = LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -299,13 +304,13 @@ class RuleStructuredOutputGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
@model_validate(RuleStructuredOutputPayload)
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
try:
structured_output = LLMGenerator.generate_structured_output(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -332,36 +337,36 @@ class InstructionGenerateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, current_tenant_id: str):
args = InstructionGeneratePayload.model_validate(console_ns.payload)
@model_validate(InstructionGeneratePayload)
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] | None = next(
(p for p in providers if p.is_accept_language(args.language)), None
(p for p in providers if p.is_accept_language(req_data.language)), None
)
code_template = code_provider.get_default_code() if code_provider else ""
try:
# Generate from nothing for a workflow node
if (args.current in (code_template, "")) and args.node_id != "":
if (req_data.current in (code_template, "")) and req_data.node_id != "":
app = session.scalar(
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
)
if not app:
return {"error": f"app {args.flow_id} not found"}, 400
return {"error": f"app {req_data.flow_id} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
if not workflow:
return {"error": f"workflow {args.flow_id} not found"}, 400
return {"error": f"workflow {req_data.flow_id} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == args.node_id]
node = [node for node in nodes if node["id"] == req_data.node_id]
if len(node) == 0:
return {"error": f"node {args.node_id} not found"}, 400
return {"error": f"node {req_data.node_id} not found"}, 400
node_type = node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -369,8 +374,8 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -378,31 +383,31 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=RuleCodeGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
code_language=args.language,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
code_language=req_data.language,
),
)
case _:
return {"error": f"invalid node type: {node_type}"}
if args.node_id == "" and args.current != "": # For legacy app without a workflow
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
)
if args.node_id != "" and args.current != "": # For workflow node
if req_data.node_id != "" and req_data.current != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
node_id=args.node_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
node_id=req_data.node_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
workflow_service=WorkflowService(),
)
return {"error": "incompatible parameters"}, 400
@@ -426,9 +431,9 @@ class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
args = InstructionTemplatePayload.model_validate(console_ns.payload)
match args.type:
@model_validate(InstructionTemplatePayload)
def post(self, req_data: InstructionTemplatePayload):
match req_data.type:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
@@ -438,7 +443,7 @@ class InstructionGenerationTemplateApi(Resource):
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
case _:
raise ValueError(f"Invalid type: {args.type}")
raise ValueError(f"Invalid type: {req_data.type}")
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
@@ -492,24 +497,24 @@ class WorkflowGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Reject empty / over-length instructions at the boundary (shared with
# the streaming endpoint) before spending a planner+builder roundtrip.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
try:
result = WorkflowGeneratorService.generate_workflow_graph(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
if args.current_graph
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None,
)
except ProviderTokenNotInitError as ex:
@@ -547,13 +552,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
@model_validate(WorkflowInstructionSuggestionsPayload)
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
tenant_id=current_tenant_id,
mode=args.mode,
language=args.language,
count=args.count,
mode=req_data.mode,
language=req_data.language,
count=req_data.count,
)
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
@@ -583,12 +588,12 @@ class WorkflowGenerateStreamApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Same boundary guards as the blocking endpoint — return a normal 400
# JSON for these BEFORE opening the stream.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
@@ -596,12 +601,14 @@ class WorkflowGenerateStreamApi(Resource):
try:
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=(
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None
),
):
body = {"event": event_name, **payload}

View File

@@ -28,6 +28,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
args = FeedbackExportQuery.model_validate(request.args.to_dict())
@model_validate(FeedbackExportQuery)
def get(self, req_data: FeedbackExportQuery, app_model: App):
# Import the service function
from services.feedback_service import FeedbackService
@@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource):
export_data = FeedbackService.export_feedbacks(
app_model.id,
session=db.session(),
from_source=args.from_source,
rating=args.rating,
has_comment=args.has_comment,
start_date=args.start_date,
end_date=args.end_date,
format_type=args.format,
from_source=req_data.from_source,
rating=req_data.rating,
has_comment=req_data.has_comment,
start_date=req_data.start_date,
end_date=req_data.end_date,
format_type=req_data.format,
)
return export_data

View File

@@ -17,6 +17,7 @@ from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -98,8 +99,8 @@ class AppSite(Resource):
@with_current_user
@with_session
@get_app_model
def post(self, session: Session, current_user: Account, app_model: App):
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(AppSiteUpdatePayload)
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
if not site:
raise NotFound
@@ -123,7 +124,7 @@ class AppSite(Resource):
"show_workflow_steps",
"use_icon_as_answer_icon",
]:
value = getattr(args, attr_name)
value = getattr(req_data, attr_name)
if value is not None:
setattr(site, attr_name, value)

View File

@@ -1,7 +1,7 @@
from decimal import Decimal
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -177,7 +178,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -237,7 +238,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -296,7 +297,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -357,7 +358,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("c.created_at")
sql_query = f"""SELECT
@@ -427,7 +428,7 @@ FROM
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("m.created_at")
sql_query = f"""SELECT
@@ -502,7 +503,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -567,7 +568,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -630,7 +631,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -2,7 +2,6 @@ from datetime import datetime
from typing import Any
from dateutil.parser import isoparse
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow app logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
# get paginate workflow app logs
workflow_app_service = WorkflowAppService()
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
session=session,
app_model=app_model,
keyword=args.keyword,
status=args.status,
created_at_before=args.created_at__before,
created_at_after=args.created_at__after,
page=args.page,
limit=args.limit,
detail=args.detail,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
keyword=req_data.keyword,
status=req_data.status,
created_at_before=req_data.created_at__before,
created_at_after=req_data.created_at__after,
page=req_data.page,
limit=req_data.limit,
detail=req_data.detail,
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
created_by_account=req_data.created_by_account,
)
return WorkflowAppLogPaginationResponse.model_validate(
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow archived logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
workflow_app_service = WorkflowAppService()
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
session=session,
app_model=app_model,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
)
return WorkflowArchivedLogPaginationResponse.model_validate(

View File

@@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(WorkflowCommentCreatePayload)
def post(
self,
req_data: WorkflowCommentCreatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
):
"""Create a new workflow comment."""
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
created_by=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentCreate, result), 201
@@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentUpdatePayload)
def put(
self,
req_data: WorkflowCommentUpdatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Update a workflow comment."""
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.update_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
user_id=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentUpdate, result)
@@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentReplyPayload)
def post(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Add a reply to a workflow comment."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_reply(
comment_id=comment_id,
content=payload.content,
content=req_data.content,
created_by=current_user.id,
mentioned_user_ids=payload.mentioned_user_ids,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyCreate, result), 201
@@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
@model_validate(WorkflowCommentReplyPayload)
def put(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
reply_id: str,
):
"""Update a comment reply."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
reply = WorkflowCommentService.update_reply(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
reply_id=reply_id,
user_id=current_user.id,
content=payload.content,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyUpdate, reply)

View File

@@ -4,7 +4,7 @@ from functools import wraps
from typing import Any, Concatenate, Self, TypedDict, override
from uuid import UUID
from flask import Response, request
from flask import Response
from flask_restx import Resource, fields, marshal, marshal_with
from pydantic import BaseModel, ConfigDict, Field, model_validator
from sqlalchemy.orm import sessionmaker
@@ -22,6 +22,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -359,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
def get(self, current_user: Account, app_model: App):
@model_validate(WorkflowDraftVariableListQuery)
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
"""
Get draft workflow
"""
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
# fetch draft workflow by app_model
workflow_service = WorkflowService()
@@ -378,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=app_model.id,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
user_id=current_user.id,
)
@@ -479,7 +480,14 @@ class VariableApi(Resource):
@console_ns.response(404, "Variable not found")
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
@model_validate(WorkflowDraftVariableUpdatePayload)
def patch(
self,
req_data: WorkflowDraftVariableUpdatePayload,
current_user: Account,
app_model: App,
variable_id: UUID,
):
# Request payload for file types:
#
# Local File:
@@ -504,7 +512,6 @@ class VariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
variable_id_str = str(variable_id)
variable = ensure_variable_access(
@@ -514,8 +521,8 @@ class VariableApi(Resource):
current_user_id=current_user.id,
)
new_name = args_model.name
raw_value = args_model.value
new_name = req_data.name
raw_value = req_data.value
if new_name is None and raw_value is None:
return variable
@@ -660,13 +667,13 @@ class ConversationVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def post(self, current_user: Account, app_model: App):
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(ConversationVariableUpdatePayload)
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
conversation_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
]
conversation_variables = [
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
@@ -755,24 +762,24 @@ class EnvironmentVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def post(self, current_user: Account, app_model: App):
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(EnvironmentVariableUpdatePayload)
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
environment_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
]
environment_variables = [
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
]
if payload.patch:
if req_data.patch:
workflow_service.patch_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
deleted_environment_variable_ids=payload.deleted_environment_variable_ids,
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
session=db.session(),
)
else:

View File

@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
from typing import Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get advanced chat app workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get advanced chat workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)

View File

@@ -1,4 +1,4 @@
from flask import abort, jsonify, request
from flask import abort, jsonify
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -10,6 +10,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -104,13 +105,13 @@ class WorkflowDailyRunsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -148,13 +149,13 @@ class WorkflowDailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -192,13 +193,13 @@ class WorkflowDailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -236,13 +237,13 @@ class WorkflowAverageAppInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -1,7 +1,6 @@
import logging
from datetime import datetime
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, field_validator
from sqlalchemy import select
@@ -25,6 +24,7 @@ from ..wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -102,11 +102,11 @@ class WebhookTriggerApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__])
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model(mode=AppMode.WORKFLOW)
def get(self, app_model: App):
@model_validate(Parser)
def get(self, req_data: Parser, app_model: App):
"""Get webhook trigger for a node"""
args = Parser.model_validate(request.args.to_dict(flat=True))
node_id = args.node_id
node_id = req_data.node_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Get webhook trigger for this app and node
@@ -175,11 +175,11 @@ class AppTriggerEnableApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
@with_current_tenant_id
@get_app_model(mode=AppMode.WORKFLOW)
def post(self, current_tenant_id: str, app_model: App):
@model_validate(ParserEnable)
def post(self, req_data: ParserEnable, current_tenant_id: str, app_model: App):
"""Update app trigger (enable/disable)"""
args = ParserEnable.model_validate(console_ns.payload)
trigger_id = args.trigger_id
trigger_id = req_data.trigger_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Find the trigger using select
trigger = session.execute(
@@ -194,7 +194,7 @@ class AppTriggerEnableApi(Resource):
raise NotFound("Trigger not found")
# Update status based on enable_trigger boolean
trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED
trigger.status = AppTriggerStatus.ENABLED if req_data.enable_trigger else AppTriggerStatus.DISABLED
# Add computed icon field
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"

View File

@@ -36,6 +36,7 @@ from ..wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -229,12 +230,18 @@ class DataSourceNotionListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account) -> tuple[dict[str, Any], int]:
query = DataSourceNotionListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(DataSourceNotionListQuery)
def get(
self,
req_data: DataSourceNotionListQuery,
session: Session,
current_tenant_id: str,
current_user: Account,
) -> tuple[dict[str, Any], int]:
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=query.credential_id,
credential_id=req_data.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -242,8 +249,8 @@ class DataSourceNotionListApi(Resource):
raise NotFound("Credential not found.")
exist_page_ids = []
# import notion in the exist dataset
if query.dataset_id:
dataset = DatasetService.get_dataset(query.dataset_id, session)
if req_data.dataset_id:
dataset = DatasetService.get_dataset(req_data.dataset_id, session)
if not dataset:
raise NotFound("Dataset not found.")
if dataset.data_source_type != "notion_import":
@@ -251,7 +258,7 @@ class DataSourceNotionListApi(Resource):
documents = session.scalars(
select(Document).where(
Document.dataset_id == query.dataset_id,
Document.dataset_id == req_data.dataset_id,
Document.tenant_id == current_tenant_id,
Document.data_source_type == "notion_import",
Document.enabled.is_(True),
@@ -318,13 +325,19 @@ class DataSourceNotionPreviewApi(Resource):
@console_ns.doc(params=query_params_from_model(DataSourceNotionPreviewQuery))
@console_ns.response(200, "Success", console_ns.models[TextContentResponse.__name__])
@with_current_tenant_id
def get(self, current_tenant_id: str, page_id: UUID, page_type: str) -> tuple[dict[str, str], int]:
query = DataSourceNotionPreviewQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(DataSourceNotionPreviewQuery)
def get(
self,
req_data: DataSourceNotionPreviewQuery,
current_tenant_id: str,
page_id: UUID,
page_type: str,
) -> tuple[dict[str, str], int]:
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=query.credential_id,
credential_id=req_data.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -354,12 +367,17 @@ class DataSourceNotionIndexingEstimateApi(Resource):
@console_ns.response(200, "Success", console_ns.models[IndexingEstimate.__name__])
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str) -> tuple[dict[str, Any], int]:
payload = NotionEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
@model_validate(NotionEstimatePayload)
def post(
self,
req_data: NotionEstimatePayload,
session: Session,
current_tenant_id: str,
) -> tuple[dict[str, Any], int]:
args = req_data.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
notion_info_list = payload.notion_info_list
notion_info_list = req_data.notion_info_list
extract_settings = []
for notion_info in notion_info_list:
workspace_id = notion_info["workspace_id"]

View File

@@ -26,6 +26,7 @@ from controllers.console.wraps import (
cloud_edition_billing_rate_limit_check,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -572,9 +573,8 @@ class DatasetListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = DatasetCreatePayload.model_validate(console_ns.payload or {})
@model_validate(DatasetCreatePayload)
def post(self, req_data: DatasetCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
@@ -582,20 +582,20 @@ class DatasetListApi(Resource):
if dify_config.RBAC_ENABLED:
permission = DatasetPermissionEnum.ALL_TEAM
else:
permission = payload.permission or DatasetPermissionEnum.ONLY_ME
permission = req_data.permission or DatasetPermissionEnum.ONLY_ME
try:
dataset = DatasetService.create_empty_dataset(
session=session,
tenant_id=current_tenant_id,
name=payload.name,
description=payload.description,
indexing_technique=payload.indexing_technique,
name=req_data.name,
description=req_data.description,
indexing_technique=req_data.indexing_technique,
account=current_user,
permission=permission,
provider=payload.provider,
external_knowledge_api_id=payload.external_knowledge_api_id,
external_knowledge_id=payload.external_knowledge_id,
provider=req_data.provider,
external_knowledge_api_id=req_data.external_knowledge_api_id,
external_knowledge_id=req_data.external_knowledge_id,
)
except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError()
@@ -715,28 +715,35 @@ class DatasetApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def patch(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
@model_validate(DatasetUpdatePayload)
def patch(
self,
req_data: DatasetUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
payload = DatasetUpdatePayload.model_validate(console_ns.payload or {})
# check embedding model setting
if (
payload.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and payload.embedding_model_provider is not None
and payload.embedding_model is not None
req_data.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and req_data.embedding_model_provider is not None
and req_data.embedding_model is not None
):
is_multimodal = DatasetService.check_is_multimodal_model(
dataset.tenant_id, payload.embedding_model_provider, payload.embedding_model
dataset.tenant_id, req_data.embedding_model_provider, req_data.embedding_model
)
payload.is_multimodal = is_multimodal
payload_data = payload.model_dump(exclude_unset=True)
req_data.is_multimodal = is_multimodal
payload_data = req_data.model_dump(exclude_unset=True)
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
if not dify_config.RBAC_ENABLED:
DatasetPermissionService.check_permission(
current_user, dataset, payload.permission, payload.partial_member_list, session=session
current_user, dataset, req_data.permission, req_data.partial_member_list, session=session
)
dataset = DatasetService.update_dataset(dataset_id_str, payload_data, current_user, session=session)
@@ -754,12 +761,12 @@ class DatasetApi(Resource):
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
tenant_id = current_tenant_id
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
if req_data.partial_member_list is not None and req_data.permission == DatasetPermissionEnum.PARTIAL_TEAM:
DatasetPermissionService.update_partial_member_list(
tenant_id, dataset_id_str, payload.partial_member_list, session
tenant_id, dataset_id_str, req_data.partial_member_list, session
)
# clear partial member list when permission is only_me or all_team_members
elif payload.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
elif req_data.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
DatasetPermissionService.clear_partial_member_list(dataset_id_str, session)
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, session)
@@ -872,9 +879,9 @@ class DatasetIndexingEstimateApi(Resource):
@console_ns.expect(console_ns.models[IndexingEstimatePayload.__name__])
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str):
payload = IndexingEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
@model_validate(IndexingEstimatePayload)
def post(self, req_data: IndexingEstimatePayload, session: Session, current_tenant_id: str):
args = req_data.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
extract_settings = []

View File

@@ -36,6 +36,7 @@ from controllers.console.wraps import (
cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -414,8 +415,10 @@ class DatasetDocumentSegmentAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentCreatePayload)
def post(
self,
req_data: SegmentCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -455,8 +458,7 @@ class DatasetDocumentSegmentAddApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
payload = SegmentCreatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
payload_dict = req_data.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
segment = type_cast(DocumentSegment, SegmentService.create_segment(payload_dict, document, dataset, session))
summary = SummaryIndexService.get_segment_summary(
@@ -485,8 +487,10 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentUpdatePayload)
def patch(
self,
req_data: SegmentUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -532,13 +536,12 @@ class DatasetDocumentSegmentUpdateApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload = SegmentUpdatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
payload_dict = req_data.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
# Update segment (summary update with change detection is handled in SegmentService.update_segment)
segment = SegmentService.update_segment(
SegmentUpdateArgs.model_validate(payload.model_dump(exclude_none=True)),
SegmentUpdateArgs.model_validate(req_data.model_dump(exclude_none=True)),
segment,
document,
dataset,
@@ -616,8 +619,10 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(BatchImportPayload)
def post(
self,
req_data: BatchImportPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -635,8 +640,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
if not document:
raise NotFound("Document not found.")
payload = BatchImportPayload.model_validate(console_ns.payload or {})
upload_file_id = payload.upload_file_id
upload_file_id = req_data.upload_file_id
upload_file = session.scalar(select(UploadFile).where(UploadFile.id == upload_file_id).limit(1))
if not upload_file:
@@ -697,8 +701,10 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkCreatePayload)
def post(
self,
req_data: ChildChunkCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -742,8 +748,7 @@ class ChildChunkAddApi(Resource):
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
try:
payload = ChildChunkCreatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, session)
child_chunk = SegmentService.create_child_chunk(req_data.content, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200
@@ -812,8 +817,10 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkBatchUpdatePayload)
def patch(
self,
req_data: ChildChunkBatchUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -843,9 +850,8 @@ class ChildChunkAddApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload = ChildChunkBatchUpdatePayload.model_validate(console_ns.payload or {})
try:
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, session)
child_chunks = SegmentService.update_child_chunks(req_data.chunks, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkBatchUpdateResponse, {"data": child_chunks}), 200
@@ -918,8 +924,10 @@ class ChildChunkUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkUpdatePayload)
def patch(
self,
req_data: ChildChunkUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -955,9 +963,8 @@ class ChildChunkUpdateApi(Resource):
raise NotFound("Child chunk not found.")
# validate args
try:
payload = ChildChunkUpdatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.update_child_chunk(
payload.content, child_chunk, segment, document, dataset, session
req_data.content, child_chunk, segment, document, dataset, session
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))

View File

@@ -5,7 +5,6 @@ from datetime import datetime
from typing import Any
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, field_validator
from sqlalchemy.orm import Session
@@ -26,6 +25,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -186,18 +186,18 @@ class ExternalApiTemplateListApi(Resource):
@with_current_tenant_id
@account_initialization_required
@with_session(write=False)
def get(self, session: Session, current_tenant_id: str):
query = ExternalApiTemplateListQuery.model_validate(request.args.to_dict())
@model_validate(ExternalApiTemplateListQuery)
def get(self, req_data: ExternalApiTemplateListQuery, session: Session, current_tenant_id: str):
external_knowledge_apis, total = ExternalDatasetService.get_external_knowledge_apis(
query.page, query.limit, current_tenant_id, query.keyword, session=session
req_data.page, req_data.limit, current_tenant_id, req_data.keyword, session=session
)
return ExternalKnowledgeApiListResponse(
data=[external_knowledge_api_response(item, session=session) for item in external_knowledge_apis],
has_more=len(external_knowledge_apis) == query.limit,
limit=query.limit,
has_more=len(external_knowledge_apis) == req_data.limit,
limit=req_data.limit,
total=total,
page=query.page,
page=req_data.page,
).model_dump(mode="json"), 200
@console_ns.doc("create_external_api_template")
@@ -215,10 +215,16 @@ class ExternalApiTemplateListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
@model_validate(ExternalKnowledgeApiPayload)
def post(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
ExternalDatasetService.validate_api_list(payload.settings)
ExternalDatasetService.validate_api_list(req_data.settings)
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -228,7 +234,7 @@ class ExternalApiTemplateListApi(Resource):
external_knowledge_api = ExternalDatasetService.create_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=payload.model_dump(),
args=req_data.model_dump(),
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -279,17 +285,24 @@ class ExternalApiTemplateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def patch(self, session: Session, current_tenant_id: str, current_user: Account, external_knowledge_api_id: UUID):
@model_validate(ExternalKnowledgeApiPayload)
def patch(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
external_knowledge_api_id: UUID,
):
external_knowledge_api_id_str = str(external_knowledge_api_id)
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
ExternalDatasetService.validate_api_list(payload.settings)
ExternalDatasetService.validate_api_list(req_data.settings)
external_knowledge_api = ExternalDatasetService.update_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
external_knowledge_api_id=external_knowledge_api_id_str,
args=payload.model_dump(),
args=req_data.model_dump(),
session=session,
)
@@ -354,9 +367,15 @@ class ExternalDatasetCreateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
@model_validate(ExternalDatasetCreatePayload)
def post(
self,
req_data: ExternalDatasetCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
# The role of the current user in the ta table must be admin, owner, or editor
payload = ExternalDatasetCreatePayload.model_validate(console_ns.payload or {})
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -366,7 +385,7 @@ class ExternalDatasetCreateApi(Resource):
dataset = ExternalDatasetService.create_external_dataset(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=payload,
args=req_data,
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -405,7 +424,8 @@ class ExternalKnowledgeHitTestingApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
@with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID):
@model_validate(ExternalHitTestingPayload)
def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -416,17 +436,16 @@ class ExternalKnowledgeHitTestingApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
payload = ExternalHitTestingPayload.model_validate(console_ns.payload or {})
HitTestingService.hit_testing_args_check(payload.model_dump())
HitTestingService.hit_testing_args_check(req_data.model_dump())
try:
response = HitTestingService.external_retrieve(
session=session,
dataset=dataset,
query=payload.query,
query=req_data.query,
account=current_user,
external_retrieval_model=payload.external_retrieval_model,
metadata_filtering_conditions=payload.metadata_filtering_conditions,
external_retrieval_model=req_data.external_retrieval_model,
metadata_filtering_conditions=req_data.metadata_filtering_conditions,
)
return dump_response(ExternalHitTestingResponse, response)
@@ -441,11 +460,11 @@ class BedrockRetrievalApi(Resource):
@console_ns.doc(description="Bedrock retrieval test (internal use only)")
@console_ns.expect(console_ns.models[BedrockRetrievalPayload.__name__])
@console_ns.response(200, "Bedrock retrieval test completed", console_ns.models[BedrockRetrievalResponse.__name__])
def post(self):
payload = BedrockRetrievalPayload.model_validate(console_ns.payload or {})
@model_validate(BedrockRetrievalPayload)
def post(self, req_data: BedrockRetrievalPayload):
# Call the knowledge retrieval service
result = ExternalDatasetTestService.knowledge_retrieval(
payload.retrieval_setting, payload.query, payload.knowledge_id
req_data.retrieval_setting, req_data.query, req_data.knowledge_id
)
return dump_response(BedrockRetrievalResponse, result), 200

View File

@@ -14,6 +14,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
enterprise_license_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -59,9 +60,15 @@ class DatasetMetadataCreateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
@model_validate(MetadataArgs)
def post(
self,
req_data: MetadataArgs,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -69,7 +76,7 @@ class DatasetMetadataCreateApi(Resource):
DatasetService.check_dataset_permission(dataset, current_user, session)
metadata = MetadataService.create_metadata(
dataset_id_str, metadata_args, current_user, current_tenant_id, session=session
dataset_id_str, req_data, current_user, current_tenant_id, session=session
)
return dump_response(DatasetMetadataResponse, metadata), 201
@@ -103,16 +110,17 @@ class DatasetMetadataApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(MetadataUpdatePayload)
def patch(
self,
req_data: MetadataUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
metadata_id: UUID,
):
payload = MetadataUpdatePayload.model_validate(console_ns.payload or {})
name = payload.name
name = req_data.name
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
@@ -203,16 +211,15 @@ class DocumentMetadataEditApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID):
@model_validate(MetadataOperationData)
def post(self, req_data: MetadataOperationData, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, session)
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
MetadataService.update_documents_metadata(dataset, metadata_args, current_user, session=session)
MetadataService.update_documents_metadata(dataset, req_data, current_user, session=session)
# Frontend callers only await success and invalidate caches; no response body is consumed.
return "", 204

View File

@@ -14,6 +14,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -275,8 +276,8 @@ class DatasourceAuth(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCredentialPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceCredentialPayload)
def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
@@ -284,8 +285,8 @@ class DatasourceAuth(Resource):
datasource_provider_service.add_datasource_api_key_provider(
tenant_id=current_tenant_id,
provider_id=datasource_provider_id,
credentials=payload.credentials,
name=payload.name,
credentials=req_data.credentials,
name=req_data.name,
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
@@ -325,16 +326,16 @@ class DatasourceAuthDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
@model_validate(DatasourceCredentialDeletePayload)
def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
plugin_id = datasource_provider_id.plugin_id
provider_name = datasource_provider_id.provider_name
payload = DatasourceCredentialDeletePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.remove_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=payload.credential_id,
auth_id=req_data.credential_id,
provider=provider_name,
plugin_id=plugin_id,
session=db.session(),
@@ -354,18 +355,18 @@ class DatasourceAuthUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
@model_validate(DatasourceCredentialUpdatePayload)
def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
payload = DatasourceCredentialUpdatePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=payload.credential_id,
auth_id=req_data.credential_id,
provider=datasource_provider_id.provider_name,
plugin_id=datasource_provider_id.plugin_id,
credentials=payload.credentials or {},
name=payload.name,
credentials=req_data.credentials or {},
name=req_data.name,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
@@ -420,15 +421,15 @@ class DatasourceAuthOauthCustomClient(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCustomClientPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceCustomClientPayload)
def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.setup_oauth_custom_client_params(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
client_params=payload.client_params or {},
enabled=payload.enable_oauth_custom_client or False,
client_params=req_data.client_params or {},
enabled=req_data.enable_oauth_custom_client or False,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -457,14 +458,14 @@ class DatasourceAuthDefaultApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceDefaultPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceDefaultPayload)
def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.set_default_datasource_provider(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
credential_id=payload.id,
credential_id=req_data.id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -479,14 +480,14 @@ class DatasourceUpdateProviderNameApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceUpdateNamePayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceUpdateNamePayload)
def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_provider_name(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
name=payload.name,
credential_id=payload.credential_id,
name=req_data.name,
credential_id=req_data.credential_id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200

View File

@@ -8,7 +8,7 @@ from pydantic import BaseModel
from controllers.common.schema import register_schema_models
from controllers.console import console_ns
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
from extensions.ext_database import db
from libs.login import login_required
from models import Account
@@ -34,14 +34,14 @@ class DataSourceContentPreviewApi(Resource):
@account_initialization_required
@get_rag_pipeline
@with_current_user
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(Parser)
def post(self, req_data: Parser, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run datasource content preview
"""
args = Parser.model_validate(console_ns.payload)
inputs = args.inputs
datasource_type = args.datasource_type
inputs = req_data.inputs
datasource_type = req_data.datasource_type
rag_pipeline_service = RagPipelineService(db.session())
preview_content = rag_pipeline_service.run_datasource_node_preview(
pipeline=pipeline,
@@ -50,6 +50,6 @@ class DataSourceContentPreviewApi(Resource):
account=current_user,
datasource_type=datasource_type,
is_published=True,
credential_id=args.credential_id,
credential_id=req_data.credential_id,
)
return preview_content, 200

View File

@@ -1,7 +1,6 @@
import logging
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy import select
@@ -21,6 +20,7 @@ from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
knowledge_pipeline_publish_enabled,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -104,12 +104,17 @@ class PipelineTemplateListApi(Resource):
@enterprise_license_required
@with_current_tenant_id
@with_session
def get(self, session: Session, current_tenant_id: str) -> JsonResponseWithStatus:
query = PipelineTemplateListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(PipelineTemplateListQuery)
def get(
self,
req_data: PipelineTemplateListQuery,
session: Session,
current_tenant_id: str,
) -> JsonResponseWithStatus:
# get pipeline templates
pipeline_templates = RagPipelineService.get_pipeline_templates(
type=query.type,
language=query.language,
type=req_data.type,
language=req_data.language,
current_tenant_id=current_tenant_id,
session=session,
)
@@ -125,11 +130,11 @@ class PipelineTemplateDetailApi(Resource):
@account_initialization_required
@enterprise_license_required
@with_session
def get(self, session: Session, template_id: str) -> JsonResponseWithStatus:
query = PipelineTemplateDetailQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(PipelineTemplateDetailQuery)
def get(self, req_data: PipelineTemplateDetailQuery, session: Session, template_id: str) -> JsonResponseWithStatus:
pipeline_template = RagPipelineService.get_pipeline_template_detail(
template_id,
type=query.type,
type=req_data.type,
session=session,
)
if pipeline_template is None:
@@ -147,9 +152,15 @@ class CustomizedPipelineTemplateApi(Resource):
@enterprise_license_required
@with_current_user
@with_current_tenant_id
def patch(self, current_tenant_id: str, current_user: Account, template_id: str) -> tuple[str, int]:
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(payload.model_dump())
@model_validate(CustomizedPipelineTemplatePayload)
def patch(
self,
req_data: CustomizedPipelineTemplatePayload,
current_tenant_id: str,
current_user: Account,
template_id: str,
) -> tuple[str, int]:
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(req_data.model_dump())
RagPipelineService.update_customized_pipeline_template(
template_id, pipeline_template_info, current_user, current_tenant_id, session=db.session()
)
@@ -192,10 +203,16 @@ class PublishCustomizedPipelineTemplateApi(Resource):
@knowledge_pipeline_publish_enabled
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, pipeline_id: str) -> tuple[str, int]:
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
@model_validate(CustomizedPipelineTemplatePayload)
def post(
self,
req_data: CustomizedPipelineTemplatePayload,
current_tenant_id: str,
current_user: Account,
pipeline_id: str,
) -> tuple[str, int]:
rag_pipeline_service = RagPipelineService(db.session())
rag_pipeline_service.publish_customized_pipeline_template(
pipeline_id, payload.model_dump(), current_user, current_tenant_id, session=db.session()
pipeline_id, req_data.model_dump(), current_user, current_tenant_id, session=db.session()
)
return "", 204

View File

@@ -10,6 +10,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_import import RagPip
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_rate_limit_check,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -47,8 +48,13 @@ class CreateRagPipelineDatasetApi(Resource):
@cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account) -> JsonResponseWithStatus:
payload = RagPipelineDatasetImportPayload.model_validate(console_ns.payload or {})
@model_validate(RagPipelineDatasetImportPayload)
def post(
self,
req_data: RagPipelineDatasetImportPayload,
current_tenant_id: str,
current_user: Account,
) -> JsonResponseWithStatus:
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
@@ -62,7 +68,7 @@ class CreateRagPipelineDatasetApi(Resource):
),
permission=DatasetPermissionEnum.ONLY_ME,
partial_member_list=None,
yaml_content=payload.yaml_content,
yaml_content=req_data.yaml_content,
)
try:
rag_pipeline_dsl_service = RagPipelineDslService(db.session())

View File

@@ -4,7 +4,7 @@ from functools import wraps
from typing import Any, Concatenate, NoReturn
from uuid import UUID
from flask import Response, request
from flask import Response
from flask_restx import Resource, marshal, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker
@@ -24,7 +24,7 @@ from controllers.console.app.workflow_draft_variable import (
workflow_draft_variable_model,
)
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
from core.app.file_access import DatabaseFileAccessController
from core.workflow.llm_environment_variable import LLMEnvironmentVariable, environment_variable_value_type
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
@@ -92,11 +92,11 @@ class RagPipelineVariableCollectionApi(Resource):
)
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
def get(self, current_user: Account, pipeline: Pipeline):
@model_validate(PaginationQuery)
def get(self, req_data: PaginationQuery, current_user: Account, pipeline: Pipeline):
"""
Get draft workflow
"""
query = PaginationQuery.model_validate(request.args.to_dict())
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService(db.session())
@@ -111,8 +111,8 @@ class RagPipelineVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=pipeline.id,
page=query.page,
limit=query.limit,
page=req_data.page,
limit=req_data.limit,
user_id=current_user.id,
)
@@ -196,7 +196,14 @@ class RagPipelineVariableApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
@console_ns.expect(console_ns.models[WorkflowDraftVariablePatchPayload.__name__])
def patch(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
@model_validate(WorkflowDraftVariablePatchPayload)
def patch(
self,
req_data: WorkflowDraftVariablePatchPayload,
_current_user: Account,
pipeline: Pipeline,
variable_id: UUID,
):
# Request payload for file types:
#
# Local File:
@@ -221,8 +228,7 @@ class RagPipelineVariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
payload = WorkflowDraftVariablePatchPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
variable_id_str = str(variable_id)
variable = draft_var_srv.get_variable(variable_id=variable_id_str)

View File

@@ -1,4 +1,3 @@
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -85,9 +85,9 @@ class RagPipelineImportApi(Resource):
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
@with_current_user
def post(self, current_user: Account) -> JsonResponseWithStatus:
@model_validate(RagPipelineImportPayload)
def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus:
# Check user role first
payload = RagPipelineImportPayload.model_validate(console_ns.payload or {})
# Use a plain Session so that caught exceptions inside the service
# (which return FAILED status instead of re-raising) do not leave the
@@ -98,11 +98,11 @@ class RagPipelineImportApi(Resource):
account = current_user
result = import_service.import_rag_pipeline(
account=account,
import_mode=payload.mode,
yaml_content=payload.yaml_content,
yaml_url=payload.yaml_url,
pipeline_id=payload.pipeline_id,
dataset_name=payload.name,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
pipeline_id=req_data.pipeline_id,
dataset_name=req_data.name,
)
if result.status == ImportStatus.FAILED:
session.rollback()
@@ -179,14 +179,14 @@ class RagPipelineExportApi(Resource):
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_IMPORT_EXPORT_DSL)
def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
@model_validate(IncludeSecretQuery)
def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus:
# Add include_secret params
query = IncludeSecretQuery.model_validate(request.args.to_dict())
with Session(db.engine, expire_on_commit=False) as session:
export_service = RagPipelineDslService(session)
result = export_service.export_rag_pipeline_dsl(
pipeline=pipeline, include_secret=query.include_secret == "true"
pipeline=pipeline, include_secret=req_data.include_secret == "true"
)
return dump_response(SimpleDataResponse, {"data": result}), 200

View File

@@ -33,6 +33,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -274,12 +275,12 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunPayload)
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow iteration node
"""
payload = NodeRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = PipelineGenerateService.generate_single_iteration(
@@ -309,12 +310,12 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunPayload)
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow loop node
"""
payload = NodeRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = PipelineGenerateService.generate_single_loop(
@@ -344,13 +345,13 @@ class DraftRagPipelineRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
@model_validate(DraftWorkflowRunPayload)
def post(self, req_data: DraftWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
"""
Run draft workflow
"""
pipeline = load_rag_pipeline(session, str(pipeline_id))
payload = DraftWorkflowRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
args = req_data.model_dump()
try:
response = PipelineGenerateService.generate(
@@ -378,14 +379,14 @@ class PublishedRagPipelineRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
@model_validate(PublishedWorkflowRunPayload)
def post(self, req_data: PublishedWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
"""
Run published workflow
"""
pipeline = load_rag_pipeline(session, str(pipeline_id))
payload = PublishedWorkflowRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
streaming = payload.response_mode == "streaming"
args = req_data.model_dump(exclude_none=True)
streaming = req_data.response_mode == "streaming"
try:
response = PipelineGenerateService.generate(
@@ -393,7 +394,7 @@ class PublishedRagPipelineRunApi(Resource):
pipeline=pipeline,
user=current_user,
args=args,
invoke_from=InvokeFrom.DEBUGGER if payload.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
invoke_from=InvokeFrom.DEBUGGER if req_data.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
streaming=streaming,
)
@@ -413,11 +414,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(DatasourceNodeRunPayload)
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run rag pipeline datasource
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
@@ -425,11 +426,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
rag_pipeline_service.run_datasource_workflow_node(
pipeline=pipeline,
node_id=node_id,
user_inputs=payload.inputs,
user_inputs=req_data.inputs,
account=current_user,
datasource_type=payload.datasource_type,
datasource_type=req_data.datasource_type,
is_published=False,
credential_id=payload.credential_id,
credential_id=req_data.credential_id,
)
)
)
@@ -446,11 +447,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
@account_initialization_required
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(DatasourceNodeRunPayload)
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run rag pipeline datasource
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
@@ -458,11 +459,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
rag_pipeline_service.run_datasource_workflow_node(
pipeline=pipeline,
node_id=node_id,
user_inputs=payload.inputs,
user_inputs=req_data.inputs,
account=current_user,
datasource_type=payload.datasource_type,
datasource_type=req_data.datasource_type,
is_published=False,
credential_id=payload.credential_id,
credential_id=req_data.credential_id,
)
)
)
@@ -483,12 +484,12 @@ class RagPipelineDraftNodeRunApi(Resource):
@account_initialization_required
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunRequiredPayload)
def post(self, req_data: NodeRunRequiredPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow node
"""
payload = NodeRunRequiredPayload.model_validate(console_ns.payload or {})
inputs = payload.inputs
inputs = req_data.inputs
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.run_draft_workflow_node(
@@ -617,16 +618,16 @@ class DefaultRagPipelineBlockConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@get_rag_pipeline
def get(self, pipeline: Pipeline, block_type: str):
@model_validate(DefaultBlockConfigQuery)
def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str):
"""
Get default block config
"""
query = DefaultBlockConfigQuery.model_validate(request.args.to_dict())
filters = None
if query.q:
if req_data.q:
try:
filters = json.loads(query.q)
filters = json.loads(req_data.q)
except json.JSONDecodeError:
raise ValueError("Invalid filters")
@@ -651,16 +652,16 @@ class PublishedAllRagPipelineApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def get(self, current_user: Account, pipeline: Pipeline):
@model_validate(WorkflowListQuery)
def get(self, req_data: WorkflowListQuery, current_user: Account, pipeline: Pipeline):
"""
Get published workflows
"""
query = WorkflowListQuery.model_validate(request.args.to_dict())
page = query.page
limit = query.limit
user_id = query.user_id
named_only = query.named_only
page = req_data.page
limit = req_data.limit
user_id = req_data.user_id
named_only = req_data.named_only
if user_id:
if user_id != current_user.id:
@@ -733,12 +734,12 @@ class RagPipelineByIdApi(Resource):
@with_current_user
@get_rag_pipeline
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
def patch(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
@model_validate(WorkflowUpdatePayload)
def patch(self, req_data: WorkflowUpdatePayload, current_user: Account, pipeline: Pipeline, workflow_id: str):
"""
Update workflow attributes
"""
payload = WorkflowUpdatePayload.model_validate(console_ns.payload or {})
update_data = payload.model_dump(exclude_unset=True)
update_data = req_data.model_dump(exclude_unset=True)
if not update_data:
return {"message": "No valid fields to update"}, 400
@@ -803,12 +804,12 @@ class PublishedRagPipelineSecondStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get second step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
@@ -826,12 +827,12 @@ class PublishedRagPipelineFirstStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get first step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
@@ -849,12 +850,12 @@ class DraftRagPipelineFirstStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get first step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
return {
@@ -872,12 +873,12 @@ class DraftRagPipelineSecondStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get second step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
@@ -1045,11 +1046,12 @@ class RagPipelineDatasourceVariableApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, pipeline: Pipeline):
@model_validate(DatasourceVariablesPayload)
def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline):
"""
Set datasource variables
"""
args = DatasourceVariablesPayload.model_validate(console_ns.payload or {}).model_dump()
args = req_data.model_dump()
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.set_datasource_variables(
@@ -1071,9 +1073,11 @@ class RagPipelineRecommendedPluginApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
query = RagPipelineRecommendedPluginQuery.model_validate(request.args.to_dict())
@model_validate(RagPipelineRecommendedPluginQuery)
def get(self, req_data: RagPipelineRecommendedPluginQuery, current_tenant_id: str, current_user: Account):
rag_pipeline_service = RagPipelineService(db.session())
recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type, current_user, current_tenant_id)
recommended_plugins = rag_pipeline_service.get_recommended_plugins(
req_data.type, current_user, current_tenant_id
)
return recommended_plugins

View File

@@ -41,6 +41,7 @@ from controllers.console.wraps import (
cloud_edition_billing_enabled,
enable_change_email,
enterprise_license_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_user,
@@ -332,9 +333,9 @@ class AccountAvatarApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def get(self, current_user: Account):
args = AccountAvatarQuery.model_validate(request.args.to_dict(flat=True))
avatar = args.avatar
@model_validate(AccountAvatarQuery)
def get(self, req_data: AccountAvatarQuery, current_user: Account):
avatar = req_data.avatar
if avatar.startswith(("http://", "https://")):
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")

View File

@@ -11,7 +11,6 @@ from enum import StrEnum
from http import HTTPStatus
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -294,14 +293,14 @@ class EndpointListApi(Resource):
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def get(self, tenant_id: str, user_id: str):
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(EndpointListQuery)
def get(self, req_data: EndpointListQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=args.page,
page_size=args.page_size,
page=req_data.page,
page_size=req_data.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@@ -322,15 +321,15 @@ class EndpointListForSinglePluginApi(Resource):
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def get(self, tenant_id: str, user_id: str):
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(EndpointListForPluginQuery)
def get(self, req_data: EndpointListForPluginQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=args.plugin_id,
page=args.page,
page_size=args.page_size,
plugin_id=req_data.plugin_id,
page=req_data.page,
page_size=req_data.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")

View File

@@ -1,7 +1,6 @@
import logging
from typing import Any, cast
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -18,6 +17,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -212,12 +212,12 @@ class DefaultModelApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserGetDefault.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserGetDefault)
def get(self, req_data: ParserGetDefault, tenant_id: str):
model_provider_service = ModelProviderService()
default_model_entity = model_provider_service.get_default_model_of_model_type(
tenant_id=tenant_id, model_type=args.model_type
tenant_id=tenant_id, model_type=req_data.model_type
)
return DefaultModelDataResponse(data=default_model_entity).model_dump(mode="json")
@@ -230,10 +230,10 @@ class DefaultModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserPostDefault.model_validate(console_ns.payload)
@model_validate(ParserPostDefault)
def post(self, req_data: ParserPostDefault, tenant_id: str):
model_provider_service = ModelProviderService()
model_settings = args.model_settings
model_settings = req_data.model_settings
for model_setting in model_settings:
if model_setting.provider is None:
continue
@@ -279,43 +279,43 @@ class ModelProviderModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
@model_validate(ParserPostModels)
def post(self, req_data: ParserPostModels, tenant_id: str, provider: str):
# To save the model's load balance configs
args = ParserPostModels.model_validate(console_ns.payload)
if args.config_from == "custom-model":
if not args.credential_id:
if req_data.config_from == "custom-model":
if not req_data.credential_id:
raise ValueError("credential_id is required when configuring a custom-model")
service = ModelProviderService()
service.switch_active_custom_model_credential(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
model_load_balancing_service = ModelLoadBalancingService()
if args.load_balancing and args.load_balancing.configs:
if req_data.load_balancing and req_data.load_balancing.configs:
# save load balancing configs
model_load_balancing_service.update_load_balancing_configs(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
configs=args.load_balancing.configs,
config_from=args.config_from or "",
model=req_data.model,
model_type=req_data.model_type,
configs=req_data.load_balancing.configs,
config_from=req_data.config_from or "",
session=db.session(),
)
if args.load_balancing.enabled:
if req_data.load_balancing.enabled:
model_load_balancing_service.enable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
else:
model_load_balancing_service.disable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -328,12 +328,12 @@ class ModelProviderModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def delete(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def delete(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return "", 204
@@ -352,29 +352,29 @@ class ModelProviderModelCredentialApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, user: Account, provider: str):
args = ParserGetCredentials.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserGetCredentials)
def get(self, req_data: ParserGetCredentials, tenant_id: str, user: Account, provider: str):
model_provider_service = ModelProviderService()
current_credential = model_provider_service.get_model_credential(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
model_load_balancing_service = ModelLoadBalancingService()
is_load_balancing_enabled, load_balancing_configs = model_load_balancing_service.get_load_balancing_configs(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
model=req_data.model,
model_type=req_data.model_type,
session=db.session(),
config_from=args.config_from or "",
config_from=req_data.config_from or "",
)
if args.config_from == "predefined-model":
if req_data.config_from == "predefined-model":
# Only the predefined-model branch needs visibility filtering by user.
# The account is injected once by the handler and only passed into the
# service branch that needs user-scoped credential visibility.
@@ -387,8 +387,8 @@ class ModelProviderModelCredentialApi(Resource):
available_credentials = model_provider_service.get_provider_model_available_credentials(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
model_type=req_data.model_type,
model=req_data.model,
)
credentials: dict[str, Any] = {}
@@ -414,8 +414,8 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
args = ParserCreateCredential.model_validate(console_ns.payload)
@model_validate(ParserCreateCredential)
def post(self, req_data: ParserCreateCredential, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -423,17 +423,17 @@ class ModelProviderModelCredentialApi(Resource):
model_provider_service.create_model_credential(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
credentials=args.credentials,
credential_name=args.name,
model=req_data.model,
model_type=req_data.model_type,
credentials=req_data.credentials,
credential_name=req_data.name,
)
except CredentialsValidateFailedError as ex:
logger.exception(
"Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s",
tenant_id,
args.model,
args.model_type,
req_data.model,
req_data.model_type,
)
raise ValueError(str(ex))
@@ -447,8 +447,8 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def put(self, current_tenant_id: str, provider: str):
args = ParserUpdateCredential.model_validate(console_ns.payload)
@model_validate(ParserUpdateCredential)
def put(self, req_data: ParserUpdateCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -456,11 +456,11 @@ class ModelProviderModelCredentialApi(Resource):
model_provider_service.update_model_credential(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credentials=args.credentials,
credential_id=args.credential_id,
credential_name=args.name,
model_type=req_data.model_type,
model=req_data.model,
credentials=req_data.credentials,
credential_id=req_data.credential_id,
credential_name=req_data.name,
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
@@ -475,16 +475,16 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def delete(self, current_tenant_id: str, provider: str):
args = ParserDeleteCredential.model_validate(console_ns.payload)
@model_validate(ParserDeleteCredential)
def delete(self, req_data: ParserDeleteCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model_credential(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
return "", 204
@@ -500,16 +500,16 @@ class ModelProviderModelCredentialSwitchApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str, provider: str):
args = ParserSwitch.model_validate(console_ns.payload)
@model_validate(ParserSwitch)
def post(self, req_data: ParserSwitch, current_tenant_id: str, provider: str):
service = ModelProviderService()
service.add_model_credential_to_model_list(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -525,12 +525,12 @@ class ModelProviderModelEnableApi(Resource):
@account_initialization_required
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
def patch(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.enable_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -547,12 +547,12 @@ class ModelProviderModelDisableApi(Resource):
@account_initialization_required
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
def patch(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.disable_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -579,8 +579,8 @@ class ModelProviderModelValidateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
args = ParserValidate.model_validate(console_ns.payload)
@model_validate(ParserValidate)
def post(self, req_data: ParserValidate, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -591,9 +591,9 @@ class ModelProviderModelValidateApi(Resource):
model_provider_service.validate_model_credentials(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
credentials=args.credentials,
model=req_data.model,
model_type=req_data.model_type,
credentials=req_data.credentials,
)
except CredentialsValidateFailedError as ex:
result = False
@@ -617,12 +617,12 @@ class ModelProviderModelParameterRuleApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, provider: str):
args = ParserParameter.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserParameter)
def get(self, req_data: ParserParameter, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
parameter_rules = model_provider_service.get_model_parameter_rules(
tenant_id=tenant_id, provider=provider, model=args.model
tenant_id=tenant_id, provider=provider, model=req_data.model
)
return ModelParameterRuleListResponse(data=parameter_rules).model_dump(mode="json")

View File

@@ -11,7 +11,7 @@ from werkzeug.exceptions import NotFound
from configs import dify_config
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
from core.db.session_factory import session_factory
from core.rbac import RBACResourceWhitelistScope
from extensions.ext_database import db
@@ -310,19 +310,19 @@ class RBACRolesApi(Resource):
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__])
def get(self):
@model_validate(_RolesListQuery)
def get(self, req_data: _RolesListQuery):
tenant_id, account_id = _current_ids()
query = _RolesListQuery.model_validate(request.args.to_dict(flat=True))
options = query.to_inner_options()
options = req_data.to_inner_options()
if not dify_config.RBAC_ENABLED:
result = _legacy_workspace_roles(
options, include_owner=query.include_owner, billing_enabled=dify_config.BILLING_ENABLED
options, include_owner=req_data.include_owner, billing_enabled=dify_config.BILLING_ENABLED
)
else:
result = svc.RBACService.Roles.list(
tenant_id,
account_id,
include_owner=query.include_owner,
include_owner=req_data.include_owner,
biiling_enabled=dify_config.BILLING_ENABLED,
options=options,
)

View File

@@ -39,6 +39,7 @@ from ..wraps import (
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -233,13 +234,12 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str):
@model_validate(TriggerSubscriptionBuilderCreatePayload)
def post(self, req_data: TriggerSubscriptionBuilderCreatePayload, tenant_id: str, user: Account, provider: str):
"""Add a new subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {})
try:
credential_type = CredentialType.of(payload.credential_type)
credential_type = CredentialType.of(req_data.credential_type)
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
tenant_id=tenant_id,
user_id=user.id,
@@ -298,11 +298,17 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionBuilderVerifyPayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Verify and update a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
try:
# Use atomic update_and_verify to prevent race conditions
result = TriggerSubscriptionBuilderService.update_and_verify_builder(
@@ -311,7 +317,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
credentials=payload.credentials,
credentials=req_data.credentials,
),
)
return dump_response(TriggerVerificationResponse, result)
@@ -337,10 +343,17 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Update a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
try:
return TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
tenant_id=tenant_id,
@@ -348,10 +361,10 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=payload.name,
parameters=payload.parameters,
properties=payload.properties,
credentials=payload.credentials,
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
credentials=req_data.credentials,
),
).model_dump(mode="json")
except Exception as e:
@@ -406,9 +419,16 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Build a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
try:
# Use atomic update_and_build to prevent race conditions
TriggerSubscriptionBuilderService.update_and_build_builder(
@@ -417,9 +437,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=payload.name,
parameters=payload.parameters,
properties=payload.properties,
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
),
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -442,11 +462,10 @@ class TriggerSubscriptionUpdateApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, subscription_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(self, req_data: TriggerSubscriptionBuilderUpdatePayload, tenant_id: str, subscription_id: str):
"""Update a subscription instance"""
request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
subscription = TriggerProviderService.get_subscription_by_id(
tenant_id=tenant_id,
subscription_id=subscription_id,
@@ -458,7 +477,9 @@ class TriggerSubscriptionUpdateApi(Resource):
try:
# For rename only, just update the name
rename = request.name is not None and not any((request.credentials, request.parameters, request.properties))
rename = req_data.name is not None and not any(
(req_data.credentials, req_data.parameters, req_data.properties)
)
# When credential type is UNAUTHORIZED, it indicates the subscription was manually created
# For Manually created subscription, they dont have credentials, parameters
# They only have name and properties(which is input by user)
@@ -467,8 +488,8 @@ class TriggerSubscriptionUpdateApi(Resource):
TriggerProviderService.update_trigger_subscription(
tenant_id=tenant_id,
subscription_id=subscription_id,
name=request.name,
properties=request.properties,
name=req_data.name,
properties=req_data.properties,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -476,11 +497,11 @@ class TriggerSubscriptionUpdateApi(Resource):
# we need to call third party provider(e.g. GitHub) to rebuild the subscription
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant_id,
name=request.name,
name=req_data.name,
provider_id=provider_id,
subscription_id=subscription_id,
credentials=request.credentials or subscription.credentials,
parameters=request.parameters or subscription.parameters,
credentials=req_data.credentials or subscription.credentials,
parameters=req_data.parameters or subscription.parameters,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
except ValueError as e:
@@ -740,18 +761,17 @@ class TriggerOAuthClientManageApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
@model_validate(TriggerOAuthClientPayload)
def post(self, req_data: TriggerOAuthClientPayload, tenant_id: str, provider: str):
"""Configure custom OAuth client for a provider"""
payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {})
try:
provider_id = TriggerProviderID(provider)
result = TriggerProviderService.save_custom_oauth_client_params(
tenant_id=tenant_id,
provider_id=provider_id,
client_params=payload.client_params,
enabled=payload.enabled,
client_params=req_data.client_params,
enabled=req_data.enabled,
)
return dump_response(SimpleResultResponse, result)
@@ -805,18 +825,24 @@ class TriggerSubscriptionVerifyApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_id: str):
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionBuilderVerifyPayload,
tenant_id: str,
user: Account,
provider: str,
subscription_id: str,
):
"""Verify credentials for an existing subscription (edit mode only)"""
verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
try:
result = TriggerProviderService.verify_subscription_credentials(
tenant_id=tenant_id,
user_id=user.id,
provider_id=TriggerProviderID(provider),
subscription_id=subscription_id,
credentials=verify_request.credentials,
credentials=req_data.credentials,
)
return dump_response(TriggerVerificationResponse, result)
except ValueError as e:

View File

@@ -538,8 +538,14 @@ class AgentModelResponseFormatConfig(AgentFlexibleConfig):
type: str | None = Field(default=None, max_length=64)
class AgentSoulModelSettings(BaseModel):
model_config = ConfigDict(extra="ignore")
class AgentSoulModelSettings(AgentFlexibleConfig):
"""Model parameters for the Agent Soul model.
Model plugins can declare arbitrary parameters via ``parameter_rules``
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
fields typed below, so extra keys must round-trip through persistence
rather than being dropped.
"""
temperature: float | None = None
top_p: float | None = None

View File

@@ -14933,6 +14933,13 @@ Reference to model credentials resolved only at runtime.
#### AgentSoulModelSettings
Model parameters for the Agent Soul model.
Model plugins can declare arbitrary parameters via ``parameter_rules``
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
fields typed below, so extra keys must round-trip through persistence
rather than being dropped.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| frequency_penalty | number | | No |

View File

@@ -7,7 +7,7 @@ from uuid import uuid4
from flask import Flask
from sqlalchemy.orm import Session
from controllers.console.datasets.data_source import DataSourceNotionListApi
from controllers.console.datasets.data_source import DataSourceNotionListApi, DataSourceNotionListQuery
from models import Account
from models.dataset import Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
@@ -78,7 +78,11 @@ def test_notion_page_is_marked_bound_from_persisted_document(
),
):
response, status = unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(), db_session_with_containers, tenant_id, account
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="c1", dataset_id=dataset_id),
db_session_with_containers,
tenant_id,
account,
)
assert status == 200

View File

@@ -119,6 +119,29 @@ def test_request_builder_separates_agent_soul_and_workflow_job_prompt():
assert dumped["composition"]["layers"][2]["config"]["user"] == "Summarize the report."
def test_request_builder_forwards_plugin_specific_model_settings_via_extra_body():
run_input = _run_input().model_copy(
update={
"model": AgentBackendModelConfig(
plugin_id="langgenius/tongyi",
model_provider="tongyi",
model="qwen-plus-latest",
credentials={"api_key": "secret-key"},
model_settings={"temperature": 0.7, "enable_thinking": True, "thinking_budget": 4096},
)
}
)
request = AgentBackendRunRequestBuilder().build_for_workflow_node(run_input)
layers = {layer.name: layer for layer in request.composition.layers}
model_config = cast(DifyPluginLLMLayerConfig, layers[DIFY_AGENT_MODEL_LAYER_ID].config)
assert model_config.model_settings == {
"temperature": 0.7,
"extra_body": {"enable_thinking": True, "thinking_budget": 4096},
}
@pytest.mark.parametrize("agent_config_version_kind", ["snapshot", "draft"])
def test_agent_app_request_builder_keeps_agent_soul_prompt_for_snapshot_and_draft(
agent_config_version_kind: str,

View File

@@ -59,6 +59,7 @@ from controllers.console.app.workflow import AdvancedChatWorkflowRunPayload, Syn
from controllers.console.app.workflow_app_log import WorkflowAppLogQuery
from controllers.console.app.workflow_draft_variable import (
EnvironmentVariableUpdatePayload,
WorkflowDraftVariableListQuery,
WorkflowDraftVariableUpdatePayload,
)
from controllers.console.app.workflow_statistic import WorkflowStatisticQuery
@@ -144,7 +145,13 @@ class TestCompletionEndpoints:
Session(sqlite_engine) as session,
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
):
resp = method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
resp = method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
assert resp == {"result": {"text": "ok"}}
@@ -167,7 +174,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(NotFound),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
def test_completion_api_provider_not_initialized(
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine
@@ -186,7 +199,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(completion_module.ProviderNotInitializeError),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
def test_completion_api_quota_exceeded(
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine
@@ -205,7 +224,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(completion_module.ProviderQuotaExceededError),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
class TestAppEndpoints:
@@ -232,7 +257,17 @@ class TestAppEndpoints:
app.test_request_context("/console/api/apps/app-1", method="PUT", json=payload),
patch.object(type(console_ns), "payload", payload),
):
response = method(api, unbound_session, app_model=_make_app(icon_type=app_module.IconType.EMOJI))
response = method(
api,
app_module.UpdateAppPayload(
name="Updated App",
description="Updated description",
icon="🤖",
icon_background="#FFFFFF",
),
unbound_session,
app_model=_make_app(icon_type=app_module.IconType.EMOJI),
)
assert response == {"id": "app-1"}
assert app_service.update_app.call_args.args[1]["icon_type"] is None
@@ -271,7 +306,16 @@ class TestAppEndpoints:
app.test_request_context("/console/api/apps/app-1/icon", method="POST", json=payload),
patch.object(type(console_ns), "payload", payload),
):
response = method(api, unbound_session, app_model=_make_app())
response = method(
api,
app_module.AppIconPayload(
icon="https://example.com/icon.png",
icon_type=app_module.IconType.IMAGE,
icon_background="#FFFFFF",
),
unbound_session,
app_model=_make_app(),
)
assert response == {"id": "app-1"}
assert app_service.update_app_icon.call_args.args[1:] == (
@@ -378,7 +422,13 @@ class TestSiteEndpoints:
site = self._add_site(db.session)
with database_app.test_request_context("/", json={"title": "My Site", "input_placeholder": "Ask me anything"}):
result = method(api, db.session, _make_account(), app_model=_make_app())
result = method(
api,
AppSiteUpdatePayload(title="My Site", input_placeholder="Ask me anything"),
db.session,
_make_account(),
app_model=_make_app(),
)
db.session.refresh(site)
assert isinstance(result, dict)
@@ -445,7 +495,7 @@ class TestWorkflowAppLogEndpoints:
)
with database_app.test_request_context("/?page=1&limit=20"):
result = method(api, app_model=_make_app("app-1"))
result = method(api, WorkflowAppLogQuery(page=1, limit=20), app_model=_make_app("app-1"))
assert result == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}
@@ -475,7 +525,12 @@ class TestWorkflowDraftVariableEndpoints:
monkeypatch.setattr(workflow_draft_variable_module, "WorkflowService", DummyWorkflowService)
with database_app.test_request_context("/?page=1&limit=20"):
result = method(api, _make_account(), app_model=_make_app("app-1"))
result = method(
api,
WorkflowDraftVariableListQuery(page=1, limit=20),
_make_account(),
app_model=_make_app("app-1"),
)
assert result == {"items": [], "total": 0}
@@ -528,7 +583,16 @@ class TestWorkflowDraftVariableEndpoints:
"deleted_environment_variable_ids": ["env-b"],
},
):
result = method(api, _make_account(), app_model=_make_app())
result = method(
api,
EnvironmentVariableUpdatePayload(
environment_variables=[{"id": "env-a", "name": "a", "value_type": "string", "value": "new-a"}],
patch=True,
deleted_environment_variable_ids=["env-b"],
),
_make_account(),
app_model=_make_app(),
)
assert result == {"result": "success"}
assert [(variable.id, variable.value) for variable in captured["environment_variables"]] == [("env-a", "new-a")]
@@ -574,7 +638,7 @@ class TestWorkflowStatisticEndpoints:
with database_app.test_request_context("/"):
account = _make_account()
account.timezone = "UTC"
response = method(api, account, app_model=_make_app("app-1", tenant_id="t1"))
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
assert response.get_json() == {"data": [{"date": "2024-01-01"}]}
@@ -604,7 +668,7 @@ class TestWorkflowStatisticEndpoints:
with database_app.test_request_context("/"):
account = _make_account()
account.timezone = "UTC"
response = method(api, account, app_model=_make_app("app-1", tenant_id="t1"))
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
assert response.get_json() == {"data": [{"date": "2024-01-02"}]}
@@ -634,7 +698,7 @@ class TestWorkflowTriggerEndpoints:
db.session.commit()
with database_app.test_request_context("/?node_id=node-1"):
result = method(api, app_model=_make_app())
result = method(api, Parser(node_id="node-1"), app_model=_make_app())
assert isinstance(result, dict)
assert {"id", "webhook_id", "webhook_url", "webhook_debug_url", "node_id", "created_at"} <= set(result.keys())

View File

@@ -159,7 +159,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.rollbacks == 1
assert transaction_events.commits == 0
@@ -185,7 +185,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.commits == 1
assert transaction_events.rollbacks == 0
@@ -213,7 +213,7 @@ class TestAppImportApi:
monkeypatch.setattr(app_import_module.EnterpriseService.WebAppAuth, "update_app_access_mode", update_access)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.commits == 1
assert transaction_events.rollbacks == 0
@@ -251,7 +251,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method()
response, status = method(app_import_module.AppImportPayload(mode="yaml-content"))
assert transaction_events.commits == 1
_assert_app_persistence(sqlite_app_engine, app_id, persisted=True)
@@ -291,7 +291,7 @@ class TestAppImportApi:
method="POST",
json={"mode": "yaml-content", "app_id": "existing-app"},
):
response, status = method()
response, status = method(app_import_module.AppImportPayload(mode="yaml-content", app_id="existing-app"))
assert transaction_events.commits == 1
_assert_app_persistence(sqlite_app_engine, app_id, persisted=True)

View File

@@ -331,19 +331,9 @@ def test_app_list_query_accepts_single_repeated_tag_id(app_module):
assert query.tag_ids == [tag_id]
def test_create_app_endpoint_rejects_agent_mode(app_module, monkeypatch: pytest.MonkeyPatch, unbound_session: Session):
payload = {"name": "Iris", "mode": "agent", "description": "Agent app"}
app_service = MagicMock()
monkeypatch.setattr(app_module, "AppService", lambda: app_service)
app_module.console_ns.payload = payload
try:
with pytest.raises(ValidationError):
_unwrap(app_module.AppListApi().post)(unbound_session, "tenant-1", SimpleNamespace(id="account-1"))
finally:
app_module.console_ns.payload = None
app_service.create_app.assert_not_called()
def test_create_app_endpoint_rejects_agent_mode(app_module):
with pytest.raises(ValidationError):
app_module.CreateAppPayload.model_validate({"name": "Iris", "mode": "agent", "description": "Agent app"})
def test_app_partial_serialization_uses_aliases(app_models):
@@ -650,7 +640,17 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
replace_whitelist,
)
resp, status = method(app_module.AppListApi(), unbound_session, "tenant-1", SimpleNamespace(id="acct-1"))
resp, status = method(
app_module.AppListApi(),
app_module.CreateAppPayload(
name="Created App",
description="Summary",
mode="advanced-chat",
),
unbound_session,
"tenant-1",
SimpleNamespace(id="acct-1"),
)
assert status == 201
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
@@ -1076,6 +1076,7 @@ def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session:
resp, status = method(
app_module.AppCopyApi(),
app_module.CopyAppPayload(),
"tenant-1",
SimpleNamespace(id="acct-1"),
app_model=SimpleNamespace(id="app-original"),

View File

@@ -17,6 +17,8 @@ from controllers.console.app.audio import (
ChatMessageAudioApi,
ChatMessageTextApi,
TextModesApi,
TextToSpeechPayload,
TextToSpeechVoiceQuery,
)
from controllers.console.app.error import (
AppUnavailableError,
@@ -290,7 +292,7 @@ def test_console_text_api_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -
method="POST",
json={"text": "hello", "voice": "v"},
):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "ok"}
@@ -315,7 +317,7 @@ def test_console_text_api_builds_message_ref(app: Flask, monkeypatch: pytest.Mon
),
patch("controllers.console.app.audio.current_user", SimpleNamespace(id="account-1")),
):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechPayload(text="hello", message_id="message-1"), app_model=app_model)
assert response == {"audio": "ok"}
assert calls["message_ref"] == MessageRef(AppRef("tenant-1", "app-1"), "message-1", account_id="account-1")
@@ -334,7 +336,7 @@ def test_console_text_api_error_mapping(app: Flask, monkeypatch: pytest.MonkeyPa
json={"text": "hello"},
):
with pytest.raises(ProviderQuotaExceededError):
handler(api, app_model=app_model)
handler(api, TextToSpeechPayload(text="hello"), app_model=app_model)
def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -346,7 +348,7 @@ def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch)
app_model = SimpleNamespace(tenant_id="t1")
with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert response == expected_voices
@@ -364,7 +366,7 @@ def test_console_text_modes_language_error(app: Flask, monkeypatch: pytest.Monke
with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"):
with pytest.raises(AppUnavailableError):
handler(api, app_model=app_model)
handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
def test_audio_to_text_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -424,7 +426,7 @@ def test_text_to_audio_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N
method="POST",
json={"text": "hello"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "ok"}
@@ -443,7 +445,7 @@ def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatc
method="GET",
query_string={"language": "en-US"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert response == expected_voices
@@ -481,7 +483,7 @@ def test_text_to_audio_with_language_param(app: Flask, monkeypatch: pytest.Monke
method="POST",
json={"text": "hello", "language": "en-US"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "test"}
@@ -501,5 +503,5 @@ def test_text_to_audio_voices_with_language_filter(app: Flask, monkeypatch: pyte
"/console/api/apps/app-1/text-to-audio/voices?language=en-US",
method="GET",
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert isinstance(response, list)

View File

@@ -58,7 +58,13 @@ def test_completion_conversation_list_returns_paginated_result(
paginate_result.items = []
monkeypatch.setattr(conversation_module, "paginate_query", lambda *_args, **_kwargs: paginate_result)
with app.test_request_context("/console/api/apps/app-1/completion-conversations", method="GET"):
response = method(api, unbound_session, account, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_module.CompletionConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1"),
)
assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}
@@ -77,7 +83,13 @@ def test_completion_conversation_list_invalid_time_range(
"/console/api/apps/app-1/completion-conversations", method="GET", query_string={"start": "bad"}
):
with pytest.raises(BadRequest):
method(api, unbound_session, account, app_model=SimpleNamespace(id="app-1"))
method(
api,
conversation_module.CompletionConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1"),
)
def test_chat_conversation_list_advanced_chat_calls_paginate(
@@ -96,7 +108,11 @@ def test_chat_conversation_list_advanced_chat_calls_paginate(
monkeypatch.setattr(conversation_module, "paginate_query", lambda *_args, **_kwargs: paginate_result)
with app.test_request_context("/console/api/apps/app-1/chat-conversations", method="GET"):
response = method(
api, unbound_session, account, app_model=SimpleNamespace(id="app-1", mode=AppMode.ADVANCED_CHAT)
api,
conversation_module.ChatConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1", mode=AppMode.ADVANCED_CHAT),
)
assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}

View File

@@ -51,7 +51,11 @@ def test_get_conversation_variables_returns_paginated_response(
method="GET",
query_string={"conversation_id": "conv-1"},
):
response = method(api, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"),
app_model=SimpleNamespace(id="app-1"),
)
assert response["page"] == 1
assert response["limit"] == 100
@@ -90,7 +94,11 @@ def test_get_conversation_variables_normalizes_value_type_and_value(
method="GET",
query_string={"conversation_id": "conv-1"},
):
response = method(api, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"),
app_model=SimpleNamespace(id="app-1"),
)
assert response["data"][0]["value_type"] == "number"
assert response["data"][0]["value"] == "42"
@@ -102,4 +110,4 @@ def test_get_conversation_variables_requires_conversation_id(app) -> None:
with app.test_request_context("/console/api/apps/app-1/conversation-variables", method="GET"):
with pytest.raises(ValidationError):
method(api, app_model=SimpleNamespace(id="app-1"))
conversation_variables_module.ConversationVariablesQuery.model_validate({})

View File

@@ -6,11 +6,19 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from flask import Flask
from flask import Flask, request
from sqlalchemy.orm import Session
from controllers.console.app import generator as generator_module
from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.app.generator import (
InstructionGeneratePayload,
InstructionTemplatePayload,
RuleCodeGeneratePayload,
RuleGeneratePayload,
WorkflowGeneratePayload,
WorkflowInstructionSuggestionsPayload,
)
from core.errors.error import ProviderTokenNotInitError
from models.model import App, AppMode
@@ -61,7 +69,7 @@ def test_rule_generate_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N
method="POST",
json={"instruction": "do it", "model_config": _model_config_payload()},
):
response = method(api, "t1")
response = method(api, RuleGeneratePayload.model_validate(request.get_json()), "t1")
assert response == {"rules": []}
@@ -81,7 +89,7 @@ def test_rule_code_generate_maps_token_error(app: Flask, monkeypatch: pytest.Mon
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(ProviderNotInitializeError):
method(api, "t1")
method(api, RuleCodeGeneratePayload.model_validate(request.get_json()), "t1")
@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True)
@@ -100,7 +108,9 @@ def test_instruction_generate_app_not_found(app: Flask, sqlite_session: Session)
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "app app-1 not found"
@@ -127,7 +137,9 @@ def test_instruction_generate_workflow_not_found(
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "workflow app-1 not found"
@@ -155,7 +167,9 @@ def test_instruction_generate_node_missing(
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "node node-1 not found"
@@ -188,7 +202,7 @@ def test_instruction_generate_code_node(app: Flask, monkeypatch: pytest.MonkeyPa
"model_config": _model_config_payload(),
},
):
response = method(api, sqlite_session, "t1")
response = method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")
assert response == {"code": "x"}
assert workflow_service.app_model is app_model
@@ -218,7 +232,7 @@ def test_instruction_generate_legacy_modify(
"model_config": _model_config_payload(),
},
):
response = method(api, sqlite_session, "t1")
response = method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")
assert response == {"instruction": "ok"}
@@ -238,7 +252,9 @@ def test_instruction_generate_incompatible_params(app: Flask, sqlite_session: Se
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "incompatible parameters"
@@ -253,7 +269,7 @@ def test_instruction_template_prompt(app: Flask) -> None:
method="POST",
json={"type": "prompt"},
):
response = method(api)
response = method(api, InstructionTemplatePayload.model_validate(request.get_json()))
assert "data" in response
@@ -268,7 +284,7 @@ def test_instruction_template_invalid_type(app: Flask) -> None:
json={"type": "unknown"},
):
with pytest.raises(ValueError):
method(api)
method(api, InstructionTemplatePayload.model_validate(request.get_json()))
# ─ /workflow-generate ─────────────────────────────────────────────────────────
@@ -331,7 +347,7 @@ def test_workflow_generate_returns_service_result(app: Flask, monkeypatch: pytes
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert response == expected
@@ -361,7 +377,7 @@ def test_workflow_generate_maps_provider_token_error(app: Flask, monkeypatch: py
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderNotInitializeError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_quota_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -379,7 +395,7 @@ def test_workflow_generate_maps_quota_error(app: Flask, monkeypatch: pytest.Monk
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_model_not_support_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -397,7 +413,7 @@ def test_workflow_generate_maps_model_not_support_error(app: Flask, monkeypatch:
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_invoke_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -415,7 +431,7 @@ def test_workflow_generate_maps_invoke_error(app: Flask, monkeypatch: pytest.Mon
json=_workflow_generate_payload(),
):
with pytest.raises(CompletionRequestError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_accepts_advanced_chat_mode(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -442,7 +458,7 @@ def test_workflow_generate_accepts_advanced_chat_mode(app: Flask, monkeypatch: p
method="POST",
json=payload,
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["mode"] == "advanced-chat"
assert captured["instruction"] == "Summarize a URL"
@@ -474,7 +490,7 @@ def test_workflow_generate_forwards_current_graph_for_refine(app: Flask, monkeyp
method="POST",
json=payload,
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["current_graph"] == graph
@@ -501,7 +517,7 @@ def test_workflow_generate_current_graph_defaults_to_none(app: Flask, monkeypatc
method="POST",
json=_workflow_generate_payload(),
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["current_graph"] is None
@@ -528,7 +544,7 @@ def test_workflow_generate_accepts_auto_mode(app: Flask, monkeypatch: pytest.Mon
payload = _workflow_generate_payload()
payload["mode"] = "auto"
with app.test_request_context("/console/api/workflow-generate", method="POST", json=payload):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["mode"] == "auto"
assert response["mode"] == "advanced-chat"
@@ -608,7 +624,7 @@ def test_workflow_instruction_suggestions_route_returns_list(app: Flask, monkeyp
method="POST",
json={"mode": "workflow", "language": "French", "count": 3},
):
response = method(api, "t1")
response = method(api, WorkflowInstructionSuggestionsPayload.model_validate(request.get_json()), "t1")
assert response == {"suggestions": ["Summarize a URL", "Translate text"]}
assert captured["mode"] == "workflow"
@@ -632,7 +648,7 @@ def test_workflow_instruction_suggestions_route_empty_is_valid_200(app: Flask, m
method="POST",
json={"mode": "advanced-chat"},
):
response = method(api, "t1")
response = method(api, WorkflowInstructionSuggestionsPayload.model_validate(request.get_json()), "t1")
assert response == {"suggestions": []}
@@ -667,7 +683,7 @@ def test_workflow_generate_stream_emits_plan_then_result(app: Flask, monkeypatch
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert response.mimetype == "text/event-stream"
frames = _read_sse_frames(response)
@@ -694,7 +710,7 @@ def test_workflow_generate_stream_provider_error_emits_result_event(
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
frames = _read_sse_frames(response)
assert len(frames) == 1
@@ -711,7 +727,7 @@ def test_workflow_generate_stream_rejects_empty_instruction(app: Flask, monkeypa
payload = _workflow_generate_payload()
payload["instruction"] = " "
with app.test_request_context("/console/api/workflow-generate/stream", method="POST", json=payload):
response, status = method(api, "t1")
response, status = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert status == 400
assert response["errors"][0]["code"] == "EMPTY_INSTRUCTION"

View File

@@ -1,8 +1,14 @@
import pytest
from flask import Flask
from flask import Flask, request
from sqlalchemy.orm import Session
from controllers.console.app import generator as generator_module
from controllers.console.app.generator import (
InstructionGeneratePayload,
RuleCodeGeneratePayload,
RuleGeneratePayload,
RuleStructuredOutputPayload,
)
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from graphon.model_runtime.errors.invoke import InvokeError
@@ -47,7 +53,7 @@ def test_rule_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleGeneratePayload.model_validate(request.get_json()), "t1")
def test_rule_code_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -73,7 +79,7 @@ def test_rule_code_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPat
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleCodeGeneratePayload.model_validate(request.get_json()), "t1")
def test_structured_output_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -100,7 +106,7 @@ def test_structured_output_generate_exceptions(app: Flask, monkeypatch: pytest.M
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleStructuredOutputPayload.model_validate(request.get_json()), "t1")
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
@@ -138,4 +144,4 @@ def test_instruction_generate_exceptions(
},
):
with pytest.raises(expected_exception):
method(api, sqlite_session, "t1")
method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")

View File

@@ -53,7 +53,12 @@ def test_daily_message_statistic_returns_rows(app: Flask, monkeypatch: pytest.Mo
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-01", "message_count": 3}]}
@@ -67,7 +72,12 @@ def test_daily_conversation_statistic_returns_rows(app: Flask, monkeypatch: pyte
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]}
@@ -81,7 +91,12 @@ def test_daily_token_cost_statistic_returns_rows(app: Flask, monkeypatch: pytest
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 1
@@ -99,7 +114,12 @@ def test_daily_terminals_statistic_returns_rows(app: Flask, monkeypatch: pytest.
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-end-users", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-04", "terminal_count": 7}]}
@@ -126,7 +146,12 @@ def test_daily_message_statistic_with_invalid_time_range(app: Flask, monkeypatch
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
with pytest.raises(BadRequest):
method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -142,7 +167,12 @@ def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.M
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 3
@@ -156,7 +186,12 @@ def test_daily_message_statistic_empty_result(app: Flask, monkeypatch: pytest.Mo
_install_db(monkeypatch, [])
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": []}
@@ -175,7 +210,12 @@ def test_daily_conversation_statistic_with_time_range(app: Flask, monkeypatch: p
monkeypatch.setattr(statistic_module, "convert_datetime_to_date", lambda field: field)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]}
@@ -192,7 +232,12 @@ def test_daily_token_cost_with_multiple_currencies(app: Flask, monkeypatch: pyte
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 2

View File

@@ -98,7 +98,11 @@ def test_workflow_run_list_returns_frontend_history_contract(app: Flask, monkeyp
handler = unwrap(api.get)
with app.test_request_context("/apps/app-1/workflow-runs?limit=10", method="GET"):
payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"))
payload = handler(
api,
workflow_run_module.WorkflowRunListQuery(limit=10),
app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"),
)
response = _serialize_200_response(api.get, payload)
@@ -139,7 +143,11 @@ def test_advanced_chat_workflow_run_list_keeps_message_fields(app: Flask, monkey
handler = unwrap(api.get)
with app.test_request_context("/apps/app-1/advanced-chat/workflow-runs?limit=1", method="GET"):
payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"))
payload = handler(
api,
workflow_run_module.WorkflowRunListQuery(limit=1),
app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"),
)
response = _serialize_200_response(api.get, payload)

View File

@@ -142,7 +142,12 @@ def test_app_trigger_enable_uses_injected_tenant_id(app: Flask, database_session
app.test_request_context("/", json=payload),
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
):
response = method(api, app_model.tenant_id, app_model)
response = method(
api,
workflow_trigger_module.ParserEnable(trigger_id=trigger.id, enable_trigger=True),
app_model.tenant_id,
app_model,
)
assert response["id"] == trigger.id
assert response["status"] == "enabled"

View File

@@ -14,9 +14,15 @@ from controllers.console.datasets.rag_pipeline.datasource_auth import (
DatasourceAuthListApi,
DatasourceAuthOauthCustomClient,
DatasourceAuthUpdateApi,
DatasourceCredentialDeletePayload,
DatasourceCredentialPayload,
DatasourceCredentialUpdatePayload,
DatasourceCustomClientPayload,
DatasourceDefaultPayload,
DatasourceHardCodeAuthListApi,
DatasourceOAuthCallback,
DatasourcePluginOAuthAuthorizationUrl,
DatasourceUpdateNamePayload,
DatasourceUpdateProviderNameApi,
)
from core.plugin.impl.oauth import OAuthHandler
@@ -405,7 +411,8 @@ class TestDatasourceAuth:
return_value=None,
) as add_api_key_provider,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -431,7 +438,7 @@ class TestDatasourceAuth:
),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialPayload.model_validate(payload), "tenant-1", "notion")
def test_get_success(self, app: Flask):
api = DatasourceAuth()
@@ -462,7 +469,7 @@ class TestDatasourceAuth:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialPayload.model_validate(payload), "tenant-1", "notion")
def test_get_empty_list(self, app: Flask):
api = DatasourceAuth()
@@ -499,7 +506,8 @@ class TestDatasourceAuthDeleteApi:
return_value=None,
) as remove_datasource_credentials,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialDeletePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -522,7 +530,7 @@ class TestDatasourceAuthDeleteApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialDeletePayload.model_validate(payload), "tenant-1", "notion")
class TestDatasourceAuthUpdateApi:
@@ -545,7 +553,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_datasource_credentials,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 201
@@ -573,7 +582,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
@@ -595,7 +605,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
),
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
assert status == 201
@@ -615,7 +626,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
@@ -716,7 +728,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
) as setup_custom_client,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -760,7 +773,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
),
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
assert status == 200
@@ -783,7 +797,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
) as setup_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
setup_mock.assert_called_once()
@@ -808,7 +823,8 @@ class TestDatasourceAuthDefaultApi:
return_value=None,
) as set_default_datasource_provider,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceDefaultPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -828,7 +844,7 @@ class TestDatasourceAuthDefaultApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceDefaultPayload.model_validate(payload), "tenant-1", "notion")
class TestDatasourceUpdateProviderNameApi:
@@ -847,7 +863,8 @@ class TestDatasourceUpdateProviderNameApi:
return_value=None,
) as update_datasource_provider_name,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceUpdateNamePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -871,7 +888,7 @@ class TestDatasourceUpdateProviderNameApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceUpdateNamePayload.model_validate(payload), "tenant-1", "notion")
def test_update_name_missing_credential_id(self, app: Flask):
api = DatasourceUpdateProviderNameApi()
@@ -884,4 +901,4 @@ class TestDatasourceUpdateProviderNameApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceUpdateNamePayload.model_validate(payload), "tenant-1", "notion")

View File

@@ -7,6 +7,7 @@ from flask import Flask
from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline.datasource_content_preview import (
DataSourceContentPreviewApi,
Parser,
)
from models import Account
from models.dataset import Pipeline
@@ -49,7 +50,8 @@ class TestDataSourceContentPreviewApi:
return_value=service_instance,
),
):
response, status = method(api, account, pipeline, node_id)
req_data = Parser.model_validate(payload)
response, status = method(api, req_data, account, pipeline, node_id)
service_instance.run_datasource_node_preview.assert_called_once_with(
pipeline=pipeline,
@@ -80,7 +82,7 @@ class TestDataSourceContentPreviewApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, account, pipeline, "node-1")
method(api, Parser.model_validate(payload), account, pipeline, "node-1")
def test_post_without_credential_id(self, app: Flask):
api = DataSourceContentPreviewApi()
@@ -106,7 +108,8 @@ class TestDataSourceContentPreviewApi:
return_value=service_instance,
),
):
response, status = method(api, account, pipeline, "node-1")
req_data = Parser.model_validate(payload)
response, status = method(api, req_data, account, pipeline, "node-1")
service_instance.run_datasource_node_preview.assert_called_once()
assert status == 200

View File

@@ -15,8 +15,11 @@ from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline import rag_pipeline as module
from controllers.console.datasets.rag_pipeline.rag_pipeline import (
CustomizedPipelineTemplateApi,
CustomizedPipelineTemplatePayload,
PipelineTemplateDetailApi,
PipelineTemplateDetailQuery,
PipelineTemplateListApi,
PipelineTemplateListQuery,
PublishCustomizedPipelineTemplateApi,
)
from models.account import Account
@@ -90,7 +93,7 @@ class TestPipelineTemplateListApi:
app.test_request_context("/rag/pipeline/templates"),
patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates),
):
response, status = method(api, session, tenant_id)
response, status = method(api, PipelineTemplateListQuery(), session, tenant_id)
assert status == 200
assert service_calls == [("built-in", "en-US", tenant_id)]
@@ -120,7 +123,9 @@ class TestPipelineTemplateListApi:
app.test_request_context("/rag/pipeline/templates?type=customized&language=ja-JP"),
patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates),
):
response, status = method(api, session, tenant_id)
response, status = method(
api, PipelineTemplateListQuery(type="customized", language="ja-JP"), session, tenant_id
)
assert status == 200
assert response == {"pipeline_templates": []}
@@ -147,7 +152,7 @@ class TestPipelineTemplateDetailApi:
side_effect=get_pipeline_template_detail,
),
):
response, status = method(api, session, "template-1")
response, status = method(api, PipelineTemplateDetailQuery(type="customized"), session, "template-1")
assert status == 200
assert response == {**_template_detail(), "created_by": None}
@@ -170,7 +175,7 @@ class TestPipelineTemplateDetailApi:
),
pytest.raises(NotFound),
):
method(api, session, "missing")
method(api, PipelineTemplateDetailQuery(), session, "missing")
class TestCustomizedPipelineTemplateApi:
@@ -198,7 +203,9 @@ class TestCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template),
):
response, status = method(api, tenant_id, account, "template-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "template-1"
)
assert (response, status) == ("", 204)
assert len(service_calls) == 1
@@ -242,7 +249,9 @@ class TestCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template),
):
response, status = method(api, tenant_id, account, "template-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "template-1"
)
assert (response, status) == ("", 204)
assert len(service_calls) == 1
@@ -349,7 +358,9 @@ class TestPublishCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module, "RagPipelineService", Service),
):
response, status = method(api, tenant_id, account, "pipeline-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "pipeline-1"
)
assert (response, status) == ("", 204)
assert service_calls == [("pipeline-1", payload, account, tenant_id)]
@@ -386,7 +397,9 @@ class TestPublishCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module, "RagPipelineService", Service),
):
response, status = method(api, tenant_id, account, "pipeline-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "pipeline-1"
)
assert (response, status) == ("", 204)
assert service_calls == [

View File

@@ -15,6 +15,7 @@ from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.datasets.rag_pipeline.rag_pipeline_datasets import (
CreateEmptyRagPipelineDatasetApi,
CreateRagPipelineDatasetApi,
RagPipelineDatasetImportPayload,
)
from services.entities.dsl_entities import ImportStatus
@@ -50,7 +51,7 @@ class TestCreateRagPipelineDatasetApi:
return_value=mock_service,
),
):
response, status = method(api, "tenant-1", user)
response, status = method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
assert status == 201
assert response == {
@@ -75,7 +76,7 @@ class TestCreateRagPipelineDatasetApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(Forbidden):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
def test_post_dataset_name_duplicate(self, app: Flask) -> None:
api = CreateRagPipelineDatasetApi()
@@ -96,7 +97,7 @@ class TestCreateRagPipelineDatasetApi:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
def test_post_invalid_payload(self, app: Flask) -> None:
api = CreateRagPipelineDatasetApi()
@@ -110,7 +111,7 @@ class TestCreateRagPipelineDatasetApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
class TestCreateEmptyRagPipelineDatasetApi:

View File

@@ -8,12 +8,14 @@ from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.console import console_ns
from controllers.console.app.error import DraftWorkflowNotExist
from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import (
PaginationQuery,
RagPipelineEnvironmentVariableCollectionApi,
RagPipelineNodeVariableCollectionApi,
RagPipelineSystemVariableCollectionApi,
RagPipelineVariableApi,
RagPipelineVariableCollectionApi,
RagPipelineVariableResetApi,
WorkflowDraftVariablePatchPayload,
)
from core.workflow.llm_environment_variable import LLMEnvironmentVariable
from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID
@@ -72,7 +74,7 @@ class TestRagPipelineVariableCollectionApi:
return_value=draft_srv,
),
):
result = method(api, editor_user, pipeline)
result = method(api, PaginationQuery(page=1, limit=10), editor_user, pipeline)
assert result is var_list
draft_srv.list_variables_without_values.assert_called_once_with(
@@ -100,7 +102,7 @@ class TestRagPipelineVariableCollectionApi:
),
):
with pytest.raises(DraftWorkflowNotExist):
method(api, editor_user, pipeline)
method(api, PaginationQuery(), editor_user, pipeline)
def test_delete_variables_success(self, app: Flask, fake_db, editor_user):
api = RagPipelineVariableCollectionApi()
@@ -198,7 +200,7 @@ class TestRagPipelineVariableApi:
),
):
with pytest.raises(InvalidArgumentError):
method(api, editor_user, pipeline, "v1")
method(api, WorkflowDraftVariablePatchPayload.model_validate(payload), editor_user, pipeline, "v1")
def test_delete_variable_success(self, app: Flask, fake_db, editor_user):
api = RagPipelineVariableApi()

View File

@@ -11,10 +11,12 @@ from flask import Flask
from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline.rag_pipeline_import import (
IncludeSecretQuery,
RagPipelineExportApi,
RagPipelineImportApi,
RagPipelineImportCheckDependenciesApi,
RagPipelineImportConfirmApi,
RagPipelineImportPayload,
)
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
from models.dataset import Pipeline
@@ -67,7 +69,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 200
assert response == {
@@ -105,7 +107,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 400
assert response["status"] == "failed"
@@ -139,7 +141,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 202
assert response["status"] == "pending"
@@ -287,7 +289,7 @@ class TestRagPipelineExportApi:
return_value=service,
),
):
response, status = method(api, pipeline)
response, status = method(api, IncludeSecretQuery(), pipeline)
assert status == 200
assert response == {"data": "yaml: data"}

View File

@@ -16,6 +16,14 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
from controllers.console.datasets.rag_pipeline import rag_pipeline_workflow as module
from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
DraftWorkflowRunPayload,
NodeIdQuery,
PublishedWorkflowRunPayload,
RagPipelineRecommendedPluginQuery,
WorkflowListQuery,
WorkflowUpdatePayload,
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from models.account import Account, TenantAccountRole
from models.dataset import Pipeline
@@ -133,7 +141,9 @@ def test_published_rag_pipeline_workflows_serialize_items_before_session_closes(
method="GET",
query_string={"page": 1, "limit": 10, "user_id": "", "named_only": "false"},
):
response = handler(api, _account(), pipeline=pipeline)
response = handler(
api, WorkflowListQuery(page=1, limit=10, user_id="", named_only=False), _account(), pipeline=pipeline
)
assert response["items"][0]["id"] == DEFAULT_WORKFLOW_ID
assert response["page"] == 1
@@ -157,6 +167,7 @@ def test_rag_pipeline_workflow_patch_serializes_response_model(
):
response = handler(
api,
WorkflowUpdatePayload.model_validate(payload),
_account(),
pipeline=_pipeline(),
workflow_id=DEFAULT_WORKFLOW_ID,
@@ -198,7 +209,7 @@ def test_draft_rag_pipeline_second_step_parameters_serializes_variables(database
database_app.test_request_context("/?node_id=node-1"),
patch.object(RagPipelineService, "get_second_step_parameters", return_value=variables),
):
response = handler(api, _pipeline())
response = handler(api, NodeIdQuery(node_id="node-1"), _pipeline())
assert response["variables"] == variables
@@ -215,7 +226,7 @@ def test_rag_pipeline_recommended_plugins_serializes_known_envelope(database_app
database_app.test_request_context("/?type=tool"),
patch.object(RagPipelineService, "get_recommended_plugins", return_value=recommended_plugins),
):
response = handler(api, DEFAULT_WORKFLOW_TENANT_ID, _account())
response = handler(api, RagPipelineRecommendedPluginQuery(type="tool"), DEFAULT_WORKFLOW_TENANT_ID, _account())
assert response == recommended_plugins
@@ -270,7 +281,12 @@ def test_rag_pipeline_run_uses_sqlite_session(
patch.object(module.PipelineGenerateService, "generate", return_value=MagicMock()) as generate,
patch.object(module.helper, "compact_generate_response", return_value={"ok": True}),
):
response = handler(api, session, _account(), pipeline.id)
req_data = (
DraftWorkflowRunPayload.model_validate(payload)
if api_type is module.DraftRagPipelineRunApi
else PublishedWorkflowRunPayload.model_validate(payload)
)
response = handler(api, req_data, session, _account(), pipeline.id)
assert response == {"ok": True}
load_pipeline.assert_called_once_with(session, pipeline.id)
@@ -293,6 +309,11 @@ def test_rag_pipeline_run_translates_rate_limit(
api = api_type()
handler = unwrap_all(api.post)
pipeline = _pipeline()
req_data = (
DraftWorkflowRunPayload.model_validate(payload)
if api_type is module.DraftRagPipelineRunApi
else PublishedWorkflowRunPayload.model_validate(payload)
)
with (
Session(sqlite_engine) as session,
@@ -301,4 +322,4 @@ def test_rag_pipeline_run_translates_rate_limit(
patch.object(module.PipelineGenerateService, "generate", side_effect=InvokeRateLimitError("limit")),
pytest.raises(InvokeRateLimitHttpError),
):
handler(api, session, _account(), pipeline.id)
handler(api, req_data, session, _account(), pipeline.id)

View File

@@ -22,8 +22,12 @@ import services
from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync
from controllers.console.datasets.rag_pipeline import rag_pipeline_workflow as workflow_controller
from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
DatasourceVariablesPayload,
DefaultBlockConfigQuery,
DefaultRagPipelineBlockConfigApi,
DraftRagPipelineApi,
NodeRunPayload,
NodeRunRequiredPayload,
PublishedAllRagPipelineApi,
PublishedRagPipelineApi,
RagPipelineByIdApi,
@@ -33,9 +37,12 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
RagPipelineDraftRunLoopNodeApi,
RagPipelineDraftWorkflowRestoreApi,
RagPipelineRecommendedPluginApi,
RagPipelineRecommendedPluginQuery,
RagPipelineTaskStopApi,
RagPipelineWorkflowLastRunApi,
RagPipelineWorkflowRunNodeExecutionListApi,
WorkflowListQuery,
WorkflowUpdatePayload,
)
from graphon.enums import WorkflowNodeExecutionStatus
from libs.datetime_utils import naive_utc_now
@@ -418,7 +425,7 @@ class TestDraftRunNodes:
return_value={"ok": True},
),
):
result = method(api, user, pipeline, "node")
result = method(api, NodeRunPayload(), user, pipeline, "node")
assert result == {"ok": True}
def test_iteration_node_conversation_not_exists(self, app: Flask) -> None:
@@ -436,7 +443,7 @@ class TestDraftRunNodes:
),
):
with pytest.raises(NotFound):
method(api, user, pipeline, "node")
method(api, NodeRunPayload(), user, pipeline, "node")
def test_loop_node_success(self, app: Flask) -> None:
api = RagPipelineDraftRunLoopNodeApi()
@@ -456,7 +463,7 @@ class TestDraftRunNodes:
return_value={"ok": True},
),
):
assert method(api, user, pipeline, "node") == {"ok": True}
assert method(api, NodeRunPayload(), user, pipeline, "node") == {"ok": True}
class TestDraftNodeRun:
@@ -478,7 +485,7 @@ class TestDraftNodeRun:
),
):
with pytest.raises(ValueError):
method(api, user, pipeline, "node")
method(api, NodeRunRequiredPayload(inputs={}), user, pipeline, "node")
class TestPublishedPipelineApis:
@@ -551,7 +558,7 @@ class TestMiscApis:
return_value=service,
),
):
result = method(api, tenant_id, user)
result = method(api, RagPipelineRecommendedPluginQuery(type="all"), tenant_id, user)
assert result == recommended_plugins
service.get_recommended_plugins.assert_called_once_with("all", user, tenant_id)
@@ -573,7 +580,7 @@ class TestDefaultBlockConfigApi:
return_value=service,
),
):
result = method(api, pipeline, "llm")
result = method(api, DefaultBlockConfigQuery(q="{}"), pipeline, "llm")
assert result == {"k": "v"}
def test_get_block_config_invalid_json(self, app: Flask) -> None:
@@ -584,7 +591,7 @@ class TestDefaultBlockConfigApi:
with app.test_request_context("/?q=bad-json"):
with pytest.raises(ValueError):
method(api, pipeline, "llm")
method(api, DefaultBlockConfigQuery(q="bad-json"), pipeline, "llm")
class TestPublishedAllRagPipelineApi:
@@ -605,7 +612,7 @@ class TestPublishedAllRagPipelineApi:
return_value=service,
),
):
result = method(api, user, pipeline)
result = method(api, WorkflowListQuery(), user, pipeline)
assert result["items"][0]["id"] == "w1"
assert result["items"][0]["graph"] == {"nodes": [], "edges": []}
@@ -622,7 +629,7 @@ class TestPublishedAllRagPipelineApi:
app.test_request_context("/?user_id=u2"),
):
with pytest.raises(Forbidden):
method(api, user, pipeline)
method(api, WorkflowListQuery(user_id="u2"), user, pipeline)
class TestRagPipelineByIdApi:
@@ -647,7 +654,7 @@ class TestRagPipelineByIdApi:
return_value=service,
),
):
result = method(api, user, pipeline, "w1")
result = method(api, WorkflowUpdatePayload.model_validate(payload), user, pipeline, "w1")
assert result["id"] == "w1"
assert result["marked_name"] == "test"
@@ -661,7 +668,7 @@ class TestRagPipelineByIdApi:
user = make_account()
with app.test_request_context("/", json={}):
result, status = method(api, user, pipeline, "w1")
result, status = method(api, WorkflowUpdatePayload(), user, pipeline, "w1")
assert status == 400
def test_delete_success(self, app: Flask) -> None:
@@ -797,6 +804,6 @@ class TestRagPipelineDatasourceVariableApi:
return_value=service,
),
):
result = method(api, user, pipeline)
result = method(api, DatasourceVariablesPayload.model_validate(payload), user, pipeline)
assert result["node_id"] == "n1"
assert result["process_data"] == {}

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
from controllers.console.datasets import data_source as module
from controllers.console.datasets.data_source import DataSourceApi, DataSourceNotionListApi
from controllers.console.datasets.data_source import DataSourceApi, DataSourceNotionListApi, DataSourceNotionListQuery
from models import Account, DataSourceOauthBinding
from models.engine import db
@@ -217,7 +217,11 @@ def test_notion_pre_import_pages_serializes_frontend_list_shape(
patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime),
):
response, status = unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(), sqlite_session, "tenant-1", current_user
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1"),
sqlite_session,
"tenant-1",
current_user,
)
assert status == 200
@@ -255,7 +259,13 @@ def test_notion_pre_import_pages_rejects_missing_credential(
patch.object(module.DatasourceProviderService, "get_datasource_credentials", return_value=None),
pytest.raises(NotFound, match="Credential not found"),
):
unwrap(DataSourceNotionListApi().get)(DataSourceNotionListApi(), sqlite_session, TENANT_ID, current_user)
unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1"),
sqlite_session,
TENANT_ID,
current_user,
)
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
@@ -276,4 +286,10 @@ def test_notion_pre_import_pages_rejects_non_notion_dataset(
patch.object(module.DatasetService, "get_dataset", return_value=dataset),
pytest.raises(ValueError, match="Dataset is not notion type"),
):
unwrap(DataSourceNotionListApi().get)(DataSourceNotionListApi(), sqlite_session, TENANT_ID, current_user)
unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1", dataset_id="dataset-1"),
sqlite_session,
TENANT_ID,
current_user,
)

View File

@@ -15,6 +15,8 @@ from controllers.console.datasets.data_source import (
DataSourceNotionDocumentSyncApi,
DataSourceNotionIndexingEstimateApi,
DataSourceNotionPreviewApi,
DataSourceNotionPreviewQuery,
NotionEstimatePayload,
)
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account
@@ -45,7 +47,7 @@ class TestDataSourceNotionPreviewApi:
return_value=extractor,
),
):
response, status = method(api, "tenant-1", "p1", "page")
response, status = method(api, DataSourceNotionPreviewQuery(credential_id="c1"), "tenant-1", "p1", "page")
assert status == 200
@@ -80,7 +82,7 @@ class TestDataSourceNotionIndexingEstimateApi:
return_value=MagicMock(model_dump=lambda: {"total_pages": 1}),
),
):
response, status = method(api, sqlite_session, "tenant-1")
response, status = method(api, NotionEstimatePayload.model_validate(payload), sqlite_session, "tenant-1")
assert status == 200

View File

@@ -18,6 +18,7 @@ from controllers.console.datasets.datasets import (
DatasetApiDeleteApi,
DatasetApiKeyApi,
DatasetAutoDisableLogApi,
DatasetCreatePayload,
DatasetEnableApiApi,
DatasetErrorDocs,
DatasetIndexingEstimateApi,
@@ -28,7 +29,9 @@ from controllers.console.datasets.datasets import (
DatasetRelatedAppListApi,
DatasetRetrievalSettingApi,
DatasetRetrievalSettingMockApi,
DatasetUpdatePayload,
DatasetUseCheckApi,
IndexingEstimatePayload,
_get_retrieval_methods_by_vector_type,
)
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
@@ -489,7 +492,7 @@ class TestDatasetListApiPost:
patch.object(type(console_ns), "payload", payload),
patch.object(DatasetService, "create_empty_dataset", return_value=dataset),
):
_, status = method(api, MagicMock(), "tenant-1", user)
_, status = method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
assert status == 201
def test_post_forbidden(self, app: Flask):
@@ -499,7 +502,7 @@ class TestDatasetListApiPost:
user = make_account(TenantAccountRole.NORMAL)
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", user)
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
def test_post_duplicate_name(self, app: Flask):
api = DatasetListApi()
@@ -514,14 +517,14 @@ class TestDatasetListApiPost:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", user)
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
def test_post_invalid_payload_missing_name(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
with app.test_request_context("/datasets", json={}), patch.object(type(console_ns), "payload", {}):
with pytest.raises(ValueError):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(), MagicMock(), "tenant-1", make_account())
def test_post_invalid_indexing_technique(self, app: Flask):
api = DatasetListApi()
@@ -529,7 +532,7 @@ class TestDatasetListApiPost:
payload = {"name": "bad", "indexing_technique": "invalid-tech"}
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(ValueError, match="Invalid indexing technique"):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account())
def test_post_invalid_provider(self, app: Flask):
api = DatasetListApi()
@@ -537,7 +540,7 @@ class TestDatasetListApiPost:
payload = {"name": "bad", "provider": "unknown"}
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(ValueError, match="Invalid provider"):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account())
class TestDatasetApiGet:
@@ -692,7 +695,7 @@ class TestDatasetApiPatch:
patch.object(DatasetService, "update_dataset", return_value=dataset),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]),
):
result, status = method(api, MagicMock(), tenant_id, user, dataset_id)
result, status = method(api, DatasetUpdatePayload(), MagicMock(), tenant_id, user, dataset_id)
assert status == 200
assert result["partial_member_list"] == []
@@ -704,7 +707,7 @@ class TestDatasetApiPatch:
patch.object(DatasetService, "get_dataset", return_value=None),
):
with pytest.raises(NotFound, match="Dataset not found"):
method(api, MagicMock(), "tenant-1", make_account(), "missing")
method(api, DatasetUpdatePayload(), MagicMock(), "tenant-1", make_account(), "missing")
def test_patch_permission_denied(self, app: Flask):
api = DatasetApi()
@@ -719,7 +722,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "check_permission", side_effect=Forbidden("no permission")),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant", make_account(), dataset_id)
method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
def test_patch_partial_members_update(self, app: Flask):
api = DatasetApi()
@@ -736,7 +739,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "update_partial_member_list", return_value=None),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=["u1", "u2"]),
):
result, _ = method(api, MagicMock(), "tenant", make_account(), dataset_id)
result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
assert result["partial_member_list"] == ["u1", "u2"]
def test_patch_clear_partial_members(self, app: Flask):
@@ -754,7 +757,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "clear_partial_member_list", return_value=None),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]),
):
result, _ = method(api, MagicMock(), "tenant", make_account(), dataset_id)
result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
assert result["partial_member_list"] == []
@@ -1014,7 +1017,12 @@ class TestDatasetIndexingEstimateApi:
patch("controllers.console.datasets.datasets.DocumentService.estimate_args_validate", return_value=None),
patch("controllers.console.datasets.datasets.IndexingRunner.indexing_estimate", return_value=mock_response),
):
response, status = method(api, session, "tenant-1")
response, status = method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
assert status == 200
assert response == {
"tokens": 0,
@@ -1036,7 +1044,12 @@ class TestDatasetIndexingEstimateApi:
patch("controllers.console.datasets.datasets.DocumentService.estimate_args_validate", return_value=None),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_llm_bad_request_error(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1055,7 +1068,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_provider_token_not_init(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1074,7 +1092,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_generic_exception(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1092,7 +1115,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(IndexingEstimateError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
class TestDatasetRelatedAppListApi:

View File

@@ -9,9 +9,11 @@ from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload
from controllers.console import console_ns
from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.datasets.datasets_segments import (
BatchImportPayload,
ChildChunkAddApi,
ChildChunkBatchUpdatePayload,
ChildChunkUpdateApi,
@@ -20,6 +22,8 @@ from controllers.console.datasets.datasets_segments import (
DatasetDocumentSegmentBatchImportApi,
DatasetDocumentSegmentListApi,
DatasetDocumentSegmentUpdateApi,
SegmentCreatePayload,
SegmentUpdatePayload,
)
from controllers.console.datasets.error import ChildChunkDeleteIndexError, ChildChunkIndexingError, InvalidActionError
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
@@ -50,6 +54,7 @@ def _segment():
status=SegmentStatus.COMPLETED,
updated_by="u1",
)
segment.id = "seg-1"
segment.created_at = naive_utc_now()
segment.updated_at = naive_utc_now()
@@ -354,7 +359,9 @@ class TestDatasetDocumentSegmentAddApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, SegmentCreatePayload(content="test content"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 200
assert response["data"]["id"] == "seg-1"
@@ -378,7 +385,9 @@ class TestDatasetDocumentSegmentAddApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
def test_post_provider_token_not_init(self, app: Flask):
api = DatasetDocumentSegmentAddApi()
@@ -400,7 +409,9 @@ class TestDatasetDocumentSegmentAddApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
class TestDatasetDocumentSegmentUpdateApi:
@@ -441,7 +452,9 @@ class TestDatasetDocumentSegmentUpdateApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1", "seg-1")
response, status = method(
api, SegmentUpdatePayload(content="test content"), session, "tenant-1", user, "ds-1", "doc-1", "seg-1"
)
assert status == 200
assert "data" in response
@@ -467,7 +480,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_patch_segment_not_found(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
@@ -495,7 +517,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_patch_llm_bad_request(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
@@ -525,7 +556,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
class TestDatasetDocumentSegmentBatchImportApi:
@@ -564,7 +604,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 200
assert response["job_status"] == "waiting"
@@ -581,7 +623,15 @@ class TestDatasetDocumentSegmentBatchImportApi:
patch("controllers.console.datasets.datasets_segments.DatasetService.get_dataset", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_post_document_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -599,7 +649,15 @@ class TestDatasetDocumentSegmentBatchImportApi:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_post_upload_file_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -619,7 +677,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_post_invalid_file_type(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -641,7 +701,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
),
):
with pytest.raises(ValueError):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_post_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -665,7 +727,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
"controllers.console.datasets.datasets_segments.redis_client.setnx", side_effect=Exception("redis down")
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 500
assert "error" in response
@@ -747,7 +811,9 @@ class TestChildChunkAddApi:
return_value=child_chunk,
),
):
response, status = method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
response, status = method(
api, ChildChunkCreatePayload(content="child"), MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1"
)
assert status == 200
assert response["data"]["id"] == "cc-1"
@@ -778,7 +844,16 @@ class TestChildChunkAddApi:
),
):
with pytest.raises(ChildChunkIndexingError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
ChildChunkCreatePayload(content="child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_post_permission_denied(self, app: Flask):
api = ChildChunkAddApi()
@@ -799,7 +874,16 @@ class TestChildChunkAddApi:
),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
ChildChunkCreatePayload(content="child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
class TestChildChunkUpdateApi:
@@ -922,7 +1006,17 @@ class TestChildChunkUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1", "cc-1")
method(
api,
ChildChunkUpdatePayload(content="updated child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
"cc-1",
)
class TestSegmentListAdvancedCases:
@@ -1047,7 +1141,9 @@ class TestSegmentOperationCases:
),
):
with pytest.raises(ProviderTokenNotInitError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
def test_batch_import_with_document_not_found(self, app: Flask):
"""Test batch import with document not found"""
@@ -1063,7 +1159,15 @@ class TestSegmentOperationCases:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_batch_import_with_invalid_file(self, app: Flask):
"""Test batch import with invalid file type"""
@@ -1083,7 +1187,9 @@ class TestSegmentOperationCases:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=document),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_batch_import_with_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -1122,7 +1228,9 @@ class TestSegmentOperationCases:
side_effect=Exception("Task failed"),
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 500
assert "error" in response

View File

@@ -12,10 +12,14 @@ from controllers.console import console_ns
from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.datasets.external import (
BedrockRetrievalApi,
BedrockRetrievalPayload,
ExternalApiTemplateApi,
ExternalApiTemplateListApi,
ExternalApiTemplateListQuery,
ExternalApiUseCheckApi,
ExternalDatasetCreateApi,
ExternalHitTestingPayload,
ExternalKnowledgeApiPayload,
ExternalKnowledgeHitTestingApi,
)
from models.account import Account, TenantAccountRole
@@ -174,7 +178,9 @@ class TestExternalApiTemplateListApi:
return_value=([api_item], 3),
) as get_external_knowledge_apis,
):
resp, status = method(api, session, "tenant-1")
resp, status = method(
api, ExternalApiTemplateListQuery(page=2, limit=1, keyword="vector"), session, "tenant-1"
)
assert status == 200
assert resp == {
@@ -213,7 +219,9 @@ class TestExternalApiTemplateListApi:
return_value=created,
) as create_external_knowledge_api,
):
resp, status = method(api, session, "tenant-1", current_user)
resp, status = method(
api, ExternalKnowledgeApiPayload.model_validate(payload), session, "tenant-1", current_user
)
assert status == 201
assert resp == _external_api_dict("api-created")
@@ -239,7 +247,7 @@ class TestExternalApiTemplateListApi:
patch.object(ExternalDatasetService, "validate_api_list"),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
def test_post_duplicate_name(self, app: Flask, current_user: Account):
api = ExternalApiTemplateListApi()
@@ -258,7 +266,7 @@ class TestExternalApiTemplateListApi:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalApiTemplateApi:
@@ -325,7 +333,14 @@ class TestExternalApiTemplateApi:
return_value=updated,
) as update_external_knowledge_api,
):
resp, status = method(api, session, "tenant-1", current_user, "api-updated")
resp, status = method(
api,
ExternalKnowledgeApiPayload.model_validate(payload),
session,
"tenant-1",
current_user,
"api-updated",
)
assert status == 200
assert resp == _external_api_dict("api-updated")
@@ -405,7 +420,9 @@ class TestExternalDatasetCreateApi:
) as dataset_response_source,
):
session = MagicMock()
resp, status = method(api, session, "tenant-1", current_user)
resp, status = method(
api, ExternalDatasetCreatePayload.model_validate(payload), session, "tenant-1", current_user
)
assert status == 201
assert resp == _expected_dataset_detail_payload()
@@ -433,7 +450,7 @@ class TestExternalDatasetCreateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalDatasetCreatePayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalKnowledgeHitTestingApi:
@@ -450,7 +467,7 @@ class TestExternalKnowledgeHitTestingApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), current_user, "dataset-id")
method(api, ExternalHitTestingPayload(query="test"), MagicMock(), current_user, "dataset-id")
def test_hit_testing_success(self, app: Flask, current_user: Account):
api = ExternalKnowledgeHitTestingApi()
@@ -496,7 +513,7 @@ class TestExternalKnowledgeHitTestingApi:
) as external_retrieve,
patch("controllers.console.datasets.external.dump_response", side_effect=lambda _model, value: value),
):
resp = method(api, session, current_user, "dataset-id")
resp = method(api, ExternalHitTestingPayload.model_validate(payload), session, current_user, "dataset-id")
assert resp == retrieve_response
check_dataset_permission.assert_called_once_with(dataset, current_user, session)
@@ -549,7 +566,7 @@ class TestBedrockRetrievalApi:
return_value=retrieval_response,
) as knowledge_retrieval,
):
resp, status = method()
resp, status = method(api, BedrockRetrievalPayload.model_validate(payload))
assert status == 200
assert resp == retrieval_response
@@ -576,7 +593,7 @@ class TestExternalApiTemplateListApiAdvanced:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
def test_get_with_pagination(self, app: Flask):
api = ExternalApiTemplateListApi()
@@ -591,7 +608,7 @@ class TestExternalApiTemplateListApiAdvanced:
return_value=(templates, 25),
) as get_external_knowledge_apis,
):
resp, status = method(api, MagicMock(), "tenant-1")
resp, status = method(api, ExternalApiTemplateListQuery(page=2, limit=3), MagicMock(), "tenant-1")
assert status == 200
assert resp == {
@@ -621,7 +638,7 @@ class TestExternalDatasetCreateApiAdvanced:
with app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalDatasetCreatePayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalKnowledgeHitTestingApiAdvanced:
@@ -644,7 +661,7 @@ class TestExternalKnowledgeHitTestingApiAdvanced:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), current_user, "ds-1")
method(api, ExternalHitTestingPayload.model_validate(payload), MagicMock(), current_user, "ds-1")
def test_hit_testing_with_custom_retrieval_model(self, app: Flask, current_user: Account):
api = ExternalKnowledgeHitTestingApi()
@@ -682,7 +699,7 @@ class TestExternalKnowledgeHitTestingApiAdvanced:
},
) as external_retrieve,
):
resp = method(api, session, current_user, "ds-1")
resp = method(api, ExternalHitTestingPayload.model_validate(payload), session, current_user, "ds-1")
assert resp == {
"query": {"content": "test query"},
@@ -727,4 +744,4 @@ class TestBedrockRetrievalApiAdvanced:
),
):
with pytest.raises(ValueError):
method()
method(api, BedrockRetrievalPayload.model_validate(payload))

View File

@@ -7,6 +7,7 @@ from flask import Flask
from pytest_mock import MockerFixture
from werkzeug.exceptions import NotFound
from controllers.common.controller_schemas import MetadataUpdatePayload
from controllers.console import console_ns
from controllers.console.datasets.metadata import (
DatasetMetadataApi,
@@ -24,6 +25,7 @@ from services.metadata_service import MetadataService
@pytest.fixture
def app():
app = Flask("test_dataset_metadata")
app.config["TESTING"] = True
return app
@@ -76,7 +78,9 @@ class TestDatasetMetadataCreateApi:
MetadataService, "create_metadata", return_value={"id": "m1", "type": "string", "name": "author"}
),
):
result, status = method(api, MagicMock(), "tenant-1", current_user, dataset_id)
result, status = method(
api, MetadataArgs(type="string", name="author"), MagicMock(), "tenant-1", current_user, dataset_id
)
assert status == 201
assert result["type"] == "string"
assert result["name"] == "author"
@@ -92,7 +96,9 @@ class TestDatasetMetadataCreateApi:
patch.object(DatasetService, "get_dataset", return_value=None),
):
with pytest.raises(NotFound, match="Dataset not found"):
method(api, MagicMock(), "tenant-1", current_user, dataset_id)
method(
api, MetadataArgs(type="string", name="author"), MagicMock(), "tenant-1", current_user, dataset_id
)
class TestDatasetMetadataGetApi:
@@ -140,7 +146,15 @@ class TestDatasetMetadataApi:
return_value={"id": "m1", "type": "string", "name": "updated-name"},
),
):
result, status = method(api, MagicMock(), "tenant-1", current_user, dataset_id, metadata_id)
result, status = method(
api,
MetadataUpdatePayload(name="updated-name"),
MagicMock(),
"tenant-1",
current_user,
dataset_id,
metadata_id,
)
assert status == 200
assert result["type"] == "string"
assert result["name"] == "updated-name"
@@ -201,9 +215,14 @@ class TestDocumentMetadataEditApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(DatasetService, "get_dataset", return_value=dataset),
patch.object(DatasetService, "check_dataset_permission"),
patch.object(MetadataOperationData, "model_validate", return_value=MagicMock()),
patch.object(MetadataService, "update_documents_metadata"),
):
result, status = method(api, MagicMock(), current_user, dataset_id)
result, status = method(
api,
MetadataOperationData(operation_data=[{"document_id": "doc-1", "metadata_list": []}]),
MagicMock(),
current_user,
dataset_id,
)
assert status == 204
assert result == ""

View File

@@ -16,6 +16,7 @@ from controllers.console.auth.error import (
from controllers.console.error import AccountInFreezeError
from controllers.console.workspace.account import (
AccountAvatarApi,
AccountAvatarQuery,
AccountDeleteApi,
AccountDeleteVerifyApi,
AccountInitApi,
@@ -216,7 +217,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/example",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=file_id), user)
assert result == {"avatar_url": "https://signed/example"}
sign_mock.assert_called_once_with(upload_file_id=file_id)
@@ -256,7 +257,7 @@ class TestAccountAvatarApiGet:
) as sign_mock,
):
with pytest.raises(NotFound):
method(api, user)
method(api, AccountAvatarQuery(avatar=file_id), user)
sign_mock.assert_not_called()
@@ -289,7 +290,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/example",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=file_id), user)
assert result == {"avatar_url": "https://signed/example"}
sign_mock.assert_called_once_with(upload_file_id=file_id)
@@ -308,7 +309,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/should-not-use",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=external), user)
assert result == {"avatar_url": external}
sign_mock.assert_not_called()

View File

@@ -17,7 +17,9 @@ from controllers.console.workspace.endpoint import (
EndpointIdPayload,
EndpointItemApi,
EndpointListApi,
EndpointListForPluginQuery,
EndpointListForSinglePluginApi,
EndpointListQuery,
EndpointUpdatePayload,
LegacyEndpointUpdatePayload,
)
@@ -146,7 +148,7 @@ class TestEndpointListApi:
return_value=[endpoint_entity],
),
):
result = method(api, "t1", "u1")
result = method(api, EndpointListQuery(page=1, page_size=10), "t1", "u1")
endpoint = result["endpoints"][0]
assert endpoint["id"] == "e1"
@@ -180,7 +182,7 @@ class TestEndpointListApi:
app.test_request_context("/?page=0&page_size=10"),
):
with pytest.raises(ValueError):
method(api, "t1", "u1")
method(api, EndpointListQuery(page=0, page_size=10), "t1", "u1")
class TestEndpointListForSinglePluginApi:
@@ -195,7 +197,7 @@ class TestEndpointListForSinglePluginApi:
return_value=[_endpoint_entity()],
),
):
result = method(api, "t1", "u1")
result = method(api, EndpointListForPluginQuery(page=1, page_size=10, plugin_id="p1"), "t1", "u1")
assert result["endpoints"][0]["id"] == "e1"
assert result["endpoints"][0]["settings"]["api_key"] == "pl********et"
@@ -209,7 +211,7 @@ class TestEndpointListForSinglePluginApi:
app.test_request_context("/?page=1&page_size=10"),
):
with pytest.raises(ValueError):
method(api, "t1", "u1")
method(api, EndpointListForPluginQuery(page=1, page_size=10), "t1", "u1")
class TestEndpointItemApi:

View File

@@ -15,6 +15,16 @@ from controllers.console.workspace.models import (
ModelProviderModelEnableApi,
ModelProviderModelParameterRuleApi,
ModelProviderModelValidateApi,
ParserCreateCredential,
ParserDeleteCredential,
ParserDeleteModels,
ParserGetCredentials,
ParserGetDefault,
ParserParameter,
ParserPostDefault,
ParserPostModels,
ParserSwitch,
ParserValidate,
)
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
@@ -43,7 +53,7 @@ class TestDefaultModelApi:
},
}
result = method(api, "tenant1")
result = method(api, ParserGetDefault(model_type=ModelType.LLM), "tenant1")
assert "data" in result
@@ -65,7 +75,7 @@ class TestDefaultModelApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1")
result = method(api, ParserPostDefault.model_validate(payload), "tenant1")
assert result["result"] == "success"
@@ -79,7 +89,7 @@ class TestDefaultModelApi:
):
service.return_value.get_default_model_of_model_type.return_value = None
result = method(api, "t1")
result = method(api, ParserGetDefault(model_type=ModelType.LLM), "t1")
assert "data" in result
@@ -117,7 +127,7 @@ class TestModelProviderModelApi:
patch("controllers.console.workspace.models.ModelProviderService"),
patch("controllers.console.workspace.models.ModelLoadBalancingService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserPostModels.model_validate(payload), "tenant1", "openai")
assert status == 200
@@ -134,7 +144,7 @@ class TestModelProviderModelApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert status == 204
@@ -177,7 +187,13 @@ class TestModelProviderModelCredentialApi:
provider_service.return_value.provider_manager.get_provider_model_available_credentials.return_value = []
lb_service.return_value.get_load_balancing_configs.return_value = (False, [])
result = method(api, "tenant1", SimpleNamespace(id="u1"), "openai")
result = method(
api,
ParserGetCredentials(model="gpt-4", model_type=ModelType.LLM),
"tenant1",
SimpleNamespace(id="u1"),
"openai",
)
assert "credentials" in result
@@ -195,7 +211,7 @@ class TestModelProviderModelCredentialApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserCreateCredential.model_validate(payload), "tenant1", "openai")
assert status == 201
@@ -212,7 +228,13 @@ class TestModelProviderModelCredentialApi:
service.return_value.provider_manager.get_provider_model_available_credentials.return_value = []
lb.return_value.get_load_balancing_configs.return_value = (False, [])
result = method(api, "t1", SimpleNamespace(id="u1"), "openai")
result = method(
api,
ParserGetCredentials(model="gpt", model_type=ModelType.LLM),
"t1",
SimpleNamespace(id="u1"),
"openai",
)
assert result["credentials"] == {}
@@ -230,7 +252,7 @@ class TestModelProviderModelCredentialApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "t1", "openai")
result, status = method(api, ParserDeleteCredential.model_validate(payload), "t1", "openai")
assert status == 204
@@ -250,7 +272,7 @@ class TestModelProviderModelCredentialSwitchApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserSwitch.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -269,7 +291,7 @@ class TestModelEnableDisableApis:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -286,7 +308,7 @@ class TestModelEnableDisableApis:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -306,7 +328,7 @@ class TestModelProviderModelValidateApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -327,7 +349,7 @@ class TestModelProviderModelValidateApi:
):
service_mock.return_value.validate_model_credentials.side_effect = CredentialsValidateFailedError("invalid")
result = method(api, "tenant1", "openai")
result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai")
assert result["result"] == "error"
@@ -343,7 +365,7 @@ class TestParameterAndAvailableModels:
):
service_mock.return_value.get_model_parameter_rules.return_value = []
result = method(api, "tenant1", "openai")
result = method(api, ParserParameter(model="gpt-4"), "tenant1", "openai")
assert "data" in result
@@ -371,7 +393,7 @@ class TestParameterAndAvailableModels:
):
service.return_value.get_model_parameter_rules.return_value = []
result = method(api, "t1", "openai")
result = method(api, ParserParameter(model="gpt"), "t1", "openai")
assert result["data"] == []

View File

@@ -26,6 +26,7 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.console.workspace import rbac as rbac_mod
from controllers.console.workspace.rbac import _RolesListQuery
@pytest.fixture
@@ -175,7 +176,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"page": 1, "limit": 2, "include_owner": 1}),
)
owner_permission_keys = rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"]
valid_owner_permission_keys = []
@@ -230,7 +234,7 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery())
names = [r["name"] for r in response["data"]]
assert "owner" not in names
@@ -242,7 +246,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"include_owner": 1}),
)
names = [r["name"] for r in response["data"]]
assert "owner" in names
@@ -254,7 +261,7 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery())
names = [r["name"] for r in response["data"]]
assert "owner" not in names
@@ -267,7 +274,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
):
inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"page": 2, "limit": 50, "reverse": True, "include_owner": 1}),
)
_, kwargs = mock_list.call_args
options = kwargs["options"]

View File

@@ -15,15 +15,19 @@ from controllers.console.workspace.trigger_providers import (
TriggerOAuthAuthorizeApi,
TriggerOAuthCallbackApi,
TriggerOAuthClientManageApi,
TriggerOAuthClientPayload,
TriggerProviderIconApi,
TriggerProviderInfoApi,
TriggerProviderListApi,
TriggerSubscriptionBuilderBuildApi,
TriggerSubscriptionBuilderCreateApi,
TriggerSubscriptionBuilderCreatePayload,
TriggerSubscriptionBuilderGetApi,
TriggerSubscriptionBuilderLogsApi,
TriggerSubscriptionBuilderUpdateApi,
TriggerSubscriptionBuilderUpdatePayload,
TriggerSubscriptionBuilderVerifyApi,
TriggerSubscriptionBuilderVerifyPayload,
TriggerSubscriptionListApi,
TriggerSubscriptionUpdateApi,
TriggerSubscriptionVerifyApi,
@@ -163,7 +167,13 @@ class TestTriggerSubscriptionBuilderApis:
return_value=subscription_builder(),
),
):
result = method(api, "t1", mock_user(), "github")
result = method(
api,
TriggerSubscriptionBuilderCreatePayload(credential_type="UNAUTHORIZED"),
"t1",
mock_user(),
"github",
)
assert result["subscription_builder"]["id"] == "b1"
def test_get_builder(self, app: Flask) -> None:
@@ -196,7 +206,14 @@ class TestTriggerSubscriptionBuilderApis:
return_value={"verified": True},
),
):
assert method(api, "t1", mock_user(), "github", "b1") == {"verified": True}
assert method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={"a": 1}),
"t1",
mock_user(),
"github",
"b1",
) == {"verified": True}
def test_verify_builder_error(self, app: Flask) -> None:
api = TriggerSubscriptionBuilderVerifyApi()
@@ -210,7 +227,14 @@ class TestTriggerSubscriptionBuilderApis:
),
):
with pytest.raises(ValueError):
method(api, "t1", mock_user(), "github", "b1")
method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"b1",
)
def test_update_builder(self, app: Flask) -> None:
api = TriggerSubscriptionBuilderUpdateApi()
@@ -223,7 +247,17 @@ class TestTriggerSubscriptionBuilderApis:
return_value=subscription_builder(),
) as mock_update_builder,
):
assert method(api, "t1", mock_user(), "github", "b1")["id"] == "b1"
assert (
method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="n"),
"t1",
mock_user(),
"github",
"b1",
)["id"]
== "b1"
)
mock_update_builder.assert_called_once_with(
tenant_id="t1",
user_id="u1",
@@ -263,7 +297,14 @@ class TestTriggerSubscriptionBuilderApis:
return_value=None,
),
):
assert method(api, "t1", mock_user(), "github", "b1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="x"),
"t1",
mock_user(),
"github",
"b1",
) == {"result": "success"}
class TestTriggerSubscriptionCrud:
@@ -283,7 +324,12 @@ class TestTriggerSubscriptionCrud:
),
patch("controllers.console.workspace.trigger_providers.TriggerProviderService.update_trigger_subscription"),
):
assert method(api, "t1", "s1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="x"),
"t1",
"s1",
) == {"result": "success"}
def test_update_not_found(self, app: Flask) -> None:
api = TriggerSubscriptionUpdateApi()
@@ -297,7 +343,7 @@ class TestTriggerSubscriptionCrud:
),
):
with pytest.raises(NotFoundError):
method(api, "t1", "x")
method(api, TriggerSubscriptionBuilderUpdatePayload(name="x"), "t1", "x")
def test_update_rebuild(self, app: Flask) -> None:
api = TriggerSubscriptionUpdateApi()
@@ -319,7 +365,12 @@ class TestTriggerSubscriptionCrud:
"controllers.console.workspace.trigger_providers.TriggerProviderService.rebuild_trigger_subscription"
),
):
assert method(api, "t1", "s1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(credentials={}),
"t1",
"s1",
) == {"result": "success"}
class TestTriggerOAuthApis:
@@ -499,7 +550,12 @@ class TestTriggerOAuthClientManageApi:
return_value={"result": "success"},
),
):
assert method(api, "t1", "github") == {"result": "success"}
assert method(
api,
TriggerOAuthClientPayload(enabled=True),
"t1",
"github",
) == {"result": "success"}
def test_delete_client(self, app: Flask) -> None:
api = TriggerOAuthClientManageApi()
@@ -526,7 +582,7 @@ class TestTriggerOAuthClientManageApi:
),
):
with pytest.raises(BadRequest):
method(api, "t1", "github")
method(api, TriggerOAuthClientPayload(enabled=True), "t1", "github")
class TestTriggerSubscriptionVerifyApi:
@@ -541,7 +597,14 @@ class TestTriggerSubscriptionVerifyApi:
return_value={"verified": True},
),
):
assert method(api, "t1", mock_user(), "github", "s1") == {"verified": True}
assert method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"s1",
) == {"verified": True}
@pytest.mark.parametrize("raised_exception", [ValueError("bad"), Exception("boom")])
def test_verify_errors(self, app: Flask, raised_exception: Exception) -> None:
@@ -556,4 +619,11 @@ class TestTriggerSubscriptionVerifyApi:
),
):
with pytest.raises(BadRequest):
method(api, "t1", mock_user(), "github", "s1")
method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"s1",
)

View File

@@ -2,6 +2,7 @@ import pytest
from core.workflow.file_reference import build_file_reference
from models.agent_config_entities import (
AgentSoulModelSettings,
DeclaredArrayItem,
DeclaredOutputChildConfig,
DeclaredOutputConfig,
@@ -9,6 +10,22 @@ from models.agent_config_entities import (
)
def test_agent_soul_model_settings_preserves_plugin_declared_parameters() -> None:
settings = AgentSoulModelSettings.model_validate(
{
"temperature": 0.7,
"enable_thinking": True,
"thinking_budget": 4096,
}
)
dumped = settings.model_dump(mode="json", exclude_none=True)
assert dumped["temperature"] == 0.7
assert dumped["enable_thinking"] is True
assert dumped["thinking_budget"] == 4096
def test_file_default_value_accepts_canonical_reference_mapping() -> None:
reference = build_file_reference(record_id="tool-file-1")

View File

@@ -1586,6 +1586,7 @@ export type AgentSoulModelSettings = {
stop?: Array<string> | null
temperature?: number | null
top_p?: number | null
[key: string]: unknown
}
export type AgentSandboxProviderConfig = {

View File

@@ -2037,6 +2037,13 @@ export const zAgentModelResponseFormatConfig = z.object({
/**
* AgentSoulModelSettings
*
* Model parameters for the Agent Soul model.
*
* Model plugins can declare arbitrary parameters via ``parameter_rules``
* (e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
* fields typed below, so extra keys must round-trip through persistence
* rather than being dropped.
*/
export const zAgentSoulModelSettings = z.object({
frequency_penalty: z.number().nullish(),

View File

@@ -2846,6 +2846,7 @@ export type AgentSoulModelSettings = {
stop?: Array<string> | null
temperature?: number | null
top_p?: number | null
[key: string]: unknown
}
export type AgentSandboxProviderConfig = {

View File

@@ -3643,6 +3643,13 @@ export const zAgentModelResponseFormatConfig = z.object({
/**
* AgentSoulModelSettings
*
* Model parameters for the Agent Soul model.
*
* Model plugins can declare arbitrary parameters via ``parameter_rules``
* (e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
* fields typed below, so extra keys must round-trip through persistence
* rather than being dropped.
*/
export const zAgentSoulModelSettings = z.object({
frequency_penalty: z.number().nullish(),

View File

@@ -924,6 +924,7 @@ export type AgentSoulModelSettings = {
stop?: Array<string> | null
temperature?: number | null
top_p?: number | null
[key: string]: unknown
}
export type AgentSandboxProviderConfig = {

View File

@@ -1155,6 +1155,13 @@ export const zAgentModelResponseFormatConfig = z.object({
/**
* AgentSoulModelSettings
*
* Model parameters for the Agent Soul model.
*
* Model plugins can declare arbitrary parameters via ``parameter_rules``
* (e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
* fields typed below, so extra keys must round-trip through persistence
* rather than being dropped.
*/
export const zAgentSoulModelSettings = z.object({
frequency_penalty: z.number().nullish(),

View File

@@ -22,15 +22,6 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({
default: () => mockUseNodes(),
}))
vi.mock('@/app/components/base/action-button', () => ({
__esModule: true,
default: ({ children, onClick }: { children?: ReactNode; onClick?: () => void }) => (
<button type="button" aria-label="close-preview" onClick={onClick}>
{children}
</button>
),
}))
vi.mock('@/app/components/base/badge', () => ({
__esModule: true,
default: ({ children }: { children?: ReactNode }) => <div data-testid="badge">{children}</div>,
@@ -140,7 +131,7 @@ describe('FormContentPreview', () => {
<FormContentPreview content="content" formInputs={[]} userActions={[]} onClose={onClose} />,
)
fireEvent.click(screen.getByRole('button', { name: 'close-preview' }))
fireEvent.click(screen.getByRole('button', { name: 'operation.close' }))
expect(onClose).toHaveBeenCalledTimes(1)
})

View File

@@ -1,5 +1,4 @@
'use client'
import type { FC } from 'react'
import type { FormInputItem, UserAction } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import * as React from 'react'
@@ -22,12 +21,12 @@ type FormContentPreviewProps = {
onClose: () => void
}
const FormContentPreview: FC<FormContentPreviewProps> = ({
const FormContentPreview = ({
content,
formInputs,
userActions,
onClose,
}) => {
}: FormContentPreviewProps) => {
const { t } = useTranslation()
const panelWidth = useStore((state) => state.panelWidth)
const nodes = useNodes()
@@ -69,8 +68,11 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
<Badge uppercase className="border-text-accent-secondary text-text-accent-secondary">
{t(($) => $[`${i18nPrefix}.formContent.preview`], { ns: 'workflow' })}
</Badge>
<ActionButton onClick={onClose}>
<span className="i-ri-close-line size-5 text-text-tertiary" />
<ActionButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onClose}
>
<span aria-hidden className="i-ri-close-line size-5 text-text-tertiary" />
</ActionButton>
</div>
<div className="max-h-[calc(100vh-167px)] overflow-y-auto px-4">