diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index d35a5ae178c..58861445cae 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -21,6 +21,7 @@ env: DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DIFY_WEB_IMAGE_NAME: ${{ vars.DIFY_WEB_IMAGE_NAME || 'langgenius/dify-web' }} DIFY_API_IMAGE_NAME: ${{ vars.DIFY_API_IMAGE_NAME || 'langgenius/dify-api' }} + DIFY_AGENT_IMAGE_NAME: ${{ vars.DIFY_AGENT_IMAGE_NAME || 'langgenius/dify-agent-backend' }} jobs: build: @@ -60,6 +61,20 @@ jobs: file: "web/Dockerfile" platform: linux/arm64 runs_on: depot-ubuntu-24.04-4 + - service_name: "build-agent-amd64" + image_name_env: "DIFY_AGENT_IMAGE_NAME" + artifact_context: "agent" + build_context: "{{defaultContext}}" + file: "dify-agent/Dockerfile" + platform: linux/amd64 + runs_on: depot-ubuntu-24.04-4 + - service_name: "build-agent-arm64" + image_name_env: "DIFY_AGENT_IMAGE_NAME" + artifact_context: "agent" + build_context: "{{defaultContext}}" + file: "dify-agent/Dockerfile" + platform: linux/arm64 + runs_on: depot-ubuntu-24.04-4 steps: - name: Prepare @@ -122,6 +137,9 @@ jobs: - service_name: "validate-web-amd64" build_context: "{{defaultContext}}" file: "web/Dockerfile" + - service_name: "validate-agent-amd64" + build_context: "{{defaultContext}}" + file: "dify-agent/Dockerfile" steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -147,6 +165,9 @@ jobs: - service_name: "merge-web-images" image_name_env: "DIFY_WEB_IMAGE_NAME" context: "web" + - service_name: "merge-agent-images" + image_name_env: "DIFY_AGENT_IMAGE_NAME" + context: "agent" steps: - name: Download digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/api/controllers/common/controller_schemas.py b/api/controllers/common/controller_schemas.py index 8eeed8f0a0a..b47a1a013a0 100644 --- a/api/controllers/common/controller_schemas.py +++ b/api/controllers/common/controller_schemas.py @@ -1,7 +1,8 @@ -from typing import Any, Literal +from copy import deepcopy +from typing import Any, Literal, override from uuid import UUID -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, GetJsonSchemaHandler, model_validator from libs.helper import UUIDStrOrEmpty @@ -12,6 +13,45 @@ class ConversationRenamePayload(BaseModel): name: str | None = None auto_generate: bool = False + @classmethod + @override + def __get_pydantic_json_schema__(cls, core_schema: Any, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler.resolve_ref_schema(handler(core_schema)) + properties = schema.get("properties") + if not isinstance(properties, dict): + return schema + + auto_generate_schema = deepcopy(properties.get("auto_generate", {"type": "boolean"})) + name_schema = deepcopy(properties.get("name", {"type": "string"})) + non_blank_name_schema: dict[str, Any] = {"pattern": r".*\S.*", "type": "string"} + if isinstance(name_schema, dict) and isinstance(name_schema.get("title"), str): + non_blank_name_schema["title"] = name_schema["title"] + + auto_generate_true_schema = {**auto_generate_schema, "enum": [True]} + auto_generate_true_schema.pop("default", None) + + return { + **schema, + "anyOf": [ + { + "properties": { + "auto_generate": auto_generate_true_schema, + "name": name_schema, + }, + "required": ["auto_generate"], + "type": "object", + }, + { + "properties": { + "auto_generate": {**auto_generate_schema, "enum": [False]}, + "name": non_blank_name_schema, + }, + "required": ["name"], + "type": "object", + }, + ], + } + @model_validator(mode="after") def validate_name_requirement(self): if not self.auto_generate: @@ -101,4 +141,7 @@ class TextToAudioPayload(BaseModel): message_id: str | None = Field(default=None, description="Message ID") voice: str | None = Field(default=None, description="Voice to use for TTS") text: str | None = Field(default=None, description="Text to convert to audio") - streaming: bool | None = Field(default=None, description="Enable streaming response") + streaming: bool | None = Field( + default=None, + description="Reserved for compatibility; TTS response streaming is determined by the provider output.", + ) diff --git a/api/controllers/common/human_input.py b/api/controllers/common/human_input.py index d9b8f8f9a37..4b2e70e2d03 100644 --- a/api/controllers/common/human_input.py +++ b/api/controllers/common/human_input.py @@ -42,10 +42,11 @@ def stringify_form_default_values(values: dict[str, object]) -> dict[str, str]: """Serialize default values into strings expected by human-input form clients.""" result: dict[str, str] = {} for key, value in values.items(): - if value is None: - result[key] = "" - elif isinstance(value, (dict, list)): - result[key] = json.dumps(value, ensure_ascii=False) - else: - result[key] = str(value) + match value: + case None: + result[key] = "" + case dict() | list(): + result[key] = json.dumps(value, ensure_ascii=False) + case _: + result[key] = str(value) return result diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index d7935552ac0..c45d18ca73e 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -2,20 +2,28 @@ from uuid import UUID from flask import abort, request from flask_restx import Resource -from pydantic import BaseModel, Field, field_validator +from pydantic import AliasChoices, BaseModel, Field, field_validator 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.agent.app_helpers import resolve_agent_app_model from controllers.console.app.app import ( - AppDetailWithSite, + AppDetailWithSite as GenericAppDetailWithSite, +) +from controllers.console.app.app import ( AppListQuery, - AppPagination, - AppPartial, CopyAppPayload, - UpdateAppPayload, _normalize_app_list_query_args, ) +from controllers.console.app.app import ( + AppPagination as GenericAppPagination, +) +from controllers.console.app.app import ( + AppPartial as GenericAppPartial, +) +from controllers.console.app.app import ( + UpdateAppPayload as GenericUpdateAppPayload, +) from controllers.console.wraps import ( account_initialization_required, cloud_edition_billing_resource_check, @@ -31,6 +39,8 @@ from fields.agent_fields import ( AgentConfigSnapshotListResponse, AgentInviteOptionsResponse, AgentLogListResponse, + AgentLogMessageListResponse, + AgentLogSourceListResponse, AgentPublishedReferenceResponse, AgentRosterListResponse, AgentStatisticSummaryEnvelopeResponse, @@ -64,14 +74,33 @@ class AgentIdPath(BaseModel): class AgentAppCreatePayload(BaseModel): name: str = Field(..., min_length=1, description="Agent name") description: str | None = Field(default=None, description="Agent description (max 400 chars)", max_length=400) - role: str = Field(default="", description="Agent role", max_length=255) + role: str = Field(..., min_length=1, description="Agent role", max_length=255) icon_type: IconType | None = Field(default=None, description="Icon type") icon: str | None = Field(default=None, description="Icon") icon_background: str | None = Field(default=None, description="Icon background color") + @field_validator("role") + @classmethod + def validate_role(cls, value: str) -> str: + role = value.strip() + if not role: + raise ValueError("Agent role is required.") + return role -class AgentAppUpdatePayload(UpdateAppPayload): - role: str | None = Field(default=None, description="Agent role", max_length=255) + +# Keep agent-app roster DTOs agent-specific instead of reusing the shared +# /apps response/request models. The roster surface needs Agent-only fields such +# as `role`, while the generic console/apps contracts must stay unchanged. +class AgentAppUpdatePayload(GenericUpdateAppPayload): + role: str = Field(..., min_length=1, description="Agent role", max_length=255) + + @field_validator("role") + @classmethod + def validate_role(cls, value: str) -> str: + role = value.strip() + if not role: + raise ValueError("Agent role is required.") + return role class AgentAppPublishedReferenceResponse(BaseModel): @@ -82,19 +111,6 @@ class AgentAppPublishedReferenceResponse(BaseModel): app_icon_background: str | None = None -class AgentAppPartial(AppPartial): - published_reference_count: int = 0 - published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list) - - -class AgentAppPagination(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[AgentAppPartial] - - class AgentLogsQuery(BaseModel): page: int = Field(default=1, ge=1, description="Page number") limit: int = Field(default=20, ge=1, le=100, description="Page size") @@ -131,6 +147,26 @@ class AgentStatisticsQuery(BaseModel): return value +class AgentAppPartial(GenericAppPartial): + app_id: str | None = None + role: str | None = None + active_config_is_published: bool = False + published_reference_count: int = 0 + published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list) + + +class AgentAppDetailWithSite(GenericAppDetailWithSite): + app_id: str | None = None + role: str | None = None + active_config_is_published: bool = False + + +class AgentAppPagination(GenericAppPagination): + data: list[AgentAppPartial] = Field( # type: ignore[assignment] # pyrefly: ignore[bad-override-mutable-attribute] + validation_alias=AliasChoices("items", "data") + ) + + register_schema_models( console_ns, AgentAppCreatePayload, @@ -141,18 +177,20 @@ register_schema_models( AgentStatisticsQuery, AgentIdPath, AppListQuery, - UpdateAppPayload, RosterListQuery, ) register_response_schema_models( console_ns, - AppDetailWithSite, AgentAppPagination, AgentAppPublishedReferenceResponse, + AgentAppDetailWithSite, + AgentAppPartial, AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, AgentInviteOptionsResponse, AgentLogListResponse, + AgentLogMessageListResponse, + AgentLogSourceListResponse, AgentPublishedReferenceResponse, AgentRosterListResponse, AgentStatisticSummaryEnvelopeResponse, @@ -164,16 +202,25 @@ def _agent_roster_service() -> AgentRosterService: def _serialize_agent_app_detail(app_model) -> dict: + """Serialize an Agent App detail using roster-only DTOs. + + `/agent` responses are roster-shaped rather than raw app-shaped: `id` + becomes the backing roster Agent id, `app_id` carries the underlying App + id, and `role` is injected from the backing roster Agent. Keeping that + remap in this serializer lets generated console/agent contracts expose the + roster persona fields without widening the shared /apps detail schema. + """ + app_model = AppService().get_app(app_model) if FeatureService.get_system_features().webapp_auth.enabled: app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined] roster_service = _agent_roster_service() - agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id) + payload = AgentAppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") + agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id)) if not agent: raise AgentNotFoundError() - payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") payload.pop("bound_agent_id", None) payload["app_id"] = str(app_model.id) payload["id"] = agent.id @@ -186,6 +233,14 @@ def _serialize_agent_app_detail(app_model) -> dict: def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: + """Serialize Agent App lists with roster-shaped items. + + Each item starts from the shared App list shape, then drops + `bound_agent_id`, rewrites `id` to the backing roster Agent id, stores the + original App id in `app_id`, and injects roster-only `role` when a backing + Agent is present. + """ + app_ids = [str(app.id) for app in app_pagination.items] roster_service = _agent_roster_service() agents_by_app_id = roster_service.load_app_backing_agents_by_app_id( @@ -200,7 +255,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: tenant_id=tenant_id, agent_ids=[agent.id for agent in agents_by_app_id.values()], ) - payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json") + payload = AgentAppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json") for item in payload["data"]: app_id = item["id"] item.pop("bound_agent_id", None) @@ -266,7 +321,7 @@ class AgentAppListApi(Resource): status="normal", ) - app_pagination = AppService().get_paginate_apps(current_user.id, current_tenant_id, params) + app_pagination = AppService().get_paginate_apps(current_user.id, current_tenant_id, params, db.session) if app_pagination is None: empty = AgentAppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[]) return empty.model_dump(mode="json") @@ -274,7 +329,7 @@ class AgentAppListApi(Resource): return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id) @console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__]) - @console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(201, "Agent app created successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -302,7 +357,7 @@ class AgentAppListApi(Resource): @console_ns.route("/agent/") class AgentAppApi(Resource): - @console_ns.response(200, "Agent app detail", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(200, "Agent app detail", console_ns.models[AgentAppDetailWithSite.__name__]) @setup_required @login_required @account_initialization_required @@ -313,7 +368,7 @@ class AgentAppApi(Resource): return _serialize_agent_app_detail(app_model) @console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__]) - @console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -353,7 +408,7 @@ class AgentAppApi(Resource): @console_ns.route("/agent//copy") class AgentAppCopyApi(Resource): @console_ns.expect(console_ns.models[CopyAppPayload.__name__]) - @console_ns.response(201, "Agent app copied successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(201, "Agent app copied successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -416,6 +471,7 @@ class AgentLogsApi(Resource): try: payload = _agent_observability_service().list_logs( app=app_model, + agent_id=str(agent_id), params=AgentLogQueryParams( page=query.page, limit=query.limit, @@ -431,6 +487,53 @@ class AgentLogsApi(Resource): return dump_response(AgentLogListResponse, payload) +@console_ns.route("/agent//logs//messages") +class AgentLogMessagesApi(Resource): + @console_ns.doc(params=query_params_from_model(AgentLogsQuery)) + @console_ns.response(200, "Agent log messages", console_ns.models[AgentLogMessageListResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @with_current_user + @with_current_tenant_id + def get(self, tenant_id: str, current_user: Account, agent_id: UUID, conversation_id: UUID): + app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + query = AgentLogsQuery.model_validate(request.args.to_dict(flat=True)) + start, end = _parse_observability_time_range(query.start, query.end, current_user) + try: + payload = _agent_observability_service().list_log_messages( + app=app_model, + agent_id=str(agent_id), + conversation_id=str(conversation_id), + params=AgentLogQueryParams( + page=query.page, + limit=query.limit, + keyword=query.keyword, + status=query.status, + source=query.source, + start=start, + end=end, + ), + ) + except ValueError as exc: + abort(400, description=str(exc)) + return dump_response(AgentLogMessageListResponse, payload) + + +@console_ns.route("/agent//log-sources") +class AgentLogSourcesApi(Resource): + @console_ns.response(200, "Agent log sources", console_ns.models[AgentLogSourceListResponse.__name__]) + @setup_required + @login_required + @account_initialization_required + @with_current_user + @with_current_tenant_id + def get(self, tenant_id: str, current_user: Account, agent_id: UUID): + app_model = _resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) + payload = _agent_observability_service().list_log_sources(app=app_model, agent_id=str(agent_id)) + return dump_response(AgentLogSourceListResponse, payload) + + @console_ns.route("/agent//statistics/summary") class AgentStatisticsSummaryApi(Resource): @console_ns.doc(params=query_params_from_model(AgentStatisticsQuery)) @@ -452,6 +555,7 @@ class AgentStatisticsSummaryApi(Resource): try: payload = _agent_observability_service().get_statistics_summary( app=app_model, + agent_id=str(agent_id), params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone), ) except ValueError as exc: diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 23ccd28ad6f..6731be67831 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -30,7 +30,7 @@ from models import Account from models.agent_config_entities import AgentFileRefConfig, AgentSkillRefConfig from models.model import App, AppMode, UploadFile from services.agent.composer_service import AgentComposerService -from services.agent.skill_package_service import SkillManifest, SkillPackageError, SkillPackageService +from services.agent.skill_package_service import SkillManifest, SkillPackageError from services.agent.skill_standardize_service import SkillStandardizeService from services.agent.skill_tool_inference_service import ( SkillToolInferenceError, @@ -45,11 +45,18 @@ from services.agent_drive_service import ( normalize_drive_key, ) from services.agent_service import AgentService -from services.file_service import FileService logger = logging.getLogger(__name__) _WORKFLOW_AGENT_DRIVE_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT] +_AGENT_SKILL_UPLOAD_PARAMS = { + "file": { + "in": "formData", + "type": "file", + "required": True, + "description": "Skill package (.zip or .skill).", + } +} class AgentLogQuery(BaseModel): @@ -125,11 +132,6 @@ class AgentSkillUploadResponse(ResponseModel): manifest: SkillManifest -class AgentSkillStandardizeResponse(ResponseModel): - skill: AgentSkillRefConfig - manifest: SkillManifest - - class AgentDriveFileResponse(ResponseModel): name: str drive_key: str @@ -156,7 +158,6 @@ register_response_schema_models( AgentDriveFileCommitResponse, AgentDriveFileResponse, AgentLogResponse, - AgentSkillStandardizeResponse, AgentSkillUploadResponse, SkillToolInferenceResult, ) @@ -174,30 +175,9 @@ def _agent_not_bound() -> tuple[dict[str, str], int]: return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400 -def _upload_skill_for_app(*, current_user: Account): - if "file" not in request.files: - return {"code": "no_file", "message": "no skill file uploaded"}, 400 - if len(request.files) > 1: - return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400 +def _upload_skill_for_app(*, current_user: Account, app_model: App): + """Upload one skill package and commit its normalized files into the agent drive.""" - upload = request.files["file"] - content = upload.stream.read() - try: - manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "") - except SkillPackageError as exc: - return {"code": exc.code, "message": exc.message}, exc.status_code - - upload_file = FileService(db.engine).upload_file( - filename=upload.filename or "skill.zip", - content=content, - mimetype=upload.mimetype or "application/zip", - user=current_user, - ) - skill_ref = manifest.to_skill_ref(file_id=upload_file.id) - return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201 - - -def _standardize_skill_for_app(*, current_user: Account, app_model: App): query = query_params_from_request(AgentDriveMutationQuery) agent_id = _resolve_agent_id(app_model, query.node_id) if not agent_id: @@ -382,51 +362,9 @@ class AgentLogApi(Resource): @console_ns.route("/agent//skills/upload") class AgentSkillUploadByAgentApi(Resource): @console_ns.doc("upload_agent_skill_by_agent") - @console_ns.doc(description="Upload + validate a Skill package for an Agent App") - @console_ns.doc(params={"agent_id": "Agent ID"}) - @console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__]) - @console_ns.response(400, "Invalid skill package") - @setup_required - @login_required - @account_initialization_required - @with_current_user - @with_current_tenant_id - def post(self, tenant_id: str, current_user: Account, agent_id: UUID): - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) - return _upload_skill_for_app(current_user=current_user) - - -@console_ns.route("/apps//agent/skills/upload") -class AgentSkillUploadApi(Resource): - @console_ns.doc("upload_agent_skill") - @console_ns.doc(description="Upload + validate a Skill package (.zip/.skill) and extract its manifest") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__]) - @console_ns.response(400, "Invalid skill package") - @setup_required - @login_required - @account_initialization_required - @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES) - @with_current_user - def post(self, current_user: Account, app_model: App): - """Validate an uploaded Skill package and persist the archive. - - Returns a validated skill ref (to bind into the Agent soul config on save) - plus its manifest. Standardizing into the agent drive is ENG-594. - """ - return _upload_skill_for_app(current_user=current_user) - - -@console_ns.route("/agent//skills/standardize") -class AgentSkillStandardizeByAgentApi(Resource): - @console_ns.doc("standardize_agent_skill_by_agent") - @console_ns.doc(description="Validate + standardize a Skill into an Agent App drive") - @console_ns.doc(params={"agent_id": "Agent ID"}) - @console_ns.response( - 201, - "Skill standardized into drive", - console_ns.models[AgentSkillStandardizeResponse.__name__], - ) + @console_ns.doc(description="Upload + standardize a Skill into an Agent App drive") + @console_ns.doc(consumes=["multipart/form-data"], params={"agent_id": "Agent ID", **_AGENT_SKILL_UPLOAD_PARAMS}) + @console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__]) @console_ns.response(400, "Invalid skill package or no bound agent") @setup_required @login_required @@ -435,19 +373,22 @@ class AgentSkillStandardizeByAgentApi(Resource): @with_current_tenant_id def post(self, tenant_id: str, current_user: Account, agent_id: UUID): app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) - return _standardize_skill_for_app(current_user=current_user, app_model=app_model) + return _upload_skill_for_app(current_user=current_user, app_model=app_model) -@console_ns.route("/apps//agent/skills/standardize") -class AgentSkillStandardizeApi(Resource): - @console_ns.doc("standardize_agent_skill") - @console_ns.doc(description="Validate + standardize a Skill into the agent drive (ENG-594)") - @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveMutationQuery)}) - @console_ns.response( - 201, - "Skill standardized into drive", - console_ns.models[AgentSkillStandardizeResponse.__name__], +@console_ns.route("/apps//agent/skills/upload") +class AgentSkillUploadApi(Resource): + @console_ns.doc("upload_agent_skill") + @console_ns.doc(description="Upload + standardize a Skill into the agent drive") + @console_ns.doc( + consumes=["multipart/form-data"], + params={ + "app_id": "Application ID", + **query_params_from_model(AgentDriveMutationQuery), + **_AGENT_SKILL_UPLOAD_PARAMS, + }, ) + @console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__]) @console_ns.response(400, "Invalid skill package or no bound agent") @setup_required @login_required @@ -455,8 +396,8 @@ class AgentSkillStandardizeApi(Resource): @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES) @with_current_user def post(self, current_user: Account, app_model: App): - """Upload a Skill, validate it, and standardize it into the app agent's drive.""" - return _standardize_skill_for_app(current_user=current_user, app_model=app_model) + """Upload a Skill, validate it, and commit drive-backed skill files.""" + return _upload_skill_for_app(current_user=current_user, app_model=app_model) @console_ns.route("/agent//files") diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 2fb3a402962..0e897ac44de 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -402,8 +402,6 @@ class AppPartial(ResponseModel): bound_agent_id: str | None = None # For Agent App responses exposed through /agent. app_id: str | None = None - role: str | None = None - active_config_is_published: bool = False is_starred: bool = False @computed_field(return_type=str | None) # type: ignore @@ -457,8 +455,6 @@ class AppDetailWithSite(AppDetail): bound_agent_id: str | None = None # For Agent App responses exposed through /agent. app_id: str | None = None - role: str | None = None - active_config_is_published: bool = False @computed_field(return_type=str | None) # type: ignore @property @@ -541,10 +537,7 @@ register_schema_models( ModelConfig, Site, DeletedTool, - AppPartial, AppDetail, - AppDetailWithSite, - AppPagination, AppExportResponse, Segmentation, PreProcessingRule, @@ -564,6 +557,13 @@ register_schema_models( LoadBalancingPayload, ) +register_response_schema_models( + console_ns, + AppPartial, + AppDetailWithSite, + AppPagination, +) + @console_ns.route("/apps") class AppListApi(Resource): @@ -594,7 +594,7 @@ class AppListApi(Resource): # get app list app_service = AppService() - app_pagination = app_service.get_paginate_apps(current_user_id, current_tenant_id, params) + app_pagination = app_service.get_paginate_apps(current_user_id, current_tenant_id, params, db.session) if not app_pagination: empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[]) return empty.model_dump(mode="json"), 200 @@ -661,7 +661,7 @@ class StarredAppListApi(Resource): is_created_by_me=args.is_created_by_me, ) - app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params) + app_pagination = AppService().get_paginate_starred_apps(current_user_id, current_tenant_id, params, db.session) if not app_pagination: empty = AppPagination(page=args.page, limit=args.limit, total=0, has_more=False, data=[]) return empty.model_dump(mode="json"), 200 diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 7e7810d86da..f61bb8f6802 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -1,6 +1,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator +from sqlalchemy import select from configs import dify_config from constants.languages import supported_language @@ -11,7 +12,8 @@ from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from libs.helper import EmailStr, timezone from models import AccountStatus -from services.account_service import RegisterService +from models.account import TenantAccountJoin, TenantAccountRole +from services.account_service import RegisterService, TenantService from services.billing_service import BillingService @@ -25,18 +27,22 @@ class ActivatePayload(BaseModel): workspace_id: str | None = Field(default=None) email: EmailStr | None = Field(default=None) token: str - name: str = Field(..., max_length=30) - interface_language: str = Field(...) - timezone: str = Field(...) + name: str | None = Field(default=None, max_length=30) + interface_language: str | None = Field(default=None) + timezone: str | None = Field(default=None) @field_validator("interface_language") @classmethod - def validate_lang(cls, value: str) -> str: + def validate_lang(cls, value: str | None) -> str | None: + if value is None: + return None return supported_language(value) @field_validator("timezone") @classmethod - def validate_tz(cls, value: str) -> str: + def validate_tz(cls, value: str | None) -> str | None: + if value is None: + return None return timezone(value) @@ -48,6 +54,8 @@ class ActivationCheckData(BaseModel): workspace_name: str | None workspace_id: str | None email: str | None + account_status: str | None = None + requires_setup: bool | None = None class ActivationCheckResponse(BaseModel): @@ -95,9 +103,20 @@ class ActivateCheckApi(Resource): workspace_name = tenant.name if tenant else None workspace_id = tenant.id if tenant else None invitee_email = data.get("email") if data else None + account = invitation.get("account") + account_status = account.status if account else None + requires_setup = data.get("requires_setup") + if requires_setup is None: + requires_setup = account_status == AccountStatus.PENDING return { "is_valid": invitation is not None, - "data": {"workspace_name": workspace_name, "workspace_id": workspace_id, "email": invitee_email}, + "data": { + "workspace_name": workspace_name, + "workspace_id": workspace_id, + "email": invitee_email, + "account_status": account_status, + "requires_setup": requires_setup, + }, } else: return {"is_valid": False} @@ -126,15 +145,45 @@ class ActivateApi(Resource): if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(account.email): raise AccountInFreezeError() + tenant = invitation["tenant"] + raw_role = invitation["data"].get("role") + try: + role = TenantAccountRole(raw_role) if raw_role else TenantAccountRole.NORMAL + except ValueError: + role = TenantAccountRole.NORMAL + if not TenantAccountRole.is_non_owner_role(role): + role = TenantAccountRole.NORMAL + + membership_id = db.session.scalar( + select(TenantAccountJoin.id).where( + TenantAccountJoin.tenant_id == tenant.id, + TenantAccountJoin.account_id == account.id, + ) + ) + + requires_setup = invitation["data"].get("requires_setup") + if requires_setup is None: + requires_setup = account.status == AccountStatus.PENDING + + setup_fields: tuple[str, str, str] | None = None + if requires_setup: + if not args.name or not args.interface_language or not args.timezone: + raise AlreadyActivateError() + setup_fields = (args.name, args.interface_language, args.timezone) + RegisterService.revoke_token(args.workspace_id, normalized_request_email, args.token) - account.name = args.name + if membership_id is None: + TenantService.create_tenant_member(tenant, account, str(role)) - account.interface_language = args.interface_language - account.timezone = args.timezone - account.interface_theme = "light" - account.status = AccountStatus.ACTIVE - account.initialized_at = naive_utc_now() - db.session.commit() + if setup_fields: + account.name = setup_fields[0] + account.interface_language = setup_fields[1] + account.timezone = setup_fields[2] + account.interface_theme = "light" + account.status = AccountStatus.ACTIVE + account.initialized_at = naive_utc_now() + + TenantService.switch_tenant(account, tenant.id) return {"result": "success"} diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index b3766d9e080..91e3064266b 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -409,6 +409,7 @@ class DatasetListApi(Resource): datasets, total = DatasetService.get_datasets( query.page, query.limit, + db.session, current_tenant_id, current_user, query.keyword, diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py index 82a713f1c6f..38e7395ccf8 100644 --- a/api/controllers/console/tag/tags.py +++ b/api/controllers/console/tag/tags.py @@ -122,7 +122,7 @@ class TagListApi(Resource): raise Forbidden() payload = TagBasePayload.model_validate(console_ns.payload or {}) - tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type)) + tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type), db.session) response = TagResponse.model_validate( {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} @@ -146,9 +146,9 @@ class TagUpdateDeleteApi(Resource): raise Forbidden() payload = TagUpdateRequestPayload.model_validate(console_ns.payload or {}) - tag = TagService.update_tags(UpdateTagPayload(name=payload.name), tag_id_str) + tag = TagService.update_tags(UpdateTagPayload(name=payload.name), tag_id_str, db.session) - binding_count = TagService.get_tag_binding_count(tag_id_str) + binding_count = TagService.get_tag_binding_count(tag_id_str, db.session) response = TagResponse.model_validate( {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": binding_count} @@ -164,7 +164,7 @@ class TagUpdateDeleteApi(Resource): def delete(self, tag_id: UUID): tag_id_str = str(tag_id) - TagService.delete_tag(tag_id_str) + TagService.delete_tag(tag_id_str, db.session) return "", 204 @@ -189,7 +189,8 @@ def _create_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]: tag_ids=payload.tag_ids, target_id=payload.target_id, type=payload.type, - ) + ), + db.session, ) return {"result": "success"}, 200 @@ -203,7 +204,8 @@ def _remove_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]: tag_ids=payload.tag_ids, target_id=payload.target_id, type=payload.type, - ) + ), + db.session, ) return {"result": "success"}, 200 diff --git a/api/controllers/console/workspace/members.py b/api/controllers/console/workspace/members.py index 6fea5417152..59fd3e2c5b5 100644 --- a/api/controllers/console/workspace/members.py +++ b/api/controllers/console/workspace/members.py @@ -232,7 +232,11 @@ class MemberInviteEmailApi(Resource): ) except AccountAlreadyInTenantError: invitation_results.append( - {"status": "success", "email": invitee_email, "url": f"{console_web_url}/signin"} + { + "status": "already_member", + "email": invitee_email, + "message": "Account already in workspace.", + } ) except Exception as e: invitation_results.append({"status": "failed", "email": invitee_email, "message": str(e)}) diff --git a/api/controllers/console/workspace/snippets.py b/api/controllers/console/workspace/snippets.py index 4bd493d25e9..7fcca1f79e8 100644 --- a/api/controllers/console/workspace/snippets.py +++ b/api/controllers/console/workspace/snippets.py @@ -126,6 +126,7 @@ class CustomizedSnippetsApi(Resource): snippet_service = _snippet_service() snippets, total, has_more = snippet_service.get_snippets( tenant_id=current_tenant_id, + session=db.session, page=query.page, limit=query.limit, keyword=query.keyword, diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 84b1610d5f5..c4796313c0b 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -174,7 +174,7 @@ class AppListApi(Resource): tag_ids: list[str] | None = None if query.tag: - tags = TagService.get_tag_by_tag_name("app", workspace_id, query.tag) + tags = TagService.get_tag_by_tag_name("app", workspace_id, query.tag, db.session) if not tags: return empty tag_ids = [tag.id for tag in tags] @@ -191,7 +191,7 @@ class AppListApi(Resource): openapi_visible=True, ) - pagination = AppService().get_paginate_apps(str(auth_data.account_id), workspace_id, params) + pagination = AppService().get_paginate_apps(str(auth_data.account_id), workspace_id, params, db.session) if pagination is None: return empty diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index e99018a985d..d2522c4a41b 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -45,6 +45,13 @@ class AnnotationJobStatusResponse(ResponseModel): error_msg: str | None = None +ANNOTATION_REPLY_ACTION_PARAM = { + "description": "Action to perform: 'enable' or 'disable'", + "enum": ["enable", "disable"], + "type": "string", +} + + register_schema_models( service_api_ns, AnnotationCreatePayload, @@ -58,10 +65,22 @@ register_response_schema_models(service_api_ns, AnnotationJobStatusResponse) @service_api_ns.route("/apps/annotation-reply/") class AnnotationReplyActionApi(Resource): + @service_api_ns.doc( + summary="Configure Annotation Reply", + description=( + "Enables or disables the annotation reply feature. Requires embedding model configuration " + "when enabling. Executes asynchronously — use [Get Annotation Reply Job " + "Status](/api-reference/annotations/get-annotation-reply-job-status) to track progress." + ), + tags=["Annotations"], + responses={ + 200: "Annotation reply settings task initiated.", + }, + ) @service_api_ns.expect(service_api_ns.models[AnnotationReplyActionPayload.__name__]) @service_api_ns.doc("annotation_reply_action") @service_api_ns.doc(description="Enable or disable annotation reply feature") - @service_api_ns.doc(params={"action": "Action to perform: 'enable' or 'disable'"}) + @service_api_ns.doc(params={"action": ANNOTATION_REPLY_ACTION_PARAM}) @service_api_ns.doc( responses={ 200: "Action completed successfully", @@ -92,6 +111,18 @@ class AnnotationReplyActionApi(Resource): @service_api_ns.route("/apps/annotation-reply//status/") class AnnotationReplyActionStatusApi(Resource): + @service_api_ns.doc( + summary="Get Annotation Reply Job Status", + description=( + "Retrieves the status of an asynchronous annotation reply configuration job started by " + "[Configure Annotation Reply](/api-reference/annotations/configure-annotation-reply)." + ), + tags=["Annotations"], + responses={ + 200: "Successfully retrieved task status.", + 400: "`invalid_param` : The specified job does not exist.", + }, + ) @service_api_ns.doc("get_annotation_reply_action_status") @service_api_ns.doc(description="Get the status of an annotation reply action job") @service_api_ns.doc(params={"action": "Action type", "job_id": "Job ID"}) @@ -127,6 +158,14 @@ class AnnotationReplyActionStatusApi(Resource): @service_api_ns.route("/apps/annotations") class AnnotationListApi(Resource): + @service_api_ns.doc( + summary="List Annotations", + description="Retrieves a paginated list of annotations for the application. Supports keyword search filtering.", + tags=["Annotations"], + responses={ + 200: "Successfully retrieved annotation list.", + }, + ) @service_api_ns.doc("list_annotations") @service_api_ns.doc(description="List annotations for the application") @service_api_ns.doc(params=query_params_from_model(AnnotationListQuery)) @@ -159,6 +198,17 @@ class AnnotationListApi(Resource): ) return response.model_dump(mode="json") + @service_api_ns.doc( + summary="Create Annotation", + description=( + "Creates a new annotation. Annotations provide predefined question-answer pairs that the app " + "can match and return directly instead of generating a response." + ), + tags=["Annotations"], + responses={ + 201: "Annotation created successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[AnnotationCreatePayload.__name__]) @service_api_ns.doc("create_annotation") @service_api_ns.doc(description="Create a new annotation") @@ -185,6 +235,16 @@ class AnnotationListApi(Resource): @service_api_ns.route("/apps/annotations/") class AnnotationUpdateDeleteApi(Resource): + @service_api_ns.doc( + summary="Update Annotation", + description="Updates the question and answer of an existing annotation.", + tags=["Annotations"], + responses={ + 200: "Annotation updated successfully.", + 403: "`forbidden` : Insufficient permissions to edit annotations.", + 404: "`not_found` : Annotation does not exist.", + }, + ) @service_api_ns.expect(service_api_ns.models[AnnotationCreatePayload.__name__]) @service_api_ns.doc("update_annotation") @service_api_ns.doc(description="Update an existing annotation") @@ -212,6 +272,16 @@ class AnnotationUpdateDeleteApi(Resource): response = Annotation.model_validate(annotation, from_attributes=True) return response.model_dump(mode="json") + @service_api_ns.doc( + summary="Delete Annotation", + description="Deletes an annotation and its associated hit history.", + tags=["Annotations"], + responses={ + 204: "Annotation deleted successfully.", + 403: "`forbidden` : Insufficient permissions to edit annotations.", + 404: "`not_found` : Annotation does not exist.", + }, + ) @service_api_ns.doc("delete_annotation") @service_api_ns.doc(description="Delete an annotation") @service_api_ns.doc(params={"annotation_id": "Annotation ID"}) diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index cc55876bd5a..d670c7f5a6f 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -33,6 +33,18 @@ register_response_schema_models(service_api_ns, Parameters, AppMetaResponse, App class AppParameterApi(Resource): """Resource for app variables.""" + @service_api_ns.doc( + summary="Get App Parameters", + description=( + "Retrieve the application's input form configuration, including feature switches, input " + "parameter names, types, and default values." + ), + tags=["Applications"], + responses={ + 200: "Application parameters information.", + 400: "`app_unavailable` : App unavailable or misconfigured.", + }, + ) @service_api_ns.doc("get_app_parameters") @service_api_ns.doc(description="Retrieve application input parameters and configuration") @service_api_ns.doc( @@ -71,6 +83,14 @@ class AppParameterApi(Resource): @service_api_ns.route("/meta") class AppMetaApi(Resource): + @service_api_ns.doc( + summary="Get App Meta", + description="Retrieve metadata about this application, including tool icons and other configuration details.", + tags=["Applications"], + responses={ + 200: "Successfully retrieved application meta information.", + }, + ) @service_api_ns.doc("get_app_meta") @service_api_ns.doc(description="Get application metadata") @service_api_ns.doc( @@ -92,6 +112,14 @@ class AppMetaApi(Resource): @service_api_ns.route("/info") class AppInfoApi(Resource): + @service_api_ns.doc( + summary="Get App Info", + description="Retrieve basic information about this application, including name, description, tags, and mode.", + tags=["Applications"], + responses={ + 200: "Basic information of the application.", + }, + ) @service_api_ns.doc("get_app_info") @service_api_ns.doc(description="Get basic application information") @service_api_ns.doc( diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index 0c2047a824e..4bff8e66150 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -20,6 +20,7 @@ from controllers.service_api.app.error import ( ProviderQuotaExceededError, UnsupportedAudioTypeError, ) +from controllers.service_api.schema import binary_response, expect_with_user, multipart_file_params from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from graphon.model_runtime.errors.invoke import InvokeError @@ -39,8 +40,31 @@ register_response_schema_models(service_api_ns, AudioBinaryResponse, AudioTransc @service_api_ns.route("/audio-to-text") class AudioApi(Resource): + @service_api_ns.doc( + summary="Convert Audio to Text", + description=( + "Convert audio file to text. Supported MIME types: `audio/mp3`, `audio/mpga`, `audio/m4a`, " + "`audio/wav`, and `audio/amr`. File size limit is `30 MB`." + ), + tags=["TTS"], + responses={ + 200: "Successfully converted audio to text.", + 400: ( + "- `app_unavailable` : App unavailable or misconfigured.\n" + "- `provider_not_support_speech_to_text` : Model provider does not support speech-to-text.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model does not support this operation.\n" + "- `completion_request_error` : Speech recognition request failed." + ), + 413: "`audio_too_large` : Audio file size exceeded the limit.", + 415: "`unsupported_audio_type` : Audio type is not allowed.", + 500: "`internal_server_error` : Internal server error.", + }, + ) @service_api_ns.doc("audio_to_text") @service_api_ns.doc(description="Convert audio to text using speech-to-text") + @service_api_ns.doc(consumes=["multipart/form-data"], params=multipart_file_params(include_user=True)) @service_api_ns.doc( responses={ 200: "Audio successfully transcribed", @@ -99,7 +123,27 @@ register_schema_model(service_api_ns, TextToAudioPayload) @service_api_ns.route("/text-to-audio") class TextApi(Resource): - @service_api_ns.expect(service_api_ns.models[TextToAudioPayload.__name__]) + @service_api_ns.doc( + summary="Convert Text to Audio", + description="Convert text to speech.", + tags=["TTS"], + responses={ + 200: ( + "Returns the generated audio. Generator responses are streamed by the service as `audio/mpeg`; " + "otherwise the provider output is returned directly." + ), + 400: ( + "- `app_unavailable` : App unavailable or misconfigured.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model does not support this operation.\n" + "- `completion_request_error` : Text-to-speech request failed." + ), + 500: "`internal_server_error` : Internal server error.", + }, + ) + @expect_with_user(service_api_ns, TextToAudioPayload) + @binary_response(service_api_ns, "audio/mpeg") @service_api_ns.doc("text_to_audio") @service_api_ns.doc(description="Convert text to audio using text-to-speech") @service_api_ns.doc( @@ -110,11 +154,7 @@ class TextApi(Resource): 500: "Internal server error", } ) - @service_api_ns.response( - 200, - "Text successfully converted to audio", - service_api_ns.models[AudioBinaryResponse.__name__], - ) + @service_api_ns.response(200, "Text successfully converted to audio") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) def post(self, app_model: App, end_user: EndUser): """Convert text to audio using text-to-speech. diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 7009bbfeaf6..99e7aaecd8c 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -20,6 +20,7 @@ from controllers.service_api.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) +from controllers.service_api.schema import expect_user_json, expect_with_user, json_or_event_stream_response from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.entities.app_invoke_entities import InvokeFrom @@ -92,7 +93,33 @@ register_response_schema_models(service_api_ns, GeneratedAppResponse, SimpleResu @service_api_ns.route("/completion-messages") class CompletionApi(Resource): - @service_api_ns.expect(service_api_ns.models[CompletionRequestPayload.__name__]) + @service_api_ns.doc( + summary="Send Completion Message", + description="Send a request to the text generation application.", + tags=["Completions"], + responses={ + 200: ( + "Successful response. The content type and structure depend on the `response_mode` parameter " + "in the request.\n" + "\n" + "- If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` " + "object.\n" + "- If `response_mode` is `streaming`, returns `text/event-stream` with a stream of " + "`ChunkCompletionEvent` objects." + ), + 400: ( + "- `app_unavailable` : App unavailable or misconfigured.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model unavailable.\n" + "- `completion_request_error` : Text generation failed." + ), + 429: "`too_many_requests` : Too many concurrent requests for this app.", + 500: "`internal_server_error` : Internal server error.", + }, + ) + @expect_with_user(service_api_ns, CompletionRequestPayload) + @json_or_event_stream_response(service_api_ns) @service_api_ns.doc("create_completion") @service_api_ns.doc(description="Create a completion for the given prompt") @service_api_ns.doc( @@ -168,6 +195,15 @@ class CompletionApi(Resource): @service_api_ns.route("/completion-messages//stop") class CompletionStopApi(Resource): + @service_api_ns.doc( + summary="Stop Completion Message Generation", + description="Stops a completion message generation task. Only supported in `streaming` mode.", + tags=["Completions"], + responses={ + 400: "`app_unavailable` : App unavailable or misconfigured.", + }, + ) + @expect_user_json(service_api_ns) @service_api_ns.doc("stop_completion") @service_api_ns.doc(description="Stop a running completion task") @service_api_ns.doc(params={"task_id": "The ID of the task to stop"}) @@ -197,7 +233,39 @@ class CompletionStopApi(Resource): @service_api_ns.route("/chat-messages") class ChatApi(Resource): - @service_api_ns.expect(service_api_ns.models[ChatRequestPayload.__name__]) + @service_api_ns.doc( + summary="Send Chat Message", + description="Send a request to the chat application.", + tags=["Chats", "Chatflows"], + responses={ + 200: ( + "Successful response. The content type and structure depend on the `response_mode` parameter " + "in the request.\n" + "\n" + "- If `response_mode` is `blocking`, returns `application/json` with a " + "`ChatCompletionResponse` object.\n" + "- If `response_mode` is `streaming`, returns `text/event-stream` with a stream of " + "Server-Sent Events." + ), + 400: ( + "- `app_unavailable` : App unavailable or misconfigured.\n" + "- `not_chat_app` : App mode does not match the API route.\n" + "- `conversation_completed` : The conversation has ended.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model unavailable.\n" + "- `completion_request_error` : Text generation failed." + ), + 404: "`not_found` : Conversation does not exist.", + 429: ( + "- `too_many_requests` : Too many concurrent requests for this app.\n" + "- `rate_limit_error` : The upstream model provider rate limit was exceeded." + ), + 500: "`internal_server_error` : Internal server error.", + }, + ) + @expect_with_user(service_api_ns, ChatRequestPayload) + @json_or_event_stream_response(service_api_ns) @service_api_ns.doc("create_chat_message") @service_api_ns.doc(description="Send a message in a chat conversation") @service_api_ns.doc( @@ -276,6 +344,15 @@ class ChatApi(Resource): @service_api_ns.route("/chat-messages//stop") class ChatStopApi(Resource): + @service_api_ns.doc( + summary="Stop Chat Message Generation", + description="Stops a chat message generation task. Only supported in `streaming` mode.", + tags=["Chats", "Chatflows"], + responses={ + 400: "`not_chat_app` : App mode does not match the API route.", + }, + ) + @expect_user_json(service_api_ns) @service_api_ns.doc("stop_chat_message") @service_api_ns.doc(description="Stop a running chat message generation") @service_api_ns.doc(params={"task_id": "The ID of the task to stop"}) diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index f6be7f74cc7..a208c8fee49 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -13,6 +13,7 @@ from controllers.common.controller_schemas import ConversationRenamePayload from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError +from controllers.service_api.schema import expect_user_json, expect_with_user from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db @@ -145,6 +146,16 @@ register_response_schema_models( @service_api_ns.route("/conversations") class ConversationApi(Resource): + @service_api_ns.doc( + summary="List Conversations", + description="Retrieve the conversation list for the current user, ordered by most recently active.", + tags=["Conversations"], + responses={ + 200: "Successfully retrieved conversations list.", + 400: "`not_chat_app` : App mode does not match the API route.", + 404: "`not_found` : Last conversation does not exist (invalid `last_id`).", + }, + ) @service_api_ns.doc(params=query_params_from_model(ConversationListQuery)) @service_api_ns.doc("list_conversations") @service_api_ns.doc(description="List all conversations for the current user") @@ -197,6 +208,17 @@ class ConversationApi(Resource): @service_api_ns.route("/conversations/") class ConversationDetailApi(Resource): + @service_api_ns.doc( + summary="Delete Conversation", + description="Delete a conversation.", + tags=["Conversations"], + responses={ + 204: "Conversation deleted successfully.", + 400: "`not_chat_app` : App mode does not match the API route.", + 404: "`not_found` : Conversation does not exist.", + }, + ) + @expect_user_json(service_api_ns) @service_api_ns.doc("delete_conversation") @service_api_ns.doc(description="Delete a specific conversation") @service_api_ns.doc(params={"c_id": "Conversation ID"}) @@ -225,7 +247,20 @@ class ConversationDetailApi(Resource): @service_api_ns.route("/conversations//name") class ConversationRenameApi(Resource): - @service_api_ns.expect(service_api_ns.models[ConversationRenamePayload.__name__]) + @service_api_ns.doc( + summary="Rename Conversation", + description=( + "Rename a conversation or auto-generate a name. The conversation name is used for display on " + "clients that support multiple conversations." + ), + tags=["Conversations"], + responses={ + 200: "Conversation renamed successfully.", + 400: "`not_chat_app` : App mode does not match the API route.", + 404: "`not_found` : Conversation does not exist.", + }, + ) + @expect_with_user(service_api_ns, ConversationRenamePayload) @service_api_ns.doc("rename_conversation") @service_api_ns.doc(description="Rename a conversation or auto-generate a name") @service_api_ns.doc(params={"c_id": "Conversation ID"}) @@ -267,6 +302,16 @@ class ConversationRenameApi(Resource): @service_api_ns.route("/conversations//variables") class ConversationVariablesApi(Resource): + @service_api_ns.doc( + summary="List Conversation Variables", + description="Retrieve variables from a specific conversation.", + tags=["Conversations"], + responses={ + 200: "Successfully retrieved conversation variables.", + 400: "`not_chat_app` : App mode does not match the API route.", + 404: "`not_found` : Conversation does not exist.", + }, + ) @service_api_ns.doc(params=query_params_from_model(ConversationVariablesQuery)) @service_api_ns.doc("list_conversation_variables") @service_api_ns.doc(description="List all variables for a conversation") @@ -312,7 +357,22 @@ class ConversationVariablesApi(Resource): @service_api_ns.route("/conversations//variables/") class ConversationVariableDetailApi(Resource): - @service_api_ns.expect(service_api_ns.models[ConversationVariableUpdatePayload.__name__]) + @service_api_ns.doc( + summary="Update Conversation Variable", + description="Update the value of a specific conversation variable. The value must match the expected type.", + tags=["Conversations"], + responses={ + 200: "Variable updated successfully.", + 400: ( + "- `not_chat_app` : App mode does not match the API route.\n" + "- `bad_request` : Variable value type mismatch." + ), + 404: ( + "- `not_found` : Conversation does not exist.\n- `not_found` : Conversation variable does not exist." + ), + }, + ) + @expect_with_user(service_api_ns, ConversationVariableUpdatePayload) @service_api_ns.doc("update_conversation_variable") @service_api_ns.doc(description="Update a conversation variable's value") @service_api_ns.doc(params={"c_id": "Conversation ID", "variable_id": "Variable ID"}) diff --git a/api/controllers/service_api/app/file.py b/api/controllers/service_api/app/file.py index 687d34076df..9210c60adeb 100644 --- a/api/controllers/service_api/app/file.py +++ b/api/controllers/service_api/app/file.py @@ -12,6 +12,7 @@ from controllers.common.errors import ( ) from controllers.common.schema import register_schema_models from controllers.service_api import service_api_ns +from controllers.service_api.schema import multipart_file_params from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from extensions.ext_database import db from fields.file_fields import FileResponse @@ -23,8 +24,27 @@ register_schema_models(service_api_ns, FileResponse) @service_api_ns.route("/files/upload") class FileApi(Resource): + @service_api_ns.doc( + summary="Upload File", + description=( + "Upload a file for use when sending messages, enabling multimodal understanding of images, " + "documents, audio, and video. Uploaded files are for use by the current end-user only." + ), + tags=["Files"], + responses={ + 201: "File uploaded successfully.", + 400: ( + "- `no_file_uploaded` : No file was provided in the request.\n" + "- `too_many_files` : Only one file is allowed per request.\n" + "- `filename_not_exists_error` : The uploaded file has no filename." + ), + 413: "`file_too_large` : File size exceeded.", + 415: "`unsupported_file_type` : File type not allowed.", + }, + ) @service_api_ns.doc("upload_file") @service_api_ns.doc(description="Upload a file for use in conversations") + @service_api_ns.doc(consumes=["multipart/form-data"], params=multipart_file_params(include_user=True)) @service_api_ns.doc( responses={ 201: "File uploaded successfully", diff --git a/api/controllers/service_api/app/file_preview.py b/api/controllers/service_api/app/file_preview.py index 7e68399fb0b..0b7e057152b 100644 --- a/api/controllers/service_api/app/file_preview.py +++ b/api/controllers/service_api/app/file_preview.py @@ -15,6 +15,7 @@ from controllers.service_api.app.error import ( FileAccessDeniedError, FileNotFoundError, ) +from controllers.service_api.schema import binary_response from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from extensions.ext_database import db from extensions.ext_storage import storage @@ -30,6 +31,26 @@ class FilePreviewQuery(BaseModel): register_schema_model(service_api_ns, FilePreviewQuery) register_response_schema_model(service_api_ns, BinaryFileResponse) +FILE_PREVIEW_RESPONSE_MEDIA_TYPES = [ + "application/octet-stream", + "application/pdf", + "audio/aac", + "audio/flac", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/wav", + "audio/x-m4a", + "image/gif", + "image/jpeg", + "image/png", + "image/webp", + "text/plain", + "video/mp4", + "video/quicktime", + "video/webm", +] + @service_api_ns.route("/files//preview") class FilePreviewApi(Resource): @@ -40,7 +61,26 @@ class FilePreviewApi(Resource): Files can only be accessed if they belong to messages within the requesting app's context. """ + @service_api_ns.doc( + summary="Download File", + description=( + "Preview or download uploaded files previously uploaded via the [Upload " + "File](/api-reference/files/upload-file) API. Files can only be accessed if they belong to " + "messages within the requesting application." + ), + tags=["Files"], + responses={ + 200: ( + "Returns the raw file content. The `Content-Type` header is set to the file's MIME type. If " + "`as_attachment` is `true`, the file is returned as a download with `Content-Disposition: " + "attachment`." + ), + 403: "`file_access_denied` : Access to the requested file is denied.", + 404: "`file_not_found` : The requested file was not found.", + }, + ) @service_api_ns.doc(params=query_params_from_model(FilePreviewQuery)) + @binary_response(service_api_ns, FILE_PREVIEW_RESPONSE_MEDIA_TYPES) @service_api_ns.doc("preview_file") @service_api_ns.doc(description="Preview or download a file uploaded via Service API") @service_api_ns.doc(params={"file_id": "UUID of the file to preview"}) @@ -52,11 +92,7 @@ class FilePreviewApi(Resource): 404: "File not found", } ) - @service_api_ns.response( - 200, - "File retrieved successfully", - service_api_ns.models[BinaryFileResponse.__name__], - ) + @service_api_ns.response(200, "File retrieved successfully") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) def get(self, app_model: App, end_user: EndUser, file_id: UUID): """ diff --git a/api/controllers/service_api/app/human_input_form.py b/api/controllers/service_api/app/human_input_form.py index 1dc247d7517..07cc1eeab6d 100644 --- a/api/controllers/service_api/app/human_input_form.py +++ b/api/controllers/service_api/app/human_input_form.py @@ -18,6 +18,7 @@ from werkzeug.exceptions import BadRequest, NotFound from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns +from controllers.service_api.schema import expect_with_user from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface from extensions.ext_database import db @@ -72,6 +73,23 @@ def _ensure_form_is_allowed_for_service_api(form: Form) -> None: @service_api_ns.route("/form/human_input/") class WorkflowHumanInputFormApi(Resource): + @service_api_ns.doc( + summary="Get Human Input Form", + description=( + "Retrieve a paused Human Input form's contents using the `form_token` from a " + "`human_input_required` event. Requires **WebApp** delivery." + ), + tags=["Human Input"], + responses={ + 200: "Form contents retrieved successfully.", + 404: "`not_found` : Form not found.", + 412: ( + "- `human_input_form_submitted` : Form already submitted. Forms are one-shot; the first " + "response wins regardless of which user submits it.\n" + "- `human_input_form_expired` : The form's expiration time passed before submission arrived." + ), + }, + ) @service_api_ns.doc("get_human_input_form") @service_api_ns.doc(description="Get a paused human input form by token") @service_api_ns.doc(params={"form_token": "Human input form token"}) @@ -101,7 +119,29 @@ class WorkflowHumanInputFormApi(Resource): inputs = service.resolve_form_inputs(form) return _jsonify_form_definition(form, inputs=inputs) - @service_api_ns.expect(service_api_ns.models[HumanInputFormSubmitPayload.__name__]) + @service_api_ns.doc( + summary="Submit Human Input Form", + description=( + "Submit the recipient's response to a paused Human Input form. The workflow resumes on " + "acceptance; use [Stream Workflow Events](/api-reference/chatflows/stream-workflow-events) " + "to follow subsequent events. Requires **WebApp** delivery." + ), + tags=["Human Input"], + responses={ + 200: "Form submitted successfully. The response body is an empty object.", + 400: ( + "- `bad_request` : Form recipient type is invalid.\n" + "- `invalid_form_data` : Submission failed validation against the form definition." + ), + 404: "`not_found` : Form not found.", + 412: ( + "- `human_input_form_submitted` : Form already submitted. Forms are one-shot; the first " + "response wins regardless of which user submits it.\n" + "- `human_input_form_expired` : The form's expiration time passed before submission arrived." + ), + }, + ) + @expect_with_user(service_api_ns, HumanInputFormSubmitPayload) @service_api_ns.doc("submit_human_input_form") @service_api_ns.doc(description="Submit a paused human input form by token") @service_api_ns.doc(params={"form_token": "Human input form token"}) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index adbea6570dd..a51daf7973d 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -12,6 +12,7 @@ from controllers.common.fields import SimpleResultStringListResponse from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError +from controllers.service_api.schema import expect_with_user from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.app.entities.app_invoke_entities import InvokeFrom from fields.base import ResponseModel @@ -64,6 +65,19 @@ register_response_schema_models( @service_api_ns.route("/messages") class MessageListApi(Resource): + @service_api_ns.doc( + summary="List Conversation Messages", + description=( + "Returns historical chat records in a scrolling load format, with the first page returning " + "the latest `limit` messages, i.e., in reverse order." + ), + tags=["Conversations"], + responses={ + 200: "Successfully retrieved conversation history.", + 400: "`not_chat_app` : App mode does not match the API route.", + 404: ("- `not_found` : Conversation does not exist.\n- `not_found` : First message does not exist."), + }, + ) @service_api_ns.doc(params=query_params_from_model(MessageListQuery)) @service_api_ns.doc("list_messages") @service_api_ns.doc(description="List messages in a conversation") @@ -112,7 +126,19 @@ class MessageListApi(Resource): @service_api_ns.route("/messages//feedbacks") class MessageFeedbackApi(Resource): - @service_api_ns.expect(service_api_ns.models[MessageFeedbackPayload.__name__]) + @service_api_ns.doc( + summary="Submit Message Feedback", + description=( + "Submit feedback for a message. End users can rate messages as `like` or `dislike`, and " + "optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted " + "feedback." + ), + tags=["Feedback"], + responses={ + 404: "`not_found` : Message does not exist.", + }, + ) + @expect_with_user(service_api_ns, MessageFeedbackPayload) @service_api_ns.response(200, "Feedback submitted successfully", service_api_ns.models[ResultResponse.__name__]) @service_api_ns.doc("create_message_feedback") @service_api_ns.doc(description="Submit feedback for a message") @@ -150,6 +176,17 @@ class MessageFeedbackApi(Resource): @service_api_ns.route("/app/feedbacks") class AppGetFeedbacksApi(Resource): + @service_api_ns.doc( + summary="List App Feedbacks", + description=( + "Retrieve a paginated list of all feedback submitted for messages in this application, " + "including both end-user and admin feedback." + ), + tags=["Feedback"], + responses={ + 200: "A list of application feedbacks.", + }, + ) @service_api_ns.doc(params=query_params_from_model(FeedbackListQuery)) @service_api_ns.doc("get_app_feedbacks") @service_api_ns.doc(description="Get all feedbacks for the application") @@ -177,6 +214,20 @@ class AppGetFeedbacksApi(Resource): @service_api_ns.route("/messages//suggested") class MessageSuggestedApi(Resource): + @service_api_ns.doc( + summary="Get Next Suggested Questions", + description="Get next questions suggestions for the current message.", + tags=["Chats", "Chatflows"], + responses={ + 200: "Successfully retrieved suggested questions.", + 400: ( + "- `not_chat_app` : App mode does not match the API route.\n" + "- `bad_request` : Suggested questions feature is disabled." + ), + 404: "`not_found` : Message does not exist.", + 500: "`internal_server_error` : Internal server error.", + }, + ) @service_api_ns.response( 200, "Suggested questions retrieved successfully", diff --git a/api/controllers/service_api/app/site.py b/api/controllers/service_api/app/site.py index f5d1dcd283d..35098ca1367 100644 --- a/api/controllers/service_api/app/site.py +++ b/api/controllers/service_api/app/site.py @@ -17,6 +17,18 @@ register_response_schema_models(service_api_ns, SiteResponse) class AppSiteApi(Resource): """Resource for app sites.""" + @service_api_ns.doc( + summary="Get App WebApp Settings", + description=( + "Retrieve the WebApp settings of this application, including site configuration, theme, and " + "customization options." + ), + tags=["Applications"], + responses={ + 200: "WebApp settings of the application.", + 403: "`forbidden` : Site not found for this application or the workspace has been archived.", + }, + ) @service_api_ns.doc("get_app_site") @service_api_ns.doc(description="Get application site configuration") @service_api_ns.doc( diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index b655e0beb4a..234488885de 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -21,6 +21,11 @@ from controllers.service_api.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) +from controllers.service_api.schema import ( + expect_user_json, + expect_with_user, + json_or_event_stream_response, +) from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.apps.base_app_queue_manager import AppQueueManager @@ -177,14 +182,15 @@ register_response_schema_models( def _serialize_workflow_run(workflow_run: WorkflowRun) -> dict: status = _enum_value(workflow_run.status) raw_outputs = workflow_run.outputs_dict - if status == WorkflowExecutionStatus.PAUSED.value or raw_outputs is None: - outputs: dict = {} - elif isinstance(raw_outputs, dict): - outputs = raw_outputs - elif isinstance(raw_outputs, Mapping): - outputs = dict(raw_outputs) - else: - outputs = {} + match raw_outputs: + case _ if status == WorkflowExecutionStatus.PAUSED.value or raw_outputs is None: + outputs: dict = {} + case dict(): + outputs = raw_outputs + case _ if isinstance(raw_outputs, Mapping): + outputs = dict(raw_outputs) + case _: + outputs = {} return WorkflowRunResponse.model_validate( { "id": workflow_run.id, @@ -208,6 +214,16 @@ def _serialize_workflow_log_pagination(pagination) -> dict: @service_api_ns.route("/workflows/run/") class WorkflowRunDetailApi(Resource): + @service_api_ns.doc( + summary="Get Workflow Run Detail", + description="Retrieve the current execution results of a workflow task based on the workflow execution ID.", + tags=["Chatflows", "Workflows"], + responses={ + 200: "Successfully retrieved workflow run details.", + 400: "`not_workflow_app` : App mode does not match the API route.", + 404: "`not_found` : Workflow run not found.", + }, + ) @service_api_ns.doc("get_workflow_run_detail") @service_api_ns.doc(description="Get workflow run details") @service_api_ns.doc(params={"workflow_run_id": "Workflow run ID"}) @@ -249,7 +265,37 @@ class WorkflowRunDetailApi(Resource): @service_api_ns.route("/workflows/run") class WorkflowRunApi(Resource): - @service_api_ns.expect(service_api_ns.models[WorkflowRunPayload.__name__]) + @service_api_ns.doc( + summary="Run Workflow", + description="Execute a workflow. Cannot be executed without a published workflow.", + tags=["Workflows"], + responses={ + 200: ( + "Successful response. The content type and structure depend on the `response_mode` parameter " + "in the request.\n" + "\n" + "- If `response_mode` is `blocking`, returns `application/json` with a " + "`WorkflowBlockingResponse` object.\n" + "- If `response_mode` is `streaming`, returns `text/event-stream` with a stream of " + "`ChunkWorkflowEvent` objects." + ), + 400: ( + "- `not_workflow_app` : App mode does not match the API route.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model unavailable.\n" + "- `completion_request_error` : Workflow execution request failed.\n" + "- `invalid_param` : Invalid parameter value." + ), + 429: ( + "- `too_many_requests` : Too many concurrent requests for this app.\n" + "- `rate_limit_error` : The upstream model provider rate limit was exceeded." + ), + 500: "`internal_server_error` : Internal server error.", + }, + ) + @expect_with_user(service_api_ns, WorkflowRunPayload) + @json_or_event_stream_response(service_api_ns) @service_api_ns.doc("run_workflow") @service_api_ns.doc(description="Execute a workflow") @service_api_ns.doc( @@ -313,7 +359,42 @@ class WorkflowRunApi(Resource): @service_api_ns.route("/workflows//run") class WorkflowRunByIdApi(Resource): - @service_api_ns.expect(service_api_ns.models[WorkflowRunPayload.__name__]) + @service_api_ns.doc( + summary="Run Workflow by ID", + description=( + "Execute a specific workflow version identified by its ID. Useful for running a particular " + "published version of the workflow." + ), + tags=["Workflows"], + responses={ + 200: ( + "Successful response. The content type and structure depend on the `response_mode` parameter " + "in the request.\n" + "\n" + "- If `response_mode` is `blocking`, returns `application/json` with a " + "`WorkflowBlockingResponse` object.\n" + "- If `response_mode` is `streaming`, returns `text/event-stream` with a stream of " + "`ChunkWorkflowEvent` objects." + ), + 400: ( + "- `not_workflow_app` : App mode does not match the API route.\n" + "- `bad_request` : Workflow is a draft or has an invalid ID format.\n" + "- `provider_not_initialize` : No valid model provider credentials found.\n" + "- `provider_quota_exceeded` : Model provider quota exhausted.\n" + "- `model_currently_not_support` : Current model unavailable.\n" + "- `completion_request_error` : Workflow execution request failed.\n" + "- `invalid_param` : Required parameter missing or invalid." + ), + 404: "`not_found` : Workflow not found.", + 429: ( + "- `too_many_requests` : Too many concurrent requests for this app.\n" + "- `rate_limit_error` : The upstream model provider rate limit was exceeded." + ), + 500: "`internal_server_error` : Internal server error.", + }, + ) + @expect_with_user(service_api_ns, WorkflowRunPayload) + @json_or_event_stream_response(service_api_ns) @service_api_ns.doc("run_workflow_by_id") @service_api_ns.doc(description="Execute a specific workflow by ID") @service_api_ns.doc(params={"workflow_id": "Workflow ID to execute"}) @@ -387,6 +468,18 @@ class WorkflowRunByIdApi(Resource): @service_api_ns.route("/workflows/tasks//stop") class WorkflowTaskStopApi(Resource): + @service_api_ns.doc( + summary="Stop Workflow Task", + description="Stop a running workflow task. Only supported in `streaming` mode.", + tags=["Workflows"], + responses={ + 400: ( + "- `not_workflow_app` : App mode does not match the API route.\n" + "- `invalid_param` : Required parameter missing or invalid." + ), + }, + ) + @expect_user_json(service_api_ns) @service_api_ns.doc("stop_workflow_task") @service_api_ns.doc(description="Stop a running workflow task") @service_api_ns.doc(params={"task_id": "Task ID to stop"}) @@ -417,6 +510,14 @@ class WorkflowTaskStopApi(Resource): @service_api_ns.route("/workflows/logs") class WorkflowAppLogApi(Resource): + @service_api_ns.doc( + summary="List Workflow Logs", + description="Retrieve paginated workflow execution logs with filtering options.", + tags=["Chatflows", "Workflows"], + responses={ + 200: "Successfully retrieved workflow logs.", + }, + ) @service_api_ns.doc(params=query_params_from_model(WorkflowLogQuery)) @service_api_ns.doc("get_workflow_logs") @service_api_ns.doc(description="Get workflow execution logs") diff --git a/api/controllers/service_api/app/workflow_events.py b/api/controllers/service_api/app/workflow_events.py index 6dc9ef6e8dd..1bace170f53 100644 --- a/api/controllers/service_api/app/workflow_events.py +++ b/api/controllers/service_api/app/workflow_events.py @@ -15,6 +15,7 @@ from controllers.common.fields import EventStreamResponse from controllers.common.schema import query_params_from_model, register_response_schema_model, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotWorkflowAppError +from controllers.service_api.schema import event_stream_response from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator @@ -44,6 +45,24 @@ register_response_schema_model(service_api_ns, EventStreamResponse) class WorkflowEventsApi(Resource): """Service API for getting workflow execution events after resume.""" + @service_api_ns.doc( + summary="Stream Workflow Events", + description=( + "Resume the Server-Sent Events stream for a workflow run after a pause or a dropped SSE " + "connection. For runs that have already finished, the stream emits a single " + "`workflow_finished` event and closes." + ), + tags=["Chatflows", "Workflows"], + responses={ + 200: ( + "Server-Sent Events stream. Each event is delivered as `data: {JSON}\\n\\n`. Event payloads " + "follow the same schemas as the original streaming response." + ), + 400: "`not_workflow_app` : Please check if your app mode matches the right API route.", + 404: "`not_found` : Workflow run not found.", + }, + ) + @event_stream_response(service_api_ns) @service_api_ns.doc("get_workflow_events") @service_api_ns.doc(description="Get workflow execution events stream after resume") @service_api_ns.doc(params={"task_id": "Workflow run ID"}) diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index c307063b3e5..0ca5c5bbf6b 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -1,8 +1,8 @@ -from typing import Any, Literal +from typing import Any, Literal, override from uuid import UUID from flask import request -from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, GetJsonSchemaHandler, RootModel, field_validator, model_validator from werkzeug.exceptions import Forbidden, NotFound import services @@ -79,6 +79,13 @@ class DocumentStatusPayload(BaseModel): document_ids: list[str] = Field(default_factory=list, description="Document IDs to update") +DOCUMENT_STATUS_ACTION_PARAM = { + "description": "Action to perform: 'enable', 'disable', 'archive', or 'un_archive'", + "enum": ["enable", "disable", "archive", "un_archive"], + "type": "string", +} + + class TagNamePayload(BaseModel): name: str = Field(..., min_length=1, max_length=50) @@ -114,6 +121,45 @@ class TagUnbindingPayload(BaseModel): tag_id: str | None = None target_id: str + @classmethod + @override + def __get_pydantic_json_schema__(cls, _core_schema: object, _handler: GetJsonSchemaHandler) -> dict[str, object]: + tag_id_property = { + "description": "Legacy single tag ID accepted by the Service API.", + "type": "string", + } + tag_ids_property = { + "description": "Tag IDs to unbind. Use this for new integrations.", + "items": {"type": "string"}, + "minItems": 1, + "type": "array", + } + target_id_property = {"title": "Target Id", "type": "string"} + return { + "anyOf": [ + { + "properties": { + "tag_id": tag_id_property, + "tag_ids": tag_ids_property, + "target_id": target_id_property, + }, + "required": ["tag_id", "target_id"], + "type": "object", + }, + { + "properties": { + "tag_id": {**tag_id_property, "nullable": True}, + "tag_ids": tag_ids_property, + "target_id": target_id_property, + }, + "required": ["tag_ids", "target_id"], + "type": "object", + }, + ], + "description": "Accepts either the legacy tag_id payload or the normalized tag_ids payload.", + "title": cls.__name__, + } + @model_validator(mode="before") @classmethod def normalize_legacy_tag_id(cls, data: object) -> object: @@ -204,6 +250,14 @@ register_response_schema_models( class DatasetListApi(DatasetApiResource): """Resource for datasets.""" + @service_api_ns.doc( + summary="List Knowledge Bases", + description="Returns a paginated list of knowledge bases. Supports filtering by keyword and tags.", + tags=["Knowledge Bases"], + responses={ + 200: "List of knowledge bases.", + }, + ) @service_api_ns.doc("list_datasets") @service_api_ns.doc(description="List all datasets") @service_api_ns.doc( @@ -262,6 +316,19 @@ class DatasetListApi(DatasetApiResource): } return dump_response(DatasetListResponse, response), 200 + @service_api_ns.doc( + summary="Create an Empty Knowledge Base", + description=( + "Create a new empty knowledge base. After creation, use [Create Document by " + "Text](/api-reference/documents/create-document-by-text) or [Create Document by " + "File](/api-reference/documents/create-document-by-file) to add documents." + ), + tags=["Knowledge Bases"], + responses={ + 200: "Knowledge base created successfully.", + 409: "`dataset_name_duplicate` : The dataset name already exists. Please modify your dataset name.", + }, + ) @service_api_ns.expect(service_api_ns.models[DatasetCreatePayload.__name__]) @service_api_ns.doc("create_dataset") @service_api_ns.doc(description="Create a new dataset") @@ -327,6 +394,19 @@ class DatasetListApi(DatasetApiResource): class DatasetApi(DatasetApiResource): """Resource for dataset.""" + @service_api_ns.doc( + summary="Get Knowledge Base", + description=( + "Retrieve detailed information about a specific knowledge base, including its embedding " + "model, retrieval configuration, and document statistics." + ), + tags=["Knowledge Bases"], + responses={ + 200: "Knowledge base details.", + 403: "`forbidden` : Insufficient permissions to access this knowledge base.", + 404: "`not_found` : Dataset not found.", + }, + ) @service_api_ns.doc("get_dataset") @service_api_ns.doc(description="Get a specific dataset by ID") @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) @@ -392,6 +472,19 @@ class DatasetApi(DatasetApiResource): 200, ) + @service_api_ns.doc( + summary="Update Knowledge Base", + description=( + "Update the name, description, permissions, or retrieval settings of an existing knowledge " + "base. Only the fields provided in the request body are updated." + ), + tags=["Knowledge Bases"], + responses={ + 200: "Knowledge base updated successfully.", + 403: "`forbidden` : Insufficient permissions to access this knowledge base.", + 404: "`not_found` : Dataset not found.", + }, + ) @service_api_ns.expect(service_api_ns.models[DatasetUpdatePayload.__name__]) @service_api_ns.doc("update_dataset") @service_api_ns.doc(description="Update an existing dataset") @@ -474,6 +567,22 @@ class DatasetApi(DatasetApiResource): return DatasetDetailWithPartialMembersResponse.model_validate(result_data).model_dump(mode="json"), 200 + @service_api_ns.doc( + summary="Delete Knowledge Base", + description=( + "Permanently delete a knowledge base and all its documents. The knowledge base must not be " + "in use by any application." + ), + tags=["Knowledge Bases"], + responses={ + 204: "Success.", + 404: "`not_found` : Dataset not found.", + 409: ( + "`dataset_in_use` : The knowledge base is being used by some apps. Please remove it from the " + "apps before deleting." + ), + }, + ) @service_api_ns.doc("delete_dataset") @service_api_ns.doc(description="Delete a dataset") @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) @@ -519,6 +628,17 @@ class DatasetApi(DatasetApiResource): class DocumentStatusApi(DatasetApiResource): """Resource for batch document status operations.""" + @service_api_ns.doc( + summary="Update Document Status in Batch", + description="Enable, disable, archive, or unarchive multiple documents at once.", + tags=["Documents"], + responses={ + 200: "Documents updated successfully.", + 400: "`invalid_action` : Invalid action.", + 403: "`forbidden` : Insufficient permissions.", + 404: "`not_found` : Knowledge base not found.", + }, + ) @service_api_ns.response( 200, "Document status updated successfully", @@ -529,7 +649,7 @@ class DocumentStatusApi(DatasetApiResource): @service_api_ns.doc( params={ "dataset_id": "Dataset ID", - "action": "Action to perform: 'enable', 'disable', 'archive', or 'un_archive'", + "action": DOCUMENT_STATUS_ACTION_PARAM, } ) @service_api_ns.doc( @@ -591,6 +711,14 @@ class DocumentStatusApi(DatasetApiResource): @service_api_ns.route("/datasets/tags") class DatasetTagsApi(DatasetApiResource): + @service_api_ns.doc( + summary="List Knowledge Tags", + description="Returns the list of all knowledge base tags in the workspace.", + tags=["Tags"], + responses={ + 200: "List of tags.", + }, + ) @service_api_ns.doc("list_dataset_tags") @service_api_ns.doc(description="Get all knowledge type tags") @service_api_ns.doc( @@ -612,6 +740,14 @@ class DatasetTagsApi(DatasetApiResource): tags = TagService.get_tags(db.session(), "knowledge", cid) return dump_response(KnowledgeTagListResponse, tags), 200 + @service_api_ns.doc( + summary="Create Knowledge Tag", + description="Create a new tag for organizing knowledge bases.", + tags=["Tags"], + responses={ + 200: "Tag created successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[TagCreatePayload.__name__]) @service_api_ns.doc("create_dataset_tag") @service_api_ns.doc(description="Add a knowledge type tag") @@ -634,7 +770,7 @@ class DatasetTagsApi(DatasetApiResource): raise Forbidden() payload = TagCreatePayload.model_validate(service_api_ns.payload or {}) - tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=TagType.KNOWLEDGE)) + tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=TagType.KNOWLEDGE), db.session) response = dump_response( KnowledgeTagResponse, @@ -642,6 +778,14 @@ class DatasetTagsApi(DatasetApiResource): ) return response, 200 + @service_api_ns.doc( + summary="Update Knowledge Tag", + description="Rename an existing knowledge base tag.", + tags=["Tags"], + responses={ + 200: "Tag updated successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[TagUpdatePayload.__name__]) @service_api_ns.doc("update_dataset_tag") @service_api_ns.doc(description="Update a knowledge type tag") @@ -664,9 +808,9 @@ class DatasetTagsApi(DatasetApiResource): payload = TagUpdatePayload.model_validate(service_api_ns.payload or {}) tag_id = payload.tag_id - tag = TagService.update_tags(UpdateTagServicePayload(name=payload.name), tag_id) + tag = TagService.update_tags(UpdateTagServicePayload(name=payload.name), tag_id, db.session) - binding_count = TagService.get_tag_binding_count(tag_id) + binding_count = TagService.get_tag_binding_count(tag_id, db.session) response = dump_response( KnowledgeTagResponse, @@ -674,6 +818,14 @@ class DatasetTagsApi(DatasetApiResource): ) return response, 200 + @service_api_ns.doc( + summary="Delete Knowledge Tag", + description="Permanently delete a knowledge base tag. Does not delete the knowledge bases that were tagged.", + tags=["Tags"], + responses={ + 204: "Success.", + }, + ) @service_api_ns.expect(service_api_ns.models[TagDeletePayload.__name__]) @service_api_ns.doc("delete_dataset_tag") @service_api_ns.doc(description="Delete a knowledge type tag") @@ -688,13 +840,21 @@ class DatasetTagsApi(DatasetApiResource): def delete(self, _): """Delete a knowledge type tag.""" payload = TagDeletePayload.model_validate(service_api_ns.payload or {}) - TagService.delete_tag(payload.tag_id) + TagService.delete_tag(payload.tag_id, db.session) return "", 204 @service_api_ns.route("/datasets/tags/binding") class DatasetTagBindingApi(DatasetApiResource): + @service_api_ns.doc( + summary="Create Tag Binding", + description="Bind one or more tags to a knowledge base. A knowledge base can have multiple tags.", + tags=["Tags"], + responses={ + 204: "Success.", + }, + ) @service_api_ns.expect(service_api_ns.models[TagBindingPayload.__name__]) @service_api_ns.doc("bind_dataset_tags") @service_api_ns.doc(description="Bind tags to a dataset") @@ -713,7 +873,8 @@ class DatasetTagBindingApi(DatasetApiResource): payload = TagBindingPayload.model_validate(service_api_ns.payload or {}) TagService.save_tag_binding( - TagBindingCreatePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=TagType.KNOWLEDGE) + TagBindingCreatePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=TagType.KNOWLEDGE), + db.session, ) return "", 204 @@ -721,6 +882,14 @@ class DatasetTagBindingApi(DatasetApiResource): @service_api_ns.route("/datasets/tags/unbinding") class DatasetTagUnbindingApi(DatasetApiResource): + @service_api_ns.doc( + summary="Delete Tag Binding", + description="Remove one or more tags from a knowledge base.", + tags=["Tags"], + responses={ + 204: "Success.", + }, + ) @service_api_ns.expect(service_api_ns.models[TagUnbindingPayload.__name__]) @service_api_ns.doc("unbind_dataset_tags") @service_api_ns.doc(description="Unbind tags from a dataset") @@ -739,7 +908,8 @@ class DatasetTagUnbindingApi(DatasetApiResource): payload = TagUnbindingPayload.model_validate(service_api_ns.payload or {}) TagService.delete_tag_binding( - TagBindingDeletePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=TagType.KNOWLEDGE) + TagBindingDeletePayload(tag_ids=payload.tag_ids, target_id=payload.target_id, type=TagType.KNOWLEDGE), + db.session, ) return "", 204 @@ -747,6 +917,14 @@ class DatasetTagUnbindingApi(DatasetApiResource): @service_api_ns.route("/datasets//tags") class DatasetTagsBindingStatusApi(DatasetApiResource): + @service_api_ns.doc( + summary="Get Knowledge Base Tags", + description="Returns the list of tags bound to a specific knowledge base.", + tags=["Tags"], + responses={ + 200: "Tags bound to the knowledge base.", + }, + ) @service_api_ns.doc("get_dataset_tags_binding_status") @service_api_ns.doc(description="Get tags bound to a specific dataset") @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) @@ -766,6 +944,8 @@ class DatasetTagsBindingStatusApi(DatasetApiResource): dataset_id = kwargs.get("dataset_id") assert isinstance(current_user, Account) assert current_user.current_tenant_id is not None - tags = TagService.get_tags_by_target_id("knowledge", current_user.current_tenant_id, str(dataset_id)) + tags = TagService.get_tags_by_target_id( + "knowledge", current_user.current_tenant_id, str(dataset_id), db.session + ) tags_list = [{"id": tag.id, "name": tag.name} for tag in tags] return dump_response(DatasetBoundTagListResponse, {"data": tags_list, "total": len(tags)}), 200 diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index c71feb1aa7b..871c5e888b1 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -8,11 +8,12 @@ deprecated in generated API docs so clients migrate toward the canonical paths. import json from collections.abc import Mapping from contextlib import ExitStack -from typing import Any, Literal, Self +from copy import deepcopy +from typing import Any, Literal, Self, override from uuid import UUID from flask import request, send_file -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, Field, GetJsonSchemaHandler, field_validator, model_validator from sqlalchemy import desc, func, select from werkzeug.exceptions import Forbidden, NotFound @@ -39,6 +40,7 @@ from controllers.service_api.dataset.error import ( DocumentIndexingError, InvalidMetadataError, ) +from controllers.service_api.schema import binary_response from controllers.service_api.wraps import ( DatasetApiResource, cloud_edition_billing_rate_limit_check, @@ -104,6 +106,36 @@ class DocumentTextUpdate(BaseModel): raise ValueError("Invalid doc_form.") return value + @classmethod + @override + def __get_pydantic_json_schema__(cls, core_schema: Any, handler: GetJsonSchemaHandler) -> dict[str, Any]: + schema = handler.resolve_ref_schema(handler(core_schema)) + properties = schema.get("properties") + if not isinstance(properties, dict): + return schema + + text_branch_properties = deepcopy(properties) + text_branch_properties["text"] = _non_null_property_schema(properties.get("text")) + text_branch_properties["name"] = _non_null_property_schema(properties.get("name")) + + no_text_branch_properties = deepcopy(properties) + no_text_branch_properties["text"] = {"type": "null"} + + return { + **schema, + "anyOf": [ + { + "properties": text_branch_properties, + "required": ["name", "text"], + "type": "object", + }, + { + "properties": no_text_branch_properties, + "type": "object", + }, + ], + } + @model_validator(mode="after") def check_text_and_name(self) -> Self: if self.text is not None and self.name is None: @@ -111,6 +143,24 @@ class DocumentTextUpdate(BaseModel): return self +def _non_null_property_schema(property_schema: object) -> dict[str, Any]: + if not isinstance(property_schema, dict): + return {} + + any_of = property_schema.get("anyOf") + if isinstance(any_of, list): + non_null_candidates = [ + candidate for candidate in any_of if isinstance(candidate, dict) and candidate.get("type") != "null" + ] + if len(non_null_candidates) == 1: + return { + **{key: value for key, value in property_schema.items() if key != "anyOf"}, + **deepcopy(non_null_candidates[0]), + } + + return deepcopy(property_schema) + + class DocumentListQuery(BaseModel): page: int = Field(default=1, description="Page number") limit: int = Field(default=20, description="Number of items per page") @@ -351,6 +401,24 @@ def _update_document_by_text(tenant_id: str, dataset_id: UUID, document_id: UUID class DocumentAddByTextApi(DatasetApiResource): """Resource for the canonical text document creation route.""" + @service_api_ns.doc( + summary="Create Document by Text", + description=( + "Create a document from raw text content. The document is processed asynchronously — use the " + "returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/" + "get-document-indexing-status) to track progress." + ), + tags=["Documents"], + responses={ + 200: "Document created successfully.", + 400: ( + "- `provider_not_initialize` : No valid model provider credentials found. Please go to " + "Settings -> Model Provider to complete your provider credentials.\n" + "- `invalid_param` : Knowledge base does not exist. / indexing_technique is required. / " + "Invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`)." + ), + }, + ) @service_api_ns.expect(service_api_ns.models[DocumentTextCreatePayload.__name__]) @service_api_ns.doc("create_document_by_text") @service_api_ns.doc(description="Create a new document by providing text content") @@ -409,6 +477,25 @@ class DeprecatedDocumentAddByTextApi(DatasetApiResource): class DocumentUpdateByTextApi(DatasetApiResource): """Resource for the canonical text document update route.""" + @service_api_ns.doc( + summary="Update Document by Text", + description=( + "Update an existing document's text content, name, or processing configuration. Re-triggers " + "indexing if content changes — use the returned `batch` ID with [Get Document Indexing " + "Status](/api-reference/documents/get-document-indexing-status) to track progress." + ), + tags=["Documents"], + responses={ + 200: "Document updated successfully.", + 400: ( + "- `provider_not_initialize` : No valid model provider credentials found. Please go to " + "Settings -> Model Provider to complete your provider credentials.\n" + "- `invalid_param` : Knowledge base does not exist, name is required when text is " + "provided, or invalid doc_form (must be `text_model`, `hierarchical_model`, or " + "`qa_model`)." + ), + }, + ) @service_api_ns.expect(service_api_ns.models[DocumentTextUpdate.__name__]) @service_api_ns.doc("update_document_by_text") @service_api_ns.doc(description="Update an existing document by providing text content") @@ -463,11 +550,42 @@ class DeprecatedDocumentUpdateByTextApi(DatasetApiResource): @service_api_ns.route( "/datasets//document/create_by_file", - "/datasets//document/create-by-file", + doc={ + "post": { + "deprecated": True, + "description": ( + "Deprecated legacy alias for creating a new document by uploading a file. " + "Use /datasets/{dataset_id}/document/create-by-file instead." + ), + } + }, ) +@service_api_ns.route("/datasets//document/create-by-file") class DocumentAddByFileApi(DatasetApiResource): """Resource for documents.""" + @service_api_ns.doc( + summary="Create Document by File", + description=( + "Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, " + "etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document " + "Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress." + ), + tags=["Documents"], + responses={ + 200: "Document created successfully.", + 400: ( + "- `no_file_uploaded` : Please upload your file.\n" + "- `too_many_files` : Only one file is allowed.\n" + "- `filename_not_exists_error` : The specified filename does not exist.\n" + "- `provider_not_initialize` : No valid model provider credentials found. Please go to " + "Settings -> Model Provider to complete your provider credentials.\n" + "- `invalid_param` : Knowledge base does not exist, external datasets not supported, " + "file too large, unsupported file type, missing required fields, or invalid doc_form " + "(must be `text_model`, `hierarchical_model`, or `qa_model`)." + ), + }, + ) @service_api_ns.doc("create_document_by_file") @service_api_ns.doc(description="Create a new document by uploading a file") @service_api_ns.doc(consumes=["multipart/form-data"], params=DOCUMENT_CREATE_BY_FILE_PARAMS) @@ -658,6 +776,27 @@ def _update_document_by_file(tenant_id: str, dataset_id: UUID, document_id: UUID class DeprecatedDocumentUpdateByFileApi(DatasetApiResource): """Deprecated resource aliases for file document updates.""" + @service_api_ns.doc( + summary="Update Document by File", + description=( + "Update an existing document by uploading a new file. Re-triggers indexing — use the returned " + "`batch` ID with [Get Document Indexing Status](/api-reference/documents/" + "get-document-indexing-status) to track progress." + ), + tags=["Documents"], + responses={ + 200: "Document updated successfully.", + 400: ( + "- `too_many_files` : Only one file is allowed.\n" + "- `filename_not_exists_error` : The specified filename does not exist.\n" + "- `provider_not_initialize` : No valid model provider credentials found. Please go to " + "Settings -> Model Provider to complete your provider credentials.\n" + "- `invalid_param` : Knowledge base does not exist, external datasets not supported, " + "file too large, unsupported file type, or invalid doc_form (must be `text_model`, " + "`hierarchical_model`, or `qa_model`)." + ), + }, + ) @service_api_ns.doc("update_document_by_file_deprecated") @service_api_ns.doc(deprecated=True) @service_api_ns.doc( @@ -686,6 +825,18 @@ class DeprecatedDocumentUpdateByFileApi(DatasetApiResource): @service_api_ns.route("/datasets//documents") class DocumentListApi(DatasetApiResource): + @service_api_ns.doc( + summary="List Documents", + description=( + "Returns a paginated list of documents in the knowledge base. Supports filtering by keyword " + "and indexing status." + ), + tags=["Documents"], + responses={ + 200: "List of documents.", + 404: "`not_found` : Knowledge base not found.", + }, + ) @service_api_ns.doc("list_documents") @service_api_ns.doc(description="List all documents in a dataset") @service_api_ns.doc(params={"dataset_id": "Dataset ID", **query_params_from_model(DocumentListQuery)}) @@ -746,6 +897,19 @@ class DocumentListApi(DatasetApiResource): class DocumentBatchDownloadZipApi(DatasetApiResource): """Download multiple uploaded-file documents as a single ZIP archive.""" + @service_api_ns.doc( + summary="Download Documents as ZIP", + description=( + "Download multiple uploaded-file documents as a single ZIP archive. Accepts up to `100` document IDs." + ), + tags=["Documents"], + responses={ + 200: "ZIP archive containing the requested documents.", + 403: "`forbidden` : Insufficient permissions.", + 404: "`not_found` : Document or dataset not found.", + }, + ) + @binary_response(service_api_ns, "application/zip") @service_api_ns.expect(service_api_ns.models[DocumentBatchDownloadZipPayload.__name__]) @service_api_ns.doc("download_documents_as_zip") @service_api_ns.doc(description="Download selected uploaded documents as a single ZIP archive") @@ -758,11 +922,7 @@ class DocumentBatchDownloadZipApi(DatasetApiResource): 404: "Document or dataset not found", } ) - @service_api_ns.response( - 200, - "ZIP archive generated successfully", - service_api_ns.models[BinaryFileResponse.__name__], - ) + @service_api_ns.response(200, "ZIP archive generated successfully") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id, dataset_id: UUID): payload = DocumentBatchDownloadZipPayload.model_validate(service_api_ns.payload or {}) @@ -789,6 +949,20 @@ class DocumentBatchDownloadZipApi(DatasetApiResource): @service_api_ns.route("/datasets//documents//indexing-status") class DocumentIndexingStatusApi(DatasetApiResource): + @service_api_ns.doc( + summary="Get Document Indexing Status", + description=( + "Check the indexing progress of documents in a batch. Returns the current processing stage " + "and chunk completion counts for each document. Poll this endpoint until `indexing_status` " + "reaches `completed` or `error`. The status progresses through: `waiting` → `parsing` → " + "`cleaning` → `splitting` → `indexing` → `completed`." + ), + tags=["Documents"], + responses={ + 200: "Indexing status for documents in the batch.", + 404: "`not_found` : Knowledge base not found. / Documents not found.", + }, + ) @service_api_ns.doc("get_document_indexing_status") @service_api_ns.doc(description="Get indexing status for documents in a batch") @service_api_ns.doc(params={"dataset_id": "Dataset ID", "batch": "Batch ID"}) @@ -861,6 +1035,16 @@ class DocumentIndexingStatusApi(DatasetApiResource): class DocumentDownloadApi(DatasetApiResource): """Return a signed download URL for a document's original uploaded file.""" + @service_api_ns.doc( + summary="Download Document", + description="Get a signed download URL for a document's original uploaded file.", + tags=["Documents"], + responses={ + 200: "Download URL generated successfully.", + 403: "`forbidden` : No permission to access this document.", + 404: "`not_found` : Document not found.", + }, + ) @service_api_ns.doc("get_document_download_url") @service_api_ns.doc(description="Get a signed download URL for a document's original uploaded file") @service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) @@ -895,6 +1079,24 @@ class DocumentDownloadApi(DatasetApiResource): class DocumentApi(DatasetApiResource): METADATA_CHOICES = {"all", "only", "without"} + @service_api_ns.doc( + summary="Get Document", + description=( + "Retrieve detailed information about a specific document, including its indexing status, " + "metadata, and processing statistics." + ), + tags=["Documents"], + responses={ + 200: ( + "Document details. The response shape varies based on the `metadata` query parameter. When " + "`metadata` is `only`, only `id`, `doc_type`, and `doc_metadata` are returned. When " + "`metadata` is `without`, `doc_type` and `doc_metadata` are omitted." + ), + 400: "`invalid_metadata` : Invalid metadata value for the specified key.", + 403: "`forbidden` : No permission.", + 404: "`not_found` : Document not found.", + }, + ) @service_api_ns.doc("get_document") @service_api_ns.doc(description="Get a specific document by ID") @service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) @@ -1036,6 +1238,17 @@ class DocumentApi(DatasetApiResource): """Update document by file on the canonical document resource.""" return _update_document_by_file(tenant_id=tenant_id, dataset_id=dataset_id, document_id=document_id) + @service_api_ns.doc( + summary="Delete Document", + description="Permanently delete a document and all its chunks from the knowledge base.", + tags=["Documents"], + responses={ + 204: "Success.", + 400: "`document_indexing` : Cannot delete document during indexing.", + 403: "`archived_document_immutable` : The archived document is not editable.", + 404: "`not_found` : Document Not Exists.", + }, + ) @service_api_ns.doc("delete_document") @service_api_ns.doc(description="Delete a document") @service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) diff --git a/api/controllers/service_api/dataset/hit_testing.py b/api/controllers/service_api/dataset/hit_testing.py index 55a1c47c425..fb55b18059d 100644 --- a/api/controllers/service_api/dataset/hit_testing.py +++ b/api/controllers/service_api/dataset/hit_testing.py @@ -13,6 +13,32 @@ register_response_schema_models(service_api_ns, HitTestingResponse) @service_api_ns.route("/datasets//hit-testing", "/datasets//retrieve") class HitTestingApi(DatasetApiResource, DatasetsHitTestingBase): + @service_api_ns.doc( + summary="Retrieve Chunks from a Knowledge Base / Test Retrieval", + description=( + "Performs a search query against a knowledge base to retrieve the most relevant chunks. This " + "endpoint can be used for both production retrieval and test retrieval." + ), + tags=["Knowledge Bases"], + responses={ + 200: "Retrieval results.", + 400: ( + "- `dataset_not_initialized` : The dataset is still being initialized or indexing. Please " + "wait a moment.\n" + "- `provider_not_initialize` : No valid model provider credentials found. Please go to " + "Settings -> Model Provider to complete your provider credentials.\n" + "- `provider_quota_exceeded` : Your quota for Dify Hosted OpenAI has been exhausted. Please " + "go to Settings -> Model Provider to complete your own provider credentials.\n" + "- `model_currently_not_support` : Dify Hosted OpenAI trial currently not support the GPT-4 " + "model.\n" + "- `completion_request_error` : Completion request failed.\n" + "- `invalid_param` : Invalid parameter value." + ), + 403: "`forbidden` : Insufficient permissions.", + 404: "`not_found` : Knowledge base not found.", + 500: "`internal_server_error` : An internal error occurred during retrieval.", + }, + ) @service_api_ns.doc("dataset_hit_testing") @service_api_ns.doc(description="Perform hit testing on a dataset") @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) diff --git a/api/controllers/service_api/dataset/metadata.py b/api/controllers/service_api/dataset/metadata.py index 293a77fc5ec..82f571ab0f4 100644 --- a/api/controllers/service_api/dataset/metadata.py +++ b/api/controllers/service_api/dataset/metadata.py @@ -24,6 +24,12 @@ from services.entities.knowledge_entities.knowledge_entities import ( ) from services.metadata_service import MetadataService +BUILT_IN_METADATA_ACTION_PARAM = { + "description": "Action to perform: 'enable' or 'disable'", + "enum": ["enable", "disable"], + "type": "string", +} + register_schema_model(service_api_ns, MetadataUpdatePayload) register_schema_models( service_api_ns, @@ -43,6 +49,17 @@ register_response_schema_models( @service_api_ns.route("/datasets//metadata") class DatasetMetadataCreateServiceApi(DatasetApiResource): + @service_api_ns.doc( + summary="Create Metadata Field", + description=( + "Create a custom metadata field for the knowledge base. Metadata fields can be used to " + "annotate documents with structured information." + ), + tags=["Metadata"], + responses={ + 201: "Metadata field created successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[MetadataArgs.__name__]) @service_api_ns.doc("create_dataset_metadata") @service_api_ns.doc(description="Create metadata for a dataset") @@ -71,6 +88,17 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource): metadata = MetadataService.create_metadata(dataset_id_str, metadata_args) return dump_response(DatasetMetadataResponse, metadata), 201 + @service_api_ns.doc( + summary="List Metadata Fields", + description=( + "Returns the list of all metadata fields (both custom and built-in) for the knowledge base, " + "along with the count of documents using each field." + ), + tags=["Metadata"], + responses={ + 200: "Metadata fields for the knowledge base.", + }, + ) @service_api_ns.doc("get_dataset_metadata") @service_api_ns.doc(description="Get all metadata for a dataset") @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) @@ -96,6 +124,14 @@ class DatasetMetadataCreateServiceApi(DatasetApiResource): @service_api_ns.route("/datasets//metadata/") class DatasetMetadataServiceApi(DatasetApiResource): + @service_api_ns.doc( + summary="Update Metadata Field", + description="Rename a custom metadata field.", + tags=["Metadata"], + responses={ + 200: "Metadata field updated successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[MetadataUpdatePayload.__name__]) @service_api_ns.doc("update_dataset_metadata") @service_api_ns.doc(description="Update metadata name") @@ -125,6 +161,17 @@ class DatasetMetadataServiceApi(DatasetApiResource): metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, payload.name) return dump_response(DatasetMetadataResponse, metadata), 200 + @service_api_ns.doc( + summary="Delete Metadata Field", + description=( + "Permanently delete a custom metadata field. Documents using this field will lose their " + "metadata values for it." + ), + tags=["Metadata"], + responses={ + 204: "Success.", + }, + ) @service_api_ns.doc("delete_dataset_metadata") @service_api_ns.doc(description="Delete metadata") @service_api_ns.doc(params={"dataset_id": "Dataset ID", "metadata_id": "Metadata ID"}) @@ -152,6 +199,16 @@ class DatasetMetadataServiceApi(DatasetApiResource): @service_api_ns.route("/datasets//metadata/built-in") class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource): + @service_api_ns.doc( + summary="Get Built-in Metadata Fields", + description=( + "Returns the list of built-in metadata fields provided by the system (e.g., document type, source URL)." + ), + tags=["Metadata"], + responses={ + 200: "Built-in metadata fields.", + }, + ) @service_api_ns.doc("get_built_in_fields") @service_api_ns.doc(description="Get all built-in metadata fields") @service_api_ns.doc( @@ -173,9 +230,17 @@ class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource): @service_api_ns.route("/datasets//metadata/built-in/") class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): + @service_api_ns.doc( + summary="Update Built-in Metadata Field", + description="Enable or disable built-in metadata fields for the knowledge base.", + tags=["Metadata"], + responses={ + 200: "Built-in metadata field toggled successfully.", + }, + ) @service_api_ns.doc("toggle_built_in_field") @service_api_ns.doc(description="Enable or disable built-in metadata field") - @service_api_ns.doc(params={"dataset_id": "Dataset ID", "action": "Action to perform: 'enable' or 'disable'"}) + @service_api_ns.doc(params={"dataset_id": "Dataset ID", "action": BUILT_IN_METADATA_ACTION_PARAM}) @service_api_ns.doc( responses={ 200: "Action completed successfully", @@ -205,6 +270,17 @@ class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): @service_api_ns.route("/datasets//documents/metadata") class DocumentMetadataEditServiceApi(DatasetApiResource): + @service_api_ns.doc( + summary="Update Document Metadata in Batch", + description=( + "Update metadata values for multiple documents at once. Each document in the request " + "receives the specified metadata key-value pairs." + ), + tags=["Metadata"], + responses={ + 200: "Document metadata updated successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[MetadataOperationData.__name__]) @service_api_ns.doc("update_documents_metadata") @service_api_ns.doc(description="Update metadata for multiple documents") diff --git a/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py index a1a8b588c42..57b360370f4 100644 --- a/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py @@ -19,6 +19,11 @@ from controllers.common.schema import ( from controllers.service_api import service_api_ns from controllers.service_api.dataset.error import PipelineRunError from controllers.service_api.dataset.rag_pipeline.serializers import serialize_upload_file +from controllers.service_api.schema import ( + event_stream_response, + json_or_event_stream_response, + multipart_file_params, +) from controllers.service_api.wraps import DatasetApiResource from core.app.apps.pipeline.pipeline_generator import PipelineGenerator from core.app.entities.app_invoke_entities import InvokeFrom @@ -95,6 +100,18 @@ register_response_schema_models( class DatasourcePluginsApi(DatasetApiResource): """Resource for datasource plugins.""" + @service_api_ns.doc( + summary="List Datasource Plugins", + description=( + "List the datasource nodes configured in the knowledge pipeline. Each node includes the " + "plugin it uses plus the metadata needed to run it." + ), + tags=["Knowledge Pipeline"], + responses={ + 200: "List of datasource nodes configured in the pipeline.", + 404: "`not_found` : Dataset not found.", + }, + ) @service_api_ns.doc(shortcut="list_rag_pipeline_datasource_plugins") @service_api_ns.doc(description="List all datasource plugins for a rag pipeline") @service_api_ns.doc( @@ -137,6 +154,19 @@ class DatasourcePluginsApi(DatasetApiResource): class DatasourceNodeRunApi(DatasetApiResource): """Resource for datasource node run.""" + @service_api_ns.doc( + summary="Run Datasource Node", + description=( + "Execute a single datasource node within the knowledge pipeline. Returns a streaming " + "response with the node execution results." + ), + tags=["Knowledge Pipeline"], + responses={ + 200: "Streaming response with node execution events.", + 404: "`not_found` : Dataset not found.", + }, + ) + @event_stream_response(service_api_ns) @service_api_ns.doc(shortcut="pipeline_datasource_node_run") @service_api_ns.doc(description="Run a datasource node for a rag pipeline") @service_api_ns.doc( @@ -195,6 +225,24 @@ class DatasourceNodeRunApi(DatasetApiResource): class PipelineRunApi(DatasetApiResource): """Resource for datasource node run.""" + @service_api_ns.doc( + summary="Run Pipeline", + description=( + "Execute the full knowledge pipeline for a knowledge base. Supports both streaming and " + "blocking response modes." + ), + tags=["Knowledge Pipeline"], + responses={ + 200: ( + "Pipeline execution result. Format depends on `response_mode`: streaming returns a " + "`text/event-stream`, blocking returns a JSON object." + ), + 403: "`forbidden` : Forbidden.", + 404: "`not_found` : Dataset not found.", + 500: "`pipeline_run_error` : Pipeline execution failed.", + }, + ) + @json_or_event_stream_response(service_api_ns) @service_api_ns.doc(shortcut="pipeline_datasource_node_run") @service_api_ns.doc(description="Run a datasource node for a rag pipeline") @service_api_ns.doc( @@ -248,8 +296,24 @@ class PipelineRunApi(DatasetApiResource): class KnowledgebasePipelineFileUploadApi(DatasetApiResource): """Resource for uploading a file to a knowledgebase pipeline.""" + @service_api_ns.doc( + summary="Upload Pipeline File", + description="Upload a file for use in a knowledge pipeline. Accepts a single file via `multipart/form-data`.", + tags=["Knowledge Pipeline"], + responses={ + 201: "File uploaded successfully.", + 400: ( + "- `no_file_uploaded` : Please upload your file.\n" + "- `filename_not_exists_error` : The specified filename does not exist.\n" + "- `too_many_files` : Only one file is allowed." + ), + 413: "`file_too_large` : File size exceeded.", + 415: "`unsupported_file_type` : File type not allowed.", + }, + ) @service_api_ns.doc(shortcut="knowledgebase_pipeline_file_upload") @service_api_ns.doc(description="Upload a file to a knowledgebase pipeline") + @service_api_ns.doc(consumes=["multipart/form-data"], params=multipart_file_params(include_user=False)) @service_api_ns.doc( responses={ 201: "File uploaded successfully", diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index f93eb6a4bf1..c334649c602 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -128,6 +128,18 @@ register_response_schema_models( class SegmentApi(DatasetApiResource): """Resource for segments.""" + @service_api_ns.doc( + summary="Create Chunks", + description=( + "Create one or more chunks within a document. Each chunk can include optional keywords and an " + "answer field (for QA-mode documents)." + ), + tags=["Chunks"], + responses={ + 200: "Chunks created successfully.", + 404: "`not_found` : Document is not completed or is disabled.", + }, + ) @service_api_ns.expect(service_api_ns.models[SegmentCreatePayload.__name__]) @service_api_ns.doc("create_segments") @service_api_ns.doc(description="Create segments in a document") @@ -209,6 +221,14 @@ class SegmentApi(DatasetApiResource): } return dump_response(SegmentCreateListResponse, response), 200 + @service_api_ns.doc( + summary="List Chunks", + description="Returns a paginated list of chunks within a document. Supports filtering by keyword and status.", + tags=["Chunks"], + responses={ + 200: "List of chunks.", + }, + ) @service_api_ns.doc("list_segments") @service_api_ns.doc(description="List segments in a document") @service_api_ns.doc(params=SegmentDocParams.DATASET_DOCUMENT) @@ -294,6 +314,14 @@ class SegmentApi(DatasetApiResource): @service_api_ns.route("/datasets//documents//segments/") class DatasetSegmentApi(DatasetApiResource): + @service_api_ns.doc( + summary="Delete Chunk", + description="Permanently delete a chunk from the document.", + tags=["Chunks"], + responses={ + 204: "Success.", + }, + ) @service_api_ns.doc("delete_segment") @service_api_ns.doc(description="Delete a specific segment") @service_api_ns.doc(params=SegmentDocParams.DATASET_DOCUMENT_SEGMENT) @@ -329,6 +357,14 @@ class DatasetSegmentApi(DatasetApiResource): SegmentService.delete_segment(segment, document, dataset) return "", 204 + @service_api_ns.doc( + summary="Update Chunk", + description="Update a chunk's content, keywords, or answer. Re-triggers indexing for the modified chunk.", + tags=["Chunks"], + responses={ + 200: "Chunk updated successfully.", + }, + ) @service_api_ns.expect(service_api_ns.models[SegmentUpdatePayload.__name__]) @service_api_ns.doc("update_segment") @service_api_ns.doc(description="Update a specific segment") @@ -391,6 +427,17 @@ class DatasetSegmentApi(DatasetApiResource): } return dump_response(SegmentDetailResponse, response), 200 + @service_api_ns.doc( + summary="Get Chunk", + description=( + "Retrieve detailed information about a specific chunk, including its content, keywords, and " + "indexing status." + ), + tags=["Chunks"], + responses={ + 200: "Chunk details.", + }, + ) @service_api_ns.doc("get_segment") @service_api_ns.doc(description="Get a specific segment by ID") @service_api_ns.doc(params=SegmentDocParams.DATASET_DOCUMENT_SEGMENT) @@ -442,6 +489,15 @@ class DatasetSegmentApi(DatasetApiResource): class ChildChunkApi(DatasetApiResource): """Resource for child chunks.""" + @service_api_ns.doc( + summary="Create Child Chunk", + description="Create a child chunk under the specified segment.", + tags=["Chunks"], + responses={ + 200: "Child chunk created successfully.", + 400: "`invalid_param` : Create child chunk index failed.", + }, + ) @service_api_ns.expect(service_api_ns.models[ChildChunkCreatePayload.__name__]) @service_api_ns.doc("create_child_chunk") @service_api_ns.doc(description="Create a new child chunk for a segment") @@ -511,6 +567,14 @@ class ChildChunkApi(DatasetApiResource): return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200 + @service_api_ns.doc( + summary="List Child Chunks", + description="Returns a paginated list of child chunks under a specific parent chunk.", + tags=["Chunks"], + responses={ + 200: "List of child chunks.", + }, + ) @service_api_ns.doc("list_child_chunks") @service_api_ns.doc(description="List child chunks for a segment") @service_api_ns.doc(params=SegmentDocParams.DATASET_DOCUMENT_PARENT_SEGMENT) @@ -576,6 +640,15 @@ class ChildChunkApi(DatasetApiResource): class DatasetChildChunkApi(DatasetApiResource): """Resource for updating child chunks.""" + @service_api_ns.doc( + summary="Delete Child Chunk", + description="Permanently delete a child chunk from its parent chunk.", + tags=["Chunks"], + responses={ + 204: "Success.", + 400: "`invalid_param` : Delete child chunk index failed.", + }, + ) @service_api_ns.doc("delete_child_chunk") @service_api_ns.doc(description="Delete a specific child chunk") @service_api_ns.doc(params=SegmentDocParams.DATASET_DOCUMENT_CHILD_CHUNK) @@ -634,6 +707,15 @@ class DatasetChildChunkApi(DatasetApiResource): return "", 204 + @service_api_ns.doc( + summary="Update Child Chunk", + description="Update the content of an existing child chunk.", + tags=["Chunks"], + responses={ + 200: "Child chunk updated successfully.", + 400: "`invalid_param` : Update child chunk index failed.", + }, + ) @service_api_ns.expect(service_api_ns.models[ChildChunkUpdatePayload.__name__]) @service_api_ns.doc("update_child_chunk") @service_api_ns.doc(description="Update a specific child chunk") diff --git a/api/controllers/service_api/end_user/end_user.py b/api/controllers/service_api/end_user/end_user.py index d2d018492a5..607ed12e5b6 100644 --- a/api/controllers/service_api/end_user/end_user.py +++ b/api/controllers/service_api/end_user/end_user.py @@ -17,6 +17,18 @@ register_response_schema_models(service_api_ns, EndUserDetail) class EndUserApi(Resource): """Resource for retrieving end user details by ID.""" + @service_api_ns.doc( + summary="Get End User Info", + description=( + "Retrieve an end user by ID. Useful when other APIs return an end-user ID (e.g., " + "`created_by` from [Upload File](/api-reference/files/upload-file))." + ), + tags=["End Users"], + responses={ + 200: "End user retrieved successfully.", + 404: "`end_user_not_found` : End user not found.", + }, + ) @service_api_ns.doc("get_end_user") @service_api_ns.doc(description="Get an end user by ID") @service_api_ns.doc( diff --git a/api/controllers/service_api/schema.py b/api/controllers/service_api/schema.py new file mode 100644 index 00000000000..ed528e4fc9d --- /dev/null +++ b/api/controllers/service_api/schema.py @@ -0,0 +1,113 @@ +"""Service API OpenAPI documentation helpers. + +These helpers keep documentation-only request shapes next to controller +definitions without changing the Pydantic models used for runtime validation. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from copy import deepcopy +from typing import cast + +from flask_restx import Namespace +from pydantic import BaseModel + +USER_PROPERTY_SCHEMA: dict[str, object] = {"description": "End user identifier", "type": "string"} +USER_QUERY_PARAM: dict[str, object] = {"description": "End user identifier", "in": "query", "type": "string"} +USER_FORM_PARAM: dict[str, object] = {"description": "End user identifier", "in": "formData", "type": "string"} +FILE_FORM_PARAM: dict[str, object] = {"in": "formData", "required": True, "type": "file"} +USER_FETCH_FROM_ATTR = "_dify_service_api_user_fetch_from" +USER_REQUIRED_ATTR = "_dify_service_api_user_required" +JSON_USER_FETCH_FROM = "JSON" + + +def expect_with_user(namespace: Namespace, model: type[BaseModel]): + """Document a JSON request body as ``model`` plus Service API ``user``.""" + + source_model = namespace.models[model.__name__] + model_name = f"{model.__name__}WithUser" + + def decorator(view_func): + required = _json_user_required(view_func) + schema = cast(dict[str, object], deepcopy(source_model.__schema__)) + _add_user_property(schema, required=required) + if model_name not in namespace.models: + namespace.schema_model(model_name, schema) + return namespace.expect(namespace.models[model_name], validate=False)(view_func) + + return decorator + + +def expect_user_json(namespace: Namespace): + """Document a JSON request body that only carries the Service API ``user``.""" + + def decorator(view_func): + required = _json_user_required(view_func) + schema: dict[str, object] = {"properties": {}, "title": "ServiceApiUserPayload", "type": "object"} + _add_user_property(schema, required=required) + model_name = "RequiredServiceApiUserPayload" if required else "OptionalServiceApiUserPayload" + if model_name not in namespace.models: + namespace.schema_model(model_name, schema) + return namespace.expect(namespace.models[model_name], validate=False)(view_func) + + return decorator + + +def multipart_file_params(*, include_user: bool) -> dict[str, dict[str, object]]: + params: dict[str, dict[str, object]] = {"file": FILE_FORM_PARAM} + if include_user: + params["user"] = USER_FORM_PARAM + return deepcopy(params) + + +def json_or_event_stream_response(namespace: Namespace): + return namespace.doc(produces=["application/json", "text/event-stream"]) + + +def event_stream_response(namespace: Namespace): + return namespace.doc(produces=["text/event-stream"]) + + +def binary_response(namespace: Namespace, media_type: str | Sequence[str]): + media_types = [media_type] if isinstance(media_type, str) else list(media_type) + return namespace.doc(produces=media_types) + + +def _json_user_required(view_func) -> bool: + fetch_from = getattr(view_func, USER_FETCH_FROM_ATTR, None) + if fetch_from != JSON_USER_FETCH_FROM: + raise ValueError("JSON user documentation must match validate_app_token(fetch_user_arg=WhereisUserArg.JSON)") + + return bool(getattr(view_func, USER_REQUIRED_ATTR, False)) + + +def _add_user_property(schema: dict[str, object], *, required: bool) -> None: + variants: list[dict[str, object]] = [] + for keyword in ("anyOf", "oneOf"): + candidates = schema.get(keyword) + if isinstance(candidates, list): + variants.extend(candidate for candidate in candidates if isinstance(candidate, dict)) + + if variants: + for variant in variants: + _add_user_property_to_object_schema(variant, required=required) + + _add_user_property_to_object_schema(schema, required=required) + + +def _add_user_property_to_object_schema(schema: dict[str, object], *, required: bool) -> None: + properties = schema.setdefault("properties", {}) + if isinstance(properties, dict): + cast(dict[str, object], properties)["user"] = USER_PROPERTY_SCHEMA + + if required: + required_fields = schema.setdefault("required", []) + if isinstance(required_fields, list) and "user" not in required_fields: + required_fields.append("user") + else: + required_fields = schema.get("required") + if isinstance(required_fields, list) and "user" in required_fields: + required_fields.remove("user") + if required_fields == []: + schema.pop("required", None) diff --git a/api/controllers/service_api/workspace/models.py b/api/controllers/service_api/workspace/models.py index 63806ab252f..9d49866e87a 100644 --- a/api/controllers/service_api/workspace/models.py +++ b/api/controllers/service_api/workspace/models.py @@ -19,6 +19,17 @@ register_response_schema_models(service_api_ns, ProviderWithModelsListResponse) @service_api_ns.route("/workspaces/current/models/model-types/") class ModelProviderAvailableModelApi(Resource): + @service_api_ns.doc( + summary="Get Available Models", + description=( + "Retrieve the list of available models by type. Primarily used to query `text-embedding` and " + "`rerank` models for knowledge base configuration." + ), + tags=["Models"], + responses={ + 200: "Available models for the specified type.", + }, + ) @service_api_ns.doc("get_available_models") @service_api_ns.doc(description="Get available models by model type") @service_api_ns.doc(params={"model_type": "Type of model to retrieve"}) diff --git a/api/controllers/service_api/wraps.py b/api/controllers/service_api/wraps.py index 013ea34a6ab..32e95b481f8 100644 --- a/api/controllers/service_api/wraps.py +++ b/api/controllers/service_api/wraps.py @@ -4,16 +4,23 @@ import time from collections.abc import Callable from enum import StrEnum, auto from functools import wraps -from typing import cast, overload +from typing import Protocol, cast, overload from flask import current_app, request from flask_login import user_logged_in from flask_restx import Resource +from flask_restx.utils import merge from pydantic import BaseModel from sqlalchemy import select from werkzeug.exceptions import Forbidden, NotFound, Unauthorized from configs import dify_config +from controllers.service_api.schema import ( + USER_FETCH_FROM_ATTR, + USER_FORM_PARAM, + USER_QUERY_PARAM, + USER_REQUIRED_ATTR, +) from enums.cloud_plan import CloudPlan from extensions.ext_database import db from extensions.ext_redis import redis_client @@ -28,6 +35,12 @@ from services.feature_service import FeatureService logger = logging.getLogger(__name__) +class _RestxDocumentedView(Protocol): + """Callable view object carrying Flask-RESTX documentation metadata.""" + + __apidoc__: dict[str, object] + + class WhereisUserArg(StrEnum): """ Enum for whereis_user_arg. @@ -43,6 +56,35 @@ class FetchUserArg(BaseModel): required: bool = False +APP_TOKEN_FORBIDDEN_RESPONSE = { + 403: "Forbidden - token scope, app, dataset, or workspace access denied", +} + +DATASET_TOKEN_AUTH_RESPONSES = { + 401: "Unauthorized - invalid API token", + 403: "Forbidden - dataset API access or workspace access denied", +} + + +def _document_app_token_contract(view_func: Callable[..., object], fetch_user_arg: FetchUserArg | None) -> None: + doc: dict[str, object] = {"responses": APP_TOKEN_FORBIDDEN_RESPONSE} + if fetch_user_arg is not None: + setattr(view_func, USER_FETCH_FROM_ATTR, fetch_user_arg.fetch_from.name) + setattr(view_func, USER_REQUIRED_ATTR, fetch_user_arg.required) + match fetch_user_arg.fetch_from: + case WhereisUserArg.QUERY: + doc["params"] = {"user": {**USER_QUERY_PARAM, "required": fetch_user_arg.required}} + case WhereisUserArg.FORM: + doc["params"] = {"user": {**USER_FORM_PARAM, "required": fetch_user_arg.required}} + case WhereisUserArg.JSON: + pass + + cast(_RestxDocumentedView, view_func).__apidoc__ = cast( + dict[str, object], + merge(getattr(view_func, "__apidoc__", {}), doc), + ) + + @overload def validate_app_token[**P, R](view: Callable[P, R]) -> Callable[P, R]: ... @@ -126,6 +168,7 @@ def validate_app_token[**P, R]( return view_func(*args, **kwargs) + _document_app_token_contract(decorated_view, fetch_user_arg) return decorated_view if view is None: @@ -343,6 +386,8 @@ def validate_and_get_api_token(scope: str | None = None): class DatasetApiResource(Resource): + __apidoc__ = {"responses": DATASET_TOKEN_AUTH_RESPONSES} + method_decorators = [validate_dataset_token] def get_dataset(self, dataset_id: str, tenant_id: str) -> Dataset: diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 694d6331483..55a31563d69 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -118,7 +118,7 @@ class BaseAgentRunner(AppRunner): features = model_schema.features if model_schema and model_schema.features else [] self.stream_tool_call = ModelFeature.STREAM_TOOL_CALL in features self.files = application_generate_entity.files if ModelFeature.VISION in features else [] - self.query: str | None = "" + self.query: str = "" self._current_thoughts: list[PromptMessage] = [] def _repack_app_generate_entity( diff --git a/api/core/app/apps/agent_app/app_runner.py b/api/core/app/apps/agent_app/app_runner.py index 03c6f3e410c..f3f809e4b43 100644 --- a/api/core/app/apps/agent_app/app_runner.py +++ b/api/core/app/apps/agent_app/app_runner.py @@ -72,13 +72,48 @@ def publish_text_answer( both the backend-produced answer and short-circuited answers (moderation / annotation reply) share the exact same persistence + SSE path. """ + publish_text_delta( + queue_manager=queue_manager, + model_name=model_name, + delta=answer, + user_query=user_query, + ) + publish_message_end( + queue_manager=queue_manager, + model_name=model_name, + answer=answer, + user_query=user_query, + ) + + +def publish_text_delta( + *, + queue_manager: AppQueueManager, + model_name: str, + delta: str, + user_query: str | None = None, +) -> None: + """Publish one assistant text delta through the EasyUI chat pipeline.""" + if not delta: + return prompt_messages = _prompt_messages_from_query(user_query) chunk = LLMResultChunk( model=model_name, prompt_messages=prompt_messages, - delta=LLMResultChunkDelta(index=0, message=AssistantPromptMessage(content=answer)), + delta=LLMResultChunkDelta(index=0, message=AssistantPromptMessage(content=delta)), ) queue_manager.publish(QueueLLMChunkEvent(chunk=chunk), PublishFrom.APPLICATION_MANAGER) + + +def publish_message_end( + *, + queue_manager: AppQueueManager, + model_name: str, + answer: str, + user_query: str | None = None, +) -> None: + """Publish the terminal assistant result without emitting another delta.""" + prompt_messages = _prompt_messages_from_query(user_query) queue_manager.publish( QueueMessageEndEvent( llm_result=LLMResult( @@ -151,7 +186,12 @@ class AgentAppRunner: ) create_response = self._agent_backend_client.create_run(runtime.request) - terminal = self._consume_stream(create_response.run_id, queue_manager=queue_manager) + terminal, streamed_answer = self._consume_stream( + create_response.run_id, + queue_manager=queue_manager, + model_name=model_name, + query=query, + ) if isinstance(terminal, AgentBackendDeferredToolCallInternalEvent): # ENG-635: the agent asked a human. End this turn with the question and @@ -175,7 +215,13 @@ class AgentAppRunner: raise AgentBackendError(str(error)) answer = self._extract_answer(terminal.output) - self._publish_answer(queue_manager=queue_manager, model_name=model_name, answer=answer, query=query) + self._publish_terminal_answer( + queue_manager=queue_manager, + model_name=model_name, + answer=answer, + query=query, + streamed_answer=streamed_answer, + ) self._save_session( scope=scope, backend_run_id=terminal.run_id, @@ -272,8 +318,16 @@ class AgentAppRunner: parts.append(args.markdown) return "\n\n".join(parts) - def _consume_stream(self, run_id: str, *, queue_manager: AppQueueManager): + def _consume_stream( + self, + run_id: str, + *, + queue_manager: AppQueueManager, + model_name: str, + query: str | None, + ): terminal = None + streamed_answer_parts: list[str] = [] for public_event in self._agent_backend_client.stream_events(run_id): if queue_manager.is_stopped(): self._cancel_run(run_id) @@ -286,16 +340,23 @@ class AgentAppRunner: AgentBackendInternalEventType.RUN_STARTED, AgentBackendInternalEventType.STREAM_EVENT, ): - # Stream deltas are accumulated by the backend into the - # terminal output; token-level forwarding is an S3 refinement. if isinstance(internal_event, AgentBackendStreamInternalEvent): + text_delta = self._extract_stream_text_delta(internal_event) + if text_delta: + streamed_answer_parts.append(text_delta) + publish_text_delta( + queue_manager=queue_manager, + model_name=model_name, + delta=text_delta, + user_query=query, + ) continue continue terminal = internal_event break if terminal is not None: break - return terminal + return terminal, "".join(streamed_answer_parts) def _cancel_run(self, run_id: str) -> None: try: @@ -310,6 +371,35 @@ class AgentAppRunner: # task pipeline streams the chunk over SSE and persists the message. publish_text_answer(queue_manager=queue_manager, model_name=model_name, answer=answer, user_query=query) + def _publish_terminal_answer( + self, + *, + queue_manager: AppQueueManager, + model_name: str, + answer: str, + query: str | None, + streamed_answer: str, + ) -> None: + """Finish a successful streamed turn without duplicating the final text.""" + if not streamed_answer: + self._publish_answer(queue_manager=queue_manager, model_name=model_name, answer=answer, query=query) + return + + if answer.startswith(streamed_answer): + publish_text_delta( + queue_manager=queue_manager, + model_name=model_name, + delta=answer[len(streamed_answer) :], + user_query=query, + ) + elif answer != streamed_answer: + logger.warning( + "Agent App streamed answer does not match terminal output; " + "using terminal output for message persistence." + ) + + publish_message_end(queue_manager=queue_manager, model_name=model_name, answer=answer, user_query=query) + def _save_session( self, *, @@ -357,5 +447,27 @@ class AgentAppRunner: return json.dumps(output, ensure_ascii=False) return json.dumps(output, ensure_ascii=False) + @staticmethod + def _extract_stream_text_delta(event: AgentBackendStreamInternalEvent) -> str | None: + data = event.data + if not isinstance(data, dict): + return None -__all__ = ["AgentAppRunner", "publish_text_answer"] + if data.get("event_kind") == "part_delta": + delta = data.get("delta") + if isinstance(delta, dict) and delta.get("part_delta_kind") == "text": + content_delta = delta.get("content_delta") + if isinstance(content_delta, str): + return content_delta + + if data.get("event_kind") == "part_start": + part = data.get("part") + if isinstance(part, dict) and part.get("part_kind") == "text": + content = part.get("content") + if isinstance(content, str): + return content + + return None + + +__all__ = ["AgentAppRunner", "publish_message_end", "publish_text_answer", "publish_text_delta"] diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index a89a0cf70db..7b854fec34a 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -231,22 +231,23 @@ class AppRunner: :param tenant_id: tenant id for multimodal output :return: """ - if not stream and isinstance(invoke_result, LLMResult): - self._handle_invoke_result_direct( - invoke_result=invoke_result, - queue_manager=queue_manager, - ) - elif stream and isinstance(invoke_result, Generator): - self._handle_invoke_result_stream( - invoke_result=invoke_result, - queue_manager=queue_manager, - agent=agent, - message_id=message_id, - user_id=user_id, - tenant_id=tenant_id, - ) - else: - raise NotImplementedError(f"unsupported invoke result type: {type(invoke_result)}") + match invoke_result: + case LLMResult() if not stream: + self._handle_invoke_result_direct( + invoke_result=invoke_result, + queue_manager=queue_manager, + ) + case _ if stream and isinstance(invoke_result, Generator): + self._handle_invoke_result_stream( + invoke_result=invoke_result, + queue_manager=queue_manager, + agent=agent, + message_id=message_id, + user_id=user_id, + tenant_id=tenant_id, + ) + case _: + raise NotImplementedError(f"unsupported invoke result type: {type(invoke_result)}") def _handle_invoke_result_direct( self, diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index 502b1907ba4..c9486b5821f 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -882,7 +882,7 @@ class WorkflowResponseConverter: return files @classmethod - def _get_file_var_from_value(cls, value: Union[dict, list]) -> Mapping[str, Any] | None: + def _get_file_var_from_value(cls, value: object) -> Mapping[str, Any] | None: """ Get file var from value :param value: variable value @@ -891,10 +891,11 @@ class WorkflowResponseConverter: if not value: return None - if isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY: - return value - elif isinstance(value, File): - return value.to_dict() + match value: + case dict() if value.get("dify_model_identity") == FILE_MODEL_IDENTITY: + return value + case File(): + return value.to_dict() return None diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index defec9f9461..803fdacf78d 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -241,7 +241,7 @@ class WorkflowFinishStreamResponse(StreamResponse): created_by: Mapping[str, object] = Field(default_factory=dict) created_at: int finished_at: int | None - exceptions_count: int | None = 0 + exceptions_count: int = 0 files: Sequence[Mapping[str, Any]] | None = [] event: StreamEvent = StreamEvent.WORKFLOW_FINISHED diff --git a/api/core/helper/trace_id_helper.py b/api/core/helper/trace_id_helper.py index 82b5f42885a..8b022c1d065 100644 --- a/api/core/helper/trace_id_helper.py +++ b/api/core/helper/trace_id_helper.py @@ -144,15 +144,16 @@ def extract_parent_trace_context_from_args(args: Mapping[str, Any]) -> dict[str, Returns an empty dict if the context is missing or incomplete. """ parent_trace_context = args.get("parent_trace_context") - if isinstance(parent_trace_context, ParentTraceContext): - context = parent_trace_context - elif isinstance(parent_trace_context, Mapping): - try: - context = ParentTraceContext.model_validate(parent_trace_context) - except ValidationError: + match parent_trace_context: + case ParentTraceContext(): + context = parent_trace_context + case Mapping(): + try: + context = ParentTraceContext.model_validate(parent_trace_context) + except ValidationError: + return {} + case _: return {} - else: - return {} if context.parent_node_execution_id is None: return {} diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index ba305690664..14ed8af3ef7 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -116,20 +116,21 @@ def cast_parameter_value(typ: StrEnum, value: Any, /): return value if isinstance(value, str) else str(value) case PluginParameterType.BOOLEAN: - if value is None: - return False - elif isinstance(value, str): - # Allowed YAML boolean value strings: https://yaml.org/type/bool.html - # and also '0' for False and '1' for True - match value.lower(): - case "true" | "yes" | "y" | "1": - return True - case "false" | "no" | "n" | "0": - return False - case _: - return bool(value) - else: - return value if isinstance(value, bool) else bool(value) + match value: + case None: + return False + case str(): + # Allowed YAML boolean value strings: https://yaml.org/type/bool.html + # and also '0' for False and '1' for True + match value.lower(): + case "true" | "yes" | "y" | "1": + return True + case "false" | "no" | "n" | "0": + return False + case _: + return bool(value) + case _: + return value if isinstance(value, bool) else bool(value) case PluginParameterType.NUMBER: match value: diff --git a/api/core/plugin/entities/request.py b/api/core/plugin/entities/request.py index 95321750d7e..fecad81c032 100644 --- a/api/core/plugin/entities/request.py +++ b/api/core/plugin/entities/request.py @@ -71,8 +71,8 @@ class RequestInvokeLLM(BaseRequestInvokeModel): mode: str completion_params: dict[str, Any] = Field(default_factory=dict) prompt_messages: list[PromptMessage] = Field(default_factory=list) - tools: list[PromptMessageTool] = Field(default_factory=list[PromptMessageTool]) - stop: list[str] = Field(default_factory=list[str]) + tools: list[PromptMessageTool] | None = Field(default_factory=list[PromptMessageTool]) + stop: list[str] | None = Field(default_factory=list[str]) stream: bool = False model_config = ConfigDict(protected_namespaces=()) diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 7a74b89cf51..6977f643859 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -20,6 +20,7 @@ from core.plugin.impl.exc import ( PluginDaemonNotFoundError, PluginDaemonUnauthorizedError, PluginInvokeError, + PluginLLMPollingUnsupportedError, PluginNotFoundError, PluginPermissionDeniedError, PluginUniqueIdentifierError, @@ -370,6 +371,10 @@ class BasePluginClient: raise TriggerInvokeError(error_object.get("message")) case EventIgnoreError.__name__: raise EventIgnoreError(description=error_object.get("message")) + # NOTE: current plugin sdk / plugin daemon does not raise exception with + # type `PluginLLMPollingUnsupportedError`. + case PluginLLMPollingUnsupportedError.__name__: + raise PluginLLMPollingUnsupportedError(description=error_object.get("message")) case _: raise PluginInvokeError(description=message) case PluginDaemonInternalServerError.__name__: diff --git a/api/core/plugin/impl/exc.py b/api/core/plugin/impl/exc.py index 9a4f51ef121..abb9f0b1713 100644 --- a/api/core/plugin/impl/exc.py +++ b/api/core/plugin/impl/exc.py @@ -5,6 +5,13 @@ from pydantic import TypeAdapter from extensions.ext_logging import get_request_id +# NOTE: Avoid renaming exception classes in this file, since +# the `_handle_plugin_daemon_error` in api/core/plugin/impl/base.py +# build exception instances based on the class name. +# +# Renaming of exception classes could result in incorrect exception +# being raised. + class PluginDaemonError(Exception): """Base class for all plugin daemon errors.""" @@ -75,6 +82,10 @@ class PluginInvokeError(PluginDaemonClientSideError, ValueError): ) +class PluginLLMPollingUnsupportedError(PluginInvokeError): + """Plugin-backed LLM polling is unavailable for the requested model.""" + + class PluginUniqueIdentifierError(PluginDaemonClientSideError): description: str = "Unique Identifier Error" diff --git a/api/core/plugin/impl/model.py b/api/core/plugin/impl/model.py index 47608bdfa6e..80a83fb3f21 100644 --- a/api/core/plugin/impl/model.py +++ b/api/core/plugin/impl/model.py @@ -13,13 +13,17 @@ from core.plugin.entities.plugin_daemon import ( PluginVoicesResponse, ) from core.plugin.impl.base import BasePluginClient -from graphon.model_runtime.entities.llm_entities import LLMResultChunk +from core.plugin.impl.exc import PluginInvokeError, PluginLLMPollingUnsupportedError +from graphon.model_runtime.entities.llm_entities import LLMPollingResult, LLMResultChunk from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool -from graphon.model_runtime.entities.model_entities import AIModelEntity +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult from graphon.model_runtime.utils.encoders import jsonable_encoder +_POLLING_UNSUPPORTED_INVOKE_ERROR_TYPES = frozenset((NotImplementedError.__name__,)) +_POLLING_UNSUPPORTED_ERROR_MESSAGE = "does not support polling" + class PluginModelClient(BasePluginClient): @staticmethod @@ -197,6 +201,103 @@ class PluginModelClient(BasePluginClient): except PluginDaemonInnerError as e: raise ValueError(e.message + str(e.code)) + def start_llm_polling( + self, + tenant_id: str, + user_id: str | None, + plugin_id: str, + provider: str, + model: str, + credentials: dict[str, Any], + prompt_messages: list[PromptMessage], + model_parameters: dict[str, Any] | None = None, + tools: list[PromptMessageTool] | None = None, + stop: list[str] | None = None, + json_schema: dict[str, Any] | None = None, + ) -> LLMPollingResult: + """Start an LLM polling request for plugin-backed long-running jobs.""" + try: + return self._request_with_plugin_daemon_response( + method="POST", + path=f"plugin/{tenant_id}/dispatch/model/polling/start", + type_=LLMPollingResult, + data=jsonable_encoder( + self._dispatch_payload( + user_id=user_id, + data={ + "provider": provider, + "model_type": ModelType.LLM.value, + "model": model, + "credentials": credentials, + "prompt_messages": prompt_messages, + "model_parameters": model_parameters, + "tools": tools, + "stop": stop, + "stream": False, + "json_schema": json_schema, + }, + ) + ), + headers={ + "X-Plugin-ID": plugin_id, + "Content-Type": "application/json", + }, + ) + except PluginInvokeError as error: + self._raise_typed_polling_unsupported_error(error) + raise + + def check_llm_polling( + self, + tenant_id: str, + user_id: str | None, + plugin_id: str, + provider: str, + model: str, + credentials: dict[str, Any], + plugin_state: dict[str, Any], + ) -> LLMPollingResult: + """Check the latest state for a plugin-backed LLM polling job.""" + try: + return self._request_with_plugin_daemon_response( + method="POST", + path=f"plugin/{tenant_id}/dispatch/model/polling/check", + type_=LLMPollingResult, + data=jsonable_encoder( + self._dispatch_payload( + user_id=user_id, + data={ + "provider": provider, + "model_type": ModelType.LLM.value, + "model": model, + "credentials": credentials, + "plugin_state": plugin_state, + }, + ) + ), + headers={ + "X-Plugin-ID": plugin_id, + "Content-Type": "application/json", + }, + ) + except PluginInvokeError as error: + self._raise_typed_polling_unsupported_error(error) + raise + + @staticmethod + def _raise_typed_polling_unsupported_error(error: PluginInvokeError) -> None: + """Convert plugin polling capability failures into a dedicated Dify exception.""" + if error.get_error_type() == PluginLLMPollingUnsupportedError.__name__: + raise PluginLLMPollingUnsupportedError(description=error.description) from error + + if ( + error.get_error_type() in _POLLING_UNSUPPORTED_INVOKE_ERROR_TYPES + # This is ugly, we should not rely on error messages while checking + # error types. + and _POLLING_UNSUPPORTED_ERROR_MESSAGE in error.get_error_message().lower() + ): + raise PluginLLMPollingUnsupportedError(description=error.description) from error + def get_llm_num_tokens( self, tenant_id: str, diff --git a/api/core/plugin/impl/model_runtime.py b/api/core/plugin/impl/model_runtime.py index 3d5ba94f2bf..c1b976d8f4f 100644 --- a/api/core/plugin/impl/model_runtime.py +++ b/api/core/plugin/impl/model_runtime.py @@ -6,6 +6,7 @@ from collections.abc import Generator, Iterable, Sequence from typing import IO, Any, Literal, cast, overload, override from pydantic import ValidationError +from pydantic.json_schema import JsonValue from redis import RedisError from configs import dify_config @@ -17,6 +18,7 @@ from core.plugin.impl.model import PluginModelClient from core.plugin.plugin_service import PluginService from extensions.ext_redis import redis_client from graphon.model_runtime.entities.llm_entities import ( + LLMPollingResult, LLMResult, LLMResultChunk, LLMResultChunkWithStructuredOutput, @@ -430,6 +432,54 @@ class PluginModelRuntime(ModelRuntime): tools=list(tools) if tools else None, ) + def start_llm_polling( + self, + *, + provider: str, + model: str, + credentials: dict[str, Any], + model_parameters: dict[str, Any], + prompt_messages: Sequence[PromptMessage], + tools: Sequence[PromptMessageTool] | None, + stop: Sequence[str] | None, + json_schema: dict[str, Any] | None, + ) -> LLMPollingResult: + """Start a plugin-side polling job for long-running LLM invocations.""" + plugin_id, provider_name = self._split_provider(provider) + return self.client.start_llm_polling( + tenant_id=self.tenant_id, + user_id=self.user_id, + plugin_id=plugin_id, + provider=provider_name, + model=model, + credentials=credentials, + prompt_messages=list(prompt_messages), + model_parameters=model_parameters, + tools=list(tools) if tools else None, + stop=list(stop) if stop else None, + json_schema=json_schema, + ) + + def check_llm_polling( + self, + *, + provider: str, + model: str, + credentials: dict[str, Any], + plugin_state: dict[str, JsonValue], + ) -> LLMPollingResult: + """Check the latest plugin-side polling state for an LLM invocation.""" + plugin_id, provider_name = self._split_provider(provider) + return self.client.check_llm_polling( + tenant_id=self.tenant_id, + user_id=self.user_id, + plugin_id=plugin_id, + provider=provider_name, + model=model, + credentials=credentials, + plugin_state=plugin_state, + ) + @override def invoke_text_embedding( self, diff --git a/api/core/schemas/resolver.py b/api/core/schemas/resolver.py index cd86aebc060..6d959e0e87a 100644 --- a/api/core/schemas/resolver.py +++ b/api/core/schemas/resolver.py @@ -304,22 +304,23 @@ def _has_dify_refs_recursive(schema: SchemaType) -> bool: Returns: True if any Dify $ref is found, False otherwise """ - if isinstance(schema, dict): - # Check if this dict has a $ref field - ref_uri = schema.get("$ref") - if ref_uri and _is_dify_schema_ref(ref_uri): - return True - - # Check nested values - for value in schema.values(): - if _has_dify_refs_recursive(value): + match schema: + case dict(): + # Check if this dict has a $ref field + ref_uri = schema.get("$ref") + if ref_uri and _is_dify_schema_ref(ref_uri): return True - elif isinstance(schema, list): - # Check each item in the list - for item in schema: - if _has_dify_refs_recursive(item): - return True + # Check nested values + for value in schema.values(): + if _has_dify_refs_recursive(value): + return True + + case list(): + # Check each item in the list + for item in schema: + if _has_dify_refs_recursive(item): + return True # Primitive types don't contain refs return False diff --git a/api/core/tools/builtin_tool/providers/time/tools/localtime_to_timestamp.py b/api/core/tools/builtin_tool/providers/time/tools/localtime_to_timestamp.py index 1ebb7ab3a7f..57363349458 100644 --- a/api/core/tools/builtin_tool/providers/time/tools/localtime_to_timestamp.py +++ b/api/core/tools/builtin_tool/providers/time/tools/localtime_to_timestamp.py @@ -1,6 +1,6 @@ from collections.abc import Generator -from datetime import datetime -from typing import Any, override +from datetime import datetime, tzinfo +from typing import Any, cast, override import pytz # type: ignore[import-untyped] @@ -35,17 +35,26 @@ class LocaltimeToTimestampTool(BuiltinTool): yield self.create_text_message(f"{timestamp}") - # TODO: this method's type is messy @staticmethod - def localtime_to_timestamp(localtime: str, time_format: str, local_tz=None) -> int | None: + def localtime_to_timestamp(localtime: str, time_format: str, local_tz: str | tzinfo | None = None) -> int | None: try: local_time = datetime.strptime(localtime, time_format) - if local_tz is None: - localtime = local_time.astimezone() # type: ignore - elif isinstance(local_tz, str): - local_tz = pytz.timezone(local_tz) - localtime = local_tz.localize(local_time) # type: ignore - timestamp = int(localtime.timestamp()) # type: ignore + converted_localtime: datetime + match local_tz: + case None: + converted_localtime = local_time.astimezone() + case str() as timezone_name: + timezone = pytz.timezone(timezone_name) + converted_localtime = timezone.localize(local_time) + case tzinfo(): + localize = getattr(local_tz, "localize", None) + if callable(localize): + converted_localtime = cast(datetime, localize(local_time)) + else: + converted_localtime = local_time.replace(tzinfo=local_tz) + case _: + raise ValueError("local_tz must be None, a timezone name, or a tzinfo instance") + timestamp = int(converted_localtime.timestamp()) return timestamp except Exception as e: raise ToolInvokeError(str(e)) diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py index 7a1553a4b15..195acd6e1ad 100644 --- a/api/core/tools/mcp_tool/tool.py +++ b/api/core/tools/mcp_tool/tool.py @@ -122,13 +122,14 @@ class MCPTool(Tool): def _process_json_content(self, content_json: Any) -> Generator[ToolInvokeMessage, None, None]: """Process JSON content based on its type.""" - if isinstance(content_json, dict): - yield self.create_json_message(content_json) - elif isinstance(content_json, list): - yield from self._process_json_list(content_json) - else: - # For primitive types (str, int, bool, etc.), convert to string - yield self.create_text_message(str(content_json)) + match content_json: + case dict(): + yield self.create_json_message(content_json) + case list(): + yield from self._process_json_list(content_json) + case _: + # For primitive types (str, int, bool, etc.), convert to string + yield self.create_text_message(str(content_json)) def _process_json_list(self, json_list: list) -> Generator[ToolInvokeMessage, None, None]: """Process a list of JSON items.""" @@ -222,16 +223,17 @@ class MCPTool(Tool): # Recursively search through nested structures for value in payload.values(): - if isinstance(value, Mapping): - found = cls._extract_usage_dict(value) - if found is not None: - return found - elif isinstance(value, list) and not isinstance(value, (str, bytes, bytearray)): - for item in value: - if isinstance(item, Mapping): - found = cls._extract_usage_dict(item) - if found is not None: - return found + match value: + case _ if isinstance(value, Mapping): + found = cls._extract_usage_dict(value) + if found is not None: + return found + case list() if not isinstance(value, (str, bytes, bytearray)): + for item in value: + if isinstance(item, Mapping): + found = cls._extract_usage_dict(item) + if found is not None: + return found return None @override diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 7e7b1e33008..97222f3cfae 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -196,16 +196,17 @@ class WorkflowTool(Tool): return usage_candidate for value in payload.values(): - if isinstance(value, Mapping): - found = cls._extract_usage_dict(value) - if found is not None: - return found - elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): - for item in value: - if isinstance(item, Mapping): - found = cls._extract_usage_dict(item) - if found is not None: - return found + match value: + case _ if isinstance(value, Mapping): + found = cls._extract_usage_dict(value) + if found is not None: + return found + case _ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + for item in value: + if isinstance(item, Mapping): + found = cls._extract_usage_dict(item) + if found is not None: + return found return None @override @@ -393,24 +394,25 @@ class WorkflowTool(Tool): files: list[File] = [] result = {} for key, value in outputs.items(): - if isinstance(value, list): - for item in value: - if isinstance(item, dict) and item.get("dify_model_identity") == FILE_MODEL_IDENTITY: - item = self._update_file_mapping(item) - file = build_from_mapping( - mapping=item, - tenant_id=str(self.runtime.tenant_id), - access_controller=_file_access_controller, - ) - files.append(file) - elif isinstance(value, dict) and value.get("dify_model_identity") == FILE_MODEL_IDENTITY: - value = self._update_file_mapping(value) - file = build_from_mapping( - mapping=value, - tenant_id=str(self.runtime.tenant_id), - access_controller=_file_access_controller, - ) - files.append(file) + match value: + case list(): + for item in value: + if isinstance(item, dict) and item.get("dify_model_identity") == FILE_MODEL_IDENTITY: + item = self._update_file_mapping(item) + file = build_from_mapping( + mapping=item, + tenant_id=str(self.runtime.tenant_id), + access_controller=_file_access_controller, + ) + files.append(file) + case dict() if value.get("dify_model_identity") == FILE_MODEL_IDENTITY: + value = self._update_file_mapping(value) + file = build_from_mapping( + mapping=value, + tenant_id=str(self.runtime.tenant_id), + access_controller=_file_access_controller, + ) + files.append(file) result[key] = value diff --git a/api/core/trigger/entities/entities.py b/api/core/trigger/entities/entities.py index 71a8d8a4d68..06a29bed111 100644 --- a/api/core/trigger/entities/entities.py +++ b/api/core/trigger/entities/entities.py @@ -47,7 +47,7 @@ class EventParameter(BaseModel): template: PluginParameterTemplate | None = Field(default=None, description="The template of the parameter") scope: str | None = None required: bool = False - multiple: bool | None = Field( + multiple: bool = Field( default=False, description="Whether the parameter is multiple select, only valid for select or dynamic-select type", ) diff --git a/api/core/workflow/node_factory.py b/api/core/workflow/node_factory.py index 54c6c55949e..ebeb189ab19 100644 --- a/api/core/workflow/node_factory.py +++ b/api/core/workflow/node_factory.py @@ -26,6 +26,7 @@ from core.workflow.node_runtime import ( DifyFileReferenceFactory, DifyHumanInputNodeRuntime, DifyPreparedLLM, + DifyPreparedPollingLLM, DifyPromptMessageSerializer, DifyRetrieverAttachmentLoader, DifyToolFileManager, @@ -531,7 +532,11 @@ class DifyNodeFactory(NodeFactory): node_init_kwargs: dict[str, object] = { "credentials_provider": self._llm_credentials_provider, "model_factory": self._llm_model_factory, - "model_instance": DifyPreparedLLM(model_instance) if wrap_model_instance else model_instance, + "model_instance": ( + self._wrap_model_instance_for_node(node_data=validated_node_data, model_instance=model_instance) + if wrap_model_instance + else model_instance + ), "memory": self._build_memory_for_llm_node( node_data=validated_node_data, model_instance=model_instance, @@ -555,6 +560,23 @@ class DifyNodeFactory(NodeFactory): node_init_kwargs["default_query_selector"] = system_variable_selector(SystemVariableKey.QUERY) return node_init_kwargs + @staticmethod + def _wrap_model_instance_for_node( + *, + node_data: LLMCompatibleNodeData, + model_instance: ModelInstance, + ) -> DifyPreparedLLM: + # Only graphon's LLM node consumes the polling protocol. Keep classifier + # and extractor nodes on the existing wrapper even if the same model + # advertises polling support. + if node_data.type == BuiltinNodeTypes.LLM and DifyNodeFactory._supports_plugin_llm_polling(model_instance): + return DifyPreparedPollingLLM(model_instance) + return DifyPreparedLLM(model_instance) + + @staticmethod + def _supports_plugin_llm_polling(model_instance: ModelInstance) -> bool: + return model_instance.get_model_schema().support_polling + def _build_retriever_attachment_loader(self, node_data: LLMNodeData) -> DifyRetrieverAttachmentLoader: return DifyRetrieverAttachmentLoader( file_reference_factory=self._file_reference_factory, diff --git a/api/core/workflow/node_runtime.py b/api/core/workflow/node_runtime.py index 4eced02cd10..9964f65d0b6 100644 --- a/api/core/workflow/node_runtime.py +++ b/api/core/workflow/node_runtime.py @@ -4,6 +4,7 @@ from collections.abc import Callable, Generator, Mapping, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal, cast, overload, override +from pydantic import JsonValue from sqlalchemy import select from sqlalchemy.orm import Session @@ -38,6 +39,7 @@ from factories import file_factory from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities import LLMMode from graphon.model_runtime.entities.llm_entities import ( + LLMPollingResult, LLMResult, LLMResultChunk, LLMResultChunkWithStructuredOutput, @@ -54,6 +56,7 @@ from graphon.nodes.human_input.entities import ( HumanInputNodeData, ) from graphon.nodes.llm.runtime_protocols import ( + LLMPollingCapableProtocol, LLMProtocol, PromptMessageSerializerProtocol, RetrieverAttachmentLoaderProtocol, @@ -278,6 +281,58 @@ class DifyPreparedLLM(LLMProtocol): return isinstance(error, OutputParserError) +class DifyPreparedPollingLLM(DifyPreparedLLM, LLMPollingCapableProtocol): + """Prepared workflow LLM adapter that exposes Graphon's polling protocol.""" + + def __init__(self, model_instance: ModelInstance) -> None: + from core.plugin.impl.model_runtime import PluginModelRuntime + + super().__init__(model_instance) + model_type_instance = model_instance.model_type_instance + if not isinstance(model_type_instance, LargeLanguageModel): + raise TypeError("Polling wrapper requires a large-language-model instance.") + + plugin_model_runtime = model_type_instance.model_runtime + if not isinstance(plugin_model_runtime, PluginModelRuntime): + raise TypeError("Polling wrapper requires a plugin-backed model runtime.") + + self._plugin_model_runtime = plugin_model_runtime + + @override + def start_llm_polling( + self, + *, + prompt_messages: Sequence[PromptMessage], + model_parameters: Mapping[str, Any], + tools: Sequence[PromptMessageTool] | None, + stop: Sequence[str] | None, + json_schema: Mapping[str, Any] | None, + ) -> LLMPollingResult: + return self._plugin_model_runtime.start_llm_polling( + provider=self.provider, + model=self.model_name, + credentials=self._model_instance.credentials, + prompt_messages=prompt_messages, + model_parameters=dict(model_parameters), + tools=tools, + stop=stop, + json_schema=dict(json_schema) if json_schema is not None else None, + ) + + @override + def check_llm_polling( + self, + *, + plugin_state: Mapping[str, JsonValue], + ) -> LLMPollingResult: + return self._plugin_model_runtime.check_llm_polling( + provider=self.provider, + model=self.model_name, + credentials=self._model_instance.credentials, + plugin_state=dict(plugin_state), + ) + + class DifyPromptMessageSerializer(PromptMessageSerializerProtocol): @override def serialize( diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index 1d938dd04c5..6f1660390d3 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -297,25 +297,26 @@ class KnowledgeRetrievalNode(LLMUsageTrackingMixin, Node[KnowledgeRetrievalNodeD for cond in conditions.conditions or []: value = cond.value resolved_value: str | Sequence[str] | int | float | None - if isinstance(value, str): - segment_group = variable_pool.convert_template(value) - if len(segment_group.value) == 1: - resolved_value = _normalize_metadata_filter_scalar(segment_group.value[0].to_object()) - else: - resolved_value = segment_group.text - elif isinstance(value, Sequence) and all(isinstance(v, str) for v in value): - resolved_values: list[str] = [] - for v in value: - segment_group = variable_pool.convert_template(v) + match value: + case str(): + segment_group = variable_pool.convert_template(value) if len(segment_group.value) == 1: - resolved_values.append( - _normalize_metadata_filter_sequence_item(segment_group.value[0].to_object()) - ) + resolved_value = _normalize_metadata_filter_scalar(segment_group.value[0].to_object()) else: - resolved_values.append(segment_group.text) - resolved_value = resolved_values - else: - resolved_value = value + resolved_value = segment_group.text + case _ if isinstance(value, Sequence) and all(isinstance(v, str) for v in value): + resolved_values: list[str] = [] + for v in value: + segment_group = variable_pool.convert_template(v) + if len(segment_group.value) == 1: + resolved_values.append( + _normalize_metadata_filter_sequence_item(segment_group.value[0].to_object()) + ) + else: + resolved_values.append(segment_group.text) + resolved_value = resolved_values + case _: + resolved_value = value resolved_conditions.append( Condition( name=cond.name, diff --git a/api/dev/generate_swagger_markdown_docs.py b/api/dev/generate_swagger_markdown_docs.py index 7028f740e02..72bc56daf87 100644 --- a/api/dev/generate_swagger_markdown_docs.py +++ b/api/dev/generate_swagger_markdown_docs.py @@ -167,6 +167,12 @@ def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str: return markdown +def _strip_trailing_line_whitespace(markdown: str) -> str: + """Remove converter-emitted trailing whitespace without changing line structure.""" + + return "\n".join(line.rstrip(" \t") for line in markdown.split("\n")) + + def _convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None: markdown_path.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=f"{markdown_path.stem}-") as temp_dir: @@ -201,6 +207,7 @@ def _convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None: temp_markdown_path.read_text(encoding="utf-8"), spec_path, ) + converted_markdown = _strip_trailing_line_whitespace(converted_markdown) if not converted_markdown.strip(): raise RuntimeError(f"swagger-markdown wrote an empty document for {markdown_path}") diff --git a/api/dev/generate_swagger_specs.py b/api/dev/generate_swagger_specs.py index d3b62511ea6..868f9e87776 100644 --- a/api/dev/generate_swagger_specs.py +++ b/api/dev/generate_swagger_specs.py @@ -104,11 +104,14 @@ def _field_signature(field: object) -> object: "description", "example", "max", + "max_items", "min", + "min_items", "nullable", "readonly", "required", "title", + "unique", ): if hasattr(field_instance, attr_name): signature[attr_name] = _jsonable_schema_value(getattr(field_instance, attr_name)) @@ -154,9 +157,9 @@ def create_spec_app() -> Flask: apply_runtime_defaults() - from libs.flask_restx_compat import patch_swagger_for_inline_nested_dicts + from libs.flask_restx_compat import install_swagger_compatibility - patch_swagger_for_inline_nested_dicts() + install_swagger_compatibility() app = Flask(__name__) diff --git a/api/dev/lint_response_contracts.py b/api/dev/lint_response_contracts.py index 4ba79e0fedb..75c5f67b8ff 100644 --- a/api/dev/lint_response_contracts.py +++ b/api/dev/lint_response_contracts.py @@ -354,11 +354,12 @@ def iter_method_nodes(method: MethodNode) -> Iterable[ast.AST]: def target_names(target: ast.AST) -> Iterable[str]: - if isinstance(target, ast.Name): - yield target.id - elif isinstance(target, ast.Tuple | ast.List): - for item in target.elts: - yield from target_names(item) + match target: + case ast.Name(): + yield target.id + case ast.Tuple() | ast.List(): + for item in target.elts: + yield from target_names(item) def record_assignment( diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index 724e5ecf7db..1dc83ab3aff 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -107,17 +107,58 @@ class AgentInviteOptionsResponse(ResponseModel): has_more: bool -class AgentLogItemResponse(ResponseModel): +class AgentLogSourceResponse(ResponseModel): + id: str + type: Literal["webapp", "workflow"] + app_id: str + app_name: str + app_icon_type: str | None = None + app_icon: str | None = None + app_icon_background: str | None = None + workflow_id: str | None = None + workflow_version: str | None = None + node_id: str | None = None + + +class AgentLogSourceGroupResponse(ResponseModel): + type: Literal["webapp", "workflow"] + label: str + sources: list[AgentLogSourceResponse] = Field(default_factory=list) + + +class AgentLogSourceListResponse(ResponseModel): + data: list[AgentLogSourceResponse] + groups: list[AgentLogSourceGroupResponse] + + +class AgentLogConversationItemResponse(ResponseModel): + id: str + conversation_id: str + title: str | None = None + end_user_id: str | None = None + message_count: int + user_rate: float | None = None + operation_rate: float | None = None + unread: bool + source: AgentLogSourceResponse | None = None + status: Literal["success", "failed", "paused"] + created_at: int | None = None + updated_at: int | None = None + + @field_validator("created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return to_timestamp(value) + + +class AgentLogMessageItemResponse(ResponseModel): id: str message_id: str conversation_id: str - conversation_name: str | None = None query: str answer: str status: str error: str | None = None - source: str | None = None - from_source: str | None = None from_end_user_id: str | None = None from_account_id: str | None = None message_tokens: int @@ -136,7 +177,15 @@ class AgentLogItemResponse(ResponseModel): class AgentLogListResponse(ResponseModel): - data: list[AgentLogItemResponse] + data: list[AgentLogConversationItemResponse] + page: int + limit: int + total: int + has_more: bool + + +class AgentLogMessageListResponse(ResponseModel): + data: list[AgentLogMessageItemResponse] page: int limit: int total: int diff --git a/api/libs/broadcast_channel/redis/_subscription.py b/api/libs/broadcast_channel/redis/_subscription.py index 15355a77620..5af42d12538 100644 --- a/api/libs/broadcast_channel/redis/_subscription.py +++ b/api/libs/broadcast_channel/redis/_subscription.py @@ -94,12 +94,13 @@ class RedisSubscriptionBase(Subscription): continue channel_field = raw_message.get("channel") - if isinstance(channel_field, bytes): - channel_name = channel_field.decode("utf-8") - elif isinstance(channel_field, str): - channel_name = channel_field - else: - channel_name = str(channel_field) + match channel_field: + case bytes(): + channel_name = channel_field.decode("utf-8") + case str(): + channel_name = channel_field + case _: + channel_name = str(channel_field) if channel_name != self._topic: _logger.warning( diff --git a/api/libs/broadcast_channel/redis/sharded_channel.py b/api/libs/broadcast_channel/redis/sharded_channel.py index a7303c07823..68e9f8b23ef 100644 --- a/api/libs/broadcast_channel/redis/sharded_channel.py +++ b/api/libs/broadcast_channel/redis/sharded_channel.py @@ -88,22 +88,23 @@ class _RedisShardedSubscription(RedisSubscriptionBase): # # Since we have already filtered at the caller's site, we can safely set # `ignore_subscribe_messages=False`. - if isinstance(self._client, RedisCluster): - # NOTE(QuantumGhost): due to an issue in upstream code, calling `get_sharded_message` without - # specifying the `target_node` argument would use busy-looping to wait - # for incoming message, consuming excessive CPU quota. - # - # Here we specify the `target_node` to mitigate this problem. - node = self._client.get_node_from_key(self._topic) - return self._pubsub.get_sharded_message( # type: ignore[attr-defined] - ignore_subscribe_messages=False, - timeout=1, - target_node=node, - ) - elif isinstance(self._client, Redis): - return self._pubsub.get_sharded_message(ignore_subscribe_messages=False, timeout=1) # type: ignore[attr-defined] - else: - raise AssertionError("client should be either Redis or RedisCluster.") + match self._client: + case RedisCluster(): + # NOTE(QuantumGhost): due to an issue in upstream code, calling `get_sharded_message` without + # specifying the `target_node` argument would use busy-looping to wait + # for incoming message, consuming excessive CPU quota. + # + # Here we specify the `target_node` to mitigate this problem. + node = self._client.get_node_from_key(self._topic) + return self._pubsub.get_sharded_message( # type: ignore[attr-defined] + ignore_subscribe_messages=False, + timeout=1, + target_node=node, + ) + case Redis(): + return self._pubsub.get_sharded_message(ignore_subscribe_messages=False, timeout=1) # type: ignore[attr-defined] + case _: + raise AssertionError("client should be either Redis or RedisCluster.") @override def _get_message_type(self) -> str: diff --git a/api/libs/broadcast_channel/redis/streams_channel.py b/api/libs/broadcast_channel/redis/streams_channel.py index 30c14585793..62e58798ab3 100644 --- a/api/libs/broadcast_channel/redis/streams_channel.py +++ b/api/libs/broadcast_channel/redis/streams_channel.py @@ -138,10 +138,11 @@ class _StreamsSubscription(Subscription): if isinstance(fields, dict): data = fields.get(b"data") data_bytes: bytes | None = None - if isinstance(data, str): - data_bytes = data.encode() - elif isinstance(data, (bytes, bytearray)): - data_bytes = bytes(data) + match data: + case str(): + data_bytes = data.encode() + case bytes() | bytearray(): + data_bytes = bytes(data) if data_bytes is not None: self._queue.put_nowait(data_bytes) last_id = entry_id diff --git a/api/libs/external_api.py b/api/libs/external_api.py index 43f7c409f5b..06419b16f88 100644 --- a/api/libs/external_api.py +++ b/api/libs/external_api.py @@ -9,7 +9,7 @@ from werkzeug.http import HTTP_STATUS_CODES from configs import dify_config from core.errors.error import AppInvokeQuotaExceededError -from libs.flask_restx_compat import patch_swagger_for_inline_nested_dicts +from libs.flask_restx_compat import install_swagger_compatibility from libs.token import build_force_logout_cookie_headers @@ -127,16 +127,22 @@ def register_external_error_handlers(api: Api, body_formatter: ErrorBodyFormatte class ExternalApi(Api): _authorizations = { "Bearer": { - "type": "apiKey", - "in": "header", - "name": "Authorization", - "description": "Type: Bearer {your-api-key}", + "bearerFormat": "API_KEY", + "description": "Use the Service API key as a Bearer token in the Authorization header.", + "scheme": "bearer", + "type": "http", } } - def __init__(self, app: Blueprint | Flask, *args, error_body_formatter: ErrorBodyFormatter | None = None, **kwargs): + def __init__( + self, + app: Blueprint | Flask, + *args, + error_body_formatter: ErrorBodyFormatter | None = None, + **kwargs, + ): self._error_body_formatter = error_body_formatter - patch_swagger_for_inline_nested_dicts() + install_swagger_compatibility() kwargs.setdefault("authorizations", self._authorizations) kwargs.setdefault("security", "Bearer") kwargs["add_specs"] = dify_config.SWAGGER_UI_ENABLED diff --git a/api/libs/flask_restx_compat.py b/api/libs/flask_restx_compat.py index 08fd3d9055d..a442be01d84 100644 --- a/api/libs/flask_restx_compat.py +++ b/api/libs/flask_restx_compat.py @@ -8,12 +8,14 @@ spec export fail or succeed in the same way. import hashlib import json -from typing import TypeGuard +from typing import TypeGuard, cast from flask import current_app from flask_restx import fields +from flask_restx import swagger as restx_swagger from flask_restx.model import Model, OrderedModel, instance from flask_restx.swagger import Swagger +from flask_restx.utils import not_none def _is_inline_field_map(value: object) -> TypeGuard[dict[object, object]]: @@ -98,20 +100,28 @@ def _inline_model_name(nested_fields: dict[object, object]) -> str: return f"_AnonymousInlineModel_{digest}" -def patch_swagger_for_inline_nested_dicts() -> None: - """Allow OpenAPI generation to handle legacy inline Flask-RESTX field dicts. +def install_swagger_compatibility() -> None: + """Install Dify's Flask-RESTX OpenAPI compatibility hooks. Some existing controllers use raw field mappings in `fields.Nested({...})` or directly in `@namespace.response(...)`. Runtime marshalling accepts that, but Flask-RESTX registration expects a named model. Convert those anonymous mappings into temporary named models during docs generation. + + Flask-RESTX also drops parameter descriptions from generated schemas and + does not expose the Werkzeug `uuid` route converter as `format: uuid`. """ - if getattr(Swagger, "_dify_inline_nested_dict_patch", False): + if getattr(Swagger, "_dify_swagger_compatibility_installed", False): return original_register_model = Swagger.register_model original_register_field = Swagger.register_field + original_extract_path_params = restx_swagger.extract_path_params + original_schema_from_parameter = Swagger.schema_from_parameter + original_description_for = Swagger.description_for + original_serialize_operation = Swagger.serialize_operation + original_parameters_and_request_body_for = Swagger.parameters_and_request_body_for original_as_dict = Swagger.as_dict def get_or_create_inline_model(self: Swagger, nested_fields: dict[object, object]) -> object: @@ -134,6 +144,65 @@ def patch_swagger_for_inline_nested_dicts() -> None: original_register_field(self, field) + def schema_from_parameter_with_description(self: Swagger, param: dict[str, object]) -> dict[str, object]: + schema = cast(dict[str, object], original_schema_from_parameter(self, param)) + description = param.get("description") + if isinstance(description, str): + schema["description"] = description + return schema + + def extract_path_params_with_uuid_format(path: str): + params = original_extract_path_params(path) + for converter, _arguments, variable in restx_swagger.parse_rule(path): + if converter == "uuid" and variable in params: + params[variable]["format"] = "uuid" + return params + + def description_for_with_explicit_summary(self: Swagger, doc: dict[str, object], method: str): + method_doc = doc.get(method) + if ( + isinstance(method_doc, dict) + and isinstance(method_doc.get("summary"), str) + and isinstance(method_doc.get("description"), str) + ): + return method_doc["description"] + return original_description_for(self, doc, method) + + def serialize_operation_with_explicit_summary_tags( + self: Swagger, doc: dict[str, object], method: str, inherited_request_body=None + ): + operation = original_serialize_operation(self, doc, method, inherited_request_body) + method_doc = doc.get(method) + if not isinstance(method_doc, dict): + return operation + + summary = method_doc.get("summary") + if isinstance(summary, str): + operation["summary"] = summary + + tags = method_doc.get("tags") + if isinstance(tags, list) and all(isinstance(tag, str) for tag in tags): + operation["tags"] = tags + + return operation + + def serialize_resource_with_explicit_operation_tags(self: Swagger, ns, resource, url, route_doc=None, **kwargs): + doc = self.extract_resource_doc(resource, url, route_doc=route_doc) + if doc is False: + return None + + path_params, path_request_body = original_parameters_and_request_body_for(self, doc) + path: dict[str, object] = {"parameters": path_params or None} + methods = [method.lower() for method in resource.methods or []] + requested_methods = [method.lower() for method in kwargs.get("methods", [])] + for method in methods: + if doc[method] is False or requested_methods and method not in requested_methods: + continue + operation = self.serialize_operation(doc, method, path_request_body) + operation.setdefault("tags", [ns.name]) + path[method] = operation + return not_none(path) + def as_dict_with_inline_dict_support(self: Swagger): # Temporary set RESTX_INCLUDE_ALL_MODELS = false to prevent "length changed while iterating" error include_all_models = current_app.config.get("RESTX_INCLUDE_ALL_MODELS", False) @@ -145,5 +214,10 @@ def patch_swagger_for_inline_nested_dicts() -> None: Swagger.register_model = register_model_with_inline_dict_support Swagger.register_field = register_field_with_inline_dict_support + restx_swagger.extract_path_params = extract_path_params_with_uuid_format + Swagger.schema_from_parameter = schema_from_parameter_with_description + Swagger.description_for = description_for_with_explicit_summary + Swagger.serialize_operation = serialize_operation_with_explicit_summary_tags + Swagger.serialize_resource = serialize_resource_with_explicit_operation_tags Swagger.as_dict = as_dict_with_inline_dict_support - Swagger._dify_inline_nested_dict_patch = True + Swagger._dify_swagger_compatibility_installed = True diff --git a/api/models/model.py b/api/models/model.py index 09809b85f6b..69d2a4a7f19 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -1174,34 +1174,32 @@ class Conversation(Base): # Convert file mapping to File object for key, value in inputs.items(): - if ( - isinstance(value, dict) - and cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY - ): - value_dict = cast(dict[str, Any], value) - inputs[key] = build_file_from_input_mapping( - file_mapping=value_dict, - tenant_resolver=tenant_resolver, - ) - elif isinstance(value, list): - value_list = value - if all( - isinstance(item, dict) - and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY - for item in value_list - ): - file_list: list[File] = [] - for item in value_list: - if not isinstance(item, dict): - continue - item_dict = cast(dict[str, Any], item) - file_list.append( - build_file_from_input_mapping( - file_mapping=item_dict, - tenant_resolver=tenant_resolver, + match value: + case dict() if cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY: + value_dict = cast(dict[str, Any], value) + inputs[key] = build_file_from_input_mapping( + file_mapping=value_dict, + tenant_resolver=tenant_resolver, + ) + case list(): + value_list = value + if all( + isinstance(item, dict) + and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY + for item in value_list + ): + file_list: list[File] = [] + for item in value_list: + if not isinstance(item, dict): + continue + item_dict = cast(dict[str, Any], item) + file_list.append( + build_file_from_input_mapping( + file_mapping=item_dict, + tenant_resolver=tenant_resolver, + ) ) - ) - inputs[key] = file_list + inputs[key] = file_list return inputs @@ -1516,46 +1514,45 @@ class Message(Base): owner_tenant_id=cast(str | None, getattr(self, "_owner_tenant_id", None)), ) for key, value in inputs.items(): - if ( - isinstance(value, dict) - and cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY - ): - value_dict = cast(dict[str, Any], value) - inputs[key] = build_file_from_input_mapping( - file_mapping=value_dict, - tenant_resolver=tenant_resolver, - ) - elif isinstance(value, list): - value_list = value - if all( - isinstance(item, dict) - and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY - for item in value_list - ): - file_list: list[File] = [] - for item in value_list: - if not isinstance(item, dict): - continue - item_dict = cast(dict[str, Any], item) - file_list.append( - build_file_from_input_mapping( - file_mapping=item_dict, - tenant_resolver=tenant_resolver, + match value: + case dict() if cast(dict[str, Any], value).get("dify_model_identity") == FILE_MODEL_IDENTITY: + value_dict = cast(dict[str, Any], value) + inputs[key] = build_file_from_input_mapping( + file_mapping=value_dict, + tenant_resolver=tenant_resolver, + ) + case list(): + value_list = value + if all( + isinstance(item, dict) + and cast(dict[str, Any], item).get("dify_model_identity") == FILE_MODEL_IDENTITY + for item in value_list + ): + file_list: list[File] = [] + for item in value_list: + if not isinstance(item, dict): + continue + item_dict = cast(dict[str, Any], item) + file_list.append( + build_file_from_input_mapping( + file_mapping=item_dict, + tenant_resolver=tenant_resolver, + ) ) - ) - inputs[key] = file_list + inputs[key] = file_list return inputs @inputs.setter def inputs(self, value: Mapping[str, Any]): inputs = dict(value) for k, v in inputs.items(): - if isinstance(v, File): - inputs[k] = v.model_dump() - elif isinstance(v, list): - v_list = v - if all(isinstance(item, File) for item in v_list): - inputs[k] = [item.model_dump() for item in v_list if isinstance(item, File)] + match v: + case File(): + inputs[k] = v.model_dump() + case list(): + v_list = v + if all(isinstance(item, File) for item in v_list): + inputs[k] = [item.model_dump() for item in v_list if isinstance(item, File)] self._inputs = inputs @property diff --git a/api/models/types.py b/api/models/types.py index 092db638565..c5a9231ad4a 100644 --- a/api/models/types.py +++ b/api/models/types.py @@ -96,12 +96,13 @@ class JSONModelColumn[T: BaseModel](TypeDecorator[T | None]): def process_bind_param(self, value: T | dict[str, Any] | str | None, dialect: Dialect) -> str | None: if value is None: return None - if isinstance(value, self._model_class): - model = value - elif isinstance(value, str): - model = self._model_class.model_validate_json(value) - else: - model = self._model_class.model_validate(value) + match value: + case _ if isinstance(value, self._model_class): + model = value + case str(): + model = self._model_class.model_validate_json(value) + case _: + model = self._model_class.model_validate(value) return json.dumps(model.model_dump(mode="json"), ensure_ascii=False, sort_keys=True, separators=(",", ":")) @override diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index b5dd329c80c..70c7a1aa9f1 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -4,10 +4,9 @@ Console management APIs for app configuration, monitoring, and administration ## Version: 1.0 ### Available authorizations -#### Bearer (API Key Authentication) -Type: Bearer {your-api-key} -**Name:** Authorization -**In:** header +#### Bearer (HTTP, bearer) +Use the Service API key as a Bearer token in the Authorization header. +Bearer format: API_KEY --- ## console @@ -324,7 +323,7 @@ Check if activation token is valid | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Agent app created successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 201 | Agent app created successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -349,7 +348,7 @@ Check if activation token is valid | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -363,20 +362,20 @@ Check if activation token is valid | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app detail | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 200 | Agent app detail | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| ### [PUT] /agent/{agent_id} #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Request Body @@ -388,7 +387,7 @@ Check if activation token is valid | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app updated successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 200 | Agent app updated successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -399,7 +398,7 @@ Get Agent App chat messages for a conversation with pagination | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | conversation_id | query | Conversation ID | Yes | string | | first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | @@ -418,8 +417,8 @@ Get suggested questions for an Agent App message | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | -| message_id | path | Message ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | +| message_id | path | Message ID | Yes | string (uuid) | #### Responses @@ -435,7 +434,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | task_id | path | Task ID to stop | Yes | string | #### Responses @@ -449,7 +448,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -462,7 +461,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Request Body @@ -481,7 +480,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -494,7 +493,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Request Body @@ -513,7 +512,7 @@ Stop a running Agent App chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Request Body @@ -525,7 +524,7 @@ Stop a running Agent App chat message generation | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Agent app copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 201 | Agent app copied successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -536,7 +535,7 @@ List agent drive entries for an Agent App | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | prefix | query | Key prefix filter: '/' for one skill, 'files/' for files | No | string | #### Responses @@ -552,7 +551,7 @@ Time-limited external signed URL for one Agent App drive value | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string | #### Responses @@ -568,7 +567,7 @@ Truncated text preview of one Agent App drive value | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string | #### Responses @@ -584,7 +583,7 @@ Update an Agent App's presentation features (opener, follow-up, citations, ...) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | #### Request Body @@ -607,7 +606,7 @@ Create or update Agent App message feedback | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | #### Request Body @@ -629,7 +628,7 @@ Delete one Agent App drive file by key | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | key | query | Drive key, e.g. files/sample.pdf | Yes | string | #### Responses @@ -645,7 +644,7 @@ Commit an uploaded file into the Agent App drive under files/ | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | #### Request Body @@ -659,6 +658,19 @@ Commit an uploaded file into the Agent App drive under files/ | ---- | ----------- | ------ | | 201 | File committed into the agent drive | **application/json**: [AgentDriveFileCommitResponse](#agentdrivefilecommitresponse)
| +### [GET] /agent/{agent_id}/log-sources +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent log sources | **application/json**: [AgentLogSourceListResponse](#agentlogsourcelistresponse)
| + ### [GET] /agent/{agent_id}/logs #### Parameters @@ -671,7 +683,7 @@ Commit an uploaded file into the Agent App drive under files/ | source | query | Filter by all, console/explore, api/service-api, web-app, debugger, openapi, or trigger | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | | status | query | Filter by success, failed, or paused | No | string | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -679,6 +691,27 @@ Commit an uploaded file into the Agent App drive under files/ | ---- | ----------- | ------ | | 200 | Agent logs | **application/json**: [AgentLogListResponse](#agentloglistresponse)
| +### [GET] /agent/{agent_id}/logs/{conversation_id}/messages +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| keyword | query | Search query, answer, or conversation name | No | string | +| limit | query | Page size | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | +| source | query | Filter by all, console/explore, api/service-api, web-app, debugger, openapi, or trigger | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | +| status | query | Filter by success, failed, or paused | No | string | +| agent_id | path | | Yes | string (uuid) | +| conversation_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent log messages | **application/json**: [AgentLogMessageListResponse](#agentlogmessagelistresponse)
| + ### [GET] /agent/{agent_id}/messages/{message_id} Get Agent App message details by ID @@ -686,8 +719,8 @@ Get Agent App message details by ID | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | -| message_id | path | Message ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | +| message_id | path | Message ID | Yes | string (uuid) | #### Responses @@ -703,7 +736,7 @@ List workflow apps that reference this Agent App's bound Agent (read-only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | #### Responses @@ -719,7 +752,7 @@ List a directory in an Agent App conversation sandbox | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | conversation_id | query | Agent App conversation ID | Yes | string | | path | query | Directory path relative to the sandbox workspace | No | string,
**Default:** . | @@ -736,7 +769,7 @@ Read a text/binary preview file in an Agent App conversation sandbox | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | conversation_id | query | Agent App conversation ID | Yes | string | | path | query | File path relative to the sandbox workspace | Yes | string | @@ -753,7 +786,7 @@ Upload one Agent App sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Request Body @@ -767,37 +800,27 @@ Upload one Agent App sandbox file as a Dify ToolFile mapping | ---- | ----------- | ------ | | 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)
| -### [POST] /agent/{agent_id}/skills/standardize -Validate + standardize a Skill into an Agent App drive - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)
| -| 400 | Invalid skill package or no bound agent | | - ### [POST] /agent/{agent_id}/skills/upload -Upload + validate a Skill package for an Agent App +Upload + standardize a Skill into an Agent App drive #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary }
| #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| -| 400 | Invalid skill package | | +| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| +| 400 | Invalid skill package or no bound agent | | ### [DELETE] /agent/{agent_id}/skills/{slug} Delete a standardized skill from an Agent App drive @@ -806,7 +829,7 @@ Delete a standardized skill from an Agent App drive | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | slug | path | Skill slug (single path segment) | Yes | string | #### Responses @@ -822,7 +845,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string | +| agent_id | path | Agent ID | Yes | string (uuid) | | slug | path | Skill slug (single path segment) | Yes | string | #### Responses @@ -839,7 +862,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | source | query | Filter by all, console/explore, api/service-api, web-app, debugger, openapi, or trigger | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -852,7 +875,7 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | #### Responses @@ -865,8 +888,8 @@ Infer CLI tool + ENV suggestions from a standardized Agent App skill | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | -| version_id | path | | Yes | string | +| agent_id | path | | Yes | string (uuid) | +| version_id | path | | Yes | string (uuid) | #### Responses @@ -919,7 +942,7 @@ Delete API-based extension | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| id | path | Extension ID | Yes | string | +| id | path | Extension ID | Yes | string (uuid) | #### Responses @@ -934,7 +957,7 @@ Get API-based extension by ID | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| id | path | Extension ID | Yes | string | +| id | path | Extension ID | Yes | string (uuid) | #### Responses @@ -949,7 +972,7 @@ Update API-based extension | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| id | path | Extension ID | Yes | string | +| id | path | Extension ID | Yes | string (uuid) | #### Request Body @@ -988,7 +1011,7 @@ Update API-based extension | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| binding_id | path | | Yes | string | +| binding_id | path | | Yes | string (uuid) | #### Responses @@ -1157,7 +1180,7 @@ Delete application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1175,7 +1198,7 @@ Get application details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1192,7 +1215,7 @@ Update application details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1217,7 +1240,7 @@ Get advanced chat workflow run list | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | last_id | query | Last run ID for pagination | No | string | | limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | | status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | @@ -1236,7 +1259,7 @@ Get advanced chat workflow run list | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | | time_range | query | Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), 30m (30 minutes), 30s (30 seconds). Filters by created_at field. | No | string | | triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | @@ -1256,7 +1279,7 @@ Get human input form preview for advanced chat workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -1280,7 +1303,7 @@ Submit human input form preview for advanced chat workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -1304,7 +1327,7 @@ Run draft workflow iteration node for advanced chat | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -1330,7 +1353,7 @@ Run draft workflow loop node for advanced chat | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -1356,7 +1379,7 @@ Run draft workflow for advanced chat application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1379,7 +1402,7 @@ List agent drive entries (read-only inspector; one endpoint for both tabs) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | query | Workflow node ID (workflow composer variant) | No | string | | prefix | query | Key prefix filter: '/' for one skill, 'files/' for files | No | string | @@ -1396,7 +1419,7 @@ Time-limited external signed URL for one drive value (no streaming proxy) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string | | node_id | query | Workflow node ID (workflow composer variant) | No | string | @@ -1413,7 +1436,7 @@ Truncated text preview of one drive value (binary-safe; SKILL.md is the main cas | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | key | query | Drive key, e.g. tender-analyzer/SKILL.md | Yes | string | | node_id | query | Workflow node ID (workflow composer variant) | No | string | @@ -1430,7 +1453,7 @@ Delete one drive file by key; soul ref first, then the KV row (ENG-625 D5) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | key | query | Drive key, e.g. files/sample.pdf | Yes | string | | node_id | query | Workflow node ID (workflow composer variant) | No | string | @@ -1449,7 +1472,7 @@ Commit an uploaded file into the agent drive under files/ (ENG-625 D3) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | query | Workflow node ID (workflow composer variant) | No | string | #### Request Body @@ -1473,7 +1496,7 @@ Get agent execution logs for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | conversation_id | query | Conversation UUID | Yes | string | | message_id | query | Message UUID | Yes | string | @@ -1484,45 +1507,31 @@ Get agent execution logs for an application | 200 | Agent logs retrieved successfully | **application/json**: [AgentLogResponse](#agentlogresponse)
| | 400 | Invalid request parameters | | -### [POST] /apps/{app_id}/agent/skills/standardize -**Upload a Skill, validate it, and standardize it into the app agent's drive** +### [POST] /apps/{app_id}/agent/skills/upload +**Upload a Skill, validate it, and commit drive-backed skill files** -Validate + standardize a Skill into the agent drive (ENG-594) +Upload + standardize a Skill into the agent drive #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | query | Workflow node ID (workflow composer variant) | No | string | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary }
| + #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)
| +| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| | 400 | Invalid skill package or no bound agent | | -### [POST] /apps/{app_id}/agent/skills/upload -**Validate an uploaded Skill package and persist the archive** - -Upload + validate a Skill package (.zip/.skill) and extract its manifest -Returns a validated skill ref (to bind into the Agent soul config on save) -plus its manifest. Standardizing into the agent drive is ENG-594. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| -| 400 | Invalid skill package | | - ### [DELETE] /apps/{app_id}/agent/skills/{slug} Delete a standardized skill: soul ref first, then the / drive prefix (ENG-625 D5) @@ -1530,7 +1539,7 @@ Delete a standardized skill: soul ref first, then the / drive prefix (ENG- | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | slug | path | Skill slug (single path segment) | Yes | string | | node_id | query | Workflow node ID (workflow composer variant) | No | string | @@ -1550,7 +1559,7 @@ Saving still goes through composer validation. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | slug | path | Skill slug (single path segment) | Yes | string | | node_id | query | Workflow node ID (workflow composer variant) | No | string | @@ -1568,7 +1577,7 @@ Enable or disable annotation reply for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action to perform (enable/disable) | Yes | string | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1591,8 +1600,8 @@ Get status of annotation reply action job | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action type | Yes | string | -| app_id | path | Application ID | Yes | string | -| job_id | path | Job ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| job_id | path | Job ID | Yes | string (uuid) | #### Responses @@ -1608,7 +1617,7 @@ Get annotation settings for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1624,8 +1633,8 @@ Update annotation settings for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_setting_id | path | Annotation setting ID | Yes | string | -| app_id | path | Application ID | Yes | string | +| annotation_setting_id | path | Annotation setting ID | Yes | string (uuid) | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1645,7 +1654,7 @@ Update annotation settings for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -1660,7 +1669,7 @@ Get annotations for an app with pagination | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | keyword | query | Search keyword | No | string | | limit | query | Page size | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | @@ -1679,7 +1688,7 @@ Create a new annotation for an app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1701,7 +1710,7 @@ Batch import annotations from CSV file with rate limiting and security checks | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1720,8 +1729,8 @@ Get status of batch import job | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| job_id | path | Job ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| job_id | path | Job ID | Yes | string (uuid) | #### Responses @@ -1737,7 +1746,7 @@ Get count of message annotations for the app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1752,7 +1761,7 @@ Export all annotations for an app with CSV injection protection | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -1766,8 +1775,8 @@ Export all annotations for an app with CSV injection protection | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_id | path | | Yes | string | -| app_id | path | | Yes | string | +| annotation_id | path | | Yes | string (uuid) | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -1782,8 +1791,8 @@ Update or delete an annotation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_id | path | Annotation ID | Yes | string | -| app_id | path | Application ID | Yes | string | +| annotation_id | path | Annotation ID | Yes | string (uuid) | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1806,8 +1815,8 @@ Get hit histories for an annotation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_id | path | Annotation ID | Yes | string | -| app_id | path | Application ID | Yes | string | +| annotation_id | path | Annotation ID | Yes | string (uuid) | +| app_id | path | Application ID | Yes | string (uuid) | | limit | query | Page size | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | @@ -1825,7 +1834,7 @@ Enable or disable app API | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -1847,7 +1856,7 @@ Transcript audio to text for chat messages | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | App ID | Yes | string | +| app_id | path | App ID | Yes | string (uuid) | #### Responses @@ -1864,7 +1873,7 @@ Get chat conversations with pagination, filtering and summary | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | keyword | query | Search keyword | No | string | @@ -1887,8 +1896,8 @@ Delete a chat conversation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| conversation_id | path | Conversation ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| conversation_id | path | Conversation ID | Yes | string (uuid) | #### Responses @@ -1905,8 +1914,8 @@ Get chat conversation details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| conversation_id | path | Conversation ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| conversation_id | path | Conversation ID | Yes | string (uuid) | #### Responses @@ -1923,7 +1932,7 @@ Get chat messages for a conversation with pagination | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | conversation_id | query | Conversation ID | Yes | string | | first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | @@ -1942,8 +1951,8 @@ Get suggested questions for a message | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| message_id | path | Message ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| message_id | path | Message ID | Yes | string (uuid) | #### Responses @@ -1959,7 +1968,7 @@ Stop a running chat message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | task_id | path | Task ID to stop | Yes | string | #### Responses @@ -1975,7 +1984,7 @@ Get completion conversations with pagination and filtering | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | keyword | query | Search keyword | No | string | @@ -1997,8 +2006,8 @@ Delete a completion conversation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| conversation_id | path | Conversation ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| conversation_id | path | Conversation ID | Yes | string (uuid) | #### Responses @@ -2015,8 +2024,8 @@ Get completion conversation details with messages | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| conversation_id | path | Conversation ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| conversation_id | path | Conversation ID | Yes | string (uuid) | #### Responses @@ -2033,7 +2042,7 @@ Generate completion message for debugging | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2056,7 +2065,7 @@ Stop a running completion message generation | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | task_id | path | Task ID to stop | Yes | string | #### Responses @@ -2072,7 +2081,7 @@ Get conversation variables for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | conversation_id | query | Conversation ID to filter variables | Yes | string | #### Responses @@ -2092,7 +2101,7 @@ Convert Completion App to Workflow App | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2117,7 +2126,7 @@ Create a copy of an existing application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID to copy | Yes | string | +| app_id | path | Application ID to copy | Yes | string (uuid) | #### Request Body @@ -2141,7 +2150,7 @@ Export application configuration as DSL | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID to export | Yes | string | +| app_id | path | Application ID to export | Yes | string (uuid) | | include_secret | query | Include secrets in export | No | boolean | | workflow_id | query | Specific workflow ID to export | No | string | @@ -2159,7 +2168,7 @@ Create or update message feedback (like/dislike) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2182,7 +2191,7 @@ Export user feedback data for Google Sheets | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end_date | query | End date (YYYY-MM-DD) | No | string | | format | query | Export format | No | string,
**Available values:** "csv", "json",
**Default:** csv | | from_source | query | Filter by feedback source | No | string,
**Available values:** "admin", "user" | @@ -2205,7 +2214,7 @@ Update application icon | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2227,8 +2236,8 @@ Get message details by ID | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| message_id | path | Message ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| message_id | path | Message ID | Yes | string (uuid) | #### Responses @@ -2246,7 +2255,7 @@ Update application model configuration | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2269,7 +2278,7 @@ Check if app name is available | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2290,7 +2299,7 @@ Check if app name is available | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -2305,7 +2314,7 @@ Get MCP server configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -2320,7 +2329,7 @@ Create MCP server configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2342,7 +2351,7 @@ Update MCP server configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2365,7 +2374,7 @@ Update application site configuration | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2388,7 +2397,7 @@ Enable or disable app site | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2410,7 +2419,7 @@ Reset access token for application site | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -2427,7 +2436,7 @@ Remove the current account's star from an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -2443,7 +2452,7 @@ Star an application for the current account | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -2459,7 +2468,7 @@ Get average response time statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2476,7 +2485,7 @@ Get average session interaction statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2493,7 +2502,7 @@ Get daily conversation statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2510,7 +2519,7 @@ Get daily terminal/end-user statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2527,7 +2536,7 @@ Get daily message statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2544,7 +2553,7 @@ Get daily token cost statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2561,7 +2570,7 @@ Get tokens per second statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2578,7 +2587,7 @@ Get user satisfaction rate statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date (YYYY-MM-DD HH:MM) | No | string | @@ -2595,7 +2604,7 @@ Convert text to speech for chat messages | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | App ID | Yes | string | +| app_id | path | App ID | Yes | string (uuid) | #### Request Body @@ -2617,7 +2626,7 @@ Get available TTS voices for a specific language | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | App ID | Yes | string | +| app_id | path | App ID | Yes | string (uuid) | | language | query | Language code | Yes | string | #### Responses @@ -2636,7 +2645,7 @@ Get app tracing configuration | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -2651,7 +2660,7 @@ Update app tracing configuration | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2675,7 +2684,7 @@ Delete an existing tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | tracing_provider | query | Tracing provider name | Yes | string | #### Responses @@ -2692,7 +2701,7 @@ Get tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | tracing_provider | query | Tracing provider name | Yes | string | #### Responses @@ -2711,7 +2720,7 @@ Update an existing tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2735,7 +2744,7 @@ Create a new tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -2757,7 +2766,7 @@ Create a new tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -2778,7 +2787,7 @@ Create a new tracing configuration for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -2795,7 +2804,7 @@ Get workflow application execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | created_at__after | query | Filter logs created after this timestamp | No | dateTime | | created_at__before | query | Filter logs created before this timestamp | No | dateTime | | created_by_account | query | Filter by account | No | string | @@ -2821,7 +2830,7 @@ Get workflow archived execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | created_at__after | query | Filter logs created after this timestamp | No | dateTime | | created_at__before | query | Filter logs created before this timestamp | No | dateTime | | created_by_account | query | Filter by account | No | string | @@ -2845,7 +2854,7 @@ Get workflow archived execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | last_id | query | Last run ID for pagination | No | string | | limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | | status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | @@ -2864,7 +2873,7 @@ Get workflow archived execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | | time_range | query | Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), 30m (30 minutes), 30s (30 seconds). Filters by created_at field. | No | string | | triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | @@ -2884,7 +2893,7 @@ Stop running workflow task | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | task_id | path | Task ID | Yes | string | #### Responses @@ -2902,8 +2911,8 @@ Stop running workflow task | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -2919,8 +2928,8 @@ Generate a download URL for an archived workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -2935,8 +2944,8 @@ Generate a download URL for an archived workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -2952,9 +2961,9 @@ List a directory in a workflow Agent node sandbox | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Workflow Agent node ID | Yes | string | -| workflow_run_id | path | Workflow run ID | Yes | string | +| workflow_run_id | path | Workflow run ID | Yes | string (uuid) | | node_execution_id | query | Optional workflow node execution ID. When omitted, the latest active session for the node is used. | No | string | | path | query | Directory path relative to the sandbox workspace | No | string,
**Default:** . | @@ -2971,9 +2980,9 @@ Read a text/binary preview file in a workflow Agent node sandbox | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Workflow Agent node ID | Yes | string | -| workflow_run_id | path | Workflow run ID | Yes | string | +| workflow_run_id | path | Workflow run ID | Yes | string (uuid) | | node_execution_id | query | Optional workflow node execution ID. When omitted, the latest active session for the node is used. | No | string | | path | query | File path relative to the sandbox workspace | Yes | string | @@ -2990,9 +2999,9 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | -| workflow_run_id | path | | Yes | string | +| workflow_run_id | path | | Yes | string (uuid) | #### Request Body @@ -3013,7 +3022,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3028,7 +3037,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3049,7 +3058,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3064,7 +3073,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | #### Responses @@ -3080,7 +3089,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | #### Responses @@ -3096,7 +3105,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | #### Request Body @@ -3118,7 +3127,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | #### Request Body @@ -3140,7 +3149,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | | reply_id | path | Reply ID | Yes | string | @@ -3157,7 +3166,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | | reply_id | path | Reply ID | Yes | string | @@ -3180,7 +3189,7 @@ Upload one workflow Agent sandbox file as a Dify ToolFile mapping | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | comment_id | path | Comment ID | Yes | string | #### Responses @@ -3196,7 +3205,7 @@ Get workflow average app interaction statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | @@ -3213,7 +3222,7 @@ Get workflow daily runs statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | @@ -3230,7 +3239,7 @@ Get workflow daily terminals statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | @@ -3247,7 +3256,7 @@ Get workflow daily token cost statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | | start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | @@ -3266,7 +3275,7 @@ Get all published workflows for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | limit | query | | No | integer,
**Default:** 10 | | named_only | query | | No | boolean | | page | query | | No | integer,
**Default:** 1 | @@ -3287,7 +3296,7 @@ Get default block configurations for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3304,7 +3313,7 @@ Get default block configuration by type | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | block_type | path | Block type | Yes | string | | q | query | | No | string | @@ -3324,7 +3333,7 @@ Get draft workflow for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3342,7 +3351,7 @@ Sync draft workflow configuration | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -3365,7 +3374,7 @@ Get conversation variables for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3381,7 +3390,7 @@ Update conversation variables for workflow draft | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3404,7 +3413,7 @@ Get environment variables for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3420,7 +3429,7 @@ Update environment variables for workflow draft | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3441,7 +3450,7 @@ Update draft workflow features | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3464,7 +3473,7 @@ Test human input delivery for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3488,7 +3497,7 @@ Get human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3512,7 +3521,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3534,7 +3543,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3558,7 +3567,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3580,7 +3589,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Responses @@ -3594,7 +3603,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Request Body @@ -3614,7 +3623,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Responses @@ -3628,7 +3637,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Request Body @@ -3648,7 +3657,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Request Body @@ -3668,7 +3677,7 @@ Submit human input form preview for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Request Body @@ -3690,7 +3699,7 @@ Get last run result for draft workflow node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Responses @@ -3708,7 +3717,7 @@ Get last run result for draft workflow node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Request Body @@ -3732,7 +3741,7 @@ Get last run result for draft workflow node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Responses @@ -3750,7 +3759,7 @@ Delete all variables for a specific node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | node_id | path | | Yes | string | #### Responses @@ -3766,7 +3775,7 @@ Get variables for a specific node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID | Yes | string | #### Responses @@ -3782,7 +3791,7 @@ Get variables for a specific node | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3804,8 +3813,8 @@ Snapshot of every node's declared outputs for a draft workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -3821,8 +3830,8 @@ Server-Sent Events stream of inspector deltas for a draft workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -3838,9 +3847,9 @@ One node's declared outputs for a draft workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID inside the workflow graph | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -3856,10 +3865,10 @@ Full value for one declared output, including signed download URL for files. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID inside the workflow graph | Yes | string | | output_name | path | Declared output name as exposed by Composer | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -3875,7 +3884,7 @@ Get system variables for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -3890,7 +3899,7 @@ Get system variables for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3913,7 +3922,7 @@ Get system variables for workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Request Body @@ -3936,7 +3945,7 @@ Delete all draft workflow variables | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -3953,7 +3962,7 @@ Get draft workflow variables | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | limit | query | Items per page | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | @@ -3970,8 +3979,8 @@ Delete a workflow variable | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -3987,8 +3996,8 @@ Get a specific workflow variable | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| variable_id | path | Variable ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| variable_id | path | Variable ID | Yes | string (uuid) | #### Responses @@ -4004,8 +4013,8 @@ Update a workflow variable | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Request Body @@ -4027,8 +4036,8 @@ Reset a workflow variable to its default value | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| variable_id | path | Variable ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| variable_id | path | Variable ID | Yes | string (uuid) | #### Responses @@ -4047,7 +4056,7 @@ Get published workflow for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | #### Responses @@ -4062,7 +4071,7 @@ Get published workflow for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -4083,8 +4092,8 @@ Snapshot of every node's declared outputs for a published workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -4100,8 +4109,8 @@ Server-Sent Events stream of inspector deltas for a published workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -4117,9 +4126,9 @@ One node's declared outputs for a published workflow run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID inside the workflow graph | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -4135,10 +4144,10 @@ Full value for one declared output of a published run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | node_id | path | Node ID inside the workflow graph | Yes | string | | output_name | path | Declared output name as exposed by Composer | Yes | string | -| run_id | path | Workflow run ID | Yes | string | +| run_id | path | Workflow run ID | Yes | string (uuid) | #### Responses @@ -4155,7 +4164,7 @@ Full value for one declared output of a published run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | query | | Yes | string | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -4170,7 +4179,7 @@ Full value for one declared output of a published run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | workflow_id | path | | Yes | string | #### Responses @@ -4188,7 +4197,7 @@ Update workflow by ID | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | workflow_id | path | Workflow ID | Yes | string | #### Request Body @@ -4212,7 +4221,7 @@ Restore a published workflow version into the draft workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string | +| app_id | path | Application ID | Yes | string (uuid) | | workflow_id | path | Published workflow ID | Yes | string | #### Responses @@ -4230,7 +4239,7 @@ Restore a published workflow version into the draft workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| resource_id | path | App ID | Yes | string | +| resource_id | path | App ID | Yes | string (uuid) | #### Responses @@ -4245,7 +4254,7 @@ Restore a published workflow version into the draft workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| resource_id | path | App ID | Yes | string | +| resource_id | path | App ID | Yes | string (uuid) | #### Responses @@ -4261,8 +4270,8 @@ Restore a published workflow version into the draft workflow | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| api_key_id | path | API key ID | Yes | string | -| resource_id | path | App ID | Yes | string | +| api_key_id | path | API key ID | Yes | string (uuid) | +| resource_id | path | App ID | Yes | string (uuid) | #### Responses @@ -4277,7 +4286,7 @@ Refresh MCP server configuration and regenerate server code | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| server_id | path | Server ID | Yes | string | +| server_id | path | Server ID | Yes | string (uuid) | #### Responses @@ -4534,7 +4543,7 @@ Get compliance document download link | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | -| binding_id | path | | Yes | string | +| binding_id | path | | Yes | string (uuid) | #### Responses @@ -4548,7 +4557,7 @@ Get compliance document download link | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | -| binding_id | path | | Yes | string | +| binding_id | path | | Yes | string (uuid) | #### Responses @@ -4625,7 +4634,7 @@ Delete dataset API key | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| api_key_id | path | API key ID | Yes | string | +| api_key_id | path | API key ID | Yes | string (uuid) | #### Responses @@ -4638,7 +4647,7 @@ Delete dataset API key | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| job_id | path | | Yes | string | +| job_id | path | | Yes | string (uuid) | #### Responses @@ -4651,7 +4660,7 @@ Delete dataset API key | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| job_id | path | | Yes | string | +| job_id | path | | Yes | string (uuid) | #### Request Body @@ -4717,7 +4726,7 @@ Get external knowledge API templates | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| external_knowledge_api_id | path | | Yes | string | +| external_knowledge_api_id | path | | Yes | string (uuid) | #### Responses @@ -4732,7 +4741,7 @@ Get external knowledge API template details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| external_knowledge_api_id | path | External knowledge API ID | Yes | string | +| external_knowledge_api_id | path | External knowledge API ID | Yes | string (uuid) | #### Responses @@ -4746,7 +4755,7 @@ Get external knowledge API template details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| external_knowledge_api_id | path | | Yes | string | +| external_knowledge_api_id | path | | Yes | string (uuid) | #### Request Body @@ -4767,7 +4776,7 @@ Check if external knowledge API is being used | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| external_knowledge_api_id | path | External knowledge API ID | Yes | string | +| external_knowledge_api_id | path | External knowledge API ID | Yes | string (uuid) | #### Responses @@ -4870,7 +4879,7 @@ Get mock dataset retrieval settings by vector type | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -4885,7 +4894,7 @@ Get dataset details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -4902,7 +4911,7 @@ Update dataset details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -4923,7 +4932,7 @@ Update dataset details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | | status | path | | Yes | string | #### Responses @@ -4939,7 +4948,7 @@ Get dataset auto disable logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -4954,7 +4963,7 @@ Get dataset auto disable logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | batch | path | | Yes | string | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -4968,7 +4977,7 @@ Get dataset auto disable logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | batch | path | | Yes | string | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -4981,7 +4990,7 @@ Get dataset auto disable logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -4996,7 +5005,7 @@ Get documents in a dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | | fetch | query | Fetch full details (default: false) | No | string | | keyword | query | Search keyword | No | string | | limit | query | Number of items per page (default: 20) | No | string | @@ -5015,7 +5024,7 @@ Get documents in a dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -5038,7 +5047,7 @@ Download selected dataset documents as a single ZIP archive (upload-file only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -5064,7 +5073,7 @@ then asynchronously generates summary indexes for the provided documents. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Request Body @@ -5086,7 +5095,7 @@ then asynchronously generates summary indexes for the provided documents. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -5106,7 +5115,7 @@ then asynchronously generates summary indexes for the provided documents. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -5119,8 +5128,8 @@ then asynchronously generates summary indexes for the provided documents. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5135,8 +5144,8 @@ Get document details | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | | metadata | query | Metadata inclusion (all/only/without) | No | string | #### Responses @@ -5153,8 +5162,8 @@ Get a signed download URL for a dataset document's original uploaded file | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5169,8 +5178,8 @@ Estimate document indexing cost | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Responses @@ -5187,8 +5196,8 @@ Get document indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Responses @@ -5204,8 +5213,8 @@ Update document metadata | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Request Body @@ -5226,8 +5235,8 @@ Update document metadata | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5240,8 +5249,8 @@ Update document metadata | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5256,8 +5265,8 @@ Update document metadata | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5272,8 +5281,8 @@ Update document metadata | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5289,8 +5298,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action to perform (pause/resume) | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Responses @@ -5305,8 +5314,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Request Body @@ -5325,8 +5334,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Request Body @@ -5346,8 +5355,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | | segment_id | query | Segment IDs | No | [ string ] | #### Responses @@ -5361,8 +5370,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | | segment_id | query | Segment IDs | No | [ string ] | #### Responses @@ -5376,8 +5385,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | | enabled | query | | No | string,
**Default:** all | | hit_count_gte | query | | No | integer | | keyword | query | | No | string | @@ -5396,8 +5405,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5410,8 +5419,8 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Request Body @@ -5430,9 +5439,9 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Segment ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Segment ID | Yes | string (uuid) | #### Responses @@ -5445,9 +5454,9 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Segment ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Segment ID | Yes | string (uuid) | #### Request Body @@ -5466,9 +5475,9 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | | keyword | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | | page | query | | No | integer,
**Default:** 1 | @@ -5484,9 +5493,9 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | #### Request Body @@ -5505,9 +5514,9 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | #### Request Body @@ -5526,10 +5535,10 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| child_chunk_id | path | Child chunk ID | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | +| child_chunk_id | path | Child chunk ID | Yes | string (uuid) | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | #### Responses @@ -5542,10 +5551,10 @@ Update document processing status (pause/resume) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| child_chunk_id | path | Child chunk ID | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | +| child_chunk_id | path | Child chunk ID | Yes | string (uuid) | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | #### Request Body @@ -5576,8 +5585,8 @@ Returns: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | #### Responses @@ -5593,8 +5602,8 @@ Returns: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| document_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| document_id | path | | Yes | string (uuid) | #### Responses @@ -5609,7 +5618,7 @@ Get dataset error documents | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5625,7 +5634,7 @@ Test external knowledge retrieval for dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Request Body @@ -5648,7 +5657,7 @@ Test dataset knowledge retrieval | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Request Body @@ -5671,7 +5680,7 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5684,7 +5693,7 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -5697,7 +5706,7 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -5717,7 +5726,7 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -5730,8 +5739,8 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| metadata_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| metadata_id | path | | Yes | string (uuid) | #### Responses @@ -5744,8 +5753,8 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| metadata_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | +| metadata_id | path | | Yes | string (uuid) | #### Request Body @@ -5764,7 +5773,7 @@ Get dataset indexing status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -5779,7 +5788,7 @@ Get dataset permission user list | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5796,7 +5805,7 @@ Get dataset query history | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5811,7 +5820,7 @@ Get applications related to dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5826,7 +5835,7 @@ Get applications related to dataset | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Request Body @@ -5847,7 +5856,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5862,7 +5871,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| resource_id | path | Dataset ID | Yes | string | +| resource_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5877,7 +5886,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| resource_id | path | Dataset ID | Yes | string | +| resource_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5893,8 +5902,8 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| api_key_id | path | API key ID | Yes | string | -| resource_id | path | Dataset ID | Yes | string | +| api_key_id | path | API key ID | Yes | string (uuid) | +| resource_id | path | Dataset ID | Yes | string (uuid) | #### Responses @@ -5998,7 +6007,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -6050,7 +6059,7 @@ Check if dataset is in use | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| file_id | path | | Yes | string | +| file_id | path | | Yes | string (uuid) | #### Responses @@ -6192,7 +6201,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6205,7 +6214,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6224,7 +6233,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6237,7 +6246,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6256,7 +6265,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -6270,7 +6279,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6289,7 +6298,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -6306,7 +6315,7 @@ Request body: | last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | | pinned | query | | No | boolean | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6319,8 +6328,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | | Yes | string | -| installed_app_id | path | | Yes | string | +| c_id | path | | Yes | string (uuid) | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6333,8 +6342,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | | Yes | string | -| installed_app_id | path | | Yes | string | +| c_id | path | | Yes | string (uuid) | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6353,8 +6362,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | | Yes | string | -| installed_app_id | path | | Yes | string | +| c_id | path | | Yes | string (uuid) | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6367,8 +6376,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | | Yes | string | -| installed_app_id | path | | Yes | string | +| c_id | path | | Yes | string (uuid) | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6384,7 +6393,7 @@ Request body: | conversation_id | query | Conversation UUID | Yes | string | | first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6397,8 +6406,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | +| message_id | path | | Yes | string (uuid) | #### Request Body @@ -6418,8 +6427,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | response_mode | query | | Yes | string,
**Available values:** "blocking", "streaming" | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | +| message_id | path | | Yes | string (uuid) | #### Responses @@ -6432,8 +6441,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | +| message_id | path | | Yes | string (uuid) | #### Responses @@ -6448,7 +6457,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6463,7 +6472,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6478,7 +6487,7 @@ Request body: | ---- | ---------- | ----------- | -------- | ------ | | last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Responses @@ -6491,7 +6500,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6510,8 +6519,8 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | +| message_id | path | | Yes | string (uuid) | #### Responses @@ -6524,7 +6533,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6545,7 +6554,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | #### Request Body @@ -6566,7 +6575,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | +| installed_app_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -6676,7 +6685,7 @@ Mark a notification as dismissed for the current user. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | credential_id | query | Credential ID | Yes | string | -| page_id | path | | Yes | string | +| page_id | path | | Yes | string (uuid) | | page_type | path | | Yes | string | #### Responses @@ -6776,7 +6785,7 @@ Sync data from OAuth data source | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| binding_id | path | Data source binding ID | Yes | string | +| binding_id | path | Data source binding ID | Yes | string (uuid) | | provider | path | Data source provider name (notion) | Yes | string | #### Responses @@ -7089,7 +7098,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | +| dataset_id | path | | Yes | string (uuid) | #### Responses @@ -7139,7 +7148,7 @@ Initiate OAuth login process | ---- | ---------- | ----------- | -------- | ------ | | last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7154,7 +7163,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -7170,8 +7179,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| run_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| run_id | path | | Yes | string (uuid) | #### Responses @@ -7186,8 +7195,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| run_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| run_id | path | | Yes | string (uuid) | #### Responses @@ -7206,7 +7215,7 @@ Initiate OAuth login process | named_only | query | | No | boolean | | page | query | | No | integer,
**Default:** 1 | | user_id | query | | No | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7222,7 +7231,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7239,7 +7248,7 @@ Initiate OAuth login process | ---- | ---------- | ----------- | -------- | ------ | | q | query | | No | string | | block_type | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7254,7 +7263,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7270,7 +7279,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7292,7 +7301,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7313,7 +7322,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7334,7 +7343,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7350,7 +7359,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7372,7 +7381,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7392,7 +7401,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7408,7 +7417,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7428,7 +7437,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7442,7 +7451,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7458,7 +7467,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | query | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7474,7 +7483,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | query | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7489,7 +7498,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7508,7 +7517,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7521,7 +7530,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7538,7 +7547,7 @@ Initiate OAuth login process | ---- | ---------- | ----------- | -------- | ------ | | limit | query | | No | integer,
**Default:** 20 | | page | query | | No | integer,
**Default:** 1 | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7551,8 +7560,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -7565,8 +7574,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -7579,8 +7588,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Request Body @@ -7599,8 +7608,8 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -7616,7 +7625,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7631,7 +7640,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7647,7 +7656,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7669,7 +7678,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7691,7 +7700,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | query | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7707,7 +7716,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | query | | Yes | string | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Responses @@ -7722,7 +7731,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | #### Request Body @@ -7743,7 +7752,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | | workflow_id | path | | Yes | string | #### Responses @@ -7759,7 +7768,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | | workflow_id | path | | Yes | string | #### Request Body @@ -7782,7 +7791,7 @@ Initiate OAuth login process | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | +| pipeline_id | path | | Yes | string (uuid) | | workflow_id | path | | Yes | string | #### Responses @@ -7897,7 +7906,7 @@ Generate structured output rules using LLM | ---- | ---------- | ----------- | -------- | ------ | | last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -7915,7 +7924,7 @@ command channel for backward compatibility. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -7932,8 +7941,8 @@ command channel for backward compatibility. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| run_id | path | | Yes | string | -| snippet_id | path | | Yes | string | +| run_id | path | | Yes | string (uuid) | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -7949,8 +7958,8 @@ command channel for backward compatibility. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| run_id | path | | Yes | string | -| snippet_id | path | | Yes | string | +| run_id | path | | Yes | string (uuid) | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -7967,7 +7976,7 @@ Get all published workflows for a snippet | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | | limit | query | | No | integer,
**Default:** 10 | | page | query | | No | integer,
**Default:** 1 | @@ -7984,7 +7993,7 @@ Get all published workflows for a snippet | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -7999,7 +8008,7 @@ Get all published workflows for a snippet | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8015,7 +8024,7 @@ Get all published workflows for a snippet | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Request Body @@ -8037,7 +8046,7 @@ Get all published workflows for a snippet | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8052,7 +8061,7 @@ Conversation variables are not used in snippet workflows; returns an empty list | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8067,7 +8076,7 @@ Get environment variables from snippet draft workflow graph | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8088,7 +8097,7 @@ Returns an SSE event stream with iteration progress and results. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | Node ID | Yes | string | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Request Body @@ -8115,7 +8124,7 @@ Returns an SSE event stream with loop progress and results. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | Node ID | Yes | string | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Request Body @@ -8142,7 +8151,7 @@ including status, inputs, outputs, and timing information. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | Node ID | Yes | string | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Responses @@ -8163,7 +8172,7 @@ Returns the node execution result including status, outputs, and timing. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | Node ID | Yes | string | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Request Body @@ -8186,7 +8195,7 @@ Delete all variables for a specific node (snippet draft workflow) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8202,7 +8211,7 @@ Get variables for a specific node (snippet draft workflow) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8220,7 +8229,7 @@ and returns an SSE event stream with execution progress and results. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Request Body @@ -8242,7 +8251,7 @@ System variables are not used in snippet workflows; returns an empty list for AP | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8257,7 +8266,7 @@ Delete all draft workflow variables for the current user (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8274,7 +8283,7 @@ List draft workflow variables without values (paginated, snippet scope) | ---- | ---------- | ----------- | -------- | ------ | | limit | query | Items per page | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8289,8 +8298,8 @@ Delete a draft workflow variable (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -8306,8 +8315,8 @@ Get a specific draft workflow variable (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -8323,8 +8332,8 @@ Update a draft workflow variable (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Request Body @@ -8346,8 +8355,8 @@ Reset a draft workflow variable to its default value (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | -| variable_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | +| variable_id | path | | Yes | string (uuid) | #### Responses @@ -8364,7 +8373,7 @@ Reset a draft workflow variable to its default value (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8380,7 +8389,7 @@ Reset a draft workflow variable to its default value (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Request Body @@ -8402,7 +8411,7 @@ Reset a draft workflow variable to its default value (snippet scope) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | | workflow_id | path | Published workflow ID | Yes | string | #### Responses @@ -8501,7 +8510,7 @@ Remove one or more tag bindings from a target. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| tag_id | path | | Yes | string | +| tag_id | path | | Yes | string (uuid) | #### Responses @@ -8514,7 +8523,7 @@ Remove one or more tag bindings from a target. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| tag_id | path | | Yes | string | +| tag_id | path | | Yes | string (uuid) | #### Request Body @@ -8550,7 +8559,7 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8563,7 +8572,7 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8576,7 +8585,7 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -8595,7 +8604,7 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -8617,7 +8626,7 @@ Bedrock retrieval test (internal use only) | ids | query | Dataset IDs | No | [ string ] | | limit | query | Number of items per page | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8630,8 +8639,8 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| message_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | +| message_id | path | | Yes | string (uuid) | #### Responses @@ -8646,7 +8655,7 @@ Bedrock retrieval test (internal use only) | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8663,7 +8672,7 @@ Returns the site configuration for the application including theme, icons, and t | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8676,7 +8685,7 @@ Returns the site configuration for the application including theme, icons, and t | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -8697,7 +8706,7 @@ Returns the site configuration for the application including theme, icons, and t | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Responses @@ -8712,7 +8721,7 @@ Returns the site configuration for the application including theme, icons, and t | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | #### Request Body @@ -8733,7 +8742,7 @@ Returns the site configuration for the application including theme, icons, and t | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | +| app_id | path | | Yes | string (uuid) | | task_id | path | | Yes | string | #### Responses @@ -8958,7 +8967,7 @@ Get list of available agent providers | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8974,7 +8983,7 @@ Get list of available agent providers | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Responses @@ -8990,7 +8999,7 @@ Get list of available agent providers | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | | Yes | string | +| snippet_id | path | | Yes | string (uuid) | #### Request Body @@ -9013,7 +9022,7 @@ Get list of available agent providers | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Responses @@ -9031,7 +9040,7 @@ Export snippet configuration as DSL | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | Snippet ID to export | Yes | string | +| snippet_id | path | Snippet ID to export | Yes | string (uuid) | | include_secret | query | Whether to include secret variables | No | string,
**Default:** false | #### Responses @@ -9050,7 +9059,7 @@ Increment snippet use count by 1 | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| snippet_id | path | Snippet ID | Yes | string | +| snippet_id | path | Snippet ID | Yes | string (uuid) | #### Responses @@ -9319,7 +9328,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| member_id | path | | Yes | string | +| member_id | path | | Yes | string (uuid) | #### Responses @@ -9332,7 +9341,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| member_id | path | | Yes | string | +| member_id | path | | Yes | string (uuid) | #### Request Body @@ -9351,7 +9360,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| member_id | path | | Yes | string | +| member_id | path | | Yes | string (uuid) | #### Request Body @@ -11244,9 +11253,9 @@ Default namespace | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | email | string | | No | -| interface_language | string | | Yes | -| name | string | | Yes | -| timezone | string | | Yes | +| interface_language | string | | No | +| name | string | | No | +| timezone | string | | No | | token | string | | Yes | | workspace_id | string | | No | @@ -11254,7 +11263,9 @@ Default namespace | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| account_status | string | | No | | email | string | | Yes | +| requires_setup | boolean | | No | | workspace_id | string | | Yes | | workspace_name | string | | Yes | @@ -11343,7 +11354,40 @@ Default namespace | icon_background | string | Icon background color | No | | icon_type | [IconType](#icontype) | Icon type | No | | name | string | Agent name | Yes | -| role | string | Agent role | No | +| role | string | Agent role | Yes | + +#### AgentAppDetailWithSite + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| access_mode | string | | No | +| active_config_is_published | boolean | | No | +| api_base_url | string | | No | +| app_id | string | | No | +| bound_agent_id | string | | No | +| created_at | integer | | No | +| created_by | string | | No | +| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No | +| description | string | | No | +| enable_api | boolean | | Yes | +| enable_site | boolean | | Yes | +| icon | string | | No | +| icon_background | string | | No | +| icon_type | string | | No | +| icon_url | string | | Yes | +| id | string | | Yes | +| max_active_requests | integer | | No | +| mode | string | | Yes | +| model_config | [ModelConfig](#modelconfig) | | No | +| name | string | | Yes | +| role | string | | No | +| site | [Site](#site) | | No | +| tags | [ [Tag](#tag) ] | | No | +| tracing | [JSONValue](#jsonvalue) | | No | +| updated_at | integer | | No | +| updated_by | string | | No | +| use_icon_as_answer_icon | boolean | | No | +| workflow | [WorkflowPartial](#workflowpartial) | | No | #### AgentAppFeaturesPayload @@ -11425,7 +11469,7 @@ default (the config form sends the full desired feature state on save). | icon_type | [IconType](#icontype) | Icon type | No | | max_active_requests | integer | Maximum active requests | No | | name | string | App name | Yes | -| role | string | Agent role | No | +| role | string | Agent role | Yes | | use_icon_as_answer_icon | boolean | Use icon as answer icon | No | #### AgentAverageResponseTimeStatisticResponse @@ -11942,36 +11986,60 @@ the current roster/workflow APIs scoped to Dify Agent. | ---- | ---- | ----------- | -------- | | AgentKnowledgeQueryMode | string | | | -#### AgentLogItemResponse +#### AgentLogConversationItemResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| conversation_id | string | | Yes | +| created_at | integer | | No | +| end_user_id | string | | No | +| id | string | | Yes | +| message_count | integer | | Yes | +| operation_rate | number | | No | +| source | [AgentLogSourceResponse](#agentlogsourceresponse) | | No | +| status | string,
**Available values:** "failed", "paused", "success" | *Enum:* `"failed"`, `"paused"`, `"success"` | Yes | +| title | string | | No | +| unread | boolean | | Yes | +| updated_at | integer | | No | +| user_rate | number | | No | + +#### AgentLogListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| data | [ [AgentLogConversationItemResponse](#agentlogconversationitemresponse) ] | | Yes | +| has_more | boolean | | Yes | +| limit | integer | | Yes | +| page | integer | | Yes | +| total | integer | | Yes | + +#### AgentLogMessageItemResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | answer | string | | Yes | | answer_tokens | integer | | Yes | | conversation_id | string | | Yes | -| conversation_name | string | | No | | created_at | integer | | No | | currency | string | | Yes | | error | string | | No | | from_account_id | string | | No | | from_end_user_id | string | | No | -| from_source | string | | No | | id | string | | Yes | | latency | number | | Yes | | message_id | string | | Yes | | message_tokens | integer | | Yes | | query | string | | Yes | -| source | string | | No | | status | string | | Yes | | total_price | string | | Yes | | total_tokens | integer | | Yes | | updated_at | integer | | No | -#### AgentLogListResponse +#### AgentLogMessageListResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| data | [ [AgentLogItemResponse](#agentlogitemresponse) ] | | Yes | +| data | [ [AgentLogMessageItemResponse](#agentlogmessageitemresponse) ] | | Yes | | has_more | boolean | | Yes | | limit | integer | | Yes | | page | integer | | Yes | @@ -12004,6 +12072,36 @@ the current roster/workflow APIs scoped to Dify Agent. | iterations | [ [AgentIterationLogResponse](#agentiterationlogresponse) ] | | Yes | | meta | [AgentLogMetaResponse](#agentlogmetaresponse) | | Yes | +#### AgentLogSourceGroupResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| label | string | | Yes | +| sources | [ [AgentLogSourceResponse](#agentlogsourceresponse) ] | | No | +| type | string,
**Available values:** "webapp", "workflow" | *Enum:* `"webapp"`, `"workflow"` | Yes | + +#### AgentLogSourceListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| data | [ [AgentLogSourceResponse](#agentlogsourceresponse) ] | | Yes | +| groups | [ [AgentLogSourceGroupResponse](#agentlogsourcegroupresponse) ] | | Yes | + +#### AgentLogSourceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| app_icon | string | | No | +| app_icon_background | string | | No | +| app_icon_type | string | | No | +| app_id | string | | Yes | +| app_name | string | | Yes | +| id | string | | Yes | +| node_id | string | | No | +| type | string,
**Available values:** "webapp", "workflow" | *Enum:* `"webapp"`, `"workflow"` | Yes | +| workflow_id | string | | No | +| workflow_version | string | | No | + #### AgentLogsQuery | Name | Type | Description | Required | @@ -12209,13 +12307,6 @@ Visibility and lifecycle scope of an Agent record. | skill_md_file_id | string | | No | | skill_md_key | string | | No | -#### AgentSkillStandardizeResponse - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| manifest | [SkillManifest](#skillmanifest) | | Yes | -| skill | [AgentSkillRefConfig](#agentskillrefconfig) | | Yes | - #### AgentSkillUploadResponse | Name | Type | Description | Required | @@ -12777,7 +12868,6 @@ Enum class for api provider schema type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| active_config_is_published | boolean | | No | | api_base_url | string | | No | | app_id | string | | No | | bound_agent_id | string | | No | @@ -12796,7 +12886,6 @@ Enum class for api provider schema type. | mode | string | | Yes | | model_config | [ModelConfig](#modelconfig) | | No | | name | string | | Yes | -| role | string | | No | | site | [Site](#site) | | No | | tags | [ [Tag](#tag) ] | | No | | tracing | [JSONValue](#jsonvalue) | | No | @@ -12890,10 +12979,10 @@ AppMCPServer Status Enum | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| has_next | boolean | | Yes | -| items | [ [AppPartial](#apppartial) ] | | Yes | +| data | [ [AppPartial](#apppartial) ] | | Yes | +| has_more | boolean | | Yes | +| limit | integer | | Yes | | page | integer | | Yes | -| per_page | integer | | Yes | | total | integer | | Yes | #### AppPartial @@ -12901,25 +12990,24 @@ AppMCPServer Status Enum | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| active_config_is_published | boolean | | No | | app_id | string | | No | -| app_model_config | [ModelConfigPartial](#modelconfigpartial) | | No | | author_name | string | | No | | bound_agent_id | string | | No | | create_user_name | string | | No | | created_at | integer | | No | | created_by | string | | No | -| desc_or_prompt | string | | No | +| description | string | | No | | has_draft_trigger | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | +| icon_url | string | | Yes | | id | string | | Yes | | is_starred | boolean | | No | | max_active_requests | integer | | No | -| mode_compatible_with_agent | string | | Yes | +| mode | string | | Yes | +| model_config | [ModelConfigPartial](#modelconfigpartial) | | No | | name | string | | Yes | -| role | string | | No | | tags | [ [Tag](#tag) ] | | No | | updated_at | integer | | No | | updated_by | string | | No | @@ -17997,7 +18085,7 @@ Model class for provider quota configuration. | ---- | ---- | ----------- | -------- | | chunk_overlap | integer | | No | | max_tokens | integer | | Yes | -| separator | string,
**Default:** +| separator | string,
**Default:** | | No | #### SelectInputConfig @@ -18286,7 +18374,7 @@ Payload for running a loop node in snippet draft workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| data | [ [_AnonymousInlineModel_efd591151ea9](#_anonymousinlinemodel_efd591151ea9) ] | | No | +| data | [ [_AnonymousInlineModel_744ff9cc03e6](#_anonymousinlinemodel_744ff9cc03e6) ] | | No | | has_more | boolean | | No | | limit | integer | | No | | page | integer | | No | @@ -18603,7 +18691,7 @@ Tag type | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | message_id | string | Message ID | No | -| streaming | boolean | Enable streaming response | No | +| streaming | boolean | Reserved for compatibility; TTS response streaming is determined by the provider output. | No | | text | string | Text to convert to audio | No | | voice | string | Voice to use for TTS | No | @@ -20070,6 +20158,25 @@ Workflow tool configuration | allow_owner_transfer | boolean | | Yes | | workspace_id | string | | Yes | +#### _AnonymousInlineModel_744ff9cc03e6 + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| author_name | string | | No | +| created_at | long | | No | +| created_by | string | | No | +| description | string | | No | +| icon_info | object | | No | +| id | string | | No | +| is_published | boolean | | No | +| name | string | | No | +| tags | [ [_AnonymousInlineModel_7b8b49ca164e](#_anonymousinlinemodel_7b8b49ca164e) ] | | No | +| type | string | | No | +| updated_at | long | | No | +| updated_by | string | | No | +| use_count | integer | | No | +| version | integer | | No | + #### _AnonymousInlineModel_7b8b49ca164e | Name | Type | Description | Required | @@ -20095,25 +20202,6 @@ Workflow tool configuration | model_provider_name | string | | No | | summary_prompt | string | | No | -#### _AnonymousInlineModel_efd591151ea9 - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| author_name | string | | No | -| created_at | long | | No | -| created_by | string | | No | -| description | string | | No | -| icon_info | object | | No | -| id | string | | No | -| is_published | boolean | | No | -| name | string | | No | -| tags | [ [_AnonymousInlineModel_7b8b49ca164e](#_anonymousinlinemodel_7b8b49ca164e) ] | | No | -| type | string | | No | -| updated_at | long | | No | -| updated_by | string | | No | -| use_count | integer | | No | -| version | integer | | No | - #### core__tools__entities__common_entities__I18nObject Model class for i18n object. diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index d203eaa4c97..337be4f74ec 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -4,10 +4,9 @@ User-scoped programmatic API (bearer auth) ## Version: 1.0 ### Available authorizations -#### Bearer (API Key Authentication) -Type: Bearer {your-api-key} -**Name:** Authorization -**In:** header +#### Bearer (HTTP, bearer) +Use the Service API key as a Bearer token in the Authorization header. +Bearer format: API_KEY --- ## openapi diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 5a0a128b4f9..fdce4d1a2c2 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -4,10 +4,9 @@ API for application services ## Version: 1.0 ### Available authorizations -#### Bearer (API Key Authentication) -Type: Bearer {your-api-key} -**Name:** Authorization -**In:** header +#### Bearer (HTTP, bearer) +Use the Service API key as a Bearer token in the Authorization header. +Bearer format: API_KEY --- ## service_api @@ -20,11 +19,138 @@ Service operations | ---- | ----------- | ------ | | 200 | Success | **application/json**: [IndexInfoResponse](#indexinforesponse)
| -### [GET] /app/feedbacks -**Get all feedbacks for the application** +### ~~[POST] /datasets/{dataset_id}/document/create_by_text~~ -Get all feedbacks for the application -Returns paginated list of all feedback submitted for messages in this app. +***DEPRECATED*** + +Deprecated legacy alias for creating a new document by providing text content. Use /datasets/{dataset_id}/document/create-by-text instead. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | Bad request - invalid parameters | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [DELETE] /datasets/{dataset_id}/documents/{document_id} +**Delete Document** + +Permanently delete a document and all its chunks from the knowledge base. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 400 | `document_indexing` : Cannot delete document during indexing. | +| 401 | Unauthorized - invalid API token | +| 403 | `archived_document_immutable` : The archived document is not editable. | +| 404 | `not_found` : Document Not Exists. | + +### [GET] /datasets/{dataset_id}/documents/{document_id} +**Get Document** + +Retrieve detailed information about a specific document, including its indexing status, metadata, and processing statistics. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| metadata | query | Metadata response mode | No | string,
**Available values:** "all", "only", "without",
**Default:** all | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document details. The response shape varies based on the `metadata` query parameter. When `metadata` is `only`, only `id`, `doc_type`, and `doc_metadata` are returned. When `metadata` is `without`, `doc_type` and `doc_metadata` are omitted. | **application/json**: [DocumentDetailResponse](#documentdetailresponse)
| +| 400 | `invalid_metadata` : Invalid metadata value for the specified key. | | +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : No permission. | | +| 404 | `not_found` : Document not found. | | + +### [PATCH] /datasets/{dataset_id}/documents/{document_id} +Update an existing document by uploading a file + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_text~~ + +***DEPRECATED*** + +Deprecated legacy alias for updating an existing document by providing text content. Use /datasets/{dataset_id}/documents/{document_id}/update-by-text instead. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +--- +## default + +### [GET] /app/feedbacks +**List App Feedbacks** + +Retrieve a paginated list of all feedback submitted for messages in this application, including both end-user and admin feedback. #### Parameters @@ -37,17 +163,49 @@ Returns paginated list of all feedback submitted for messages in this app. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Feedbacks retrieved successfully | **application/json**: [AppFeedbackListResponse](#appfeedbacklistresponse)
| +| 200 | A list of application feedbacks. | **application/json**: [AppFeedbackListResponse](#appfeedbacklistresponse)
| | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -### [POST] /apps/annotation-reply/{action} -**Enable or disable annotation reply feature** +### [POST] /messages/{message_id}/feedbacks +**Submit Message Feedback** + +Submit feedback for a message. End users can rate messages as `like` or `dislike`, and optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted feedback. #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| action | path | Action to perform: 'enable' or 'disable' | Yes | string | +| message_id | path | Message ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MessageFeedbackPayloadWithUser](#messagefeedbackpayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Feedback submitted successfully | **application/json**: [ResultResponse](#resultresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Message does not exist. | | + +--- +## default + +### [POST] /apps/annotation-reply/{action} +**Configure Annotation Reply** + +Enables or disables the annotation reply feature. Requires embedding model configuration when enabling. Executes asynchronously — use [Get Annotation Reply Job Status](/api-reference/annotations/get-annotation-reply-job-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| action | path | Action to perform: 'enable' or 'disable' | Yes | string,
**Available values:** "disable", "enable" | #### Request Body @@ -59,29 +217,36 @@ Returns paginated list of all feedback submitted for messages in this app. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Action completed successfully | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Annotation reply settings task initiated. | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | ### [GET] /apps/annotation-reply/{action}/status/{job_id} -**Get the status of an annotation reply action job** +**Get Annotation Reply Job Status** + +Retrieves the status of an asynchronous annotation reply configuration job started by [Configure Annotation Reply](/api-reference/annotations/configure-annotation-reply). #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action type | Yes | string | -| job_id | path | Job ID | Yes | string | +| job_id | path | Job ID | Yes | string (uuid) | #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Job status retrieved successfully | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Successfully retrieved task status. | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 400 | `invalid_param` : The specified job does not exist. | | | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | | 404 | Job not found | | ### [GET] /apps/annotations -**List annotations for the application** +**List Annotations** + +Retrieves a paginated list of annotations for the application. Supports keyword search filtering. #### Parameters @@ -95,11 +260,14 @@ Returns paginated list of all feedback submitted for messages in this app. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotations retrieved successfully | **application/json**: [AnnotationList](#annotationlist)
| +| 200 | Successfully retrieved annotation list. | **application/json**: [AnnotationList](#annotationlist)
| | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | ### [POST] /apps/annotations -**Create a new annotation** +**Create Annotation** + +Creates a new annotation. Annotations provide predefined question-answer pairs that the app can match and return directly instead of generating a response. #### Request Body @@ -111,35 +279,40 @@ Returns paginated list of all feedback submitted for messages in this app. | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Annotation created successfully | **application/json**: [Annotation](#annotation)
| +| 201 | Annotation created successfully. | **application/json**: [Annotation](#annotation)
| | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | ### [DELETE] /apps/annotations/{annotation_id} -**Delete an annotation** +**Delete Annotation** + +Deletes an annotation and its associated hit history. #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_id | path | Annotation ID | Yes | string | +| annotation_id | path | Annotation ID | Yes | string (uuid) | #### Responses | Code | Description | | ---- | ----------- | -| 204 | Annotation deleted successfully | +| 204 | Annotation deleted successfully. | | 401 | Unauthorized - invalid API token | -| 403 | Forbidden - insufficient permissions | -| 404 | Annotation not found | +| 403 | `forbidden` : Insufficient permissions to edit annotations. | +| 404 | `not_found` : Annotation does not exist. | ### [PUT] /apps/annotations/{annotation_id} -**Update an existing annotation** +**Update Annotation** + +Updates the question and answer of an existing annotation. #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| annotation_id | path | Annotation ID | Yes | string | +| annotation_id | path | Annotation ID | Yes | string (uuid) | #### Request Body @@ -151,54 +324,88 @@ Returns paginated list of all feedback submitted for messages in this app. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotation updated successfully | **application/json**: [Annotation](#annotation)
| +| 200 | Annotation updated successfully. | **application/json**: [Annotation](#annotation)
| | 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Annotation not found | | +| 403 | `forbidden` : Insufficient permissions to edit annotations. | | +| 404 | `not_found` : Annotation does not exist. | | + +--- +## default ### [POST] /audio-to-text -**Convert audio to text using speech-to-text** +**Convert Audio to Text** -Convert audio to text using speech-to-text -Accepts an audio file upload and returns the transcribed text. +Convert audio file to text. Supported MIME types: `audio/mp3`, `audio/mpga`, `audio/m4a`, `audio/wav`, and `audio/amr`. File size limit is `30 MB`. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary, **"user"**: string }
| #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Audio successfully transcribed | **application/json**: [AudioTranscriptResponse](#audiotranscriptresponse)
| -| 400 | Bad request - no audio or invalid audio | | +| 200 | Successfully converted audio to text. | **application/json**: [AudioTranscriptResponse](#audiotranscriptresponse)
| +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_support_speech_to_text` : Model provider does not support speech-to-text. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model does not support this operation. - `completion_request_error` : Speech recognition request failed. | | | 401 | Unauthorized - invalid API token | | -| 413 | Audio file too large | | -| 415 | Unsupported audio type | | -| 500 | Internal server error | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 413 | `audio_too_large` : Audio file size exceeded the limit. | | +| 415 | `unsupported_audio_type` : Audio type is not allowed. | | +| 500 | `internal_server_error` : Internal server error. | | + +### [POST] /text-to-audio +**Convert Text to Audio** + +Convert text to speech. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToAudioPayloadWithUser](#texttoaudiopayloadwithuser)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Returns the generated audio. Generator responses are streamed by the service as `audio/mpeg`; otherwise the provider output is returned directly. | +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model does not support this operation. - `completion_request_error` : Text-to-speech request failed. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 500 | `internal_server_error` : Internal server error. | + +--- +## default ### [POST] /chat-messages -**Send a message in a chat conversation** +**Send Chat Message** -Send a message in a chat conversation -This endpoint handles chat messages for chat, agent chat, and advanced chat applications. -Supports conversation management and both blocking and streaming response modes. +Send a request to the chat application. #### Request Body | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [ChatRequestPayload](#chatrequestpayload)
| +| Yes | **application/json**: [ChatRequestPayloadWithUser](#chatrequestpayloadwithuser)
| #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Message sent successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | Bad request - invalid parameters or workflow issues | | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | | 401 | Unauthorized - invalid API token | | -| 404 | Conversation or workflow not found | | -| 429 | Rate limit exceeded | | -| 500 | Internal server error | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Conversation does not exist. | | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | +| 500 | `internal_server_error` : Internal server error. | | ### [POST] /chat-messages/{task_id}/stop -**Stop a running chat message generation** +**Stop Chat Message Generation** + +Stops a chat message generation task. Only supported in `streaming` mode. #### Parameters @@ -206,1513 +413,49 @@ Supports conversation management and both blocking and streaming response modes. | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | The ID of the task to stop | Yes | string | -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Task not found | | - -### [POST] /completion-messages -**Create a completion for the given prompt** - -Create a completion for the given prompt -This endpoint generates a completion based on the provided inputs and query. -Supports both blocking and streaming response modes. - #### Request Body | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [CompletionRequestPayload](#completionrequestpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Completion created successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | Bad request - invalid parameters | | -| 401 | Unauthorized - invalid API token | | -| 404 | Conversation not found | | -| 500 | Internal server error | | - -### [POST] /completion-messages/{task_id}/stop -**Stop a running completion task** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| task_id | path | The ID of the task to stop | Yes | string | +| Yes | **application/json**: [RequiredServiceApiUserPayload](#requiredserviceapiuserpayload)
| #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | | 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | | 404 | Task not found | | -### [GET] /conversations -**List all conversations for the current user** - -List all conversations for the current user -Supports pagination using last_id and limit parameters. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| last_id | query | Last conversation ID for pagination | No | string | -| limit | query | Number of conversations to return | No | integer,
**Default:** 20 | -| sort_by | query | Sort order for conversations | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Conversations retrieved successfully | **application/json**: [ConversationInfiniteScrollPagination](#conversationinfinitescrollpagination)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Last conversation not found | | - -### [DELETE] /conversations/{c_id} -**Delete a specific conversation** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Conversation deleted successfully | -| 401 | Unauthorized - invalid API token | -| 404 | Conversation not found | - -### [POST] /conversations/{c_id}/name -**Rename a conversation or auto-generate a name** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ConversationRenamePayload](#conversationrenamepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Conversation renamed successfully | **application/json**: [SimpleConversation](#simpleconversation)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Conversation not found | | - -### [GET] /conversations/{c_id}/variables -**List all variables for a conversation** - -List all variables for a conversation -Conversational variables are only available for chat applications. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation ID | Yes | string | -| last_id | query | Last variable ID for pagination | No | string | -| limit | query | Number of variables to return | No | integer,
**Default:** 20 | -| variable_name | query | Filter variables by name | No | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Variables retrieved successfully | **application/json**: [ConversationVariableInfiniteScrollPaginationResponse](#conversationvariableinfinitescrollpaginationresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Conversation not found | | - -### [PUT] /conversations/{c_id}/variables/{variable_id} -**Update a conversation variable's value** - -Update a conversation variable's value -Allows updating the value of a specific conversation variable. -The value must match the variable's expected type. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation ID | Yes | string | -| variable_id | path | Variable ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ConversationVariableUpdatePayload](#conversationvariableupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Variable updated successfully | **application/json**: [ConversationVariableResponse](#conversationvariableresponse)
| -| 400 | Bad request - type mismatch | | -| 401 | Unauthorized - invalid API token | | -| 404 | Conversation or variable not found | | - -### [GET] /datasets -**Resource for getting datasets** - -List all datasets - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| include_all | query | Include all datasets | No | boolean | -| keyword | query | Search keyword | No | string | -| limit | query | Number of items per page | No | integer,
**Default:** 20 | -| page | query | Page number | No | integer,
**Default:** 1 | -| tag_ids | query | Filter by tag IDs | No | [ string ] | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Datasets retrieved successfully | **application/json**: [DatasetListResponse](#datasetlistresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets -**Resource for creating datasets** - -Create a new dataset - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DatasetCreatePayload](#datasetcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Dataset created successfully | **application/json**: [DatasetDetailResponse](#datasetdetailresponse)
| -| 400 | Bad request - invalid parameters | | -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/pipeline/file-upload -**Upload a file for use in conversations** - -Upload a file to a knowledgebase pipeline -Accepts a single file upload via multipart/form-data. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | File uploaded successfully | **application/json**: [PipelineUploadFileResponse](#pipelineuploadfileresponse)
| -| 400 | Bad request - no file or invalid file | | -| 401 | Unauthorized - invalid API token | | -| 413 | File too large | | -| 415 | Unsupported file type | | - -### [DELETE] /datasets/tags -**Delete a knowledge type tag** - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TagDeletePayload](#tagdeletepayload)
| - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Tag deleted successfully | -| 401 | Unauthorized - invalid API token | -| 403 | Forbidden - insufficient permissions | - -### [GET] /datasets/tags -**Get all knowledge type tags** - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Tags retrieved successfully | **application/json**: [KnowledgeTagListResponse](#knowledgetaglistresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [PATCH] /datasets/tags -Update a knowledge type tag - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TagUpdatePayload](#tagupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Tag updated successfully | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | - -### [POST] /datasets/tags -**Add a knowledge type tag** - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TagCreatePayload](#tagcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Tag created successfully | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | - -### [POST] /datasets/tags/binding -Bind tags to a dataset - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TagBindingPayload](#tagbindingpayload)
| - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Tags bound successfully | -| 401 | Unauthorized - invalid API token | -| 403 | Forbidden - insufficient permissions | - -### [POST] /datasets/tags/unbinding -Unbind tags from a dataset - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TagUnbindingPayload](#tagunbindingpayload)
| - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Tags unbound successfully | -| 401 | Unauthorized - invalid API token | -| 403 | Forbidden - insufficient permissions | - -### [DELETE] /datasets/{dataset_id} -**Deletes a dataset given its ID** - -Delete a dataset -Args: - _: ignore - dataset_id (UUID): The ID of the dataset to be deleted. - -Returns: - dict: A dictionary with a key 'result' and a value 'success' - if the dataset was successfully deleted. Omitted in HTTP response. - int: HTTP status code 204 indicating that the operation was successful. - -Raises: - NotFound: If the dataset with the given ID does not exist. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Dataset deleted successfully | -| 401 | Unauthorized - invalid API token | -| 404 | Dataset not found | -| 409 | Conflict - dataset is in use | - -### [GET] /datasets/{dataset_id} -Get a specific dataset by ID - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Dataset retrieved successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Dataset not found | | - -### [PATCH] /datasets/{dataset_id} -Update an existing dataset - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DatasetUpdatePayload](#datasetupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Dataset updated successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Dataset not found | | - -### [POST] /datasets/{dataset_id}/document/create-by-file -Create a new document by uploading a file - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 400 | Bad request - invalid file or parameters | | -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/document/create-by-text -Create a new document by providing text content - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 400 | Bad request - invalid parameters | | -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/document/create_by_file -Create a new document by uploading a file - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 400 | Bad request - invalid file or parameters | | -| 401 | Unauthorized - invalid API token | | - -### ~~[POST] /datasets/{dataset_id}/document/create_by_text~~ - -***DEPRECATED*** - -Deprecated legacy alias for creating a new document by providing text content. Use /datasets/{dataset_id}/document/create-by-text instead. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 400 | Bad request - invalid parameters | | -| 401 | Unauthorized - invalid API token | | - -### [GET] /datasets/{dataset_id}/documents -List all documents in a dataset - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| keyword | query | Search keyword | No | string | -| limit | query | Number of items per page | No | integer,
**Default:** 20 | -| page | query | Page number | No | integer,
**Default:** 1 | -| status | query | Document status filter | No | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Documents retrieved successfully | **application/json**: [DocumentListResponse](#documentlistresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [POST] /datasets/{dataset_id}/documents/download-zip -Download selected uploaded documents as a single ZIP archive - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | ZIP archive generated successfully | **application/json**: [BinaryFileResponse](#binaryfileresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Document or dataset not found | | - -### [POST] /datasets/{dataset_id}/documents/metadata -**Update metadata for multiple documents** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [MetadataOperationData](#metadataoperationdata)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Documents metadata updated successfully | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [PATCH] /datasets/{dataset_id}/documents/status/{action} -**Batch update document status** - -Batch update document status -Args: - tenant_id: tenant id - dataset_id: dataset id - action: action to perform (Literal["enable", "disable", "archive", "un_archive"]) - -Returns: - dict: A dictionary with a key 'result' and a value 'success' - int: HTTP status code 200 indicating that the operation was successful. - -Raises: - NotFound: If the dataset with the given ID does not exist. - Forbidden: If the user does not have permission. - InvalidActionError: If the action is invalid or cannot be performed. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| action | path | Action to perform: 'enable', 'disable', 'archive', or 'un_archive' | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentStatusPayload](#documentstatuspayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document status updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -| 400 | Bad request - invalid action | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Dataset not found | | - -### [GET] /datasets/{dataset_id}/documents/{batch}/indexing-status -Get indexing status for documents in a batch - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| batch | path | Batch ID | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Indexing status retrieved successfully | **application/json**: [DocumentStatusListResponse](#documentstatuslistresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset or documents not found | | - -### [DELETE] /datasets/{dataset_id}/documents/{document_id} -**Delete document** - -Delete a document - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Document deleted successfully | -| 401 | Unauthorized - invalid API token | -| 403 | Forbidden - document is archived | -| 404 | Document not found | - -### [GET] /datasets/{dataset_id}/documents/{document_id} -Get a specific document by ID - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| metadata | query | Metadata response mode | No | string,
**Available values:** "all", "only", "without",
**Default:** all | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document retrieved successfully | **application/json**: [DocumentDetailResponse](#documentdetailresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Document not found | | - -### [PATCH] /datasets/{dataset_id}/documents/{document_id} -Update an existing document by uploading a file - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Document not found | | - -### [GET] /datasets/{dataset_id}/documents/{document_id}/download -Get a signed download URL for a document's original uploaded file - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Download URL generated successfully | **application/json**: [UrlResponse](#urlresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - insufficient permissions | | -| 404 | Document or upload file not found | | - -### [GET] /datasets/{dataset_id}/documents/{document_id}/segments -List segments in a document - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| keyword | query | | No | string | -| limit | query | | No | integer,
**Default:** 20 | -| page | query | | No | integer,
**Default:** 1 | -| status | query | | No | [ string ] | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Segments retrieved successfully | **application/json**: [SegmentListResponse](#segmentlistresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset or document not found | | - -### [POST] /datasets/{dataset_id}/documents/{document_id}/segments -Create segments in a document - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [SegmentCreatePayload](#segmentcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Segments created successfully | **application/json**: [SegmentCreateListResponse](#segmentcreatelistresponse)
| -| 400 | Bad request - segments data is missing | | -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset or document not found | | - -### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} -Delete a specific segment - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Segment ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Segment deleted successfully | -| 401 | Unauthorized - invalid API token | -| 404 | Dataset, document, or segment not found | - -### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} -Get a specific segment by ID - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Segment ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Segment retrieved successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset, document, or segment not found | | - -### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} -Update a specific segment - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Segment ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [SegmentUpdatePayload](#segmentupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Segment updated successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset, document, or segment not found | | - -### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks -List child chunks for a segment - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | -| keyword | query | | No | string | -| limit | query | | No | integer,
**Default:** 20 | -| page | query | | No | integer,
**Default:** 1 | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Child chunks retrieved successfully | **application/json**: [ChildChunkListResponse](#childchunklistresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset, document, or segment not found | | - -### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks -Create a new child chunk for a segment - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ChildChunkCreatePayload](#childchunkcreatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Child chunk created successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset, document, or segment not found | | - -### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} -Delete a specific child chunk - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| child_chunk_id | path | Child chunk ID | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Child chunk deleted successfully | -| 401 | Unauthorized - invalid API token | -| 404 | Dataset, document, segment, or child chunk not found | - -### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} -Update a specific child chunk - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| child_chunk_id | path | Child chunk ID | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | -| segment_id | path | Parent segment ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [ChildChunkUpdatePayload](#childchunkupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Child chunk updated successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset, document, segment, or child chunk not found | | - -### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update-by-file~~ - -***DEPRECATED*** - -Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Document not found | | - -### [POST] /datasets/{dataset_id}/documents/{document_id}/update-by-text -Update an existing document by providing text content - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Document not found | | - -### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_file~~ - -***DEPRECATED*** - -Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Document not found | | - -### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_text~~ - -***DEPRECATED*** - -Deprecated legacy alias for updating an existing document by providing text content. Use /datasets/{dataset_id}/documents/{document_id}/update-by-text instead. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| document_id | path | Document ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Document not found | | - -### [POST] /datasets/{dataset_id}/hit-testing -**Perform hit testing on a dataset** - -Perform hit testing on a dataset -Tests retrieval performance for the specified dataset. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Hit testing results | **application/json**: [HitTestingResponse](#hittestingresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [GET] /datasets/{dataset_id}/metadata -**Get all metadata for a dataset** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Metadata retrieved successfully | **application/json**: [DatasetMetadataListResponse](#datasetmetadatalistresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [POST] /datasets/{dataset_id}/metadata -**Create metadata for a dataset** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [MetadataArgs](#metadataargs)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Metadata created successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [GET] /datasets/{dataset_id}/metadata/built-in -**Get all built-in metadata fields** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Built-in fields retrieved successfully | **application/json**: [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/metadata/built-in/{action} -**Enable or disable built-in metadata field** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| action | path | Action to perform: 'enable' or 'disable' | Yes | string | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Action completed successfully | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [DELETE] /datasets/{dataset_id}/metadata/{metadata_id} -**Delete metadata** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| metadata_id | path | Metadata ID | Yes | string | - -#### Responses - -| Code | Description | -| ---- | ----------- | -| 204 | Metadata deleted successfully | -| 401 | Unauthorized - invalid API token | -| 404 | Dataset or metadata not found | - -### [PATCH] /datasets/{dataset_id}/metadata/{metadata_id} -**Update metadata name** - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | -| metadata_id | path | Metadata ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [MetadataUpdatePayload](#metadataupdatepayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Metadata updated successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset or metadata not found | | - -### [GET] /datasets/{dataset_id}/pipeline/datasource-plugins -**Resource for getting datasource plugins** - -List all datasource plugins for a rag pipeline - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| is_published | query | | No | boolean,
**Default:** true | -| dataset_id | path | | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Datasource plugins retrieved successfully | **application/json**: [DatasourcePluginListResponse](#datasourcepluginlistresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/pipeline/datasource/nodes/{node_id}/run -**Resource for getting datasource plugins** - -Run a datasource node for a rag pipeline - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | -| node_id | path | | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [DatasourceNodeRunPayload](#datasourcenoderunpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Datasource node run successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/pipeline/run -**Resource for running a rag pipeline** - -Run a datasource node for a rag pipeline - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [PipelineRunApiEntity](#pipelinerunapientity)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Pipeline run successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [POST] /datasets/{dataset_id}/retrieve -**Perform hit testing on a dataset** - -Perform hit testing on a dataset -Tests retrieval performance for the specified dataset. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Hit testing results | **application/json**: [HitTestingResponse](#hittestingresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### [GET] /datasets/{dataset_id}/tags -**Get all knowledge type tags** - -Get tags bound to a specific dataset - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | Dataset ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Tags retrieved successfully | **application/json**: [DatasetBoundTagListResponse](#datasetboundtaglistresponse)
| -| 401 | Unauthorized - invalid API token | | - -### [GET] /end-users/{end_user_id} -**Get end user detail** - -Get an end user by ID -This endpoint is scoped to the current app token's tenant/app to prevent -cross-tenant/app access when an end-user ID is known. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| end_user_id | path | End user ID | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | End user retrieved successfully | **application/json**: [EndUserDetail](#enduserdetail)
| -| 401 | Unauthorized - invalid API token | | -| 404 | End user not found | | - -### [POST] /files/upload -**Upload a file for use in conversations** - -Upload a file for use in conversations -Accepts a single file upload via multipart/form-data. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| -| 400 | Bad request - no file or invalid file | | -| 401 | Unauthorized - invalid API token | | -| 413 | File too large | | -| 415 | Unsupported file type | | - -### [GET] /files/{file_id}/preview -**Preview/Download a file that was uploaded via Service API** - -Preview or download a file uploaded via Service API -Provides secure file preview/download functionality. -Files can only be accessed if they belong to messages within the requesting app's context. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| file_id | path | UUID of the file to preview | Yes | string | -| as_attachment | query | Download as attachment | No | boolean | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | File retrieved successfully | **application/json**: [BinaryFileResponse](#binaryfileresponse)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - file access denied | | -| 404 | File not found | | - -### [GET] /form/human_input/{form_token} -Get a paused human input form by token - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| form_token | path | Human input form token | Yes | string | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Form retrieved successfully | **application/json**: [HumanInputFormDefinitionResponse](#humaninputformdefinitionresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Form not found | | -| 412 | Form already submitted or expired | | - -### [POST] /form/human_input/{form_token} -Submit a paused human input form by token - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| form_token | path | Human input form token | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Form submitted successfully | **application/json**: [HumanInputFormSubmitResponse](#humaninputformsubmitresponse)
| -| 400 | Bad request - invalid submission data | | -| 401 | Unauthorized - invalid API token | | -| 404 | Form not found | | -| 412 | Form already submitted or expired | | - -### [GET] /info -**Get app information** - -Get basic application information -Returns basic information about the application including name, description, tags, and mode. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Application info retrieved successfully | **application/json**: [AppInfoResponse](#appinforesponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Application not found | | - -### [GET] /messages -**List messages in a conversation** - -List messages in a conversation -Retrieves messages with pagination support using first_id. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| conversation_id | query | Conversation UUID | Yes | string | -| first_id | query | First message ID for pagination | No | string | -| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Messages retrieved successfully | **application/json**: [MessageInfiniteScrollPagination](#messageinfinitescrollpagination)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Conversation or first message not found | | - -### [POST] /messages/{message_id}/feedbacks -**Submit feedback for a message** - -Submit feedback for a message -Allows users to rate messages as like/dislike and provide optional feedback content. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| message_id | path | Message ID | Yes | string | - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Feedback submitted successfully | **application/json**: [ResultResponse](#resultresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Message not found | | - ### [GET] /messages/{message_id}/suggested -**Get suggested follow-up questions for a message** +**Get Next Suggested Questions** -Get suggested follow-up questions for a message -Returns AI-generated follow-up questions based on the message content. +Get next questions suggestions for the current message. #### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| message_id | path | Message ID | Yes | string | +| message_id | path | Message ID | Yes | string (uuid) | +| user | query | End user identifier | Yes | string | #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | | 200 | Suggested questions retrieved successfully | **application/json**: [SimpleResultStringListResponse](#simpleresultstringlistresponse)
| -| 400 | Suggested questions feature is disabled | | +| 400 | - `not_chat_app` : App mode does not match the API route. - `bad_request` : Suggested questions feature is disabled. | | | 401 | Unauthorized - invalid API token | | -| 404 | Message not found | | -| 500 | Internal server error | | - -### [GET] /meta -**Get app metadata** - -Get application metadata -Returns metadata about the application including configuration and settings. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Metadata retrieved successfully | **application/json**: [AppMetaResponse](#appmetaresponse)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Application not found | | - -### [GET] /parameters -**Retrieve app parameters** - -Retrieve application input parameters and configuration -Returns the input form parameters and configuration for the application. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Parameters retrieved successfully | **application/json**: [Parameters](#parameters)
| -| 401 | Unauthorized - invalid API token | | -| 404 | Application not found | | - -### [GET] /site -**Retrieve app site info** - -Get application site configuration -Returns the site configuration for the application including theme, icons, and text. - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Site configuration retrieved successfully | **application/json**: [Site](#site)
| -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - site not found or tenant archived | | - -### [POST] /text-to-audio -**Convert text to audio using text-to-speech** - -Convert text to audio using text-to-speech -Converts the provided text to audio using the specified voice. - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [TextToAudioPayload](#texttoaudiopayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Text successfully converted to audio | **application/json**: [AudioBinaryResponse](#audiobinaryresponse)
| -| 400 | Bad request - invalid parameters | | -| 401 | Unauthorized - invalid API token | | -| 500 | Internal server error | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Message does not exist. | | +| 500 | `internal_server_error` : Internal server error. | | ### [GET] /workflow/{task_id}/events -Get workflow execution events stream after resume +**Stream Workflow Events** + +Resume the Server-Sent Events stream for a workflow run after a pause or a dropped SSE connection. For runs that have already finished, the stream emits a single `workflow_finished` event and closes. #### Parameters @@ -1727,15 +470,16 @@ Get workflow execution events stream after resume | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | SSE event stream | **application/json**: [EventStreamResponse](#eventstreamresponse)
| +| 200 | Server-Sent Events stream. Each event is delivered as `data: {JSON}\\n\\n`. Event payloads follow the same schemas as the original streaming response. | **text/event-stream**: [EventStreamResponse](#eventstreamresponse)
| +| 400 | `not_workflow_app` : Please check if your app mode matches the right API route. | | | 401 | Unauthorized - invalid API token | | -| 404 | Workflow run not found | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Workflow run not found. | | ### [GET] /workflows/logs -**Get workflow app logs** +**List Workflow Logs** -Get workflow execution logs -Returns paginated workflow execution logs with filtering options. +Retrieve paginated workflow execution logs with filtering options. #### Parameters @@ -1754,38 +498,14 @@ Returns paginated workflow execution logs with filtering options. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Logs retrieved successfully | **application/json**: [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse)
| +| 200 | Successfully retrieved workflow logs. | **application/json**: [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse)
| | 401 | Unauthorized - invalid API token | | - -### [POST] /workflows/run -**Execute a workflow** - -Execute a workflow -Runs a workflow with the provided inputs and returns the results. -Supports both blocking and streaming response modes. - -#### Request Body - -| Required | Schema | -| -------- | ------ | -| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Workflow executed successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | Bad request - invalid parameters or workflow issues | | -| 401 | Unauthorized - invalid API token | | -| 404 | Workflow not found | | -| 429 | Rate limit exceeded | | -| 500 | Internal server error | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | ### [GET] /workflows/run/{workflow_run_id} -**Get a workflow task running detail** +**Get Workflow Run Detail** -Get workflow run details -Returns detailed information about a specific workflow run. +Retrieve the current execution results of a workflow task based on the workflow execution ID. #### Parameters @@ -1797,12 +517,1744 @@ Returns detailed information about a specific workflow run. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run details retrieved successfully | **application/json**: [WorkflowRunResponse](#workflowrunresponse)
| +| 200 | Successfully retrieved workflow run details. | **application/json**: [WorkflowRunResponse](#workflowrunresponse)
| +| 400 | `not_workflow_app` : App mode does not match the API route. | | | 401 | Unauthorized - invalid API token | | -| 404 | Workflow run not found | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Workflow run not found. | | + +--- +## default + +### [POST] /chat-messages +**Send Chat Message** + +Send a request to the chat application. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChatRequestPayloadWithUser](#chatrequestpayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Conversation does not exist. | | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | +| 500 | `internal_server_error` : Internal server error. | | + +### [POST] /chat-messages/{task_id}/stop +**Stop Chat Message Generation** + +Stops a chat message generation task. Only supported in `streaming` mode. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| task_id | path | The ID of the task to stop | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RequiredServiceApiUserPayload](#requiredserviceapiuserpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Task not found | | + +### [GET] /messages/{message_id}/suggested +**Get Next Suggested Questions** + +Get next questions suggestions for the current message. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| message_id | path | Message ID | Yes | string (uuid) | +| user | query | End user identifier | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Suggested questions retrieved successfully | **application/json**: [SimpleResultStringListResponse](#simpleresultstringlistresponse)
| +| 400 | - `not_chat_app` : App mode does not match the API route. - `bad_request` : Suggested questions feature is disabled. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Message does not exist. | | +| 500 | `internal_server_error` : Internal server error. | | + +--- +## default + +### [POST] /completion-messages +**Send Completion Message** + +Send a request to the text generation application. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionRequestPayloadWithUser](#completionrequestpayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkCompletionEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Conversation not found | | +| 429 | `too_many_requests` : Too many concurrent requests for this app. | | +| 500 | `internal_server_error` : Internal server error. | | + +### [POST] /completion-messages/{task_id}/stop +**Stop Completion Message Generation** + +Stops a completion message generation task. Only supported in `streaming` mode. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| task_id | path | The ID of the task to stop | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RequiredServiceApiUserPayload](#requiredserviceapiuserpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 400 | `app_unavailable` : App unavailable or misconfigured. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Task not found | | + +--- +## default + +### [GET] /conversations +**List Conversations** + +Retrieve the conversation list for the current user, ordered by most recently active. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| last_id | query | Last conversation ID for pagination | No | string | +| limit | query | Number of conversations to return | No | integer,
**Default:** 20 | +| sort_by | query | Sort order for conversations | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | +| user | query | End user identifier | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved conversations list. | **application/json**: [ConversationInfiniteScrollPagination](#conversationinfinitescrollpagination)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Last conversation does not exist (invalid `last_id`). | | + +### [DELETE] /conversations/{c_id} +**Delete Conversation** + +Delete a conversation. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| c_id | path | Conversation ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [OptionalServiceApiUserPayload](#optionalserviceapiuserpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Conversation deleted successfully. | +| 400 | `not_chat_app` : App mode does not match the API route. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | `not_found` : Conversation does not exist. | + +### [POST] /conversations/{c_id}/name +**Rename Conversation** + +Rename a conversation or auto-generate a name. The conversation name is used for display on clients that support multiple conversations. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| c_id | path | Conversation ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationRenamePayloadWithUser](#conversationrenamepayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Conversation renamed successfully. | **application/json**: [SimpleConversation](#simpleconversation)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Conversation does not exist. | | + +### [GET] /conversations/{c_id}/variables +**List Conversation Variables** + +Retrieve variables from a specific conversation. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| c_id | path | Conversation ID | Yes | string (uuid) | +| last_id | query | Last variable ID for pagination | No | string | +| limit | query | Number of variables to return | No | integer,
**Default:** 20 | +| user | query | End user identifier | No | string | +| variable_name | query | Filter variables by name | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved conversation variables. | **application/json**: [ConversationVariableInfiniteScrollPaginationResponse](#conversationvariableinfinitescrollpaginationresponse)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Conversation does not exist. | | + +### [PUT] /conversations/{c_id}/variables/{variable_id} +**Update Conversation Variable** + +Update the value of a specific conversation variable. The value must match the expected type. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| c_id | path | Conversation ID | Yes | string (uuid) | +| variable_id | path | Variable ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationVariableUpdatePayloadWithUser](#conversationvariableupdatepayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Variable updated successfully. | **application/json**: [ConversationVariableResponse](#conversationvariableresponse)
| +| 400 | - `not_chat_app` : App mode does not match the API route. - `bad_request` : Variable value type mismatch. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | - `not_found` : Conversation does not exist. - `not_found` : Conversation variable does not exist. | | + +### [GET] /messages +**List Conversation Messages** + +Returns historical chat records in a scrolling load format, with the first page returning the latest `limit` messages, i.e., in reverse order. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| conversation_id | query | Conversation UUID | Yes | string | +| first_id | query | First message ID for pagination | No | string | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | +| user | query | End user identifier | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved conversation history. | **application/json**: [MessageInfiniteScrollPagination](#messageinfinitescrollpagination)
| +| 400 | `not_chat_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | - `not_found` : Conversation does not exist. - `not_found` : First message does not exist. | | + +--- +## default + +### [GET] /datasets +**List Knowledge Bases** + +Returns a paginated list of knowledge bases. Supports filtering by keyword and tags. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| include_all | query | Include all datasets | No | boolean | +| keyword | query | Search keyword | No | string | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | +| tag_ids | query | Filter by tag IDs | No | [ string ] | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of knowledge bases. | **application/json**: [DatasetListResponse](#datasetlistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [POST] /datasets +**Create an Empty Knowledge Base** + +Create a new empty knowledge base. After creation, use [Create Document by Text](/api-reference/documents/create-document-by-text) or [Create Document by File](/api-reference/documents/create-document-by-file) to add documents. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetCreatePayload](#datasetcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Knowledge base created successfully. | **application/json**: [DatasetDetailResponse](#datasetdetailresponse)
| +| 400 | Bad request - invalid parameters | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 409 | `dataset_name_duplicate` : The dataset name already exists. Please modify your dataset name. | | + +### [DELETE] /datasets/{dataset_id} +**Delete Knowledge Base** + +Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - dataset API access or workspace access denied | +| 404 | `not_found` : Dataset not found. | +| 409 | `dataset_in_use` : The knowledge base is being used by some apps. Please remove it from the apps before deleting. | + +### [GET] /datasets/{dataset_id} +**Get Knowledge Base** + +Retrieve detailed information about a specific knowledge base, including its embedding model, retrieval configuration, and document statistics. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Knowledge base details. | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Insufficient permissions to access this knowledge base. | | +| 404 | `not_found` : Dataset not found. | | + +### [PATCH] /datasets/{dataset_id} +**Update Knowledge Base** + +Update the name, description, permissions, or retrieval settings of an existing knowledge base. Only the fields provided in the request body are updated. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetUpdatePayload](#datasetupdatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Knowledge base updated successfully. | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Insufficient permissions to access this knowledge base. | | +| 404 | `not_found` : Dataset not found. | | + +### [POST] /datasets/{dataset_id}/hit-testing +**Retrieve Chunks from a Knowledge Base / Test Retrieval** + +Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Retrieval results. | **application/json**: [HitTestingResponse](#hittestingresponse)
| +| 400 | - `dataset_not_initialized` : The dataset is still being initialized or indexing. Please wait a moment. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `provider_quota_exceeded` : Your quota for Dify Hosted OpenAI has been exhausted. Please go to Settings -> Model Provider to complete your own provider credentials. - `model_currently_not_support` : Dify Hosted OpenAI trial currently not support the GPT-4 model. - `completion_request_error` : Completion request failed. - `invalid_param` : Invalid parameter value. | | +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Insufficient permissions. | | +| 404 | `not_found` : Knowledge base not found. | | +| 500 | `internal_server_error` : An internal error occurred during retrieval. | | + +### [POST] /datasets/{dataset_id}/retrieve +**Retrieve Chunks from a Knowledge Base / Test Retrieval** + +Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Retrieval results. | **application/json**: [HitTestingResponse](#hittestingresponse)
| +| 400 | - `dataset_not_initialized` : The dataset is still being initialized or indexing. Please wait a moment. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `provider_quota_exceeded` : Your quota for Dify Hosted OpenAI has been exhausted. Please go to Settings -> Model Provider to complete your own provider credentials. - `model_currently_not_support` : Dify Hosted OpenAI trial currently not support the GPT-4 model. - `completion_request_error` : Completion request failed. - `invalid_param` : Invalid parameter value. | | +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Insufficient permissions. | | +| 404 | `not_found` : Knowledge base not found. | | +| 500 | `internal_server_error` : An internal error occurred during retrieval. | | + +--- +## default + +### [POST] /datasets/pipeline/file-upload +**Upload Pipeline File** + +Upload a file for use in a knowledge pipeline. Accepts a single file via `multipart/form-data`. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | File uploaded successfully. | **application/json**: [PipelineUploadFileResponse](#pipelineuploadfileresponse)
| +| 400 | - `no_file_uploaded` : Please upload your file. - `filename_not_exists_error` : The specified filename does not exist. - `too_many_files` : Only one file is allowed. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 413 | `file_too_large` : File size exceeded. | | +| 415 | `unsupported_file_type` : File type not allowed. | | + +### [GET] /datasets/{dataset_id}/pipeline/datasource-plugins +**List Datasource Plugins** + +List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses plus the metadata needed to run it. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| is_published | query | | No | boolean,
**Default:** true | +| dataset_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of datasource nodes configured in the pipeline. | **application/json**: [DatasourcePluginListResponse](#datasourcepluginlistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | `not_found` : Dataset not found. | | + +### [POST] /datasets/{dataset_id}/pipeline/datasource/nodes/{node_id}/run +**Run Datasource Node** + +Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the node execution results. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | | Yes | string (uuid) | +| node_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceNodeRunPayload](#datasourcenoderunpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Streaming response with node execution events. | **text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | `not_found` : Dataset not found. | | + +### [POST] /datasets/{dataset_id}/pipeline/run +**Run Pipeline** + +Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response modes. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [PipelineRunApiEntity](#pipelinerunapientity)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Pipeline execution result. Format depends on `response_mode`: streaming returns a `text/event-stream`, blocking returns a JSON object. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Forbidden. | | +| 404 | `not_found` : Dataset not found. | | +| 500 | `pipeline_run_error` : Pipeline execution failed. | | + +--- +## default + +### [DELETE] /datasets/tags +**Delete Knowledge Tag** + +Permanently delete a knowledge base tag. Does not delete the knowledge bases that were tagged. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagDeletePayload](#tagdeletepayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - insufficient permissions | + +### [GET] /datasets/tags +**List Knowledge Tags** + +Returns the list of all knowledge base tags in the workspace. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of tags. | **application/json**: [KnowledgeTagListResponse](#knowledgetaglistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [PATCH] /datasets/tags +**Update Knowledge Tag** + +Rename an existing knowledge base tag. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagUpdatePayload](#tagupdatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tag updated successfully. | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - insufficient permissions | | + +### [POST] /datasets/tags +**Create Knowledge Tag** + +Create a new tag for organizing knowledge bases. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagCreatePayload](#tagcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tag created successfully. | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - insufficient permissions | | + +### [POST] /datasets/tags/binding +**Create Tag Binding** + +Bind one or more tags to a knowledge base. A knowledge base can have multiple tags. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagBindingPayload](#tagbindingpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - insufficient permissions | + +### [POST] /datasets/tags/unbinding +**Delete Tag Binding** + +Remove one or more tags from a knowledge base. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagUnbindingPayload](#tagunbindingpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - insufficient permissions | + +### [GET] /datasets/{dataset_id}/tags +**Get Knowledge Base Tags** + +Returns the list of tags bound to a specific knowledge base. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Tags bound to the knowledge base. | **application/json**: [DatasetBoundTagListResponse](#datasetboundtaglistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +--- +## default + +### [POST] /datasets/{dataset_id}/document/create-by-file +**Create Document by File** + +Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document created successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `no_file_uploaded` : Please upload your file. - `too_many_files` : Only one file is allowed. - `filename_not_exists_error` : The specified filename does not exist. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist, external datasets not supported, file too large, unsupported file type, missing required fields, or invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [POST] /datasets/{dataset_id}/document/create-by-text +**Create Document by Text** + +Create a document from raw text content. The document is processed asynchronously — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document created successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist. / indexing_technique is required. / Invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### ~~[POST] /datasets/{dataset_id}/document/create_by_file~~ + +***DEPRECATED*** + +**Create Document by File** + +Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document created successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `no_file_uploaded` : Please upload your file. - `too_many_files` : Only one file is allowed. - `filename_not_exists_error` : The specified filename does not exist. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist, external datasets not supported, file too large, unsupported file type, missing required fields, or invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [GET] /datasets/{dataset_id}/documents +**List Documents** + +Returns a paginated list of documents in the knowledge base. Supports filtering by keyword and indexing status. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| keyword | query | Search keyword | No | string | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | +| status | query | Document status filter | No | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of documents. | **application/json**: [DocumentListResponse](#documentlistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | `not_found` : Knowledge base not found. | | + +### [POST] /datasets/{dataset_id}/documents/download-zip +**Download Documents as ZIP** + +Download multiple uploaded-file documents as a single ZIP archive. Accepts up to `100` document IDs. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | ZIP archive containing the requested documents. | +| 401 | Unauthorized - invalid API token | +| 403 | `forbidden` : Insufficient permissions. | +| 404 | `not_found` : Document or dataset not found. | + +### [PATCH] /datasets/{dataset_id}/documents/status/{action} +**Update Document Status in Batch** + +Enable, disable, archive, or unarchive multiple documents at once. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| action | path | Action to perform: 'enable', 'disable', 'archive', or 'un_archive' | Yes | string,
**Available values:** "archive", "disable", "enable", "un_archive" | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentStatusPayload](#documentstatuspayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document status updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 400 | `invalid_action` : Invalid action. | | +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Insufficient permissions. | | +| 404 | `not_found` : Knowledge base not found. | | + +### [GET] /datasets/{dataset_id}/documents/{batch}/indexing-status +**Get Document Indexing Status** + +Check the indexing progress of documents in a batch. Returns the current processing stage and chunk completion counts for each document. Poll this endpoint until `indexing_status` reaches `completed` or `error`. The status progresses through: `waiting` → `parsing` → `cleaning` → `splitting` → `indexing` → `completed`. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| batch | path | Batch ID | Yes | string | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Indexing status for documents in the batch. | **application/json**: [DocumentStatusListResponse](#documentstatuslistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | `not_found` : Knowledge base not found. / Documents not found. | | + +### [DELETE] /datasets/{dataset_id}/documents/{document_id} +**Delete Document** + +Permanently delete a document and all its chunks from the knowledge base. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 400 | `document_indexing` : Cannot delete document during indexing. | +| 401 | Unauthorized - invalid API token | +| 403 | `archived_document_immutable` : The archived document is not editable. | +| 404 | `not_found` : Document Not Exists. | + +### [GET] /datasets/{dataset_id}/documents/{document_id} +**Get Document** + +Retrieve detailed information about a specific document, including its indexing status, metadata, and processing statistics. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| metadata | query | Metadata response mode | No | string,
**Available values:** "all", "only", "without",
**Default:** all | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document details. The response shape varies based on the `metadata` query parameter. When `metadata` is `only`, only `id`, `doc_type`, and `doc_metadata` are returned. When `metadata` is `without`, `doc_type` and `doc_metadata` are omitted. | **application/json**: [DocumentDetailResponse](#documentdetailresponse)
| +| 400 | `invalid_metadata` : Invalid metadata value for the specified key. | | +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : No permission. | | +| 404 | `not_found` : Document not found. | | + +### [PATCH] /datasets/{dataset_id}/documents/{document_id} +Update an existing document by uploading a file + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +### [GET] /datasets/{dataset_id}/documents/{document_id}/download +**Download Document** + +Get a signed download URL for a document's original uploaded file. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Download URL generated successfully. | **application/json**: [UrlResponse](#urlresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : No permission to access this document. | | +| 404 | `not_found` : Document not found. | | + +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update-by-file~~ + +***DEPRECATED*** + +**Update Document by File** + +Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `too_many_files` : Only one file is allowed. - `filename_not_exists_error` : The specified filename does not exist. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist, external datasets not supported, file too large, unsupported file type, or invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +### [POST] /datasets/{dataset_id}/documents/{document_id}/update-by-text +**Update Document by Text** + +Update an existing document's text content, name, or processing configuration. Re-triggers indexing if content changes — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist, name is required when text is provided, or invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_file~~ + +***DEPRECATED*** + +**Update Document by File** + +Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document updated successfully. | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| +| 400 | - `too_many_files` : Only one file is allowed. - `filename_not_exists_error` : The specified filename does not exist. - `provider_not_initialize` : No valid model provider credentials found. Please go to Settings -> Model Provider to complete your provider credentials. - `invalid_param` : Knowledge base does not exist, external datasets not supported, file too large, unsupported file type, or invalid doc_form (must be `text_model`, `hierarchical_model`, or `qa_model`). | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Document not found | | + +--- +## default + +### [POST] /datasets/{dataset_id}/documents/metadata +**Update Document Metadata in Batch** + +Update metadata values for multiple documents at once. Each document in the request receives the specified metadata key-value pairs. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataOperationData](#metadataoperationdata)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Document metadata updated successfully. | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset not found | | + +### [GET] /datasets/{dataset_id}/metadata +**List Metadata Fields** + +Returns the list of all metadata fields (both custom and built-in) for the knowledge base, along with the count of documents using each field. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Metadata fields for the knowledge base. | **application/json**: [DatasetMetadataListResponse](#datasetmetadatalistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset not found | | + +### [POST] /datasets/{dataset_id}/metadata +**Create Metadata Field** + +Create a custom metadata field for the knowledge base. Metadata fields can be used to annotate documents with structured information. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataArgs](#metadataargs)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Metadata field created successfully. | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset not found | | + +### [GET] /datasets/{dataset_id}/metadata/built-in +**Get Built-in Metadata Fields** + +Returns the list of built-in metadata fields provided by the system (e.g., document type, source URL). + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Built-in metadata fields. | **application/json**: [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | + +### [POST] /datasets/{dataset_id}/metadata/built-in/{action} +**Update Built-in Metadata Field** + +Enable or disable built-in metadata fields for the knowledge base. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| action | path | Action to perform: 'enable' or 'disable' | Yes | string,
**Available values:** "disable", "enable" | +| dataset_id | path | Dataset ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Built-in metadata field toggled successfully. | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset not found | | + +### [DELETE] /datasets/{dataset_id}/metadata/{metadata_id} +**Delete Metadata Field** + +Permanently delete a custom metadata field. Documents using this field will lose their metadata values for it. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| metadata_id | path | Metadata ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - dataset API access or workspace access denied | +| 404 | Dataset or metadata not found | + +### [PATCH] /datasets/{dataset_id}/metadata/{metadata_id} +**Update Metadata Field** + +Rename a custom metadata field. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| metadata_id | path | Metadata ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataUpdatePayload](#metadataupdatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Metadata field updated successfully. | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset or metadata not found | | + +--- +## default + +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments +**List Chunks** + +Returns a paginated list of chunks within a document. Supports filtering by keyword and status. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| keyword | query | | No | string | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | +| status | query | | No | [ string ] | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of chunks. | **application/json**: [SegmentListResponse](#segmentlistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset or document not found | | + +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments +**Create Chunks** + +Create one or more chunks within a document. Each chunk can include optional keywords and an answer field (for QA-mode documents). + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentCreatePayload](#segmentcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Chunks created successfully. | **application/json**: [SegmentCreateListResponse](#segmentcreatelistresponse)
| +| 400 | Bad request - segments data is missing | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | `not_found` : Document is not completed or is disabled. | | + +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} +**Delete Chunk** + +Permanently delete a chunk from the document. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Segment ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - dataset API access or workspace access denied | +| 404 | Dataset, document, or segment not found | + +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} +**Get Chunk** + +Retrieve detailed information about a specific chunk, including its content, keywords, and indexing status. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Segment ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Chunk details. | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset, document, or segment not found | | + +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} +**Update Chunk** + +Update a chunk's content, keywords, or answer. Re-triggers indexing for the modified chunk. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Segment ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentUpdatePayload](#segmentupdatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Chunk updated successfully. | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset, document, or segment not found | | + +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks +**List Child Chunks** + +Returns a paginated list of child chunks under a specific parent chunk. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | +| keyword | query | | No | string | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List of child chunks. | **application/json**: [ChildChunkListResponse](#childchunklistresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset, document, or segment not found | | + +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks +**Create Child Chunk** + +Create a child chunk under the specified segment. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkCreatePayload](#childchunkcreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Child chunk created successfully. | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| +| 400 | `invalid_param` : Create child chunk index failed. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset, document, or segment not found | | + +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} +**Delete Child Chunk** + +Permanently delete a child chunk from its parent chunk. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| child_chunk_id | path | Child chunk ID | Yes | string (uuid) | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Success. | +| 400 | `invalid_param` : Delete child chunk index failed. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - dataset API access or workspace access denied | +| 404 | Dataset, document, segment, or child chunk not found | + +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} +**Update Child Chunk** + +Update the content of an existing child chunk. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| child_chunk_id | path | Child chunk ID | Yes | string (uuid) | +| dataset_id | path | Dataset ID | Yes | string (uuid) | +| document_id | path | Document ID | Yes | string (uuid) | +| segment_id | path | Parent segment ID | Yes | string (uuid) | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkUpdatePayload](#childchunkupdatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Child chunk updated successfully. | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| +| 400 | `invalid_param` : Update child chunk index failed. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - dataset API access or workspace access denied | | +| 404 | Dataset, document, segment, or child chunk not found | | + +--- +## default + +### [GET] /end-users/{end_user_id} +**Get End User Info** + +Retrieve an end user by ID. Useful when other APIs return an end-user ID (e.g., `created_by` from [Upload File](/api-reference/files/upload-file)). + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| end_user_id | path | End user ID | Yes | string (uuid) | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | End user retrieved successfully. | **application/json**: [EndUserDetail](#enduserdetail)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `end_user_not_found` : End user not found. | | + +--- +## default + +### [POST] /files/upload +**Upload File** + +Upload a file for use when sending messages, enabling multimodal understanding of images, documents, audio, and video. Uploaded files are for use by the current end-user only. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary, **"user"**: string }
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | File uploaded successfully. | **application/json**: [FileResponse](#fileresponse)
| +| 400 | - `no_file_uploaded` : No file was provided in the request. - `too_many_files` : Only one file is allowed per request. - `filename_not_exists_error` : The uploaded file has no filename. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 413 | `file_too_large` : File size exceeded. | | +| 415 | `unsupported_file_type` : File type not allowed. | | + +### [GET] /files/{file_id}/preview +**Download File** + +Preview or download uploaded files previously uploaded via the [Upload File](/api-reference/files/upload-file) API. Files can only be accessed if they belong to messages within the requesting application. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| file_id | path | UUID of the file to preview | Yes | string (uuid) | +| as_attachment | query | Download as attachment | No | boolean | +| user | query | End user identifier | No | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Returns the raw file content. The `Content-Type` header is set to the file's MIME type. If `as_attachment` is `true`, the file is returned as a download with `Content-Disposition: attachment`. | +| 401 | Unauthorized - invalid API token | +| 403 | `file_access_denied` : Access to the requested file is denied. | +| 404 | `file_not_found` : The requested file was not found. | + +--- +## default + +### [GET] /form/human_input/{form_token} +**Get Human Input Form** + +Retrieve a paused Human Input form's contents using the `form_token` from a `human_input_required` event. Requires **WebApp** delivery. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| form_token | path | Human input form token | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Form contents retrieved successfully. | **application/json**: [HumanInputFormDefinitionResponse](#humaninputformdefinitionresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Form not found. | | +| 412 | - `human_input_form_submitted` : Form already submitted. Forms are one-shot; the first response wins regardless of which user submits it. - `human_input_form_expired` : The form's expiration time passed before submission arrived. | | + +### [POST] /form/human_input/{form_token} +**Submit Human Input Form** + +Submit the recipient's response to a paused Human Input form. The workflow resumes on acceptance; use [Stream Workflow Events](/api-reference/chatflows/stream-workflow-events) to follow subsequent events. Requires **WebApp** delivery. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| form_token | path | Human input form token | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayloadWithUser](#humaninputformsubmitpayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Form submitted successfully. The response body is an empty object. | **application/json**: [HumanInputFormSubmitResponse](#humaninputformsubmitresponse)
| +| 400 | - `bad_request` : Form recipient type is invalid. - `invalid_form_data` : Submission failed validation against the form definition. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Form not found. | | +| 412 | - `human_input_form_submitted` : Form already submitted. Forms are one-shot; the first response wins regardless of which user submits it. - `human_input_form_expired` : The form's expiration time passed before submission arrived. | | + +--- +## default + +### [GET] /info +**Get App Info** + +Retrieve basic information about this application, including name, description, tags, and mode. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Basic information of the application. | **application/json**: [AppInfoResponse](#appinforesponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Application not found | | + +### [GET] /meta +**Get App Meta** + +Retrieve metadata about this application, including tool icons and other configuration details. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved application meta information. | **application/json**: [AppMetaResponse](#appmetaresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Application not found | | + +### [GET] /parameters +**Get App Parameters** + +Retrieve the application's input form configuration, including feature switches, input parameter names, types, and default values. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Application parameters information. | **application/json**: [Parameters](#parameters)
| +| 400 | `app_unavailable` : App unavailable or misconfigured. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Application not found | | + +### [GET] /site +**Get App WebApp Settings** + +Retrieve the WebApp settings of this application, including site configuration, theme, and customization options. + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | WebApp settings of the application. | **application/json**: [Site](#site)
| +| 401 | Unauthorized - invalid API token | | +| 403 | `forbidden` : Site not found for this application or the workspace has been archived. | | + +--- +## default + +### [GET] /workflow/{task_id}/events +**Stream Workflow Events** + +Resume the Server-Sent Events stream for a workflow run after a pause or a dropped SSE connection. For runs that have already finished, the stream emits a single `workflow_finished` event and closes. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| task_id | path | Workflow run ID | Yes | string | +| continue_on_pause | query | Keep the stream open across workflow_paused events | No | boolean | +| include_state_snapshot | query | Replay from persisted state snapshot | No | boolean | +| user | query | End user identifier | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Server-Sent Events stream. Each event is delivered as `data: {JSON}\\n\\n`. Event payloads follow the same schemas as the original streaming response. | **text/event-stream**: [EventStreamResponse](#eventstreamresponse)
| +| 400 | `not_workflow_app` : Please check if your app mode matches the right API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Workflow run not found. | | + +### [GET] /workflows/logs +**List Workflow Logs** + +Retrieve paginated workflow execution logs with filtering options. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| created_at__after | query | | No | string | +| created_at__before | query | | No | string | +| created_by_account | query | | No | string | +| created_by_end_user_session_id | query | | No | string | +| keyword | query | | No | string | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | +| status | query | | No | string,
**Available values:** "failed", "stopped", "succeeded" | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved workflow logs. | **application/json**: [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse)
| +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | + +### [POST] /workflows/run +**Run Workflow** + +Execute a workflow. Cannot be executed without a published workflow. + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunPayloadWithUser](#workflowrunpayloadwithuser)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 400 | - `not_workflow_app` : App mode does not match the API route. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Invalid parameter value. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | Workflow not found | | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | +| 500 | `internal_server_error` : Internal server error. | | + +### [GET] /workflows/run/{workflow_run_id} +**Get Workflow Run Detail** + +Retrieve the current execution results of a workflow task based on the workflow execution ID. + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| workflow_run_id | path | Workflow run ID | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Successfully retrieved workflow run details. | **application/json**: [WorkflowRunResponse](#workflowrunresponse)
| +| 400 | `not_workflow_app` : App mode does not match the API route. | | +| 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Workflow run not found. | | ### [POST] /workflows/tasks/{task_id}/stop -**Stop a running workflow task** +**Stop Workflow Task** + +Stop a running workflow task. Only supported in `streaming` mode. #### Parameters @@ -1810,19 +2262,26 @@ Returns detailed information about a specific workflow run. | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | Task ID to stop | Yes | string | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RequiredServiceApiUserPayload](#requiredserviceapiuserpayload)
| + #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | | 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 400 | - `not_workflow_app` : App mode does not match the API route. - `invalid_param` : Required parameter missing or invalid. | | | 401 | Unauthorized - invalid API token | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | | 404 | Task not found | | ### [POST] /workflows/{workflow_id}/run -**Run specific workflow by ID** +**Run Workflow by ID** -Execute a specific workflow by ID -Executes a specific workflow version identified by its ID. +Execute a specific workflow version identified by its ID. Useful for running a particular published version of the workflow. #### Parameters @@ -1834,24 +2293,27 @@ Executes a specific workflow version identified by its ID. | Required | Schema | | -------- | ------ | -| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| +| Yes | **application/json**: [WorkflowRunPayloadWithUser](#workflowrunpayloadwithuser)
| #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow executed successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | Bad request - invalid parameters or workflow issues | | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| +| 400 | - `not_workflow_app` : App mode does not match the API route. - `bad_request` : Workflow is a draft or has an invalid ID format. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Required parameter missing or invalid. | | | 401 | Unauthorized - invalid API token | | -| 404 | Workflow not found | | -| 429 | Rate limit exceeded | | -| 500 | Internal server error | | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | +| 404 | `not_found` : Workflow not found. | | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | +| 500 | `internal_server_error` : Internal server error. | | + +--- +## default ### [GET] /workspaces/current/models/model-types/{model_type} -**Get available models by model type** +**Get Available Models** -Get available models by model type -Returns a list of available models for the specified model type. +Retrieve the list of available models by type. Primarily used to query `text-embedding` and `rerank` models for knowledge base configuration. #### Parameters @@ -1863,7 +2325,7 @@ Returns a list of available models for the specified model type. | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Models retrieved successfully | **application/json**: [ProviderWithModelsListResponse](#providerwithmodelslistresponse)
| +| 200 | Available models for the specified type. | **application/json**: [ProviderWithModelsListResponse](#providerwithmodelslistresponse)
| | 401 | Unauthorized - invalid API token | | --- @@ -2014,6 +2476,21 @@ Button styles for user actions. | trace_session_id | string | Trace session ID for observability grouping | No | | workflow_id | string | Workflow ID for advanced chat | No | +#### ChatRequestPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| auto_generate_name | boolean,
**Default:** true | Auto generate conversation name | No | +| conversation_id | string | Conversation UUID | No | +| files | [ object ] | | No | +| inputs | object | | Yes | +| query | string | | Yes | +| response_mode | string | | No | +| retriever_from | string,
**Default:** dev | | No | +| trace_session_id | string | Trace session ID for observability grouping | No | +| user | string | End user identifier | Yes | +| workflow_id | string | Workflow ID for advanced chat | No | + #### ChildChunkCreatePayload | Name | Type | Description | Required | @@ -2074,6 +2551,18 @@ Button styles for user actions. | retriever_from | string,
**Default:** dev | | No | | trace_session_id | string | Trace session ID for observability grouping | No | +#### CompletionRequestPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| files | [ object ] | | No | +| inputs | object | | Yes | +| query | string | | No | +| response_mode | string | | No | +| retriever_from | string,
**Default:** dev | | No | +| trace_session_id | string | Trace session ID for observability grouping | No | +| user | string | End user identifier | Yes | + #### Condition Condition detail @@ -2107,6 +2596,14 @@ Condition detail | auto_generate | boolean | | No | | name | string | | No | +#### ConversationRenamePayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| auto_generate | boolean | | No | +| name | string | | No | +| user | string | End user identifier | No | + #### ConversationVariableInfiniteScrollPaginationResponse | Name | Type | Description | Required | @@ -2133,6 +2630,13 @@ Condition detail | ---- | ---- | ----------- | -------- | | value | | | Yes | +#### ConversationVariableUpdatePayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | string | End user identifier | No | +| value | | | Yes | + #### ConversationVariablesQuery | Name | Type | Description | Required | @@ -2914,6 +3418,14 @@ Enum class for fetch from. | action | string | | Yes | | inputs | object | Submitted human input values keyed by output variable name. Use a string for paragraph or select input values, a file mapping for file inputs, and a list of file mappings for file-list inputs. Local file mappings use `transfer_method=local_file` with `upload_file_id`; remote file mappings use `transfer_method=remote_url` with `url` or `remote_url`. | Yes | +#### HumanInputFormSubmitPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| action | string | | Yes | +| inputs | object | Submitted human input values keyed by output variable name. Use a string for paragraph or select input values, a file mapping for file inputs, and a list of file mappings for file-list inputs. Local file mappings use `transfer_method=local_file` with `upload_file_id`; remote file mappings use `transfer_method=remote_url` with `url` or `remote_url`. | Yes | +| user | string | End user identifier | Yes | + #### HumanInputFormSubmitResponse | Name | Type | Description | Required | @@ -2982,6 +3494,14 @@ Model class for i18n object. | content | string | | No | | rating | string | | No | +#### MessageFeedbackPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | | No | +| rating | string | | No | +| user | string | End user identifier | Yes | + #### MessageFile | Name | Type | Description | Required | @@ -3101,6 +3621,12 @@ Enum class for model type. | ---- | ---- | ----------- | -------- | | ModelType | string | Enum class for model type. | | +#### OptionalServiceApiUserPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | string | End user identifier | No | + #### ParagraphInputConfig Form input definition. @@ -3218,6 +3744,12 @@ Model class for provider with models response. | status | [CustomConfigurationStatus](#customconfigurationstatus) | | Yes | | tenant_id | string | | Yes | +#### RequiredServiceApiUserPayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user | string | End user identifier | Yes | + #### RerankingModel | Name | Type | Description | Required | @@ -3398,7 +3930,7 @@ Model class for provider with models response. | ---- | ---- | ----------- | -------- | | chunk_overlap | integer | | No | | max_tokens | integer | | Yes | -| separator | string,
**Default:** +| separator | string,
**Default:** | | No | #### SelectInputConfig @@ -3525,13 +4057,11 @@ Default configuration for form inputs. #### TagUnbindingPayload -Accept the legacy single-tag Service API payload while exposing a normalized tag_ids list internally. +Accepts either the legacy tag_id payload or the normalized tag_ids payload. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| tag_id | string | | No | -| tag_ids | [ string ] | | No | -| target_id | string | | Yes | +| TagUnbindingPayload | object
object | Accepts either the legacy tag_id payload or the normalized tag_ids payload. | | #### TagUpdatePayload @@ -3545,10 +4075,20 @@ Accept the legacy single-tag Service API payload while exposing a normalized tag | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | message_id | string | Message ID | No | -| streaming | boolean | Enable streaming response | No | +| streaming | boolean | Reserved for compatibility; TTS response streaming is determined by the provider output. | No | | text | string | Text to convert to audio | No | | voice | string | Voice to use for TTS | No | +#### TextToAudioPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| message_id | string | Message ID | No | +| streaming | boolean | Reserved for compatibility; TTS response streaming is determined by the provider output. | No | +| text | string | Text to convert to audio | No | +| user | string | End user identifier | No | +| voice | string | Voice to use for TTS | No | + #### UrlResponse | Name | Type | Description | Required | @@ -3665,6 +4205,16 @@ in form definiton, or a variable while the workflow is running. | response_mode | string | | No | | trace_session_id | string | Trace session ID for observability grouping | No | +#### WorkflowRunPayloadWithUser + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| files | [ object ] | | No | +| inputs | object | | Yes | +| response_mode | string | | No | +| trace_session_id | string | Trace session ID for observability grouping | No | +| user | string | End user identifier | Yes | + #### WorkflowRunResponse | Name | Type | Description | Required | diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index ddbb6f51c79..33f73dca648 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -4,10 +4,9 @@ Public APIs for web applications including file uploads, chat interactions, and ## Version: 1.0 ### Available authorizations -#### Bearer (API Key Authentication) -Type: Bearer {your-api-key} -**Name:** Authorization -**In:** header +#### Bearer (HTTP, bearer) +Use the Service API key as a Bearer token in the Authorization header. +Bearer format: API_KEY --- ## web @@ -140,7 +139,7 @@ Delete a specific conversation. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation UUID | Yes | string | +| c_id | path | Conversation UUID | Yes | string (uuid) | #### Responses @@ -160,7 +159,7 @@ Rename a specific conversation with a custom name or auto-generate one. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation UUID | Yes | string | +| c_id | path | Conversation UUID | Yes | string (uuid) | | auto_generate | query | Auto-generate conversation name | No | boolean | | name | query | New conversation name | No | string | @@ -188,7 +187,7 @@ Pin a specific conversation to keep it at the top of the list. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation UUID | Yes | string | +| c_id | path | Conversation UUID | Yes | string (uuid) | #### Responses @@ -208,7 +207,7 @@ Unpin a specific conversation to remove it from the top of the list. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | Conversation UUID | Yes | string | +| c_id | path | Conversation UUID | Yes | string (uuid) | #### Responses @@ -494,7 +493,7 @@ Submit feedback (like/dislike) for a specific message. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| message_id | path | Message UUID | Yes | string | +| message_id | path | Message UUID | Yes | string (uuid) | | content | query | Feedback content | No | string | | rating | query | Feedback rating | No | string,
**Available values:** "dislike", "like" | @@ -523,7 +522,7 @@ Generate a new completion similar to an existing message (completion apps only). | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | response_mode | query | Response mode | Yes | string,
**Available values:** "blocking", "streaming" | -| message_id | path | | Yes | string | +| message_id | path | | Yes | string (uuid) | #### Responses @@ -543,7 +542,7 @@ Get suggested follow-up questions after a message (chat apps only). | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| message_id | path | Message UUID | Yes | string | +| message_id | path | Message UUID | Yes | string (uuid) | #### Responses @@ -731,7 +730,7 @@ Remove a message from saved messages. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| message_id | path | Message UUID to delete | Yes | string | +| message_id | path | Message UUID to delete | Yes | string (uuid) | #### Responses @@ -1633,7 +1632,7 @@ Default configuration for form inputs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | message_id | string | Message ID | No | -| streaming | boolean | Enable streaming response | No | +| streaming | boolean | Reserved for compatibility; TTS response streaming is determined by the provider output. | No | | text | string | Text to convert to audio | No | | voice | string | Voice to use for TTS | No | diff --git a/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py b/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py index 0f24adfd92e..9f779fc43de 100644 --- a/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py +++ b/api/providers/trace/trace-arize-phoenix/src/dify_trace_arize_phoenix/arize_phoenix_trace.py @@ -1150,13 +1150,14 @@ class ArizePhoenixDataTrace(BaseTraceInstance): try: # Convert outputs to string based on type outputs_mime_type = OpenInferenceMimeTypeValues.TEXT.value - if isinstance(trace_info.outputs, dict | list): - outputs_str = safe_json_dumps(trace_info.outputs) - outputs_mime_type = OpenInferenceMimeTypeValues.JSON.value - elif isinstance(trace_info.outputs, str): - outputs_str = trace_info.outputs - else: - outputs_str = str(trace_info.outputs) + match trace_info.outputs: + case dict() | list(): + outputs_str = safe_json_dumps(trace_info.outputs) + outputs_mime_type = OpenInferenceMimeTypeValues.JSON.value + case str(): + outputs_str = trace_info.outputs + case _: + outputs_str = str(trace_info.outputs) llm_attributes: dict[str, Any] = { SpanAttributes.OPENINFERENCE_SPAN_KIND: OpenInferenceSpanKindValues.LLM.value, @@ -1553,25 +1554,26 @@ class ArizePhoenixDataTrace(BaseTraceInstance): set_attribute(f"{base_path}.{ToolCallAttributes.TOOL_CALL_ID}", call_id) # Handle list of messages - if isinstance(prompts, list): - for message_index, message in enumerate(prompts): - if not isinstance(message, dict): - continue + match prompts: + case list(): + for message_index, message in enumerate(prompts): + if not isinstance(message, dict): + continue - role = message.get("role", "user") - content = message.get("text") or message.get("content") or "" + role = message.get("role", "user") + content = message.get("text") or message.get("content") or "" - set_message_attribute(message_index, MessageAttributes.MESSAGE_ROLE, role) - set_message_attribute(message_index, MessageAttributes.MESSAGE_CONTENT, content) + set_message_attribute(message_index, MessageAttributes.MESSAGE_ROLE, role) + set_message_attribute(message_index, MessageAttributes.MESSAGE_CONTENT, content) - tool_calls = message.get("tool_calls") or [] - if isinstance(tool_calls, list): - for tool_index, tool_call in enumerate(tool_calls): - set_tool_call_attributes(message_index, tool_index, tool_call) + tool_calls = message.get("tool_calls") or [] + if isinstance(tool_calls, list): + for tool_index, tool_call in enumerate(tool_calls): + set_tool_call_attributes(message_index, tool_index, tool_call) - # Handle single dict or plain string prompt - elif isinstance(prompts, (dict, str)): - set_message_attribute(0, MessageAttributes.MESSAGE_CONTENT, prompts) - set_message_attribute(0, MessageAttributes.MESSAGE_ROLE, "user") + # Handle single dict or plain string prompt + case dict() | str(): + set_message_attribute(0, MessageAttributes.MESSAGE_CONTENT, prompts) + set_message_attribute(0, MessageAttributes.MESSAGE_ROLE, "user") return attributes diff --git a/api/providers/trace/trace-langfuse/src/dify_trace_langfuse/entities/langfuse_trace_entity.py b/api/providers/trace/trace-langfuse/src/dify_trace_langfuse/entities/langfuse_trace_entity.py index 76755bf7693..742938f09f4 100644 --- a/api/providers/trace/trace-langfuse/src/dify_trace_langfuse/entities/langfuse_trace_entity.py +++ b/api/providers/trace/trace-langfuse/src/dify_trace_langfuse/entities/langfuse_trace_entity.py @@ -18,24 +18,25 @@ def validate_input_output(v, field_name): """ if v == {} or v is None: return v - if isinstance(v, str): - return [ - { - "role": "assistant" if field_name == "output" else "user", - "content": v, - } - ] - elif isinstance(v, list): - if len(v) > 0 and isinstance(v[0], dict): - v = replace_text_with_content(data=v) - return v - else: + match v: + case str(): return [ { "role": "assistant" if field_name == "output" else "user", - "content": str(v), + "content": v, } ] + case list(): + if len(v) > 0 and isinstance(v[0], dict): + v = replace_text_with_content(data=v) + return v + else: + return [ + { + "role": "assistant" if field_name == "output" else "user", + "content": str(v), + } + ] return v diff --git a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/entities/langsmith_trace_entity.py b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/entities/langsmith_trace_entity.py index be9d64ae018..07159d8a7e3 100644 --- a/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/entities/langsmith_trace_entity.py +++ b/api/providers/trace/trace-langsmith/src/dify_trace_langsmith/entities/langsmith_trace_entity.py @@ -64,40 +64,20 @@ class LangSmithRunModel(LangSmithTokenUsage, LangSmithMultiModel): "total_tokens": values.get("total_tokens", 0), } file_list = values.get("file_list", []) - if isinstance(v, str): - match field_name: - case "inputs": - return { - "messages": { - "role": "user", - "content": v, - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } - case "outputs": - return { - "choices": { - "role": "ai", - "content": v, - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } - case _: - pass - elif isinstance(v, list): - data = {} - if len(v) > 0 and isinstance(v[0], dict): - # rename text to content - v = replace_text_with_content(data=v) + match v: + case str(): match field_name: case "inputs": - data = { - "messages": v, + return { + "messages": { + "role": "user", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, } case "outputs": - data = { + return { "choices": { "role": "ai", "content": v, @@ -107,16 +87,37 @@ class LangSmithRunModel(LangSmithTokenUsage, LangSmithMultiModel): } case _: pass - return data - else: - return { - "choices": { - "role": "ai" if field_name == "outputs" else "user", - "content": str(v), - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } + case list(): + data = {} + if len(v) > 0 and isinstance(v[0], dict): + # rename text to content + v = replace_text_with_content(data=v) + match field_name: + case "inputs": + data = { + "messages": v, + } + case "outputs": + data = { + "choices": { + "role": "ai", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + case _: + pass + return data + else: + return { + "choices": { + "role": "ai" if field_name == "outputs" else "user", + "content": str(v), + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } if isinstance(v, dict): v["usage_metadata"] = usage_metadata v["file_list"] = file_list diff --git a/api/providers/trace/trace-weave/src/dify_trace_weave/entities/weave_trace_entity.py b/api/providers/trace/trace-weave/src/dify_trace_weave/entities/weave_trace_entity.py index ed6a7dabbb0..98180c80a2c 100644 --- a/api/providers/trace/trace-weave/src/dify_trace_weave/entities/weave_trace_entity.py +++ b/api/providers/trace/trace-weave/src/dify_trace_weave/entities/weave_trace_entity.py @@ -40,41 +40,19 @@ class WeaveTraceModel(WeaveTokenUsage, WeaveMultiModel): "total_tokens": values.get("total_tokens", 0), } file_list = values.get("file_list", []) - if isinstance(v, str): - if field_name == "inputs": - return { - "messages": { - "role": "user", - "content": v, - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } - elif field_name == "outputs": - return { - "choices": { - "role": "ai", - "content": v, - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } - elif isinstance(v, list): - data = {} - if len(v) > 0 and isinstance(v[0], dict): - # rename text to content - v = replace_text_with_content(data=v) + match v: + case str(): if field_name == "inputs": - data = { - "messages": [ - dict(msg, **{"usage_metadata": usage_metadata, "file_list": file_list}) # type: ignore - for msg in v - ] - if isinstance(v, list) - else v, + return { + "messages": { + "role": "user", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, } elif field_name == "outputs": - data = { + return { "choices": { "role": "ai", "content": v, @@ -82,16 +60,39 @@ class WeaveTraceModel(WeaveTokenUsage, WeaveMultiModel): "file_list": file_list, }, } - return data - else: - return { - "choices": { - "role": "ai" if field_name == "outputs" else "user", - "content": str(v), - "usage_metadata": usage_metadata, - "file_list": file_list, - }, - } + case list(): + data = {} + if len(v) > 0 and isinstance(v[0], dict): + # rename text to content + v = replace_text_with_content(data=v) + if field_name == "inputs": + data = { + "messages": [ + dict(msg, **{"usage_metadata": usage_metadata, "file_list": file_list}) # type: ignore + for msg in v + ] + if isinstance(v, list) + else v, + } + elif field_name == "outputs": + data = { + "choices": { + "role": "ai", + "content": v, + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } + return data + else: + return { + "choices": { + "role": "ai" if field_name == "outputs" else "user", + "content": str(v), + "usage_metadata": usage_metadata, + "file_list": file_list, + }, + } if isinstance(v, dict): v["usage_metadata"] = usage_metadata v["file_list"] = file_list diff --git a/api/providers/vdb/vdb-clickzetta/src/dify_vdb_clickzetta/clickzetta_vector.py b/api/providers/vdb/vdb-clickzetta/src/dify_vdb_clickzetta/clickzetta_vector.py index 6231b9a9fad..6e80c6efa2c 100644 --- a/api/providers/vdb/vdb-clickzetta/src/dify_vdb_clickzetta/clickzetta_vector.py +++ b/api/providers/vdb/vdb-clickzetta/src/dify_vdb_clickzetta/clickzetta_vector.py @@ -361,12 +361,13 @@ class ClickzettaVector(BaseVector): first_pass = json.loads(raw_metadata) # Handle double-encoded JSON - if isinstance(first_pass, str): - metadata = parse_metadata_json(first_pass) - elif isinstance(first_pass, dict): - metadata = first_pass - else: - metadata = {} + match first_pass: + case str(): + metadata = parse_metadata_json(first_pass) + case dict(): + metadata = first_pass + case _: + metadata = {} else: metadata = {} except (json.JSONDecodeError, ValueError, TypeError): @@ -942,12 +943,13 @@ class ClickzettaVector(BaseVector): # First parse may yield a string (double-encoded JSON) first_pass = json.loads(row[2]) - if isinstance(first_pass, str): - metadata = parse_metadata_json(first_pass) - elif isinstance(first_pass, dict): - metadata = first_pass - else: - metadata = {} + match first_pass: + case str(): + metadata = parse_metadata_json(first_pass) + case dict(): + metadata = first_pass + case _: + metadata = {} else: metadata = {} except (json.JSONDecodeError, ValueError, TypeError): diff --git a/api/providers/vdb/vdb-hologres/tests/integration_tests/conftest.py b/api/providers/vdb/vdb-hologres/tests/integration_tests/conftest.py index d28ded01873..290eda67e94 100644 --- a/api/providers/vdb/vdb-hologres/tests/integration_tests/conftest.py +++ b/api/providers/vdb/vdb-hologres/tests/integration_tests/conftest.py @@ -98,14 +98,15 @@ def _extract_identifiers_and_literals(query) -> list[Any]: values: list[Any] = [] if isinstance(query, psql.Composed): for part in query: - if isinstance(part, psql.Identifier): - values.append(("ident", part._obj[0] if part._obj else "")) - elif isinstance(part, psql.Literal): - values.append(("literal", part._obj)) - elif isinstance(part, psql.Composed): - for sub in part: - if isinstance(sub, psql.Literal): - values.append(("literal", sub._obj)) + match part: + case psql.Identifier(): + values.append(("ident", part._obj[0] if part._obj else "")) + case psql.Literal(): + values.append(("literal", part._obj)) + case psql.Composed(): + for sub in part: + if isinstance(sub, psql.Literal): + values.append(("literal", sub._obj)) return values diff --git a/api/pyproject.toml b/api/pyproject.toml index 8e4ebe4112e..17efcef9db0 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "resend>=2.27.0,<3.0.0", # Emerging: newer and fast-moving, use compatible pins "fastopenapi[flask]==0.7.0", - "graphon==0.5.1", + "graphon==0.5.2", "httpx-sse==0.4.3", "json-repair==0.59.4", ] diff --git a/api/repositories/sqlalchemy_api_workflow_run_repository.py b/api/repositories/sqlalchemy_api_workflow_run_repository.py index 98c605f0a17..b40eb4bdd8a 100644 --- a/api/repositories/sqlalchemy_api_workflow_run_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_run_repository.py @@ -957,21 +957,22 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository): ) pause_reason_models = [] for reason in pause_reasons: - if isinstance(reason, HumanInputRequired): - # TODO(QuantumGhost): record node_id for `WorkflowPauseReason` - pause_reason_model = WorkflowPauseReason( - pause_id=pause_model.id, - type_=reason.TYPE, - form_id=reason.form_id, - ) - elif isinstance(reason, SchedulingPause): - pause_reason_model = WorkflowPauseReason( - pause_id=pause_model.id, - type_=reason.TYPE, - message=reason.message, - ) - else: - raise AssertionError(f"unkown reason type: {type(reason)}") + match reason: + case HumanInputRequired(): + # TODO(QuantumGhost): record node_id for `WorkflowPauseReason` + pause_reason_model = WorkflowPauseReason( + pause_id=pause_model.id, + type_=reason.TYPE, + form_id=reason.form_id, + ) + case SchedulingPause(): + pause_reason_model = WorkflowPauseReason( + pause_id=pause_model.id, + type_=reason.TYPE, + message=reason.message, + ) + case _: + raise AssertionError(f"unknown reason type: {type(reason)}") pause_reason_models.append(pause_reason_model) diff --git a/api/services/account_service.py b/api/services/account_service.py index 8ab6fa3fc68..8cbdf66f8d3 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -5,7 +5,7 @@ import secrets import uuid from datetime import UTC, datetime, timedelta from hashlib import sha256 -from typing import Any, TypedDict, cast +from typing import Any, NotRequired, TypedDict, cast from pydantic import BaseModel, TypeAdapter, ValidationError from sqlalchemy import Row, delete, func, select, update @@ -18,6 +18,8 @@ class InvitationData(TypedDict): account_id: str email: str workspace_id: str + role: NotRequired[str] + requires_setup: NotRequired[bool] _invitation_adapter: TypeAdapter[InvitationData] = TypeAdapter(InvitationData) @@ -1805,6 +1807,7 @@ class RegisterService: account = AccountService.get_account_by_email_with_case_fallback(email) + requires_setup = False if not account: TenantService.check_member_permission(tenant, inviter, None, "add") name = normalized_email.split("@")[0] @@ -1819,6 +1822,7 @@ class RegisterService: # Create new tenant member for invited tenant TenantService.create_tenant_member(tenant, account, role) TenantService.switch_tenant(account, tenant.id) + requires_setup = True else: TenantService.check_member_permission(tenant, inviter, account, "add") ta = db.session.scalar( @@ -1826,15 +1830,16 @@ class RegisterService: .where(TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == account.id) .limit(1) ) + requires_setup = account.status == AccountStatus.PENDING - if not ta: + if not ta and account.status == AccountStatus.PENDING: TenantService.create_tenant_member(tenant, account, role) # Support resend invitation email when the account is pending status - if account.status != AccountStatus.PENDING: + if ta and account.status != AccountStatus.PENDING: raise AccountAlreadyInTenantError("Account already in tenant.") - token = cls.generate_invite_token(tenant, account) + token = cls.generate_invite_token(tenant, account, role, requires_setup=requires_setup) language = account.interface_language or "en-US" # send email @@ -1849,12 +1854,16 @@ class RegisterService: return token @classmethod - def generate_invite_token(cls, tenant: Tenant, account: Account) -> str: + def generate_invite_token( + cls, tenant: Tenant, account: Account, role: str = "normal", *, requires_setup: bool = False + ) -> str: token = str(uuid.uuid4()) invitation_data = { "account_id": account.id, "email": account.email, "workspace_id": tenant.id, + "role": str(role), + "requires_setup": requires_setup, } expiry_hours = dify_config.INVITE_EXPIRY_HOURS redis_client.setex(cls._get_invitation_token_key(token), expiry_hours * 60 * 60, json.dumps(invitation_data)) @@ -1889,16 +1898,7 @@ class RegisterService: if not tenant: return None - tenant_account = db.session.execute( - select(Account, TenantAccountJoin.role) - .join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id) - .where(Account.email == invitation_data["email"], TenantAccountJoin.tenant_id == tenant.id) - ).first() - - if not tenant_account: - return None - - account = tenant_account[0] + account = db.session.scalar(select(Account).where(Account.email == invitation_data["email"]).limit(1)) if not account: return None diff --git a/api/services/agent/composer_validator.py b/api/services/agent/composer_validator.py index 8554b5c1ab7..b9519272c4a 100644 --- a/api/services/agent/composer_validator.py +++ b/api/services/agent/composer_validator.py @@ -334,16 +334,17 @@ class ComposerConfigValidator: @classmethod def _reject_plaintext_secrets(cls, value: Any, *, path: str) -> None: - if isinstance(value, dict): - for key, nested in value.items(): - normalized_key = key.lower().replace("-", "_") - nested_path = f"{path}.{key}" - if normalized_key in _PLAINTEXT_SECRET_KEYS and isinstance(nested, str) and nested: - raise PlaintextSecretNotAllowedError(f"Plaintext secret is not allowed at {nested_path}") - cls._reject_plaintext_secrets(nested, path=nested_path) - elif isinstance(value, list): - for index, nested in enumerate(value): - cls._reject_plaintext_secrets(nested, path=f"{path}[{index}]") + match value: + case dict(): + for key, nested in value.items(): + normalized_key = key.lower().replace("-", "_") + nested_path = f"{path}.{key}" + if normalized_key in _PLAINTEXT_SECRET_KEYS and isinstance(nested, str) and nested: + raise PlaintextSecretNotAllowedError(f"Plaintext secret is not allowed at {nested_path}") + cls._reject_plaintext_secrets(nested, path=nested_path) + case list(): + for index, nested in enumerate(value): + cls._reject_plaintext_secrets(nested, path=f"{path}[{index}]") @classmethod def _has_install_command(cls, entry: dict[str, Any]) -> bool: diff --git a/api/services/agent/observability_service.py b/api/services/agent/observability_service.py index a150f70d7f4..18c7733651e 100644 --- a/api/services/agent/observability_service.py +++ b/api/services/agent/observability_service.py @@ -6,12 +6,15 @@ from decimal import Decimal from typing import Any import sqlalchemy as sa -from sqlalchemy import func, or_, select +from sqlalchemy import and_, func, or_, select +from sqlalchemy.orm import aliased from core.app.entities.app_invoke_entities import InvokeFrom from libs.helper import convert_datetime_to_date, escape_like_pattern, to_timestamp +from models.agent import WorkflowAgentNodeBinding from models.enums import MessageStatus from models.model import App, Conversation, Message +from models.workflow import WorkflowNodeExecutionModel, WorkflowRun @dataclass(frozen=True) @@ -33,6 +36,16 @@ class AgentStatisticsQueryParams: timezone: str = "UTC" +@dataclass(frozen=True) +class AgentSourceFilter: + kind: str + app_id: str | None = None + workflow_id: str | None = None + workflow_version: str | None = None + node_id: str | None = None + invoke_from: InvokeFrom | None = None + + class AgentObservabilityService: _SOURCE_ALIASES: dict[str, InvokeFrom] = { "api": InvokeFrom.SERVICE_API, @@ -66,6 +79,31 @@ class AgentObservabilityService: except KeyError as exc: raise ValueError(f"Unsupported source: {source}") from exc + @classmethod + def resolve_source_filter(cls, source: str | None) -> AgentSourceFilter: + if not source or source.strip().lower() == "all": + return AgentSourceFilter(kind="all") + normalized = source.strip() + lowered = normalized.lower() + if lowered == "webapp": + return AgentSourceFilter(kind="webapp") + if lowered.startswith("webapp:"): + return AgentSourceFilter(kind="webapp", app_id=normalized.split(":", 1)[1] or None) + if lowered == "workflow": + return AgentSourceFilter(kind="workflow") + if lowered.startswith("workflow:"): + parts = normalized.split(":", 4) + if len(parts) != 5 or not all(parts[1:]): + raise ValueError(f"Unsupported source: {source}") + return AgentSourceFilter( + kind="workflow", + app_id=parts[1], + workflow_id=parts[2], + workflow_version=parts[3], + node_id=parts[4], + ) + return AgentSourceFilter(kind="webapp", invoke_from=cls.resolve_source(source)) + @staticmethod def _message_status(message: Message) -> str: if message.error or message.status == MessageStatus.ERROR: @@ -104,19 +142,255 @@ class AgentObservabilityService: "updated_at": to_timestamp(message.updated_at), } - def list_logs(self, *, app: App, params: AgentLogQueryParams) -> dict[str, Any]: - source = self.resolve_source(params.source) - stmt = ( - select(Message, Conversation) - .join(Conversation, Conversation.id == Message.conversation_id) - .where(Message.app_id == app.id, Conversation.app_id == app.id) - ) - stmt = self._apply_source_filter(stmt, source) + def list_logs(self, *, app: App, agent_id: str, params: AgentLogQueryParams) -> dict[str, Any]: + source_filter = self.resolve_source_filter(params.source) + rows: list[dict[str, Any]] = [] + if source_filter.kind in {"all", "webapp"}: + rows.extend(self._list_webapp_conversation_logs(app=app, params=params, source_filter=source_filter)) + if source_filter.kind in {"all", "workflow"}: + rows.extend( + self._list_workflow_conversation_logs( + app=app, + agent_id=agent_id, + params=params, + source_filter=source_filter, + ) + ) + rows.sort(key=lambda row: (row["updated_at"] or 0, row["id"]), reverse=True) - if params.start: - stmt = stmt.where(Message.created_at >= params.start) - if params.end: - stmt = stmt.where(Message.created_at < params.end) + total = len(rows) + start = (params.page - 1) * params.limit + end = start + params.limit + return { + "data": rows[start:end], + "page": params.page, + "limit": params.limit, + "total": total, + "has_more": end < total, + } + + def list_log_messages( + self, *, app: App, agent_id: str, conversation_id: str, params: AgentLogQueryParams + ) -> dict[str, Any]: + source_filter = self.resolve_source_filter(params.source) + rows: list[Message] = [] + if source_filter.kind in {"all", "webapp"}: + rows.extend( + self._list_webapp_messages( + app=app, + conversation_id=conversation_id, + params=params, + source_filter=source_filter, + ) + ) + if source_filter.kind in {"all", "workflow"}: + rows.extend( + self._list_workflow_messages( + app=app, + agent_id=agent_id, + conversation_id=conversation_id, + params=params, + source_filter=source_filter, + ) + ) + + deduped = {message.id: message for message in rows} + sorted_rows = sorted(deduped.values(), key=lambda message: (message.created_at, message.id), reverse=True) + total = len(sorted_rows) + start = (params.page - 1) * params.limit + end = start + params.limit + return { + "data": [self.serialize_log_message(message) for message in sorted_rows[start:end]], + "page": params.page, + "limit": params.limit, + "total": total, + "has_more": end < total, + } + + def list_log_sources(self, *, app: App, agent_id: str) -> dict[str, Any]: + webapp_source = self._serialize_webapp_source(app) + workflow_sources = self._list_workflow_sources(app=app, agent_id=agent_id) + return { + "data": [webapp_source, *workflow_sources], + "groups": [ + {"type": "webapp", "label": "WEBAPP", "sources": [webapp_source]}, + {"type": "workflow", "label": "WORKFLOW", "sources": workflow_sources}, + ], + } + + def _list_webapp_conversation_logs( + self, *, app: App, params: AgentLogQueryParams, source_filter: AgentSourceFilter + ) -> list[dict[str, Any]]: + stmt = ( + select( + Conversation, + func.count(Message.id).label("message_count"), + func.max(Message.created_at).label("created_at"), + func.max(Message.updated_at).label("updated_at"), + func.sum(sa.case((Message.status == MessageStatus.PAUSED, 1), else_=0)).label("paused_count"), + func.sum( + sa.case((or_(Message.error.is_not(None), Message.status == MessageStatus.ERROR), 1), else_=0) + ).label("failed_count"), + ) + .join(Message, Message.conversation_id == Conversation.id) + .where(Message.app_id == app.id, Conversation.app_id == app.id) + .group_by(Conversation.id) + ) + stmt = self._apply_observability_filters(stmt, params=params, source_filter=source_filter) + rows = list(self._session.execute(stmt).all()) + return [ + self._serialize_conversation_log( + conversation=row[0], + message_count=row.message_count, + paused_count=row.paused_count, + failed_count=row.failed_count, + source=self._serialize_webapp_source(app), + created_at=row.created_at, + updated_at=row.updated_at, + ) + for row in rows + ] + + def _list_workflow_conversation_logs( + self, *, app: App, agent_id: str, params: AgentLogQueryParams, source_filter: AgentSourceFilter + ) -> list[dict[str, Any]]: + workflow_app = aliased(App) + stmt = ( + select( + Conversation, + workflow_app, + WorkflowAgentNodeBinding.workflow_id, + WorkflowAgentNodeBinding.workflow_version, + WorkflowAgentNodeBinding.node_id, + func.count(sa.distinct(Message.id)).label("message_count"), + func.max(Message.created_at).label("created_at"), + func.max(Message.updated_at).label("updated_at"), + func.sum(sa.case((Message.status == MessageStatus.PAUSED, 1), else_=0)).label("paused_count"), + func.sum( + sa.case((or_(Message.error.is_not(None), Message.status == MessageStatus.ERROR), 1), else_=0) + ).label("failed_count"), + ) + .select_from(Message) + .join(Conversation, Conversation.id == Message.conversation_id) + .join(WorkflowRun, WorkflowRun.id == Message.workflow_run_id) + .join( + WorkflowAgentNodeBinding, + and_( + WorkflowAgentNodeBinding.tenant_id == app.tenant_id, + WorkflowAgentNodeBinding.agent_id == agent_id, + WorkflowAgentNodeBinding.app_id == WorkflowRun.app_id, + WorkflowAgentNodeBinding.workflow_id == WorkflowRun.workflow_id, + WorkflowAgentNodeBinding.workflow_version == WorkflowRun.version, + ), + ) + .join( + WorkflowNodeExecutionModel, + and_( + WorkflowNodeExecutionModel.workflow_run_id == WorkflowRun.id, + WorkflowNodeExecutionModel.node_id == WorkflowAgentNodeBinding.node_id, + ), + ) + .join(workflow_app, workflow_app.id == WorkflowAgentNodeBinding.app_id) + .where(Message.workflow_run_id.is_not(None), Conversation.app_id == WorkflowAgentNodeBinding.app_id) + .group_by( + Conversation.id, + workflow_app.id, + WorkflowAgentNodeBinding.workflow_id, + WorkflowAgentNodeBinding.workflow_version, + WorkflowAgentNodeBinding.node_id, + ) + ) + stmt = self._apply_observability_filters(stmt, params=params, source_filter=source_filter) + stmt = self._apply_workflow_source_filter(stmt, source_filter) + rows = list(self._session.execute(stmt).all()) + return [ + self._serialize_conversation_log( + conversation=row[0], + message_count=row.message_count, + paused_count=row.paused_count, + failed_count=row.failed_count, + source=self._serialize_workflow_source( + app=row[1], + workflow_id=row.workflow_id, + workflow_version=row.workflow_version, + node_id=row.node_id, + ), + created_at=row.created_at, + updated_at=row.updated_at, + ) + for row in rows + ] + + def _list_webapp_messages( + self, *, app: App, conversation_id: str, params: AgentLogQueryParams, source_filter: AgentSourceFilter + ) -> list[Message]: + stmt = select(Message).where(Message.app_id == app.id, Message.conversation_id == conversation_id) + stmt = self._apply_message_filters(stmt, params=params, source_filter=source_filter) + return list(self._session.scalars(stmt.order_by(Message.created_at.desc(), Message.id.desc())).all()) + + def _list_workflow_messages( + self, + *, + app: App, + agent_id: str, + conversation_id: str, + params: AgentLogQueryParams, + source_filter: AgentSourceFilter, + ) -> list[Message]: + stmt = ( + select(Message) + .join(WorkflowRun, WorkflowRun.id == Message.workflow_run_id) + .join( + WorkflowAgentNodeBinding, + and_( + WorkflowAgentNodeBinding.tenant_id == app.tenant_id, + WorkflowAgentNodeBinding.agent_id == agent_id, + WorkflowAgentNodeBinding.app_id == WorkflowRun.app_id, + WorkflowAgentNodeBinding.workflow_id == WorkflowRun.workflow_id, + WorkflowAgentNodeBinding.workflow_version == WorkflowRun.version, + ), + ) + .join( + WorkflowNodeExecutionModel, + and_( + WorkflowNodeExecutionModel.workflow_run_id == WorkflowRun.id, + WorkflowNodeExecutionModel.node_id == WorkflowAgentNodeBinding.node_id, + ), + ) + .where(Message.conversation_id == conversation_id) + ) + stmt = self._apply_message_filters(stmt, params=params, source_filter=source_filter) + stmt = self._apply_workflow_source_filter(stmt, source_filter) + return list(self._session.scalars(stmt.order_by(Message.created_at.desc(), Message.id.desc())).all()) + + def _list_workflow_sources(self, *, app: App, agent_id: str) -> list[dict[str, Any]]: + workflow_app = aliased(App) + stmt = ( + select( + workflow_app, + WorkflowAgentNodeBinding.workflow_id, + WorkflowAgentNodeBinding.workflow_version, + WorkflowAgentNodeBinding.node_id, + ) + .join(workflow_app, workflow_app.id == WorkflowAgentNodeBinding.app_id) + .where(WorkflowAgentNodeBinding.tenant_id == app.tenant_id, WorkflowAgentNodeBinding.agent_id == agent_id) + .order_by(workflow_app.name.asc(), WorkflowAgentNodeBinding.node_id.asc()) + ) + rows = self._session.execute(stmt).all() + deduped: dict[str, dict[str, Any]] = {} + for row in rows: + source = self._serialize_workflow_source( + app=row[0], + workflow_id=row.workflow_id, + workflow_version=row.workflow_version, + node_id=row.node_id, + ) + deduped[source["id"]] = source + return list(deduped.values()) + + @classmethod + def _apply_observability_filters(cls, stmt, *, params: AgentLogQueryParams, source_filter: AgentSourceFilter): + stmt = cls._apply_message_filters(stmt, params=params, source_filter=source_filter, include_keyword=False) if params.keyword: escaped_keyword = escape_like_pattern(params.keyword) pattern = f"%{escaped_keyword}%" @@ -127,27 +401,41 @@ class AgentObservabilityService: Conversation.name.ilike(pattern, escape="\\"), ) ) - if params.status: - stmt = self._apply_status_filter(stmt, params.status) + return stmt - total = self._session.scalar(select(func.count()).select_from(stmt.subquery())) or 0 - rows = list( - self._session.execute( - stmt.order_by(Message.created_at.desc(), Message.id.desc()) - .offset((params.page - 1) * params.limit) - .limit(params.limit) - ).all() - ) - data = [] - for message, conversation in rows: - data.append(self.serialize_log_message(message, conversation)) - return { - "data": data, - "page": params.page, - "limit": params.limit, - "total": total, - "has_more": params.page * params.limit < total, - } + @classmethod + def _apply_message_filters( + cls, stmt, *, params: AgentLogQueryParams, source_filter: AgentSourceFilter, include_keyword: bool = True + ): + stmt = cls._apply_source_filter(stmt, source_filter.invoke_from) + if params.start: + stmt = stmt.where(Message.created_at >= params.start) + if params.end: + stmt = stmt.where(Message.created_at < params.end) + if include_keyword and params.keyword: + escaped_keyword = escape_like_pattern(params.keyword) + pattern = f"%{escaped_keyword}%" + stmt = stmt.where( + or_( + Message.query.ilike(pattern, escape="\\"), + Message.answer.ilike(pattern, escape="\\"), + ) + ) + if params.status: + stmt = cls._apply_status_filter(stmt, params.status) + return stmt + + @staticmethod + def _apply_workflow_source_filter(stmt, source_filter: AgentSourceFilter): + if source_filter.app_id: + stmt = stmt.where(WorkflowAgentNodeBinding.app_id == source_filter.app_id) + if source_filter.workflow_id: + stmt = stmt.where(WorkflowAgentNodeBinding.workflow_id == source_filter.workflow_id) + if source_filter.workflow_version: + stmt = stmt.where(WorkflowAgentNodeBinding.workflow_version == source_filter.workflow_version) + if source_filter.node_id: + stmt = stmt.where(WorkflowAgentNodeBinding.node_id == source_filter.node_id) + return stmt @classmethod def _apply_source_filter(cls, stmt, source: InvokeFrom | None): @@ -166,22 +454,95 @@ class AgentObservabilityService: return stmt.where(Message.status == MessageStatus.PAUSED) raise ValueError(f"Unsupported status: {status}") - def get_statistics_summary(self, *, app: App, params: AgentStatisticsQueryParams) -> dict[str, Any]: - source = self.resolve_source(params.source) - rows = self._load_daily_statistics(app=app, params=params, source=source) + @classmethod + def _serialize_conversation_log( + cls, + *, + conversation: Conversation, + message_count: int, + paused_count: int, + failed_count: int, + source: dict[str, Any], + created_at: datetime | None, + updated_at: datetime | None, + ) -> dict[str, Any]: + return { + "id": conversation.id, + "conversation_id": conversation.id, + "title": conversation.name, + "end_user_id": conversation.from_end_user_id, + "message_count": int(message_count or 0), + "user_rate": None, + "operation_rate": None, + "unread": conversation.read_at is None, + "source": source, + "status": cls._conversation_status(paused_count=paused_count, failed_count=failed_count), + "created_at": to_timestamp(created_at or conversation.created_at), + "updated_at": to_timestamp(updated_at or conversation.updated_at), + } + + @staticmethod + def _conversation_status(*, paused_count: int, failed_count: int) -> str: + if paused_count: + return "paused" + if failed_count: + return "failed" + return "success" + + @staticmethod + def _serialize_webapp_source(app: App) -> dict[str, Any]: + icon_type = app.icon_type.value if app.icon_type else None + return { + "id": f"webapp:{app.id}", + "type": "webapp", + "app_id": app.id, + "app_name": app.name, + "app_icon_type": icon_type, + "app_icon": app.icon, + "app_icon_background": app.icon_background, + "workflow_id": None, + "workflow_version": None, + "node_id": None, + } + + @staticmethod + def _serialize_workflow_source( + *, + app: App, + workflow_id: str, + workflow_version: str, + node_id: str, + ) -> dict[str, Any]: + icon_type = app.icon_type.value if app.icon_type else None + return { + "id": f"workflow:{app.id}:{workflow_id}:{workflow_version}:{node_id}", + "type": "workflow", + "app_id": app.id, + "app_name": app.name, + "app_icon_type": icon_type, + "app_icon": app.icon, + "app_icon_background": app.icon_background, + "workflow_id": workflow_id, + "workflow_version": workflow_version, + "node_id": node_id, + } + + def get_statistics_summary(self, *, app: App, agent_id: str, params: AgentStatisticsQueryParams) -> dict[str, Any]: + source_filter = self.resolve_source_filter(params.source) + rows = self._load_daily_statistics(app=app, agent_id=agent_id, params=params, source_filter=source_filter) charts = self._build_charts(rows) summary = self._build_summary(rows) return { - "source": source.value if source else "all", + "source": params.source or "all", "summary": summary, "charts": charts, } def _load_daily_statistics( - self, *, app: App, params: AgentStatisticsQueryParams, source: InvokeFrom | None + self, *, app: App, agent_id: str, params: AgentStatisticsQueryParams, source_filter: AgentSourceFilter ) -> list[dict[str, Any]]: converted_created_at = convert_datetime_to_date("m.created_at") - source_condition = "AND m.invoke_from != :debugger" if source is None else "AND m.invoke_from = :source" + message_scope = self._statistics_message_scope_sql(source_filter) sql_query = f"""SELECT {converted_created_at} AS date, COUNT(m.id) AS message_count, @@ -197,15 +558,24 @@ FROM messages m LEFT JOIN message_feedbacks mf ON mf.message_id = m.id AND mf.rating = 'like' WHERE - m.app_id = :app_id - {source_condition}""" + {message_scope}""" args: dict[str, Any] = { "tz": params.timezone, "app_id": app.id, + "tenant_id": app.tenant_id, + "agent_id": agent_id, "debugger": InvokeFrom.DEBUGGER, } - if source is not None: - args["source"] = source + if source_filter.invoke_from is not None: + args["source"] = source_filter.invoke_from + if source_filter.app_id: + args["source_app_id"] = source_filter.app_id + if source_filter.workflow_id: + args["workflow_id"] = source_filter.workflow_id + if source_filter.workflow_version: + args["workflow_version"] = source_filter.workflow_version + if source_filter.node_id: + args["node_id"] = source_filter.node_id if params.start: sql_query += " AND m.created_at >= :start" args["start"] = params.start @@ -216,6 +586,45 @@ WHERE return [dict(row._mapping) for row in self._session.execute(sa.text(sql_query), args).all()] + @staticmethod + def _statistics_message_scope_sql(source_filter: AgentSourceFilter) -> str: + app_scope = "m.app_id = :app_id" + if source_filter.invoke_from is None: + app_scope += " AND m.invoke_from != :debugger" + else: + app_scope += " AND m.invoke_from = :source" + workflow_binding_filters = [] + if source_filter.app_id: + workflow_binding_filters.append("wanb.app_id = :source_app_id") + if source_filter.workflow_id: + workflow_binding_filters.append("wanb.workflow_id = :workflow_id") + if source_filter.workflow_version: + workflow_binding_filters.append("wanb.workflow_version = :workflow_version") + if source_filter.node_id: + workflow_binding_filters.append("wanb.node_id = :node_id") + extra_workflow_filters = f"AND {' AND '.join(workflow_binding_filters)}" if workflow_binding_filters else "" + workflow_scope = f"""m.workflow_run_id IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM workflow_runs wr + JOIN workflow_agent_node_bindings wanb + ON wanb.tenant_id = :tenant_id + AND wanb.agent_id = :agent_id + AND wanb.app_id = wr.app_id + AND wanb.workflow_id = wr.workflow_id + AND wanb.workflow_version = wr.version + {extra_workflow_filters} + JOIN workflow_node_executions wne + ON wne.workflow_run_id = wr.id + AND wne.node_id = wanb.node_id + WHERE wr.id = m.workflow_run_id + )""" + if source_filter.kind == "webapp": + return app_scope + if source_filter.kind == "workflow": + return workflow_scope + return f"(({app_scope}) OR ({workflow_scope}))" + @staticmethod def _build_charts(rows: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: messages = [] diff --git a/api/services/agent/skill_standardize_service.py b/api/services/agent/skill_standardize_service.py index 71bb8daded9..b83004f3c4b 100644 --- a/api/services/agent/skill_standardize_service.py +++ b/api/services/agent/skill_standardize_service.py @@ -10,7 +10,9 @@ to the agent drive (Agent Files §5.4 / §4): Both are stored as ``ToolFile`` records and bound via ``AgentDriveService.commit`` with ``value_owned_by_drive=True`` (the drive owns their lifecycle). The returned skill ref records the stable drive paths + file ids (not just the raw upload id), -so the Composer can reload the bound skill list. +so the Composer can reload the bound skill list. The console ``/skills/upload`` +endpoints delegate to this service so "upload" now always means drive-backed skill +normalization. """ from __future__ import annotations @@ -34,7 +36,7 @@ def slugify_skill_name(name: str) -> str: class SkillStandardizeService: - """Validate + standardize a Skill package into a per-agent drive.""" + """Validate + standardize a Skill package into a per-agent drive upload result.""" def __init__( self, diff --git a/api/services/app_service.py b/api/services/app_service.py index c435a672520..cd0d08bf3e7 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -75,7 +75,7 @@ class CreateAppParams(BaseModel): class AppService: @staticmethod def _build_app_list_filters( - user_id: str, tenant_id: str, params: AppListBaseParams + user_id: str, tenant_id: str, params: AppListBaseParams, session: scoped_session ) -> list[sa.ColumnElement[bool]]: filters = [App.tenant_id == tenant_id, App.is_universal == False] @@ -115,7 +115,7 @@ class AppService: escaped_name = escape_like_pattern(name) filters.append(App.name.ilike(f"%{escaped_name}%", escape="\\")) if params.tag_ids and len(params.tag_ids) > 0: - target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, match_all=True) + target_ids = TagService.get_target_ids_by_tag_ids("app", tenant_id, params.tag_ids, session, match_all=True) if target_ids and len(target_ids) > 0: filters.append(App.id.in_(target_ids)) else: @@ -197,7 +197,9 @@ class AppService: ).scalars() ) - def get_paginate_apps(self, user_id: str, tenant_id: str, params: AppListParams) -> Pagination | None: + def get_paginate_apps( + self, user_id: str, tenant_id: str, params: AppListParams, session: scoped_session + ) -> Pagination | None: """ Get app list with pagination, filters, and explicit sort order. :param user_id: user id @@ -205,7 +207,7 @@ class AppService: :param params: query parameters :return: """ - filters = self._build_app_list_filters(user_id, tenant_id, params) + filters = self._build_app_list_filters(user_id, tenant_id, params, session) if not filters: return None @@ -231,12 +233,12 @@ class AppService: return app_models def get_paginate_starred_apps( - self, user_id: str, tenant_id: str, params: StarredAppListParams + self, user_id: str, tenant_id: str, params: StarredAppListParams, session: scoped_session ) -> Pagination | None: """ Get apps starred by the current account with pagination, filters, and explicit sort order. """ - filters = self._build_app_list_filters(user_id, tenant_id, params) + filters = self._build_app_list_filters(user_id, tenant_id, params, session) if not filters: return None @@ -540,17 +542,21 @@ class AppService: *, name: str | None = None, description: str | None = None, + role: str | None = None, icon_type: IconType | str | None = None, icon: str | None = None, icon_background: str | None = None, - role: str | None = None, account_id: str | None = None, updated_at: datetime | None = None, ) -> None: """Keep the Roster identity aligned with its Agent App shell. Agent Soul remains versioned through Composer. This helper only mirrors - user-facing identity fields so Roster and Agent Console do not drift. + user-facing identity fields, including the roster role/persona label, + so Roster and Agent Console do not drift. + + Role omission is intentional: ``role=None`` preserves the backing + Agent's current role, while ``role=""`` explicitly clears it. """ agent = self._get_backing_agent_for_update(app) if agent is None: @@ -560,14 +566,14 @@ class AppService: agent.name = name if description is not None: agent.description = description + if role is not None: + agent.role = role if icon_type is not None: agent.icon_type = self._to_agent_icon_type(icon_type) if icon is not None: agent.icon = icon if icon_background is not None: agent.icon_background = icon_background - if role is not None: - agent.role = role agent.updated_by = account_id if updated_at is not None: agent.updated_at = updated_at @@ -599,10 +605,12 @@ class AppService: app, name=app.name, description=app.description, + # Omitted role must stay omitted here: None means "preserve current + # backing-agent role", while an empty string is an explicit clear. + role=args.get("role"), icon_type=app.icon_type, icon=app.icon, icon_background=app.icon_background, - role=args.get("role"), account_id=current_user.id, updated_at=app.updated_at, ) diff --git a/api/services/attachment_service.py b/api/services/attachment_service.py index dad7163739f..4129613b2fe 100644 --- a/api/services/attachment_service.py +++ b/api/services/attachment_service.py @@ -14,12 +14,13 @@ class AttachmentService: _session_maker: sessionmaker def __init__(self, session_factory: sessionmaker | Engine | None = None): - if isinstance(session_factory, Engine): - self._session_maker = sessionmaker(bind=session_factory) - elif isinstance(session_factory, sessionmaker): - self._session_maker = session_factory - else: - raise AssertionError("must be a sessionmaker or an Engine.") + match session_factory: + case Engine(): + self._session_maker = sessionmaker(bind=session_factory) + case sessionmaker(): + self._session_maker = session_factory + case _: + raise AssertionError("must be a sessionmaker or an Engine.") def get_file_base64(self, file_id: str) -> str: with self._session_maker(expire_on_commit=False) as session: diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 364d9b36b9d..e8b17a137f9 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -13,7 +13,7 @@ import sqlalchemy as sa from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator from redis.exceptions import LockNotOwnedError from sqlalchemy import delete, exists, func, select, update -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import Session, scoped_session, sessionmaker from werkzeug.exceptions import Forbidden, NotFound from configs import dify_config @@ -235,7 +235,9 @@ class _EstimateArgs(BaseModel): class DatasetService: @staticmethod - def get_datasets(page, per_page, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False): + def get_datasets( + page, per_page, session: scoped_session, tenant_id=None, user=None, search=None, tag_ids=None, include_all=False + ): query = select(Dataset).where(Dataset.tenant_id == tenant_id).order_by(Dataset.created_at.desc(), Dataset.id) if user: @@ -295,6 +297,7 @@ class DatasetService: "knowledge", tenant_id, tag_ids, + session, match_all=True, ) else: diff --git a/api/services/entities/agent_entities.py b/api/services/entities/agent_entities.py index 63ae101533d..e7b5cbd7c6d 100644 --- a/api/services/entities/agent_entities.py +++ b/api/services/entities/agent_entities.py @@ -60,6 +60,7 @@ class ComposerSavePayload(BaseModel): class RosterAgentCreatePayload(BaseModel): name: str = Field(min_length=1, max_length=255) + mode: Literal["agent"] = "agent" description: str = "" role: str = Field(default="", max_length=255) icon_type: AgentIconType | None = None diff --git a/api/services/file_service.py b/api/services/file_service.py index 4d3afcc9ad4..1781f0c9727 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -39,12 +39,13 @@ class FileService: _session_maker: sessionmaker[Session] def __init__(self, session_factory: sessionmaker | Engine | None = None): - if isinstance(session_factory, Engine): - self._session_maker = sessionmaker(bind=session_factory) - elif isinstance(session_factory, sessionmaker): - self._session_maker = session_factory - else: - raise AssertionError("must be a sessionmaker or an Engine.") + match session_factory: + case Engine(): + self._session_maker = sessionmaker(bind=session_factory) + case sessionmaker(): + self._session_maker = session_factory + case _: + raise AssertionError("must be a sessionmaker or an Engine.") def upload_file( self, diff --git a/api/services/human_input_delivery_test_service.py b/api/services/human_input_delivery_test_service.py index c266d4f9586..995cb94c633 100644 --- a/api/services/human_input_delivery_test_service.py +++ b/api/services/human_input_delivery_test_service.py @@ -119,10 +119,11 @@ class HumanInputDeliveryTestService: class EmailDeliveryTestHandler: def __init__(self, session_factory: sessionmaker | Engine | None = None) -> None: - if session_factory is None: - session_factory = sessionmaker(bind=db.engine) - elif isinstance(session_factory, Engine): - session_factory = sessionmaker(bind=session_factory) + match session_factory: + case None: + session_factory = sessionmaker(bind=db.engine) + case Engine(): + session_factory = sessionmaker(bind=session_factory) self._session_factory = session_factory def supports(self, method: DeliveryChannelConfig) -> bool: @@ -179,11 +180,12 @@ class EmailDeliveryTestHandler: emails: list[str] = [] bound_reference_ids: list[str] = [] for recipient in recipients.items: - if isinstance(recipient, MemberRecipient): - bound_reference_ids.append(recipient.reference_id) - elif isinstance(recipient, ExternalRecipient): - if recipient.email: - emails.append(recipient.email) + match recipient: + case MemberRecipient(): + bound_reference_ids.append(recipient.reference_id) + case ExternalRecipient(): + if recipient.email: + emails.append(recipient.email) if recipients.include_bound_group: bound_reference_ids = [] diff --git a/api/services/snippet_service.py b/api/services/snippet_service.py index 9f16d412040..75282db9d4c 100644 --- a/api/services/snippet_service.py +++ b/api/services/snippet_service.py @@ -6,7 +6,7 @@ from datetime import UTC, datetime from typing import Any from sqlalchemy import delete, func, select -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import Session, scoped_session, sessionmaker from core.db import session_factory from core.workflow.node_factory import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING @@ -192,6 +192,7 @@ class SnippetService: self, *, tenant_id: str, + session: scoped_session, page: int = 1, limit: int = 20, keyword: str | None = None, @@ -229,20 +230,19 @@ class SnippetService: stmt = stmt.where(CustomizedSnippet.created_by.in_(creators)) if tag_ids: - target_ids = TagService.get_target_ids_by_tag_ids("snippet", tenant_id, tag_ids, match_all=True) + target_ids = TagService.get_target_ids_by_tag_ids("snippet", tenant_id, tag_ids, session, match_all=True) if target_ids: stmt = stmt.where(CustomizedSnippet.id.in_(target_ids)) else: return [], 0, False - with self._session_scope() as session: - # Get total count - count_stmt = select(func.count()).select_from(stmt.subquery()) - total = session.scalar(count_stmt) or 0 + # Get total count + count_stmt = select(func.count()).select_from(stmt.subquery()) + total = session.scalar(count_stmt) or 0 - # Apply pagination - stmt = stmt.limit(limit + 1).offset((page - 1) * limit) - snippets = list(session.scalars(stmt).all()) + # Apply pagination + stmt = stmt.limit(limit + 1).offset((page - 1) * limit) + snippets = list(session.scalars(stmt).all()) has_more = len(snippets) > limit if has_more: diff --git a/api/services/tag_service.py b/api/services/tag_service.py index 20f9a2c73d5..59dd5f7bb36 100644 --- a/api/services/tag_service.py +++ b/api/services/tag_service.py @@ -6,10 +6,9 @@ from flask_login import current_user from pydantic import BaseModel, Field from sqlalchemy import delete, func, select from sqlalchemy.engine import CursorResult -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, scoped_session from werkzeug.exceptions import NotFound -from extensions.ext_database import db from models.dataset import Dataset from models.enums import TagType from models.model import App, Tag, TagBinding @@ -56,7 +55,7 @@ class TagService: @staticmethod def get_target_ids_by_tag_ids( - tag_type: str, current_tenant_id: str, tag_ids: list[str], *, match_all: bool = False + tag_type: str, current_tenant_id: str, tag_ids: list[str], session: scoped_session, *, match_all: bool = False ): """ Return target IDs bound to tags for the given tenant and resource type. @@ -70,7 +69,7 @@ class TagService: return [] # Deduplicate repeated query params so match_all counts each requested tag once. requested_tag_ids = list(dict.fromkeys(tag_ids)) - tags = db.session.scalars( + tags = session.scalars( select(Tag.id).where( Tag.id.in_(requested_tag_ids), Tag.tenant_id == current_tenant_id, @@ -86,13 +85,13 @@ class TagService: if match_all: if len(tag_ids) != len(requested_tag_ids): return [] - return db.session.scalars( + return session.scalars( select(TagBinding.target_id) .where(TagBinding.tag_id.in_(tag_ids), TagBinding.tenant_id == current_tenant_id) .group_by(TagBinding.target_id) .having(func.count(sa.distinct(TagBinding.tag_id)) == len(tag_ids)) ).all() - tag_bindings = db.session.scalars( + tag_bindings = session.scalars( select(TagBinding.target_id).where( TagBinding.tag_id.in_(tag_ids), TagBinding.tenant_id == current_tenant_id ) @@ -100,11 +99,11 @@ class TagService: return tag_bindings @staticmethod - def get_tag_by_tag_name(tag_type: str, current_tenant_id: str, tag_name: str): + def get_tag_by_tag_name(tag_type: str, current_tenant_id: str, tag_name: str, session: scoped_session): if not tag_type or not tag_name: return [] tags = list( - db.session.scalars( + session.scalars( select(Tag).where(Tag.name == tag_name, Tag.tenant_id == current_tenant_id, Tag.type == tag_type) ).all() ) @@ -113,8 +112,8 @@ class TagService: return tags @staticmethod - def get_tags_by_target_id(tag_type: str, current_tenant_id: str, target_id: str): - tags = db.session.scalars( + def get_tags_by_target_id(tag_type: str, current_tenant_id: str, target_id: str, session: scoped_session): + tags = session.scalars( select(Tag) .join(TagBinding, Tag.id == TagBinding.tag_id) .where( @@ -128,8 +127,8 @@ class TagService: return tags or [] @staticmethod - def save_tags(payload: SaveTagPayload) -> Tag: - if TagService.get_tag_by_tag_name(payload.type, current_user.current_tenant_id, payload.name): + def save_tags(payload: SaveTagPayload, session: scoped_session) -> Tag: + if TagService.get_tag_by_tag_name(payload.type, current_user.current_tenant_id, payload.name, session): raise ValueError("Tag name already exists") tag = Tag( name=payload.name, @@ -138,17 +137,17 @@ class TagService: tenant_id=current_user.current_tenant_id, ) tag.id = str(uuid.uuid4()) - db.session.add(tag) - db.session.commit() + session.add(tag) + session.commit() return tag @staticmethod - def update_tags(payload: UpdateTagPayload, tag_id: str) -> Tag: - tag = db.session.scalar(select(Tag).where(Tag.id == tag_id).limit(1)) + def update_tags(payload: UpdateTagPayload, tag_id: str, session: scoped_session) -> Tag: + tag = session.scalar(select(Tag).where(Tag.id == tag_id).limit(1)) if not tag: raise NotFound("Tag not found") if payload.name != tag.name: - existing = db.session.scalar( + existing = session.scalar( select(Tag) .where( Tag.name == payload.name, @@ -161,31 +160,31 @@ class TagService: if existing: raise ValueError("Tag name already exists") tag.name = payload.name - db.session.commit() + session.commit() return tag @staticmethod - def get_tag_binding_count(tag_id: str) -> int: - count = db.session.scalar(select(func.count(TagBinding.id)).where(TagBinding.tag_id == tag_id)) or 0 + def get_tag_binding_count(tag_id: str, session: scoped_session) -> int: + count = session.scalar(select(func.count(TagBinding.id)).where(TagBinding.tag_id == tag_id)) or 0 return count @staticmethod - def delete_tag(tag_id: str): - tag = db.session.scalar(select(Tag).where(Tag.id == tag_id).limit(1)) + def delete_tag(tag_id: str, session: scoped_session): + tag = session.scalar(select(Tag).where(Tag.id == tag_id).limit(1)) if not tag: raise NotFound("Tag not found") - db.session.delete(tag) + session.delete(tag) # delete tag binding - tag_bindings = db.session.scalars(select(TagBinding).where(TagBinding.tag_id == tag_id)).all() + tag_bindings = session.scalars(select(TagBinding).where(TagBinding.tag_id == tag_id)).all() if tag_bindings: for tag_binding in tag_bindings: - db.session.delete(tag_binding) - db.session.commit() + session.delete(tag_binding) + session.commit() @staticmethod - def save_tag_binding(payload: TagBindingCreatePayload): - TagService.check_target_exists(payload.type, payload.target_id) - valid_tag_ids = db.session.scalars( + def save_tag_binding(payload: TagBindingCreatePayload, session: scoped_session): + TagService.check_target_exists(payload.type, payload.target_id, session) + valid_tag_ids = session.scalars( select(Tag.id).where( Tag.id.in_(payload.tag_ids), Tag.tenant_id == current_user.current_tenant_id, @@ -193,7 +192,7 @@ class TagService: ) ).all() for tag_id in valid_tag_ids: - tag_binding = db.session.scalar( + tag_binding = session.scalar( select(TagBinding) .where(TagBinding.tag_id == tag_id, TagBinding.target_id == payload.target_id) .limit(1) @@ -206,15 +205,15 @@ class TagService: tenant_id=current_user.current_tenant_id, created_by=current_user.id, ) - db.session.add(new_tag_binding) - db.session.commit() + session.add(new_tag_binding) + session.commit() @staticmethod - def delete_tag_binding(payload: TagBindingDeletePayload): - TagService.check_target_exists(payload.type, payload.target_id) + def delete_tag_binding(payload: TagBindingDeletePayload, session: scoped_session): + TagService.check_target_exists(payload.type, payload.target_id, session) result = cast( CursorResult, - db.session.execute( + session.execute( delete(TagBinding).where( TagBinding.target_id == payload.target_id, TagBinding.tag_id.in_(payload.tag_ids), @@ -230,12 +229,12 @@ class TagService: ) if result.rowcount: - db.session.commit() + session.commit() @staticmethod - def check_target_exists(type: str, target_id: str): + def check_target_exists(type: str, target_id: str, session: scoped_session): if type == "knowledge": - dataset = db.session.scalar( + dataset = session.scalar( select(Dataset) .where(Dataset.tenant_id == current_user.current_tenant_id, Dataset.id == target_id) .limit(1) @@ -243,13 +242,13 @@ class TagService: if not dataset: raise NotFound("Dataset not found") elif type == "app": - app = db.session.scalar( + app = session.scalar( select(App).where(App.tenant_id == current_user.current_tenant_id, App.id == target_id).limit(1) ) if not app: raise NotFound("App not found") elif type == "snippet": - snippet = db.session.scalar( + snippet = session.scalar( select(CustomizedSnippet) .where(CustomizedSnippet.tenant_id == current_user.current_tenant_id, CustomizedSnippet.id == target_id) .limit(1) diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index 7c9bd489bda..9c30f32a7ad 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -125,10 +125,11 @@ class DraftVarLoader(VariableLoader): # can be safely accessed before any offloading logic is applied. for draft_var in draft_vars: value = draft_var.get_value() - if isinstance(value, FileSegment): - files.append(value.value) - elif isinstance(value, ArrayFileSegment): - files.extend(value.value) + match value: + case FileSegment(): + files.append(value.value) + case ArrayFileSegment(): + files.extend(value.value) with Session(bind=self._engine) as session: storage_key_loader = StorageKeyLoader( session, diff --git a/api/services/workflow_run_service.py b/api/services/workflow_run_service.py index 29b9e72a009..2499e6cc094 100644 --- a/api/services/workflow_run_service.py +++ b/api/services/workflow_run_service.py @@ -34,10 +34,11 @@ class WorkflowRunService: def __init__(self, session_factory: Engine | sessionmaker | None = None): """Initialize WorkflowRunService with repository dependencies.""" - if session_factory is None: - session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) - elif isinstance(session_factory, Engine): - session_factory = sessionmaker(bind=session_factory, expire_on_commit=False) + match session_factory: + case None: + session_factory = sessionmaker(bind=db.engine, expire_on_commit=False) + case Engine(): + session_factory = sessionmaker(bind=session_factory, expire_on_commit=False) self._session_factory = session_factory self._node_execution_service_repo = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository( diff --git a/api/tests/helpers/legacy_model_type_migration.py b/api/tests/helpers/legacy_model_type_migration.py index 12f092a0fe3..4140119f326 100644 --- a/api/tests/helpers/legacy_model_type_migration.py +++ b/api/tests/helpers/legacy_model_type_migration.py @@ -131,10 +131,11 @@ def fetch_table_rows( for row in rows: normalized = dict(row) for key, value in normalized.items(): - if isinstance(value, datetime): - normalized[key] = value.isoformat() - elif isinstance(value, uuid.UUID): - normalized[key] = str(value) + match value: + case datetime(): + normalized[key] = value.isoformat() + case uuid.UUID(): + normalized[key] = str(value) result.append(normalized) return result diff --git a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py index 642dd3ab62d..ac166df454f 100644 --- a/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py +++ b/api/tests/test_containers_integration_tests/controllers/service_api/dataset/test_dataset.py @@ -16,7 +16,7 @@ since these test controller-level behavior. import uuid from contextlib import ExitStack from datetime import UTC, datetime -from unittest.mock import Mock, PropertyMock, patch +from unittest.mock import ANY, Mock, PropertyMock, patch import pytest from flask import Flask @@ -1129,7 +1129,7 @@ class TestDatasetTagsApiPatch: assert status == 200 assert response == {"id": "tag-1", "name": "Updated Tag", "type": "knowledge", "binding_count": "5"} mock_tag_svc.update_tags.assert_called_once() - update_payload, tag_id = mock_tag_svc.update_tags.call_args.args + update_payload, tag_id, session = mock_tag_svc.update_tags.call_args.args assert update_payload.name == "Updated Tag" assert tag_id == "tag-1" @@ -1184,7 +1184,7 @@ class TestDatasetTagsApiDelete: result = api.delete(_=None) assert result == ("", 204) - mock_tag_svc.delete_tag.assert_called_once_with("tag-1") + mock_tag_svc.delete_tag.assert_called_once_with("tag-1", ANY) @patch("libs.login.current_user") def test_delete_tag_forbidden(self, mock_current_user, app: Flask): @@ -1233,7 +1233,7 @@ class TestDatasetTagsBindingStatusApi: assert status_code == 200 assert response["data"] == [{"id": "tag_1", "name": "Test Tag"}] assert response["total"] == 1 - mock_tag_svc.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123") + mock_tag_svc.get_tags_by_target_id.assert_called_once_with("knowledge", "tenant_123", "dataset_123", ANY) class TestDatasetTagBindingApiPost: @@ -1266,7 +1266,8 @@ class TestDatasetTagBindingApiPost: from services.tag_service import TagBindingCreatePayload mock_tag_svc.save_tag_binding.assert_called_once_with( - TagBindingCreatePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE) + TagBindingCreatePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE), + ANY, ) @patch("controllers.service_api.dataset.dataset.current_user") @@ -1317,7 +1318,8 @@ class TestDatasetTagUnbindingApiPost: from services.tag_service import TagBindingDeletePayload mock_tag_svc.delete_tag_binding.assert_called_once_with( - TagBindingDeletePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE) + TagBindingDeletePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE), + ANY, ) @patch("controllers.service_api.dataset.dataset.TagService") @@ -1347,7 +1349,8 @@ class TestDatasetTagUnbindingApiPost: from services.tag_service import TagBindingDeletePayload mock_tag_svc.delete_tag_binding.assert_called_once_with( - TagBindingDeletePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE) + TagBindingDeletePayload(tag_ids=["tag-1"], target_id="ds-1", type=TagType.KNOWLEDGE), + ANY, ) @patch("controllers.service_api.dataset.dataset.current_user") diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py index 9a53ff087c5..83d2e25d224 100644 --- a/api/tests/test_containers_integration_tests/services/test_account_service.py +++ b/api/tests/test_containers_integration_tests/services/test_account_service.py @@ -2755,7 +2755,7 @@ class TestRegisterService: self, db_session_with_containers: Session, mock_external_service_dependencies ): """ - Test inviting an existing member who is not in the tenant yet. + Test inviting an existing active account who is not in the tenant yet. """ fake = Faker() tenant_name = fake.company() @@ -2791,20 +2791,20 @@ class TestRegisterService: # Mock the email task with patch("services.account_service.send_invite_member_mail_task") as mock_send_mail: mock_send_mail.delay.return_value = None - with pytest.raises(AccountAlreadyInTenantError, match="Account already in tenant."): - # Execute invitation - token = RegisterService.invite_new_member( - tenant=tenant, - email=existing_member_email, - language=language, - role="admin", - inviter=inviter, - ) - # Verify email task was not called - mock_send_mail.delay.assert_not_called() + token = RegisterService.invite_new_member( + tenant=tenant, + email=existing_member_email, + language=language, + role="admin", + inviter=inviter, + ) - # Verify tenant member was created for existing account + assert token is not None + assert len(token) > 0 + mock_send_mail.delay.assert_called_once() + + # Existing active accounts must accept the invite before becoming workspace members. from models.account import TenantAccountJoin tenant_join = ( @@ -2812,8 +2812,13 @@ class TestRegisterService: .filter_by(tenant_id=tenant.id, account_id=existing_account.id) .first() ) - assert tenant_join is not None - assert tenant_join.role == "admin" + assert tenant_join is None + + invitation = RegisterService.get_invitation_if_token_valid(None, None, token) + assert invitation is not None + assert invitation["account"].id == existing_account.id + assert invitation["data"]["role"] == "admin" + assert invitation["data"]["requires_setup"] is False def test_invite_new_member_existing_member( self, db_session_with_containers: Session, mock_external_service_dependencies diff --git a/api/tests/test_containers_integration_tests/services/test_app_service.py b/api/tests/test_containers_integration_tests/services/test_app_service.py index 384f83fce3e..43c254d407d 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_service.py @@ -234,7 +234,7 @@ class TestAppService: # Get paginated apps params = AppListParams(page=1, limit=10, mode="chat") - paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params) + paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params, db_session_with_containers) # Verify pagination results assert paginated_apps is not None @@ -295,7 +295,7 @@ class TestAppService: db_session_with_containers.commit() last_modified_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat"), db_session_with_containers ) assert last_modified_apps is not None assert [app.name for app in last_modified_apps.items] == [ @@ -305,7 +305,10 @@ class TestAppService: ] recently_created_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="recently_created") + account.id, + tenant.id, + AppListParams(page=1, limit=10, mode="chat", sort_by="recently_created"), + db_session_with_containers, ) assert recently_created_apps is not None assert [app.name for app in recently_created_apps.items] == [ @@ -315,7 +318,10 @@ class TestAppService: ] earliest_created_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created") + account.id, + tenant.id, + AppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created"), + db_session_with_containers, ) assert earliest_created_apps is not None assert [app.name for app in earliest_created_apps.items] == [ @@ -366,7 +372,7 @@ class TestAppService: assert star_count == 1 paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat"), db_session_with_containers ) assert paginated_apps is not None starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items} @@ -377,7 +383,7 @@ class TestAppService: db_session_with_containers.commit() paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat") + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat"), db_session_with_containers ) assert paginated_apps is not None starred_by_app_id = {app.id: app.is_starred for app in paginated_apps.items} @@ -442,7 +448,7 @@ class TestAppService: db_session_with_containers.commit() last_modified_apps = app_service.get_paginate_starred_apps( - account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat") + account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat"), db_session_with_containers ) assert last_modified_apps is not None assert [app.name for app in last_modified_apps.items] == [ @@ -457,6 +463,7 @@ class TestAppService: account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat", sort_by="recently_created"), + db_session_with_containers, ) assert recently_created_apps is not None assert [app.name for app in recently_created_apps.items] == [ @@ -469,6 +476,7 @@ class TestAppService: account.id, tenant.id, StarredAppListParams(page=1, limit=10, mode="chat", sort_by="earliest_created"), + db_session_with_containers, ) assert earliest_created_apps is not None assert [app.name for app in earliest_created_apps.items] == [ @@ -522,20 +530,25 @@ class TestAppService: completion_app = app_service.create_app(tenant.id, completion_app_params, account) # Test filter by mode - chat_apps = app_service.get_paginate_apps(account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat")) + chat_apps = app_service.get_paginate_apps( + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat"), db_session_with_containers + ) assert len(chat_apps.items) == 1 assert chat_apps.items[0].mode == "chat" # Test filter by name filtered_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", name="Chat") + account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", name="Chat"), db_session_with_containers ) assert len(filtered_apps.items) == 1 assert "Chat" in filtered_apps.items[0].name # Test filter by created_by_me my_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(page=1, limit=10, mode="completion", is_created_by_me=True) + account.id, + tenant.id, + AppListParams(page=1, limit=10, mode="completion", is_created_by_me=True), + db_session_with_containers, ) assert len(my_apps.items) == 1 @@ -588,6 +601,7 @@ class TestAppService: first_account.id, tenant.id, AppListParams(page=1, limit=10, mode="chat", creator_ids=[second_account.id]), + db_session_with_containers, ) assert filtered_apps is not None @@ -635,10 +649,12 @@ class TestAppService: # Test with tag filter params = AppListParams(page=1, limit=10, mode="chat", tag_ids=["tag1", "tag2"]) - paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params) + paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params, db_session_with_containers) # Verify tag service was called - mock_tag_service.assert_called_once_with("app", tenant.id, ["tag1", "tag2"], match_all=True) + mock_tag_service.assert_called_once_with( + "app", tenant.id, ["tag1", "tag2"], db_session_with_containers, match_all=True + ) # Verify results assert paginated_apps is not None @@ -651,7 +667,7 @@ class TestAppService: params = AppListParams(page=1, limit=10, mode="chat", tag_ids=["nonexistent_tag"]) - paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params) + paginated_apps = app_service.get_paginate_apps(account.id, tenant.id, params, db_session_with_containers) # Should return None when no apps match tag filter assert paginated_apps is None @@ -1467,7 +1483,7 @@ class TestAppService: # Test 1: Search with % character paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(name="50%", mode="chat", page=1, limit=10) + account.id, tenant.id, AppListParams(name="50%", mode="chat", page=1, limit=10), db_session_with_containers ) assert paginated_apps is not None assert paginated_apps.total == 1 @@ -1476,7 +1492,10 @@ class TestAppService: # Test 2: Search with _ character paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(name="test_data", mode="chat", page=1, limit=10) + account.id, + tenant.id, + AppListParams(name="test_data", mode="chat", page=1, limit=10), + db_session_with_containers, ) assert paginated_apps is not None assert paginated_apps.total == 1 @@ -1485,7 +1504,10 @@ class TestAppService: # Test 3: Search with \ character paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(name="path\\to\\app", mode="chat", page=1, limit=10) + account.id, + tenant.id, + AppListParams(name="path\\to\\app", mode="chat", page=1, limit=10), + db_session_with_containers, ) assert paginated_apps is not None assert paginated_apps.total == 1 @@ -1494,7 +1516,7 @@ class TestAppService: # Test 4: Search with % should NOT match 100% (verifies escaping works) paginated_apps = app_service.get_paginate_apps( - account.id, tenant.id, AppListParams(name="50%", mode="chat", page=1, limit=10) + account.id, tenant.id, AppListParams(name="50%", mode="chat", page=1, limit=10), db_session_with_containers ) assert paginated_apps is not None assert paginated_apps.total == 1 diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py b/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py index 4e2bf9fc103..27ab600871b 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service_retrieval.py @@ -227,7 +227,7 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page, per_page, tenant_id=tenant.id) + datasets, total = DatasetService.get_datasets(page, per_page, db_session_with_containers, tenant_id=tenant.id) # Assert assert len(datasets) == 5 @@ -257,7 +257,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page, per_page, tenant_id=tenant.id, search=search) + datasets, total = DatasetService.get_datasets( + page, per_page, db_session_with_containers, tenant_id=tenant.id, search=search + ) # Assert assert len(datasets) == 1 @@ -301,7 +303,9 @@ class TestDatasetServiceGetDatasets: tag_ids = [tag_1.id, tag_2.id] # Act - datasets, total = DatasetService.get_datasets(page, per_page, tenant_id=tenant.id, tag_ids=tag_ids) + datasets, total = DatasetService.get_datasets( + page, per_page, db_session_with_containers, tenant_id=tenant.id, tag_ids=tag_ids + ) # Assert assert len(datasets) == 1 @@ -326,7 +330,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page, per_page, tenant_id=tenant.id, tag_ids=tag_ids) + datasets, total = DatasetService.get_datasets( + page, per_page, db_session_with_containers, tenant_id=tenant.id, tag_ids=tag_ids + ) # Assert # When tag_ids is empty, tag filtering is skipped, so normal query results are returned @@ -356,7 +362,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page, per_page, tenant_id=tenant.id, user=None) + datasets, total = DatasetService.get_datasets( + page, per_page, db_session_with_containers, tenant_id=tenant.id, user=None + ) # Assert assert len(datasets) == 1 @@ -384,6 +392,7 @@ class TestDatasetServiceGetDatasets: datasets, total = DatasetService.get_datasets( page=1, per_page=20, + session=db_session_with_containers, tenant_id=tenant.id, user=owner, include_all=True, @@ -408,7 +417,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id=tenant.id, user=user) + datasets, total = DatasetService.get_datasets( + page=1, per_page=20, session=db_session_with_containers, tenant_id=tenant.id, user=user + ) # Assert assert len(datasets) == 1 @@ -432,7 +443,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id=tenant.id, user=user) + datasets, total = DatasetService.get_datasets( + page=1, per_page=20, session=db_session_with_containers, tenant_id=tenant.id, user=user + ) # Assert assert len(datasets) == 1 @@ -459,7 +472,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id=tenant.id, user=user) + datasets, total = DatasetService.get_datasets( + page=1, per_page=20, session=db_session_with_containers, tenant_id=tenant.id, user=user + ) # Assert assert len(datasets) == 1 @@ -486,7 +501,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id=tenant.id, user=operator) + datasets, total = DatasetService.get_datasets( + page=1, per_page=20, session=db_session_with_containers, tenant_id=tenant.id, user=operator + ) # Assert assert len(datasets) == 1 @@ -509,7 +526,9 @@ class TestDatasetServiceGetDatasets: ) # Act - datasets, total = DatasetService.get_datasets(page=1, per_page=20, tenant_id=tenant.id, user=operator) + datasets, total = DatasetService.get_datasets( + page=1, per_page=20, session=db_session_with_containers, tenant_id=tenant.id, user=operator + ) # Assert assert datasets == [] diff --git a/api/tests/test_containers_integration_tests/services/test_tag_service.py b/api/tests/test_containers_integration_tests/services/test_tag_service.py index 517d5d2ed4c..197415ee6bd 100644 --- a/api/tests/test_containers_integration_tests/services/test_tag_service.py +++ b/api/tests/test_containers_integration_tests/services/test_tag_service.py @@ -449,7 +449,7 @@ class TestTagService: # Act: Execute the method under test tag_ids = [tag.id for tag in tags] - result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, tag_ids) + result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, tag_ids, db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -485,7 +485,7 @@ class TestTagService: ) # Act: Execute the method under test with empty tag IDs - result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, []) + result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, [], db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -533,13 +533,19 @@ class TestTagService: # Act: Execute the method under test tag_ids = [tag.id for tag in tags] - result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, tag_ids, match_all=True) + result = TagService.get_target_ids_by_tag_ids( + "knowledge", tenant.id, tag_ids, db_session_with_containers, match_all=True + ) # Assert: Verify the expected outcomes assert result == [dataset_with_all_tags.id] missing_tag_result = TagService.get_target_ids_by_tag_ids( - "knowledge", tenant.id, [tags[0].id, str(uuid.uuid4())], match_all=True + "knowledge", + tenant.id, + [tags[0].id, str(uuid.uuid4())], + db_session_with_containers, + match_all=True, ) assert missing_tag_result == [] @@ -565,7 +571,9 @@ class TestTagService: non_existent_tag_ids = [str(uuid.uuid4()), str(uuid.uuid4())] # Act: Execute the method under test - result = TagService.get_target_ids_by_tag_ids("knowledge", tenant.id, non_existent_tag_ids) + result = TagService.get_target_ids_by_tag_ids( + "knowledge", tenant.id, non_existent_tag_ids, db_session_with_containers + ) # Assert: Verify the expected outcomes assert result is not None @@ -599,7 +607,7 @@ class TestTagService: db_session_with_containers.commit() # Act: Execute the method under test - result = TagService.get_tag_by_tag_name("app", tenant.id, "python_tag") + result = TagService.get_tag_by_tag_name("app", tenant.id, "python_tag", db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -625,7 +633,7 @@ class TestTagService: ) # Act: Execute the method under test with non-existent tag name - result = TagService.get_tag_by_tag_name("knowledge", tenant.id, "nonexistent_tag") + result = TagService.get_tag_by_tag_name("knowledge", tenant.id, "nonexistent_tag", db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -650,8 +658,8 @@ class TestTagService: ) # Act: Execute the method under test with empty parameters - result_empty_type = TagService.get_tag_by_tag_name("", tenant.id, "test_tag") - result_empty_name = TagService.get_tag_by_tag_name("knowledge", tenant.id, "") + result_empty_type = TagService.get_tag_by_tag_name("", tenant.id, "test_tag", db_session_with_containers) + result_empty_name = TagService.get_tag_by_tag_name("knowledge", tenant.id, "", db_session_with_containers) # Assert: Verify the expected outcomes assert result_empty_type is not None @@ -688,7 +696,7 @@ class TestTagService: ) # Act: Execute the method under test - result = TagService.get_tags_by_target_id("app", tenant.id, app.id) + result = TagService.get_tags_by_target_id("app", tenant.id, app.id, db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -720,7 +728,7 @@ class TestTagService: app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Act: Execute the method under test - result = TagService.get_tags_by_target_id("app", tenant.id, app.id) + result = TagService.get_tags_by_target_id("app", tenant.id, app.id, db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -745,7 +753,7 @@ class TestTagService: tag_args = SaveTagPayload(name="test_tag_name", type="knowledge") # Act: Execute the method under test - result = TagService.save_tags(tag_args) + result = TagService.save_tags(tag_args, db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -783,11 +791,11 @@ class TestTagService: # Create first tag tag_args = SaveTagPayload(name="duplicate_tag", type="app") - TagService.save_tags(tag_args) + TagService.save_tags(tag_args, db_session_with_containers) # Act & Assert: Verify proper error handling with pytest.raises(ValueError) as exc_info: - TagService.save_tags(tag_args) + TagService.save_tags(tag_args, db_session_with_containers) assert "Tag name already exists" in str(exc_info.value) def test_update_tags_success(self, db_session_with_containers: Session, mock_external_service_dependencies): @@ -807,13 +815,13 @@ class TestTagService: # Create a tag to update tag_args = SaveTagPayload(name="original_name", type="knowledge") - tag = TagService.save_tags(tag_args) + tag = TagService.save_tags(tag_args, db_session_with_containers) # Update args update_args = UpdateTagPayload(name="updated_name") # Act: Execute the method under test - result = TagService.update_tags(update_args, tag.id) + result = TagService.update_tags(update_args, tag.id, db_session_with_containers) # Assert: Verify the expected outcomes assert result is not None @@ -854,7 +862,7 @@ class TestTagService: # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: - TagService.update_tags(update_args, non_existent_tag_id) + TagService.update_tags(update_args, non_existent_tag_id, db_session_with_containers) assert "Tag not found" in str(exc_info.value) def test_update_tags_duplicate_name_error( @@ -875,17 +883,17 @@ class TestTagService: # Create two tags tag1_args = SaveTagPayload(name="first_tag", type="app") - tag1 = TagService.save_tags(tag1_args) + tag1 = TagService.save_tags(tag1_args, db_session_with_containers) tag2_args = SaveTagPayload(name="second_tag", type="app") - tag2 = TagService.save_tags(tag2_args) + tag2 = TagService.save_tags(tag2_args, db_session_with_containers) # Try to update second tag with first tag's name update_args = UpdateTagPayload(name="first_tag") # Act & Assert: Verify proper error handling with pytest.raises(ValueError) as exc_info: - TagService.update_tags(update_args, tag2.id) + TagService.update_tags(update_args, tag2.id, db_session_with_containers) assert "Tag name already exists" in str(exc_info.value) def test_get_tag_binding_count_success( @@ -917,8 +925,8 @@ class TestTagService: ) # Act: Execute the method under test - result_tag_with_bindings = TagService.get_tag_binding_count(tags[0].id) - result_tag_without_bindings = TagService.get_tag_binding_count(tags[1].id) + result_tag_with_bindings = TagService.get_tag_binding_count(tags[0].id, db_session_with_containers) + result_tag_without_bindings = TagService.get_tag_binding_count(tags[1].id, db_session_with_containers) # Assert: Verify the expected outcomes assert result_tag_with_bindings == 1 @@ -946,7 +954,7 @@ class TestTagService: non_existent_tag_id = str(uuid.uuid4()) # Act: Execute the method under test - result = TagService.get_tag_binding_count(non_existent_tag_id) + result = TagService.get_tag_binding_count(non_existent_tag_id, db_session_with_containers) # Assert: Verify the expected outcomes assert result == 0 @@ -986,7 +994,7 @@ class TestTagService: assert binding_before is not None # Act: Execute the method under test - TagService.delete_tag(tag.id) + TagService.delete_tag(tag.id, db_session_with_containers) # Assert: Verify the expected outcomes # Verify tag was deleted @@ -1018,7 +1026,7 @@ class TestTagService: # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: - TagService.delete_tag(non_existent_tag_id) + TagService.delete_tag(non_existent_tag_id, db_session_with_containers) assert "Tag not found" in str(exc_info.value) def test_save_tag_binding_success(self, db_session_with_containers: Session, mock_external_service_dependencies): @@ -1048,7 +1056,7 @@ class TestTagService: binding_payload = TagBindingCreatePayload( type="knowledge", target_id=dataset.id, tag_ids=[tag.id for tag in tags] ) - TagService.save_tag_binding(binding_payload) + TagService.save_tag_binding(binding_payload, db_session_with_containers) # Assert: Verify the expected outcomes @@ -1090,10 +1098,10 @@ class TestTagService: # Create first binding binding_payload = TagBindingCreatePayload(type="app", target_id=app.id, tag_ids=[tag.id]) - TagService.save_tag_binding(binding_payload) + TagService.save_tag_binding(binding_payload, db_session_with_containers) # Act: Try to create duplicate binding - TagService.save_tag_binding(binding_payload) + TagService.save_tag_binding(binding_payload, db_session_with_containers) # Assert: Verify the expected outcomes @@ -1173,7 +1181,7 @@ class TestTagService: delete_payload = TagBindingDeletePayload( type="knowledge", target_id=dataset.id, tag_ids=[tag.id for tag in tags] ) - TagService.delete_tag_binding(delete_payload) + TagService.delete_tag_binding(delete_payload, db_session_with_containers) # Assert: Verify the expected outcomes # Verify tag bindings were deleted @@ -1209,7 +1217,7 @@ class TestTagService: # Act: Try to delete non-existent binding delete_payload = TagBindingDeletePayload(type="app", target_id=app.id, tag_ids=[tag.id]) - TagService.delete_tag_binding(delete_payload) + TagService.delete_tag_binding(delete_payload, db_session_with_containers) # Assert: Verify the expected outcomes # No error should be raised, and database state should remain unchanged @@ -1240,7 +1248,7 @@ class TestTagService: dataset = self._create_test_dataset(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Act: Execute the method under test - TagService.check_target_exists("knowledge", dataset.id) + TagService.check_target_exists("knowledge", dataset.id, db_session_with_containers) # Assert: Verify the expected outcomes # No exception should be raised for existing dataset @@ -1268,7 +1276,7 @@ class TestTagService: # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: - TagService.check_target_exists("knowledge", non_existent_dataset_id) + TagService.check_target_exists("knowledge", non_existent_dataset_id, db_session_with_containers) assert "Dataset not found" in str(exc_info.value) def test_check_target_exists_app_success( @@ -1292,7 +1300,7 @@ class TestTagService: app = self._create_test_app(db_session_with_containers, mock_external_service_dependencies, tenant.id) # Act: Execute the method under test - TagService.check_target_exists("app", app.id) + TagService.check_target_exists("app", app.id, db_session_with_containers) # Assert: Verify the expected outcomes # No exception should be raised for existing app @@ -1320,7 +1328,7 @@ class TestTagService: # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: - TagService.check_target_exists("app", non_existent_app_id) + TagService.check_target_exists("app", non_existent_app_id, db_session_with_containers) assert "App not found" in str(exc_info.value) def test_check_target_exists_invalid_type( @@ -1346,5 +1354,5 @@ class TestTagService: # Act & Assert: Verify proper error handling with pytest.raises(NotFound) as exc_info: - TagService.check_target_exists("invalid_type", non_existent_target_id) + TagService.check_target_exists("invalid_type", non_existent_target_id, db_session_with_containers) assert "Invalid binding type" in str(exc_info.value) diff --git a/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py b/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py index 8444af741fb..f30bbd24374 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py @@ -266,6 +266,7 @@ def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path): assert module._schema_ref_name(None) is None assert module._schema_markdown_type(None) == "" assert module._schema_markdown_type({"anyOf": [{"type": "null"}]}) == "" + assert module._strip_trailing_line_whitespace("line \ncell\t \n") == "line\ncell\n" assert module._replace_schema_table_type("unchanged", "Definition", "field", "") == "unchanged" assert ( module._replace_schema_table_type( @@ -319,7 +320,10 @@ def test_convert_spec_to_markdown_patches_generated_union_tables(tmp_path, monke assert kwargs["check"] is False markdown_path = Path(args[args.index("-o") + 1]) markdown_path.write_text( - """#### FormInputConfig + "Intro line" + + " \n" + + """ +#### FormInputConfig | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | @@ -340,5 +344,7 @@ def test_convert_spec_to_markdown_patches_generated_union_tables(tmp_path, monke module._convert_spec_to_markdown(spec_path, output_path) converted = output_path.read_text(encoding="utf-8") + assert "Intro line \n" not in converted + assert "Intro line\n" in converted assert "| FormInputConfig | [ParagraphInputConfig](#paragraphinputconfig) | | |" in converted assert "| default | [StringSource](#stringsource) | | No |" in converted diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index 03af7643f3c..c30386c9d65 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -8,12 +8,13 @@ from pathlib import Path def _walk_values(value): yield value - if isinstance(value, dict): - for child in value.values(): - yield from _walk_values(child) - elif isinstance(value, list): - for child in value: - yield from _walk_values(child) + match value: + case dict(): + for child in value.values(): + yield from _walk_values(child) + case list(): + for child in value: + yield from _walk_values(child) def _load_generate_swagger_specs_module(): @@ -106,6 +107,39 @@ def test_generate_specs_writes_get_operations_without_request_bodies(tmp_path): assert all("requestBody" not in operation for operation in _get_operations(payload)) +def test_generate_specs_writes_service_api_reference_descriptions(tmp_path): + module = _load_generate_swagger_specs_module() + + written_paths = module.generate_specs(tmp_path) + service_path = next(path for path in written_paths if path.name == "service-openapi.json") + payload = json.loads(service_path.read_text(encoding="utf-8")) + + chat_operation = payload["paths"]["/chat-messages"]["post"] + assert chat_operation["summary"] == "Send Chat Message" + assert chat_operation["description"] == "Send a request to the chat application." + assert chat_operation["tags"] == ["Chatflows", "Chats"] + + rename_operation = payload["paths"]["/conversations/{c_id}/name"]["post"] + assert rename_operation["summary"] == "Rename Conversation" + + +def test_standalone_inline_model_name_includes_list_constraints(): + module = _load_generate_swagger_specs_module() + + from flask_restx import fields + + cases = ( + ({"min_items": 1}, {"min_items": 2}), + ({"max_items": 1}, {"max_items": 2}), + ({"unique": True}, {"unique": False}), + ) + for first_kwargs, second_kwargs in cases: + first_inline_model = {"items": fields.List(fields.String, **first_kwargs)} + second_inline_model = {"items": fields.List(fields.String, **second_kwargs)} + + assert module._inline_model_name(first_inline_model) != module._inline_model_name(second_inline_model) + + def test_generate_specs_is_idempotent(tmp_path): module = _load_generate_swagger_specs_module() diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index 1679cc7d6a7..320d841ca8c 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -24,7 +24,9 @@ from controllers.console.agent.roster import ( AgentAppCopyApi, AgentAppListApi, AgentInviteOptionsApi, + AgentLogMessagesApi, AgentLogsApi, + AgentLogSourcesApi, AgentRosterVersionDetailApi, AgentRosterVersionsApi, AgentStatisticsSummaryApi, @@ -93,6 +95,7 @@ def _agent_app_composer_response() -> dict: def _app_detail_obj(**overrides): data = { "id": "app-1", + "tenant_id": "tenant-1", "name": "Iris", "description": "Agent app", "mode_compatible_with_agent": "agent", @@ -116,7 +119,6 @@ def _app_detail_obj(**overrides): "deleted_tools": [], "site": None, "bound_agent_id": "00000000-0000-0000-0000-000000000001", - "tenant_id": "tenant-1", } data.update(overrides) return SimpleNamespace(**data) @@ -153,6 +155,8 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None: "/agent//chat-messages//suggested-questions", "/agent//messages/", "/agent//logs", + "/agent//logs//messages", + "/agent//log-sources", "/agent//statistics/summary", "/agent/invite-options", ): @@ -187,7 +191,7 @@ def test_agent_app_list_and_create_use_agent_route( def get_app(self, app_obj: object) -> object: return app_obj - def get_paginate_apps(self, user_id: str, tenant_id: str, params) -> object: + def get_paginate_apps(self, user_id: str, tenant_id: str, params, session) -> object: captured["list"] = {"user_id": user_id, "tenant_id": tenant_id, "params": params} return SimpleNamespace( page=1, @@ -270,7 +274,13 @@ def test_agent_app_list_and_create_use_agent_route( with app.test_request_context( "/console/api/agent", - json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"}, + json={ + "name": "Iris", + "description": "Agent app", + "role": "Coordinator", + "icon_type": "emoji", + "icon": "robot", + }, ): created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id)) @@ -283,6 +293,23 @@ def test_agent_app_list_and_create_use_agent_route( create_call = cast(dict[str, object], captured["create"]) create_params = cast(Any, create_call["params"]) assert create_params.mode == "agent" + assert create_params.agent_role == "Coordinator" + + +def test_agent_app_create_requires_role(app: Flask, account_id: str) -> None: + with app.test_request_context( + "/console/api/agent", + json={"name": "Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"}, + ): + with pytest.raises(ValueError, match="Field required"): + unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id)) + + with app.test_request_context( + "/console/api/agent", + json={"name": "Iris", "description": "Agent app", "role": " ", "icon_type": "emoji", "icon": "robot"}, + ): + with pytest.raises(ValueError, match="Agent role is required"): + unwrap(AgentAppListApi.post)(AgentAppListApi(), "tenant-1", SimpleNamespace(id=account_id)) def test_agent_app_detail_update_delete_resolve_app_from_agent_id( @@ -331,7 +358,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( with app.test_request_context( "/console/api/agent/00000000-0000-0000-0000-000000000001", - json={"name": "Renamed", "description": "", "icon_type": "emoji", "icon": "R"}, + json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"}, ): updated = unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id) @@ -343,6 +370,7 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id( assert "bound_agent_id" not in updated update_call = cast(dict[str, object], captured["update"]) assert update_call["app"] is app_model + assert cast(dict[str, object], update_call["args"])["role"] == "Reviewer" deleted, status = unwrap(AgentAppApi.delete)(AgentAppApi(), "tenant-1", agent_id) assert (deleted, status) == ("", 204) @@ -395,6 +423,45 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail( } +def test_agent_app_update_rejects_empty_role(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + agent_id = "00000000-0000-0000-0000-000000000001" + app_model = _app_detail_obj(id="app-1", bound_agent_id=agent_id) + captured: dict[str, object] = {} + + monkeypatch.setattr( + roster_controller.AgentRosterService, + "get_agent_app_model", + lambda _self, **kwargs: app_model, + ) + monkeypatch.setattr( + roster_controller.AgentRosterService, + "get_app_backing_agent", + lambda _self, **kwargs: SimpleNamespace(id=agent_id, role="", active_config_snapshot_id=None), + ) + monkeypatch.setattr( + roster_controller.FeatureService, + "get_system_features", + lambda: SimpleNamespace(webapp_auth=SimpleNamespace(enabled=False)), + ) + + class FakeAppService: + def get_app(self, app_obj: object) -> object: + return app_obj + + def update_app(self, app_obj: object, args: dict[str, object]) -> object: + captured["update"] = {"app": app_obj, "args": args} + return _app_detail_obj(id="app-1", name=args["name"], bound_agent_id=agent_id) + + monkeypatch.setattr(roster_controller, "AppService", FakeAppService) + + with app.test_request_context( + "/console/api/agent/00000000-0000-0000-0000-000000000001", + json={"name": "Renamed", "description": "", "role": "", "icon_type": "emoji", "icon": "R"}, + ): + with pytest.raises(ValueError, match="String should have at least 1 character"): + unwrap(AgentAppApi.put)(AgentAppApi(), "tenant-1", agent_id) + + def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: captured: dict[str, object] = {} @@ -461,21 +528,59 @@ def test_agent_observability_routes_resolve_app_from_agent_id( captured: dict[str, object] = {} class FakeObservabilityService: - def list_logs(self, *, app, params): - captured["logs"] = {"app": app, "params": params} + def list_logs(self, *, app, agent_id, params): + captured["logs"] = {"app": app, "agent_id": agent_id, "params": params} + return { + "data": [ + { + "conversation_id": "conversation-1", + "id": "conversation-1", + "title": "Debug", + "end_user_id": "end-user-1", + "message_count": 2, + "user_rate": None, + "operation_rate": None, + "unread": True, + "source": { + "id": "webapp:app-1", + "type": "webapp", + "app_id": "app-1", + "app_name": "Iris", + "app_icon_type": "emoji", + "app_icon": "robot", + "app_icon_background": "#fff", + "workflow_id": None, + "workflow_version": None, + "node_id": None, + }, + "status": "success", + "created_at": 1, + "updated_at": 2, + } + ], + "page": 2, + "limit": 5, + "total": 6, + "has_more": False, + } + + def list_log_messages(self, *, app, agent_id, conversation_id, params): + captured["messages"] = { + "app": app, + "agent_id": agent_id, + "conversation_id": conversation_id, + "params": params, + } return { "data": [ { "id": "message-1", "message_id": "message-1", "conversation_id": "conversation-1", - "conversation_name": "Debug", "query": "hello", "answer": "hi", "status": "success", "error": None, - "source": "explore", - "from_source": "console", "from_end_user_id": None, "from_account_id": account_id, "message_tokens": 1, @@ -488,14 +593,34 @@ def test_agent_observability_routes_resolve_app_from_agent_id( "updated_at": 2, } ], - "page": 2, - "limit": 5, - "total": 6, + "page": 1, + "limit": 20, + "total": 1, "has_more": False, } - def get_statistics_summary(self, *, app, params): - captured["statistics"] = {"app": app, "params": params} + def list_log_sources(self, *, app, agent_id): + captured["sources"] = {"app": app, "agent_id": agent_id} + return { + "data": [ + { + "id": "webapp:app-1", + "type": "webapp", + "app_id": "app-1", + "app_name": "Iris", + "app_icon_type": "emoji", + "app_icon": "robot", + "app_icon_background": "#fff", + "workflow_id": None, + "workflow_version": None, + "node_id": None, + } + ], + "groups": [{"type": "webapp", "label": "WEBAPP", "sources": []}], + } + + def get_statistics_summary(self, *, app, agent_id, params): + captured["statistics"] = {"app": app, "agent_id": agent_id, "params": params} return { "source": "all", "summary": { @@ -532,9 +657,11 @@ def test_agent_observability_routes_resolve_app_from_agent_id( ): logs = unwrap(AgentLogsApi.get)(AgentLogsApi(), "tenant-1", account, agent_id) - assert logs["data"][0]["id"] == "message-1" + assert logs["data"][0]["id"] == "conversation-1" + assert logs["data"][0]["source"]["id"] == "webapp:app-1" logs_call = cast(dict[str, object], captured["logs"]) assert logs_call["app"] is app_model + assert logs_call["agent_id"] == agent_id logs_params = cast(Any, logs_call["params"]) assert logs_params.page == 2 assert logs_params.limit == 5 @@ -542,6 +669,31 @@ def test_agent_observability_routes_resolve_app_from_agent_id( assert logs_params.status == "success" assert logs_params.source == "console" + with app.test_request_context( + "/console/api/agent/00000000-0000-0000-0000-000000000001/logs/00000000-0000-0000-0000-000000000002/messages" + ): + messages = unwrap(AgentLogMessagesApi.get)( + AgentLogMessagesApi(), + "tenant-1", + account, + agent_id, + "00000000-0000-0000-0000-000000000002", + ) + + assert messages["data"][0]["id"] == "message-1" + messages_call = cast(dict[str, object], captured["messages"]) + assert messages_call["app"] is app_model + assert messages_call["agent_id"] == agent_id + assert messages_call["conversation_id"] == "00000000-0000-0000-0000-000000000002" + + with app.test_request_context("/console/api/agent/00000000-0000-0000-0000-000000000001/log-sources"): + sources = unwrap(AgentLogSourcesApi.get)(AgentLogSourcesApi(), "tenant-1", account, agent_id) + + assert sources["data"][0]["id"] == "webapp:app-1" + sources_call = cast(dict[str, object], captured["sources"]) + assert sources_call["app"] is app_model + assert sources_call["agent_id"] == agent_id + with app.test_request_context( "/console/api/agent/00000000-0000-0000-0000-000000000001/statistics/summary?source=api" ): @@ -550,6 +702,7 @@ def test_agent_observability_routes_resolve_app_from_agent_id( assert statistics["summary"]["total_messages"] == 1 stats_call = cast(dict[str, object], captured["statistics"]) assert stats_call["app"] is app_model + assert stats_call["agent_id"] == agent_id stats_params = cast(Any, stats_call["params"]) assert stats_params.source == "api" assert stats_params.timezone == "UTC" diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py index bcb4aeab462..e94536aa012 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py @@ -10,7 +10,7 @@ from __future__ import annotations import inspect import io from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import patch from flask import Flask @@ -18,8 +18,6 @@ from controllers.console.app.agent import ( AgentDriveFilesByAgentApi, AgentSkillByAgentApi, AgentSkillInferToolsByAgentApi, - AgentSkillStandardizeApi, - AgentSkillStandardizeByAgentApi, AgentSkillUploadApi, AgentSkillUploadByAgentApi, ) @@ -45,47 +43,39 @@ _APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bou _WORKFLOW_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW, bound_agent_id=None) -def test_upload_validates_and_returns_skill_ref(): +def test_upload_standardizes_into_drive_and_returns_skill_ref(): raw = _raw(AgentSkillUploadApi.post) - manifest = MagicMock() - manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"} - manifest.model_dump.return_value = {"name": "S"} with _file_ctx(files={"file": b"zip-bytes"}): with ( - patch(f"{_MOD}.SkillPackageService") as pkg, - patch(f"{_MOD}.FileService") as fs, - patch(f"{_MOD}.db"), + patch(f"{_MOD}.SkillStandardizeService") as svc, ): - pkg.return_value.validate_and_extract.return_value = manifest - fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1") + svc.return_value.standardize.return_value = { + "skill": {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"}, + "manifest": {"name": "Skill A"}, + } body, status = raw(AgentSkillUploadApi(), _USER, _APP) assert status == 201 - assert body["skill"] == {"name": "S", "file_id": "uf-1"} - manifest.to_skill_ref.assert_called_once_with(file_id="uf-1") + assert body["skill"] == {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"} + assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" -def test_upload_by_agent_resolves_app_and_returns_skill_ref(): +def test_upload_by_agent_resolves_app_and_standardizes_into_drive(): raw = _raw(AgentSkillUploadByAgentApi.post) - manifest = MagicMock() - manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"} - manifest.model_dump.return_value = {"name": "S"} with _file_ctx(files={"file": b"zip-bytes"}): with ( patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, - patch(f"{_MOD}.SkillPackageService") as pkg, - patch(f"{_MOD}.FileService") as fs, - patch(f"{_MOD}.db"), + patch(f"{_MOD}.SkillStandardizeService") as svc, ): - pkg.return_value.validate_and_extract.return_value = manifest - fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1") + svc.return_value.standardize.return_value = {"skill": {"path": "skill-a"}, "manifest": {}} body, status = raw(AgentSkillUploadByAgentApi(), "tenant-1", _USER, "agent-1") assert status == 201 - assert body["skill"] == {"name": "S", "file_id": "uf-1"} + assert body["skill"] == {"path": "skill-a"} resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1") + assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" def test_upload_no_file_is_400(): @@ -99,8 +89,8 @@ def test_upload_no_file_is_400(): def test_upload_maps_package_error(): raw = _raw(AgentSkillUploadApi.post) with _file_ctx(files={"file": b"bad"}): - with patch(f"{_MOD}.SkillPackageService") as pkg: - pkg.return_value.validate_and_extract.side_effect = SkillPackageError( + with patch(f"{_MOD}.SkillStandardizeService") as svc: + svc.return_value.standardize.side_effect = SkillPackageError( "missing_skill_md", "no SKILL.md", status_code=400 ) body, status = raw(AgentSkillUploadApi(), _USER, _APP) @@ -108,44 +98,17 @@ def test_upload_maps_package_error(): assert body["code"] == "missing_skill_md" -def test_standardize_returns_result(): - raw = _raw(AgentSkillStandardizeApi.post) - with _file_ctx(files={"file": b"zip"}): - with patch(f"{_MOD}.SkillStandardizeService") as svc: - svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeApi(), _USER, _APP) - assert status == 201 - assert body["skill"] == {"path": "s"} - assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" - - -def test_standardize_by_agent_resolves_app(): - raw = _raw(AgentSkillStandardizeByAgentApi.post) - with _file_ctx(files={"file": b"zip"}): - with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, - patch(f"{_MOD}.SkillStandardizeService") as svc, - ): - svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeByAgentApi(), "tenant-1", _USER, "agent-1") - - assert status == 201 - assert body["skill"] == {"path": "s"} - resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1") - assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" - - -def test_standardize_no_bound_agent_is_400(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_no_bound_agent_is_400(): + raw = _raw(AgentSkillUploadApi.post) app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None) with _file_ctx(files={"file": b"zip"}): - body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent) + body, status = raw(AgentSkillUploadApi(), _USER, app_without_agent) assert status == 400 assert body["code"] == "agent_not_bound" -def test_standardize_resolves_workflow_node_agent(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_resolves_workflow_node_agent(): + raw = _raw(AgentSkillUploadApi.post) with app.test_request_context( "/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")} ): @@ -155,19 +118,19 @@ def test_standardize_resolves_workflow_node_agent(): ): composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1" svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeApi(), _USER, _WORKFLOW_APP) + body, status = raw(AgentSkillUploadApi(), _USER, _WORKFLOW_APP) assert status == 201 assert body["skill"] == {"path": "s"} assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "wf-agent-1" -def test_standardize_maps_drive_error(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_maps_drive_error(): + raw = _raw(AgentSkillUploadApi.post) with _file_ctx(files={"file": b"zip"}): with patch(f"{_MOD}.SkillStandardizeService") as svc: svc.return_value.standardize.side_effect = AgentDriveError("source_not_found", "nope", status_code=404) - body, status = raw(AgentSkillStandardizeApi(), _USER, _APP) + body, status = raw(AgentSkillUploadApi(), _USER, _APP) assert status == 404 assert body["code"] == "source_not_found" diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py index ef8f90e5c9c..9cff0c8d3ee 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py @@ -351,6 +351,7 @@ def test_app_partial_serialization_uses_aliases(app_models): create_user_name="Creator", author_name="Author", has_draft_trigger=True, + role="Should stay agent-only", ) serialized = AppPartial.model_validate(app_obj, from_attributes=True).model_dump(mode="json") @@ -363,6 +364,7 @@ def test_app_partial_serialization_uses_aliases(app_models): assert serialized["model_config"]["model"] == {"provider": "openai", "name": "gpt-4o"} assert serialized["workflow"]["id"] == "wf-1" assert serialized["tags"][0]["name"] == "Utilities" + assert "role" not in serialized def test_app_detail_with_site_includes_nested_serialization(app_models): @@ -405,6 +407,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): deleted_tools=[{"type": "api", "tool_name": "search", "provider_id": "prov"}], site=site, bound_agent_id="agent-1", + role="Should stay agent-only", ) serialized = AppDetailWithSite.model_validate(app_obj, from_attributes=True).model_dump(mode="json") @@ -415,6 +418,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): assert serialized["site"]["icon_url"] == "signed:site-icon" assert serialized["site"]["created_at"] == int(timestamp.timestamp()) assert serialized["bound_agent_id"] == "agent-1" + assert "role" not in serialized def test_app_pagination_aliases_per_page_and_has_next(app_models): diff --git a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py index 79169cfce7e..1422afd7524 100644 --- a/api/tests/unit_tests/controllers/console/auth/test_account_activation.py +++ b/api/tests/unit_tests/controllers/console/auth/test_account_activation.py @@ -66,6 +66,20 @@ class TestActivateCheckApi: assert response["data"]["workspace_id"] == "workspace-123" assert response["data"]["email"] == "invitee@example.com" + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + def test_check_valid_invitation_token_includes_account_status(self, mock_get_invitation, app, mock_invitation): + mock_account = MagicMock() + mock_account.status = AccountStatus.ACTIVE + mock_invitation["account"] = mock_account + mock_get_invitation.return_value = mock_invitation + + with app.test_request_context("/activate/check?email=invitee@example.com&token=valid_token"): + response = ActivateCheckApi().get() + + assert response["is_valid"] is True + assert response["data"]["account_status"] == AccountStatus.ACTIVE + assert response["data"]["requires_setup"] is False + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") def test_check_invalid_invitation_token(self, mock_get_invitation, app: Flask): """ @@ -177,6 +191,11 @@ class TestActivateApi: "account": mock_account, } + @pytest.fixture(autouse=True) + def mock_switch_tenant(self): + with patch("controllers.console.auth.activate.TenantService.switch_tenant") as mock: + yield mock + @patch("controllers.console.auth.activate.RegisterService.get_invitation_if_token_valid") @patch("controllers.console.auth.activate.RegisterService.revoke_token") @patch("controllers.console.auth.activate.db") @@ -224,7 +243,40 @@ class TestActivateApi: assert mock_account.status == AccountStatus.ACTIVE assert mock_account.initialized_at is not None mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") - mock_db.session.commit.assert_called_once() + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_rejects_missing_setup_fields_before_consuming_invitation( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_switch_tenant, + ): + mock_invitation["data"]["requires_setup"] = True + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = None + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + with pytest.raises(AlreadyActivateError): + ActivateApi().post() + + mock_revoke_token.assert_not_called() + mock_create_tenant_member.assert_not_called() + mock_switch_tenant.assert_not_called() @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") def test_activation_with_invalid_token(self, mock_get_invitation, app: Flask): @@ -504,3 +556,75 @@ class TestActivateApi: assert response["result"] == "success" mock_get_invitation.assert_called_once_with("workspace-123", "Invitee@Example.com", "valid_token") mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_for_existing_active_account_creates_membership_on_acceptance( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_account, + mock_switch_tenant, + ): + mock_account.status = AccountStatus.ACTIVE + mock_invitation["data"]["role"] = "admin" + mock_invitation["data"]["requires_setup"] = False + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = None + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + response = ActivateApi().post() + + assert response["result"] == "success" + mock_create_tenant_member.assert_called_once_with(mock_invitation["tenant"], mock_account, "admin") + mock_switch_tenant.assert_called_once_with(mock_account, mock_invitation["tenant"].id) + mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") + + @patch("controllers.console.auth.activate.TenantService.create_tenant_member") + @patch("controllers.console.auth.activate.RegisterService.get_invitation_with_case_fallback") + @patch("controllers.console.auth.activate.RegisterService.revoke_token") + @patch("controllers.console.auth.activate.db") + def test_activation_legacy_active_member_invitation_does_not_require_setup( + self, + mock_db, + mock_revoke_token, + mock_get_invitation, + mock_create_tenant_member, + app: Flask, + mock_invitation, + mock_account, + mock_switch_tenant, + ): + mock_account.status = AccountStatus.ACTIVE + mock_get_invitation.return_value = mock_invitation + mock_db.session.scalar.return_value = "membership-id" + + with app.test_request_context( + "/activate", + method="POST", + json={ + "workspace_id": "workspace-123", + "email": "invitee@example.com", + "token": "valid_token", + }, + ): + response = ActivateApi().post() + + assert response["result"] == "success" + mock_create_tenant_member.assert_not_called() + mock_switch_tenant.assert_called_once_with(mock_account, mock_invitation["tenant"].id) + mock_revoke_token.assert_called_once_with("workspace-123", "invitee@example.com", "valid_token") diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py index dc3dd00a6c0..84a70835437 100644 --- a/api/tests/unit_tests/controllers/console/tag/test_tags.py +++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py @@ -2,15 +2,8 @@ from types import SimpleNamespace from unittest.mock import MagicMock, PropertyMock, patch import pytest -from sqlalchemy.orm import Session - - -class SessionMatcher: - def __eq__(self, other): - return isinstance(other, Session) - - from flask import Flask +from sqlalchemy.orm import Session, scoped_session from werkzeug.exceptions import Forbidden import controllers.console.tag.tags as module @@ -27,6 +20,11 @@ from models.enums import TagType from services.tag_service import UpdateTagPayload +class SessionMatcher: + def __eq__(self, other): + return isinstance(other, Session | scoped_session) + + def unwrap(func): """ Recursively unwrap decorated functions. @@ -193,9 +191,10 @@ class TestTagUpdateDeleteApi: result, status = method(api, admin_user, "tag-1") assert status == 200 - update_payload, tag_id = update_tags_mock.call_args.args + update_payload, tag_id, session = update_tags_mock.call_args.args assert update_payload == UpdateTagPayload(name="updated") assert tag_id == "tag-1" + assert session == module.db.session assert result["binding_count"] == "3" def test_patch_forbidden(self, app: Flask, readonly_user, payload_patch): @@ -221,7 +220,7 @@ class TestTagUpdateDeleteApi: ): result, status = method(api, "tag-1") - delete_mock.assert_called_once_with("tag-1") + delete_mock.assert_called_once_with("tag-1", module.db.session) assert status == 204 diff --git a/api/tests/unit_tests/controllers/console/workspace/test_members.py b/api/tests/unit_tests/controllers/console/workspace/test_members.py index 494cbbf0c37..6c7879c8050 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_members.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_members.py @@ -190,7 +190,9 @@ class TestMemberInviteEmailApi: ): result, status = method(api, user) - assert result["invitation_results"][0]["status"] == "success" + assert status == 201 + assert result["invitation_results"][0]["status"] == "already_member" + assert result["invitation_results"][0]["message"] == "Account already in workspace." def test_invite_invalid_role(self, app: Flask): api = MemberInviteEmailApi() diff --git a/api/tests/unit_tests/controllers/console/workspace/test_snippets.py b/api/tests/unit_tests/controllers/console/workspace/test_snippets.py index b8914fc26cb..a276a181d62 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_snippets.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_snippets.py @@ -1,6 +1,6 @@ from inspect import unwrap from types import SimpleNamespace -from unittest.mock import Mock +from unittest.mock import ANY, Mock import pytest from werkzeug.exceptions import NotFound @@ -94,6 +94,7 @@ def test_list_snippets_returns_pagination(app, monkeypatch): } get_snippets.assert_called_once_with( tenant_id="tenant-1", + session=ANY, page=2, limit=10, keyword=None, diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py index 898eb2e86b7..ebdc300a8c6 100644 --- a/api/tests/unit_tests/controllers/test_swagger.py +++ b/api/tests/unit_tests/controllers/test_swagger.py @@ -57,6 +57,36 @@ def _multipart_form_schema(operation: dict[str, object]) -> dict[str, object]: return schema +def _json_body_schema(payload: dict[str, object], operation: dict[str, object]) -> dict[str, object]: + request_body = operation.get("requestBody") + assert isinstance(request_body, dict) + content = request_body.get("content") + assert isinstance(content, dict) + json_media = content.get("application/json") + assert isinstance(json_media, dict) + schema = json_media.get("schema") + assert isinstance(schema, dict) + + ref = schema.get("$ref") + if isinstance(ref, str): + schema_name = ref.removeprefix("#/components/schemas/") + resolved = payload["components"]["schemas"][schema_name] + assert isinstance(resolved, dict) + return resolved + + return schema + + +def _response_content_types(operation: dict[str, object], status_code: str = "200") -> set[str]: + responses = operation.get("responses") + assert isinstance(responses, dict) + response = responses.get(status_code) + assert isinstance(response, dict) + content = response.get("content") + assert isinstance(content, dict) + return set(content) + + @pytest.mark.parametrize( ("first_kwargs", "second_kwargs"), [ @@ -79,6 +109,25 @@ def test_inline_model_name_includes_list_constraints( assert _inline_model_name(first_inline_model) != _inline_model_name(second_inline_model) +def test_uuid_path_format_is_derived_from_route_converter(): + from flask_restx import swagger as restx_swagger + + from libs.flask_restx_compat import install_swagger_compatibility + + app = Flask(__name__) + with app.app_context(): + install_swagger_compatibility() + params = restx_swagger.extract_path_params("/resources/") + + assert params["custom_resource_uuid"] == { + "format": "uuid", + "in": "path", + "name": "custom_resource_uuid", + "required": True, + "type": "string", + } + + def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch): from configs import dify_config from controllers.console import bp as console_bp @@ -132,7 +181,10 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch: create_properties = create_schema["properties"] assert isinstance(create_properties, dict) assert create_properties["file"] == {"type": "string", "format": "binary"} - assert create_properties["data"] == {"type": "string"} + assert create_properties["data"] == { + "description": "Optional JSON string with document creation settings.", + "type": "string", + } assert create_schema["required"] == ["file"] assert create_operation["requestBody"]["required"] is True @@ -146,11 +198,39 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch: update_properties = update_schema["properties"] assert isinstance(update_properties, dict) assert update_properties["file"] == {"type": "string", "format": "binary"} - assert update_properties["data"] == {"type": "string"} + assert update_properties["data"] == { + "description": "Optional JSON string with document update settings.", + "type": "string", + } assert "required" not in update_schema assert update_operation["requestBody"]["required"] is False +def test_service_openapi_merges_public_api_reference_descriptions(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + + chat_operation = payload["paths"]["/chat-messages"]["post"] + assert chat_operation["summary"] == "Send Chat Message" + assert chat_operation["description"] == "Send a request to the chat application." + assert chat_operation["tags"] == ["Chats", "Chatflows"] + assert chat_operation["responses"]["200"]["description"].startswith("Successful response.") + + rename_operation = payload["paths"]["/conversations/{c_id}/name"]["post"] + assert rename_operation["summary"] == "Rename Conversation" + assert rename_operation["tags"] == ["Conversations"] + assert _parameters_by_name(rename_operation)["c_id"]["description"] == "Conversation ID" + + def test_service_document_list_documents_query_params_render(monkeypatch: pytest.MonkeyPatch): from configs import dify_config from controllers.service_api import bp as service_api_bp @@ -170,6 +250,272 @@ def test_service_document_list_documents_query_params_render(monkeypatch: pytest assert params[name]["in"] == "query" +def test_service_openapi_documents_decorator_user_contracts(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + required_json_user_operations = ( + ("/completion-messages", "post"), + ("/completion-messages/{task_id}/stop", "post"), + ("/chat-messages", "post"), + ("/chat-messages/{task_id}/stop", "post"), + ("/messages/{message_id}/feedbacks", "post"), + ("/form/human_input/{form_token}", "post"), + ("/workflows/run", "post"), + ("/workflows/{workflow_id}/run", "post"), + ("/workflows/tasks/{task_id}/stop", "post"), + ) + for path, method in required_json_user_operations: + schema = _json_body_schema(payload, paths[path][method]) + assert schema["properties"]["user"] == {"description": "End user identifier", "type": "string"} + assert "user" in schema["required"] + + optional_json_user_operations = ( + ("/text-to-audio", "post"), + ("/conversations/{c_id}", "delete"), + ("/conversations/{c_id}/name", "post"), + ("/conversations/{c_id}/variables/{variable_id}", "put"), + ) + for path, method in optional_json_user_operations: + schema = _json_body_schema(payload, paths[path][method]) + assert schema["properties"]["user"] == {"description": "End user identifier", "type": "string"} + assert "user" not in schema.get("required", []) + + messages_params = _parameters_by_name(paths["/messages"]["get"]) + assert messages_params["user"]["in"] == "query" + assert messages_params["user"]["required"] is False + + events_params = _parameters_by_name(paths["/workflow/{task_id}/events"]["get"]) + assert events_params["user"]["in"] == "query" + assert events_params["user"]["required"] is True + + +def test_service_openapi_documents_app_multipart_contracts(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + for path in ("/files/upload", "/audio-to-text"): + schema = _multipart_form_schema(paths[path]["post"]) + assert schema["properties"]["file"] == {"format": "binary", "type": "string"} + assert schema["properties"]["user"] == {"description": "End user identifier", "type": "string"} + assert schema["required"] == ["file"] + + pipeline_schema = _multipart_form_schema(paths["/datasets/pipeline/file-upload"]["post"]) + assert pipeline_schema["properties"]["file"] == {"format": "binary", "type": "string"} + assert pipeline_schema["required"] == ["file"] + + +def test_service_openapi_documents_non_json_response_media_types(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + assert _response_content_types(paths["/chat-messages"]["post"]) == { + "application/json", + "text/event-stream", + } + assert _response_content_types(paths["/workflow/{task_id}/events"]["get"]) == {"text/event-stream"} + assert _response_content_types(paths["/text-to-audio"]["post"]) == {"audio/mpeg"} + assert _response_content_types(paths["/files/{file_id}/preview"]["get"]) == { + "application/octet-stream", + "application/pdf", + "audio/aac", + "audio/flac", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/wav", + "audio/x-m4a", + "image/gif", + "image/jpeg", + "image/png", + "image/webp", + "text/plain", + "video/mp4", + "video/quicktime", + "video/webm", + } + assert _response_content_types(paths["/datasets/{dataset_id}/documents/download-zip"]["post"]) == { + "application/zip" + } + + +def test_service_openapi_documents_uuid_params_and_deprecated_routes(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + dataset_params = _parameters_by_name(paths["/datasets/{dataset_id}"]["get"]) + assert dataset_params["dataset_id"]["schema"] == { + "description": "Dataset ID", + "format": "uuid", + "type": "string", + } + + conversation_params = _parameters_by_name(paths["/conversations/{c_id}"]["delete"]) + assert conversation_params["c_id"]["schema"] == { + "description": "Conversation ID", + "format": "uuid", + "type": "string", + } + + assert paths["/datasets/{dataset_id}/document/create_by_file"]["post"]["deprecated"] is True + assert paths["/datasets/{dataset_id}/documents/{document_id}/update_by_text"]["post"]["deprecated"] is True + + +def test_service_openapi_documents_path_action_enums(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + annotation_params = _parameters_by_name(paths["/apps/annotation-reply/{action}"]["post"]) + assert annotation_params["action"]["schema"]["enum"] == ["enable", "disable"] + + document_status_params = _parameters_by_name(paths["/datasets/{dataset_id}/documents/status/{action}"]["patch"]) + assert document_status_params["action"]["schema"]["enum"] == ["enable", "disable", "archive", "un_archive"] + + metadata_params = _parameters_by_name(paths["/datasets/{dataset_id}/metadata/built-in/{action}"]["post"]) + assert metadata_params["action"]["schema"]["enum"] == ["enable", "disable"] + + +def test_service_openapi_documents_conditional_payload_schemas(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + rename_schema = _json_body_schema(payload, paths["/conversations/{c_id}/name"]["post"]) + auto_generate_branch, manual_name_branch = rename_schema["anyOf"] + assert auto_generate_branch["properties"]["auto_generate"]["enum"] == [True] + assert auto_generate_branch["required"] == ["auto_generate"] + assert manual_name_branch["properties"]["auto_generate"]["enum"] == [False] + assert manual_name_branch["properties"]["name"]["pattern"] == r".*\S.*" + assert manual_name_branch["required"] == ["name"] + for branch in rename_schema["anyOf"]: + assert branch["properties"]["user"] == {"description": "End user identifier", "type": "string"} + + document_update_schema = payload["components"]["schemas"]["DocumentTextUpdate"] + with_text_branch, without_text_branch = document_update_schema["anyOf"] + assert with_text_branch["properties"]["text"]["type"] == "string" + assert with_text_branch["properties"]["name"]["type"] == "string" + assert with_text_branch["required"] == ["name", "text"] + assert without_text_branch["properties"]["text"]["type"] == "null" + + +def test_service_openapi_does_not_encode_docs_coverage_boundaries(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + paths = payload["paths"] + + for path_item in paths.values(): + assert isinstance(path_item, dict) + for method in ("delete", "get", "patch", "post", "put"): + operation = path_item.get(method) + if not isinstance(operation, dict): + continue + assert "x-dify-api-reference-visibility" not in operation + assert "x-dify-api-lifecycle" not in operation + + assert paths["/datasets/{dataset_id}/document/create_by_text"]["post"]["deprecated"] is True + assert paths["/datasets/{dataset_id}/document/create_by_file"]["post"]["deprecated"] is True + assert paths["/datasets/{dataset_id}/documents/{document_id}/update-by-file"]["post"]["deprecated"] is True + + +def test_service_openapi_documents_auth_and_compatibility_payloads(monkeypatch: pytest.MonkeyPatch): + from configs import dify_config + from controllers.service_api import bp as service_api_bp + + monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True) + + app = Flask(__name__) + app.config["TESTING"] = True + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + app.register_blueprint(service_api_bp) + + payload = app.test_client().get("/v1/openapi.json").get_json() + + assert payload["components"]["securitySchemes"]["Bearer"] == { + "bearerFormat": "API_KEY", + "description": "Use the Service API key as a Bearer token in the Authorization header.", + "scheme": "bearer", + "type": "http", + } + + tag_unbinding_schema = payload["components"]["schemas"]["TagUnbindingPayload"] + assert tag_unbinding_schema["description"] == ( + "Accepts either the legacy tag_id payload or the normalized tag_ids payload." + ) + tag_id_schema, tag_ids_schema = tag_unbinding_schema["anyOf"] + assert tag_id_schema["properties"]["tag_id"]["description"] == ("Legacy single tag ID accepted by the Service API.") + assert tag_id_schema["required"] == ["tag_id", "target_id"] + assert tag_ids_schema["properties"]["tag_ids"]["minItems"] == 1 + assert tag_ids_schema["required"] == ["tag_ids", "target_id"] + + def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest.MonkeyPatch): from configs import dify_config from controllers.console import bp as console_bp diff --git a/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py b/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py index e696d4aaa0b..13f11d7a1c3 100644 --- a/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/agent_app/test_app_runner.py @@ -4,6 +4,8 @@ saved, using the deterministic fake backend client (no live stack).""" from __future__ import annotations +from collections.abc import Iterator +from datetime import UTC, datetime from types import SimpleNamespace from typing import Any, override from unittest.mock import MagicMock @@ -11,7 +13,17 @@ from unittest.mock import MagicMock import pytest from agenton.compositor import CompositorSessionSnapshot from dify_agent.layers.ask_human import AskHumanToolResult -from dify_agent.protocol import CancelRunRequest, CancelRunResponse, RuntimeLayerSpec +from dify_agent.protocol import ( + CancelRunRequest, + CancelRunResponse, + PydanticAIStreamRunEvent, + RunEvent, + RunStartedEvent, + RunSucceededEvent, + RunSucceededEventData, + RuntimeLayerSpec, +) +from pydantic_ai.messages import PartDeltaEvent, PartStartEvent, TextPart, TextPartDelta from clients.agent_backend import ( AgentBackendError, @@ -67,6 +79,58 @@ class _RecordingFakeAgentBackendRunClient(FakeAgentBackendRunClient): return super().cancel_run(run_id, request=request) +class _StreamingFakeAgentBackendRunClient(FakeAgentBackendRunClient): + @override + def stream_events(self, run_id: str, *, after: str | None = None) -> Iterator[RunEvent]: + del after + created_at = datetime(2026, 1, 1, tzinfo=UTC) + yield RunStartedEvent(id="1-0", run_id=run_id, created_at=created_at) + yield PydanticAIStreamRunEvent( + id="2-0", + run_id=run_id, + created_at=created_at, + data=PartDeltaEvent(index=0, delta=TextPartDelta(content_delta="hello ")), + ) + yield PydanticAIStreamRunEvent( + id="3-0", + run_id=run_id, + created_at=created_at, + data=PartDeltaEvent(index=0, delta=TextPartDelta(content_delta="agent")), + ) + yield RunSucceededEvent( + id="4-0", + run_id=run_id, + created_at=created_at, + data=RunSucceededEventData( + output={"text": "hello agent"}, + session_snapshot=CompositorSessionSnapshot(layers=[]), + ), + ) + + +class _StreamingPartStartFakeAgentBackendRunClient(FakeAgentBackendRunClient): + @override + def stream_events(self, run_id: str, *, after: str | None = None) -> Iterator[RunEvent]: + del after + created_at = datetime(2026, 1, 1, tzinfo=UTC) + yield RunStartedEvent(id="1-0", run_id=run_id, created_at=created_at) + yield PydanticAIStreamRunEvent( + id="2-0", + run_id=run_id, + created_at=created_at, + data=PartStartEvent(index=0, part=TextPart(content="hello")), + ) + yield RunSucceededEvent( + id="3-0", + run_id=run_id, + created_at=created_at, + data=RunSucceededEventData( + output={"text": "hello agent"}, + session_snapshot=CompositorSessionSnapshot(layers=[]), + ), + ) + + class _FakeSessionStore: def __init__( self, @@ -165,9 +229,13 @@ def _message_end(qm: _FakeQueueManager) -> QueueMessageEndEvent: def _saved_user_query(qm: _FakeQueueManager) -> str: - prompt_messages = _message_end(qm).llm_result.prompt_messages + llm_result = _message_end(qm).llm_result + assert llm_result is not None + prompt_messages = llm_result.prompt_messages assert len(prompt_messages) == 1 - return prompt_messages[0].content + content = prompt_messages[0].content + assert isinstance(content, str) + return content def test_successful_turn_publishes_chunk_and_message_end_and_saves_session(): @@ -204,6 +272,35 @@ def test_successful_turn_publishes_chunk_and_message_end_and_saves_session(): ] +def test_successful_turn_forwards_agent_backend_stream_text_deltas_without_duplicate_terminal_chunk(): + client = _StreamingFakeAgentBackendRunClient() + store = _FakeSessionStore() + qm = _FakeQueueManager() + + _run(_runner(client, store), qm) + + chunk_events = [e for e in qm.events if isinstance(e, QueueLLMChunkEvent)] + end_events = [e for e in qm.events if isinstance(e, QueueMessageEndEvent)] + assert [event.chunk.delta.message.content for event in chunk_events] == ["hello ", "agent"] + assert len(end_events) == 1 + assert end_events[0].llm_result.message.content == "hello agent" + assert store.saved + + +def test_successful_turn_forwards_part_start_text_and_publishes_missing_terminal_suffix(): + client = _StreamingPartStartFakeAgentBackendRunClient() + store = _FakeSessionStore() + qm = _FakeQueueManager() + + _run(_runner(client, store), qm) + + chunk_events = [e for e in qm.events if isinstance(e, QueueLLMChunkEvent)] + end_events = [e for e in qm.events if isinstance(e, QueueMessageEndEvent)] + assert [event.chunk.delta.message.content for event in chunk_events] == ["hello", " agent"] + assert len(end_events) == 1 + assert end_events[0].llm_result.message.content == "hello agent" + + def test_prior_session_snapshot_is_threaded_into_request(): prior = CompositorSessionSnapshot(layers=[]) client = FakeAgentBackendRunClient() diff --git a/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py b/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py index bea808516d7..613982f9c03 100644 --- a/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py +++ b/api/tests/unit_tests/core/plugin/impl/test_base_client_impl.py @@ -7,6 +7,7 @@ from pytest_mock import MockerFixture from core.plugin.endpoint.exc import EndpointSetupFailedError from core.plugin.entities.plugin_daemon import PluginDaemonInnerError from core.plugin.impl.base import PLUGIN_DAEMON_MAX_PATH_LENGTH, BasePluginClient +from core.plugin.impl.exc import PluginLLMPollingUnsupportedError from core.trigger.errors import ( EventIgnoreError, TriggerInvokeError, @@ -167,3 +168,10 @@ class TestBasePluginClientImpl: with pytest.raises(expected): client._handle_plugin_daemon_error("PluginInvokeError", message) + + def test_handle_plugin_daemon_error_maps_unsupported_polling_to_typed_exception(self): + client = BasePluginClient() + message = json.dumps({"error_type": PluginLLMPollingUnsupportedError.__name__, "message": "m"}) + + with pytest.raises(PluginLLMPollingUnsupportedError): + client._handle_plugin_daemon_error("PluginInvokeError", message) diff --git a/api/tests/unit_tests/core/plugin/impl/test_model_client.py b/api/tests/unit_tests/core/plugin/impl/test_model_client.py index 6dc572310c3..ac3df1e56fc 100644 --- a/api/tests/unit_tests/core/plugin/impl/test_model_client.py +++ b/api/tests/unit_tests/core/plugin/impl/test_model_client.py @@ -1,13 +1,17 @@ from __future__ import annotations import io +import json from types import SimpleNamespace import pytest from pytest_mock import MockerFixture from core.plugin.entities.plugin_daemon import PluginDaemonInnerError +from core.plugin.impl.exc import PluginInvokeError, PluginLLMPollingUnsupportedError from core.plugin.impl.model import PluginModelClient +from graphon.model_runtime.entities.llm_entities import LLMPollingResult, LLMPollingStatus, LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import AssistantPromptMessage class TestPluginModelClient: @@ -183,6 +187,113 @@ class TestPluginModelClient: ) ) + def test_start_llm_polling(self, mocker: MockerFixture): + client = PluginModelClient() + polling_result = LLMPollingResult( + status=LLMPollingStatus.RUNNING, + plugin_state={"task_id": "poll-1"}, + next_check_after_seconds=3, + ) + request_mock = mocker.patch.object( + client, + "_request_with_plugin_daemon_response", + return_value=polling_result, + ) + + result = client.start_llm_polling( + tenant_id="tenant-1", + user_id="user-1", + plugin_id="org/plugin:1", + provider="provider-a", + model="gpt-test", + credentials={"api_key": "key"}, + prompt_messages=[], + model_parameters={"temperature": 0.1}, + tools=[], + stop=["STOP"], + json_schema={"type": "object"}, + ) + + assert result == polling_result + call_kwargs = request_mock.call_args.kwargs + assert call_kwargs["path"] == "plugin/tenant-1/dispatch/model/polling/start" + assert call_kwargs["data"]["data"] == { + "provider": "provider-a", + "model_type": "llm", + "model": "gpt-test", + "credentials": {"api_key": "key"}, + "prompt_messages": [], + "model_parameters": {"temperature": 0.1}, + "tools": [], + "stop": ["STOP"], + "stream": False, + "json_schema": {"type": "object"}, + } + + def test_check_llm_polling(self, mocker: MockerFixture): + client = PluginModelClient() + polling_result = LLMPollingResult( + status=LLMPollingStatus.SUCCEEDED, + result=LLMResult( + model="gpt-test", + prompt_messages=[], + message=AssistantPromptMessage(content="done"), + usage=LLMUsage.empty_usage(), + ), + ) + request_mock = mocker.patch.object( + client, + "_request_with_plugin_daemon_response", + return_value=polling_result, + ) + + result = client.check_llm_polling( + tenant_id="tenant-1", + user_id="user-1", + plugin_id="org/plugin:1", + provider="provider-a", + model="gpt-test", + credentials={"api_key": "key"}, + plugin_state={"task_id": "poll-1"}, + ) + + assert result == polling_result + call_kwargs = request_mock.call_args.kwargs + assert call_kwargs["path"] == "plugin/tenant-1/dispatch/model/polling/check" + assert call_kwargs["data"]["data"] == { + "provider": "provider-a", + "model_type": "llm", + "model": "gpt-test", + "credentials": {"api_key": "key"}, + "plugin_state": {"task_id": "poll-1"}, + } + + def test_start_llm_polling_maps_unsupported_polling_invoke_error(self, mocker: MockerFixture): + client = PluginModelClient() + mocker.patch.object( + client, + "_request_with_plugin_daemon_response", + side_effect=PluginInvokeError( + json.dumps( + { + "error_type": PluginLLMPollingUnsupportedError.__name__, + "message": "Model `gpt-test` does not support polling.", + } + ) + ), + ) + + with pytest.raises(PluginLLMPollingUnsupportedError): + client.start_llm_polling( + tenant_id="tenant-1", + user_id="user-1", + plugin_id="org/plugin:1", + provider="provider-a", + model="gpt-test", + credentials={"api_key": "key"}, + prompt_messages=[], + ) + def test_get_llm_num_tokens(self, mocker: MockerFixture): client = PluginModelClient() mocker.patch.object( diff --git a/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py b/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py index 3fd885b28fb..17973916779 100644 --- a/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py +++ b/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py @@ -14,7 +14,13 @@ from core.plugin.impl.model_runtime import TENANT_SCOPE_SCHEMA_CACHE_USER_ID, Pl from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime from core.plugin.plugin_service import PluginService from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.llm_entities import LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.llm_entities import ( + LLMPollingResult, + LLMPollingStatus, + LLMResultChunk, + LLMResultChunkDelta, + LLMUsage, +) from graphon.model_runtime.entities.message_entities import AssistantPromptMessage from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity @@ -282,6 +288,74 @@ class TestPluginModelRuntime: stream=True, ) + def test_start_llm_polling_resolves_plugin_fields(self) -> None: + client = Mock(spec=PluginModelClient) + polling_result = LLMPollingResult( + status=LLMPollingStatus.RUNNING, + plugin_state={"task_id": "poll-1"}, + next_check_after_seconds=2, + ) + client.start_llm_polling.return_value = polling_result + runtime = PluginModelRuntime(tenant_id="tenant", user_id="user", client=client, plugin_service=PluginService) + + result = runtime.start_llm_polling( + provider="langgenius/openai/openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + model_parameters={"temperature": 0.2}, + prompt_messages=[], + tools=None, + stop=("END",), + json_schema={"type": "object"}, + ) + + assert result == polling_result + client.start_llm_polling.assert_called_once_with( + tenant_id="tenant", + user_id="user", + plugin_id="langgenius/openai", + provider="openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + prompt_messages=[], + model_parameters={"temperature": 0.2}, + tools=None, + stop=["END"], + json_schema={"type": "object"}, + ) + + def test_check_llm_polling_resolves_plugin_fields(self) -> None: + client = Mock(spec=PluginModelClient) + polling_result = LLMPollingResult( + status=LLMPollingStatus.SUCCEEDED, + result=model_runtime_module.LLMResult( + model="gpt-4o-mini", + prompt_messages=[], + message=AssistantPromptMessage(content="done"), + usage=LLMUsage.empty_usage(), + ), + ) + client.check_llm_polling.return_value = polling_result + runtime = PluginModelRuntime(tenant_id="tenant", user_id="user", client=client, plugin_service=PluginService) + + result = runtime.check_llm_polling( + provider="langgenius/openai/openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + plugin_state={"task_id": "poll-1"}, + ) + + assert result == polling_result + client.check_llm_polling.assert_called_once_with( + tenant_id="tenant", + user_id="user", + plugin_id="langgenius/openai", + provider="openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + plugin_state={"task_id": "poll-1"}, + ) + def test_invoke_llm_rejects_per_call_user_override(self) -> None: client = Mock(spec=PluginModelClient) client.invoke_llm.return_value = sentinel.result diff --git a/api/tests/unit_tests/core/schemas/test_resolver.py b/api/tests/unit_tests/core/schemas/test_resolver.py index ba6fa0d5365..d53b01364eb 100644 --- a/api/tests/unit_tests/core/schemas/test_resolver.py +++ b/api/tests/unit_tests/core/schemas/test_resolver.py @@ -703,9 +703,12 @@ class TestSchemaResolverClass: # For schemas without refs, hybrid should be competitive or better if not expected: # No refs case - # Hybrid might be slightly slower due to JSON serialization overhead, - # but should not be dramatically worse - assert avg_hybrid < avg_recursive * 5 # At most 5x slower + relative_slowdown_limit = 5.0 + absolute_noise_budget_seconds = 2e-4 + + # JSON serialization has a fixed overhead that dominates tiny schemas, + # so allow a small absolute noise budget on top of the relative limit. + assert avg_hybrid < (avg_recursive * relative_slowdown_limit) + absolute_noise_budget_seconds def test_string_matching_edge_cases(self): """Test edge cases for string-based detection""" diff --git a/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py b/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py index 3f6b1ec1545..a99fd1f248f 100644 --- a/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py +++ b/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py @@ -4,6 +4,7 @@ import calendar import math from datetime import date from types import SimpleNamespace +from zoneinfo import ZoneInfo import pytest @@ -66,6 +67,20 @@ def test_localtime_to_timestamp_tool(): ts_value = float(ts_message.strip()) assert math.isfinite(ts_value) assert ts_value >= 0 + assert ( + LocaltimeToTimestampTool.localtime_to_timestamp( + "2024-01-01 10:00:00", + "%Y-%m-%d %H:%M:%S", + ZoneInfo("UTC"), + ) + == 1704103200 + ) + with pytest.raises(ToolInvokeError, match="local_tz must be"): + LocaltimeToTimestampTool.localtime_to_timestamp( + "2024-01-01 10:00:00", + "%Y-%m-%d %H:%M:%S", + object(), # type: ignore[arg-type] + ) with pytest.raises(ToolInvokeError): LocaltimeToTimestampTool.localtime_to_timestamp("bad", "%Y-%m-%d %H:%M:%S", "UTC") diff --git a/api/tests/unit_tests/core/tools/utils/test_tool_engine_serialization.py b/api/tests/unit_tests/core/tools/utils/test_tool_engine_serialization.py index 4029edfb686..da699ef6101 100644 --- a/api/tests/unit_tests/core/tools/utils/test_tool_engine_serialization.py +++ b/api/tests/unit_tests/core/tools/utils/test_tool_engine_serialization.py @@ -459,23 +459,24 @@ class TestEndToEndSerialization: def _verify_all_complex_types_converted(self, data): """Helper method to verify all complex types were properly converted""" - if isinstance(data, dict): - for key, value in data.items(): - if key in ["id", "checksum"]: - # These should be strings (UUID/bytes converted) - assert isinstance(value, str) - elif key in ["created_at", "last_login", "timestamp", "uploaded_at"]: - # These should be strings (datetime converted) - assert isinstance(value, str) - elif key in ["total_time", "duration"]: - # These should be floats (Decimal converted) - assert isinstance(value, float) - elif key == "metrics": - # This should be a list (ndarray converted) - assert isinstance(value, list) - else: - # Recursively check nested structures - self._verify_all_complex_types_converted(value) - elif isinstance(data, list): - for item in data: - self._verify_all_complex_types_converted(item) + match data: + case dict(): + for key, value in data.items(): + if key in ["id", "checksum"]: + # These should be strings (UUID/bytes converted) + assert isinstance(value, str) + elif key in ["created_at", "last_login", "timestamp", "uploaded_at"]: + # These should be strings (datetime converted) + assert isinstance(value, str) + elif key in ["total_time", "duration"]: + # These should be floats (Decimal converted) + assert isinstance(value, float) + elif key == "metrics": + # This should be a list (ndarray converted) + assert isinstance(value, list) + else: + # Recursively check nested structures + self._verify_all_complex_types_converted(value) + case list(): + for item in data: + self._verify_all_complex_types_converted(item) diff --git a/api/tests/unit_tests/core/workflow/test_node_factory.py b/api/tests/unit_tests/core/workflow/test_node_factory.py index bd18402c583..0baee47665c 100644 --- a/api/tests/unit_tests/core/workflow/test_node_factory.py +++ b/api/tests/unit_tests/core/workflow/test_node_factory.py @@ -1,18 +1,26 @@ from collections.abc import Mapping from types import SimpleNamespace -from unittest.mock import MagicMock, patch, sentinel +from unittest.mock import MagicMock, Mock, patch, sentinel import pytest from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext, InvokeFrom, UserFrom +from core.plugin.impl.model import PluginModelClient +from core.plugin.impl.model_runtime import PluginModelRuntime +from core.plugin.plugin_service import PluginService from core.workflow import node_factory from core.workflow import template_rendering as workflow_template_rendering +from core.workflow.node_runtime import DifyPreparedLLM from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelFeature, ModelType +from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel from graphon.nodes.code.entities import CodeLanguage from graphon.nodes.llm.entities import LLMNodeData from graphon.nodes.llm.node import LLMNode +from graphon.nodes.llm.runtime_protocols import LLMPollingCapableProtocol from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData from graphon.variables.segments import ArrayObjectSegment, StringSegment @@ -35,6 +43,41 @@ def _node_constructor(*, return_value): return constructor +def _build_llm_model_schema(*, features: list[ModelFeature] | None = None) -> AIModelEntity: + return AIModelEntity( + model="model", + label=I18nObject(en_US="Model"), + model_type=ModelType.LLM, + fetch_from=FetchFrom.PREDEFINED_MODEL, + model_properties={}, + features=features, + ) + + +class _ModelTypeInstanceStub(LargeLanguageModel): + def __init__(self, *, model_runtime: object) -> None: + self.model_runtime = model_runtime + + +class _ModelInstanceStub: + def __init__( + self, + *, + model_runtime: object, + model_schema: AIModelEntity, + ) -> None: + self.provider = "langgenius/openai/openai" + self.model_name = "model" + self.credentials = {"api_key": "secret"} + self.parameters = {} + self.stop = () + self.model_type_instance = _ModelTypeInstanceStub(model_runtime=model_runtime) + self._model_schema = model_schema + + def get_model_schema(self) -> AIModelEntity: + return self._model_schema + + class TestResolveWorkflowNodeClass: def test_matching_version_uses_registry_mapping(self, monkeypatch) -> None: document_extractor_class = sentinel.document_extractor_class @@ -667,7 +710,7 @@ class TestDifyNodeFactoryCreateNode: memory = sentinel.memory factory._build_model_instance_for_llm_node = MagicMock(return_value=sentinel.model_instance) factory._build_memory_for_llm_node = MagicMock(return_value=memory) - with patch.object(node_factory, "DifyPreparedLLM", return_value=wrapped_model_instance) as prepared_llm: + with patch.object(factory, "_wrap_model_instance_for_node", return_value=wrapped_model_instance) as wrap_model: kwargs = factory._build_llm_compatible_node_init_kwargs( node_class=sentinel.node_class, node_data=node_data, @@ -686,9 +729,70 @@ class TestDifyNodeFactoryCreateNode: node_data=node_data, model_instance=sentinel.model_instance, ) - prepared_llm.assert_called_once_with(sentinel.model_instance) + wrap_model.assert_called_once_with( + node_data=node_data, + model_instance=sentinel.model_instance, + ) assert kwargs["model_instance"] is wrapped_model_instance + def test_build_llm_compatible_node_init_kwargs_uses_polling_wrapper_for_polling_llm_node(self, factory): + node_data = LLMNodeData.model_validate( + { + "type": BuiltinNodeTypes.LLM, + "title": "LLM", + "model": {"provider": "provider", "name": "model", "mode": "chat", "completion_params": {}}, + "prompt_template": [{"role": "system", "text": "x"}], + "context": {"enabled": False, "variable_selector": []}, + "vision": {"enabled": False}, + } + ) + plugin_runtime = PluginModelRuntime( + tenant_id="tenant-id", + user_id="user-id", + client=Mock(spec=PluginModelClient), + plugin_service=PluginService, + ) + model_instance = _ModelInstanceStub( + model_runtime=plugin_runtime, + model_schema=_build_llm_model_schema(features=[ModelFeature.POLLING]), + ) + factory._build_model_instance_for_llm_node = MagicMock(return_value=model_instance) + factory._build_memory_for_llm_node = MagicMock(return_value=sentinel.memory) + + kwargs = factory._build_llm_compatible_node_init_kwargs( + node_class=sentinel.node_class, + node_data=node_data, + wrap_model_instance=True, + include_http_client=False, + include_llm_file_saver=False, + include_prompt_message_serializer=False, + include_retriever_attachment_loader=False, + include_jinja2_template_renderer=False, + ) + + assert isinstance(kwargs["model_instance"], LLMPollingCapableProtocol) + + @pytest.mark.parametrize("node_type", [BuiltinNodeTypes.QUESTION_CLASSIFIER, BuiltinNodeTypes.PARAMETER_EXTRACTOR]) + def test_wrap_model_instance_keeps_non_llm_graph_nodes_on_plain_wrapper(self, node_type): + plugin_runtime = PluginModelRuntime( + tenant_id="tenant-id", + user_id="user-id", + client=Mock(spec=PluginModelClient), + plugin_service=PluginService, + ) + model_instance = _ModelInstanceStub( + model_runtime=plugin_runtime, + model_schema=_build_llm_model_schema(features=[ModelFeature.POLLING]), + ) + + wrapped = node_factory.DifyNodeFactory._wrap_model_instance_for_node( + node_data=SimpleNamespace(type=node_type), + model_instance=model_instance, + ) + + assert type(wrapped) is DifyPreparedLLM + assert not isinstance(wrapped, LLMPollingCapableProtocol) + def test_create_node_passes_alias_preserving_llm_data_to_constructor(self, monkeypatch, factory): created_node = object() constructor = _node_constructor(return_value=created_node) diff --git a/api/tests/unit_tests/core/workflow/test_node_runtime.py b/api/tests/unit_tests/core/workflow/test_node_runtime.py index bdccea478d9..b32b9f74df7 100644 --- a/api/tests/unit_tests/core/workflow/test_node_runtime.py +++ b/api/tests/unit_tests/core/workflow/test_node_runtime.py @@ -7,6 +7,10 @@ import pytest from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext, InvokeFrom, UserFrom from core.app.file_access import FileAccessScope, bind_file_access_scope, grant_retriever_segment_access from core.llm_generator.output_parser.errors import OutputParserError +from core.plugin.impl.exc import PluginLLMPollingUnsupportedError +from core.plugin.impl.model import PluginModelClient +from core.plugin.impl.model_runtime import PluginModelRuntime +from core.plugin.plugin_service import PluginService from core.workflow import node_runtime from core.workflow.file_reference import parse_file_reference from core.workflow.human_input_adapter import ( @@ -21,6 +25,7 @@ from core.workflow.node_runtime import ( DifyFileReferenceFactory, DifyHumanInputNodeRuntime, DifyPreparedLLM, + DifyPreparedPollingLLM, DifyPromptMessageSerializer, DifyRetrieverAttachmentLoader, DifyToolFileManager, @@ -31,23 +36,61 @@ from core.workflow.node_runtime import ( ) from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from graphon.model_runtime.entities.llm_entities import LLMPollingResult, LLMPollingStatus +from graphon.model_runtime.entities.message_entities import AssistantPromptMessage +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelFeature, ModelType +from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel from graphon.nodes.human_input.entities import FileInputConfig, FileListInputConfig, HumanInputNodeData +from graphon.nodes.llm.runtime_protocols import LLMPollingCapableProtocol from graphon.nodes.tool.entities import ToolNodeData, ToolProviderType from graphon.variables.segments import ArrayFileSegment, FileSegment from tests.workflow_test_utils import build_test_run_context -def _build_model_schema() -> AIModelEntity: +def _build_model_schema(*, features: list[ModelFeature] | None = None) -> AIModelEntity: return AIModelEntity( model="gpt-4o-mini", label=I18nObject(en_US="GPT-4o mini"), model_type=ModelType.LLM, fetch_from=FetchFrom.PREDEFINED_MODEL, model_properties={}, + features=features, ) +class _ModelTypeInstanceStub(LargeLanguageModel): + def __init__( + self, + *, + model_schema: AIModelEntity | None, + model_runtime: object | None = None, + ) -> None: + self.model_runtime = model_runtime + self.get_model_schema = Mock(return_value=model_schema) + + +class _ModelInstanceStub: + def __init__( + self, + *, + model_schema: AIModelEntity | None, + model_runtime: object | None = None, + invoke_llm_result: object = sentinel.result, + get_llm_num_tokens_result: int = 32, + ) -> None: + self.provider = "langgenius/openai/openai" + self.model_name = "gpt-4o-mini" + self.parameters = {"temperature": 0.2} + self.stop = ("stop",) + self.credentials = {"api_key": "secret"} + self.model_type_instance = _ModelTypeInstanceStub( + model_schema=model_schema, + model_runtime=model_runtime, + ) + self.get_llm_num_tokens = Mock(return_value=get_llm_num_tokens_result) + self.invoke_llm = Mock(return_value=invoke_llm_result) + + def _build_run_context(*, invoke_from: InvokeFrom | str = InvokeFrom.DEBUGGER) -> dict[str, object]: return build_test_run_context( tenant_id="tenant-id", @@ -126,17 +169,8 @@ def test_dify_file_reference_factory_passes_tenant_id(monkeypatch: pytest.Monkey def test_dify_prepared_llm_wraps_model_instance_calls() -> None: model_schema = _build_model_schema() - model_type_instance = SimpleNamespace(get_model_schema=Mock(return_value=model_schema)) - model_instance = SimpleNamespace( - provider="langgenius/openai/openai", - model_name="gpt-4o-mini", - parameters={"temperature": 0.2}, - stop=("stop",), - credentials={"api_key": "secret"}, - model_type_instance=model_type_instance, - get_llm_num_tokens=Mock(return_value=32), - invoke_llm=Mock(return_value=sentinel.result), - ) + model_instance = _ModelInstanceStub(model_schema=model_schema) + model_type_instance = model_instance.model_type_instance prepared = DifyPreparedLLM(model_instance) assert prepared.provider == "langgenius/openai/openai" @@ -167,11 +201,8 @@ def test_dify_prepared_llm_wraps_model_instance_calls() -> None: def test_dify_prepared_llm_requires_model_schema() -> None: - model_instance = SimpleNamespace( - model_name="gpt-4o-mini", - credentials={}, - model_type_instance=SimpleNamespace(get_model_schema=Mock(return_value=None)), - ) + model_instance = _ModelInstanceStub(model_schema=None) + model_instance.credentials = {} prepared = DifyPreparedLLM(model_instance) with pytest.raises(ValueError, match="Model schema not found"): @@ -179,12 +210,7 @@ def test_dify_prepared_llm_requires_model_schema() -> None: def test_dify_prepared_llm_delegates_structured_output_helper(monkeypatch: pytest.MonkeyPatch) -> None: - model_instance = SimpleNamespace( - provider="langgenius/openai/openai", - model_name="gpt-4o-mini", - credentials={"api_key": "secret"}, - model_type_instance=SimpleNamespace(get_model_schema=Mock(return_value=_build_model_schema())), - ) + model_instance = _ModelInstanceStub(model_schema=_build_model_schema()) prepared = DifyPreparedLLM(model_instance) invoke_structured = MagicMock(return_value=sentinel.structured) monkeypatch.setattr(node_runtime, "invoke_llm_with_structured_output", invoke_structured) @@ -217,6 +243,94 @@ def test_dify_prepared_llm_identifies_structured_output_errors() -> None: assert prepared.is_structured_output_parse_error(ValueError("other")) is False +def test_dify_prepared_polling_llm_delegates_to_plugin_runtime() -> None: + polling_result = LLMPollingResult( + status=LLMPollingStatus.RUNNING, + plugin_state={"task_id": "poll-1"}, + next_check_after_seconds=2, + ) + plugin_runtime = PluginModelRuntime( + tenant_id="tenant-id", + user_id="user-id", + client=Mock(spec=PluginModelClient), + plugin_service=PluginService, + ) + plugin_runtime.start_llm_polling = Mock(return_value=polling_result) # type: ignore[method-assign] + plugin_runtime.check_llm_polling = Mock(return_value=polling_result) # type: ignore[method-assign] + model_instance = _ModelInstanceStub( + model_schema=_build_model_schema(features=[ModelFeature.POLLING]), + model_runtime=plugin_runtime, + ) + + prepared = DifyPreparedPollingLLM(model_instance) + + assert isinstance(prepared, LLMPollingCapableProtocol) + assert ( + prepared.start_llm_polling( + prompt_messages=[], + model_parameters={"temperature": 0.1}, + tools=[], + stop=("END",), + json_schema={"type": "object"}, + ) + == polling_result + ) + assert ( + prepared.check_llm_polling( + plugin_state={"task_id": "poll-1"}, + ) + == polling_result + ) + plugin_runtime.start_llm_polling.assert_called_once_with( + provider="langgenius/openai/openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + prompt_messages=[], + model_parameters={"temperature": 0.1}, + tools=[], + stop=("END",), + json_schema={"type": "object"}, + ) + plugin_runtime.check_llm_polling.assert_called_once_with( + provider="langgenius/openai/openai", + model="gpt-4o-mini", + credentials={"api_key": "secret"}, + plugin_state={"task_id": "poll-1"}, + ) + + +def test_dify_prepared_polling_llm_raise_exception_when_polling_is_unsupported() -> None: + llm_result = node_runtime.LLMResult( + model="gpt-4o-mini", + prompt_messages=[], + message=AssistantPromptMessage(content="sync-result"), + usage=node_runtime.LLMUsage.empty_usage(), + ) + plugin_runtime = PluginModelRuntime( + tenant_id="tenant-id", + user_id="user-id", + client=Mock(), + plugin_service=Mock(), + ) + plugin_runtime.start_llm_polling = Mock(side_effect=PluginLLMPollingUnsupportedError("Polling unsupported")) # type: ignore[method-assign] + model_instance = _ModelInstanceStub( + model_schema=_build_model_schema(features=[ModelFeature.POLLING]), + model_runtime=plugin_runtime, + invoke_llm_result=llm_result, + ) + + prepared = DifyPreparedPollingLLM(model_instance) + + with pytest.raises(PluginLLMPollingUnsupportedError): + prepared.start_llm_polling( + prompt_messages=[], + model_parameters={"temperature": 0.1}, + tools=None, + stop=None, + json_schema=None, + ) + + def test_dify_prompt_message_serializer_delegates(monkeypatch: pytest.MonkeyPatch) -> None: serialize = MagicMock(return_value={"prompt": "value"}) monkeypatch.setattr(node_runtime.PromptMessageUtil, "prompt_messages_to_prompt_for_saving", serialize) diff --git a/api/tests/unit_tests/services/agent/test_agent_observability_service.py b/api/tests/unit_tests/services/agent/test_agent_observability_service.py index 1ce8edad788..ab67b0e38da 100644 --- a/api/tests/unit_tests/services/agent/test_agent_observability_service.py +++ b/api/tests/unit_tests/services/agent/test_agent_observability_service.py @@ -20,6 +20,60 @@ def test_resolve_source_accepts_frontend_aliases() -> None: AgentObservabilityService.resolve_source("unknown") +def test_resolve_source_filter_accepts_structured_sources() -> None: + assert AgentObservabilityService.resolve_source_filter(None).kind == "all" + assert AgentObservabilityService.resolve_source_filter("webapp").kind == "webapp" + assert AgentObservabilityService.resolve_source_filter("webapp:app-1").app_id == "app-1" + + workflow_filter = AgentObservabilityService.resolve_source_filter("workflow:app-2:workflow-1:v1:node-1") + assert workflow_filter.kind == "workflow" + assert workflow_filter.app_id == "app-2" + assert workflow_filter.workflow_id == "workflow-1" + assert workflow_filter.workflow_version == "v1" + assert workflow_filter.node_id == "node-1" + + legacy_filter = AgentObservabilityService.resolve_source_filter("console") + assert legacy_filter.kind == "webapp" + assert legacy_filter.invoke_from == InvokeFrom.EXPLORE + + with pytest.raises(ValueError, match="Unsupported source"): + AgentObservabilityService.resolve_source_filter("workflow:broken") + + +def test_source_serializers_return_structured_frontend_shape() -> None: + app = SimpleNamespace( + id="app-1", + name="Iris", + icon_type=SimpleNamespace(value="emoji"), + icon="robot", + icon_background="#fff", + ) + + webapp_source = AgentObservabilityService._serialize_webapp_source(app) # type: ignore[arg-type] + workflow_source = AgentObservabilityService._serialize_workflow_source( + app=app, # type: ignore[arg-type] + workflow_id="workflow-1", + workflow_version="v1", + node_id="node-1", + ) + + assert webapp_source == { + "id": "webapp:app-1", + "type": "webapp", + "app_id": "app-1", + "app_name": "Iris", + "app_icon_type": "emoji", + "app_icon": "robot", + "app_icon_background": "#fff", + "workflow_id": None, + "workflow_version": None, + "node_id": None, + } + assert workflow_source["id"] == "workflow:app-1:workflow-1:v1:node-1" + assert workflow_source["type"] == "workflow" + assert workflow_source["workflow_id"] == "workflow-1" + + def test_serialize_log_message_returns_frontend_log_shape() -> None: created_at = datetime(2026, 6, 17, 1, 2, 3, tzinfo=UTC) updated_at = datetime(2026, 6, 17, 1, 3, 3, tzinfo=UTC) diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index fb0a316648a..b0a099c9bd8 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -100,7 +100,7 @@ def test_agent_soul_has_model(): assert agent_soul_has_model(AgentSoulConfig()) is False -def test_load_workflow_composer_returns_empty_state(monkeypatch): +def test_load_workflow_composer_returns_empty_state(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1")) monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: None) @@ -118,7 +118,7 @@ def test_load_workflow_composer_returns_empty_state(monkeypatch): assert files_output["array_item"] == {"type": "file", "description": None, "children": []} -def test_load_workflow_composer_serializes_existing_binding(monkeypatch): +def test_load_workflow_composer_serializes_existing_binding(monkeypatch: pytest.MonkeyPatch): binding = SimpleNamespace( agent_id="agent-1", binding_type=WorkflowAgentBindingType.ROSTER_AGENT, @@ -220,7 +220,7 @@ def test_save_workflow_composer_rejects_agent_app_variant(): ) -def test_save_agent_app_composer_creates_agent_when_missing(monkeypatch): +def test_save_agent_app_composer_creates_agent_when_missing(monkeypatch: pytest.MonkeyPatch): fake_session = FakeSession(scalar=[None]) created_version = SimpleNamespace(id="version-1") @@ -249,7 +249,7 @@ def test_save_agent_app_composer_creates_agent_when_missing(monkeypatch): assert fake_session.commits == 1 -def test_save_agent_app_composer_updates_current_version(monkeypatch): +def test_save_agent_app_composer_updates_current_version(monkeypatch: pytest.MonkeyPatch): agent = SimpleNamespace(id="agent-1", active_config_snapshot_id="version-1", updated_by=None) fake_session = FakeSession(scalar=[agent]) updated = {} @@ -283,7 +283,7 @@ def test_save_agent_app_composer_updates_current_version(monkeypatch): assert fake_session.commits == 1 -def test_agent_app_composer_candidates_and_impact(monkeypatch): +def test_agent_app_composer_candidates_and_impact(monkeypatch: pytest.MonkeyPatch): bindings = [ SimpleNamespace(app_id="app-1", workflow_id="workflow-1", node_id="node-1"), SimpleNamespace(app_id="app-1", workflow_id="workflow-1", node_id="node-2"), @@ -316,7 +316,7 @@ def test_agent_app_composer_candidates_and_impact(monkeypatch): assert impact["bindings"][1]["node_id"] == "node-2" -def test_serialize_workflow_state_changes_lock_and_save_options(monkeypatch): +def test_serialize_workflow_state_changes_lock_and_save_options(monkeypatch: pytest.MonkeyPatch): binding = WorkflowAgentNodeBinding( id="binding-1", tenant_id="tenant-1", @@ -342,7 +342,7 @@ def test_serialize_workflow_state_changes_lock_and_save_options(monkeypatch): assert effective_names == ["text", "files", "json"] -def test_serialize_workflow_state_passes_user_declared_outputs_through_effective(monkeypatch): +def test_serialize_workflow_state_passes_user_declared_outputs_through_effective(monkeypatch: pytest.MonkeyPatch): binding = WorkflowAgentNodeBinding( id="binding-1", tenant_id="tenant-1", @@ -369,7 +369,7 @@ def test_serialize_workflow_state_passes_user_declared_outputs_through_effective assert effective[0]["required"] is True -def test_composer_save_helpers_create_and_rebind_agents(monkeypatch): +def test_composer_save_helpers_create_and_rebind_agents(monkeypatch: pytest.MonkeyPatch): fake_session = FakeSession() monkeypatch.setattr(composer_service.db, "session", fake_session) workflow_agent = SimpleNamespace(id="inline-agent-1", active_config_snapshot_id="inline-version-1") @@ -611,7 +611,7 @@ def test_composer_create_agents_syncs_active_config_has_model(monkeypatch): assert roster_agent.active_config_has_model is True -def test_composer_version_helpers_and_lookup_errors(monkeypatch): +def test_composer_version_helpers_and_lookup_errors(monkeypatch: pytest.MonkeyPatch): fake_session = FakeSession( scalar=[ 1, @@ -670,7 +670,7 @@ def test_composer_version_helpers_and_lookup_errors(monkeypatch): assert workflow.id == "workflow-1" -def test_composer_current_version_and_error_paths(monkeypatch): +def test_composer_current_version_and_error_paths(monkeypatch: pytest.MonkeyPatch): fake_session = FakeSession(scalar=[2]) monkeypatch.setattr(composer_service.db, "session", fake_session) payload = ComposerSavePayload.model_validate( @@ -717,7 +717,7 @@ def test_composer_current_version_and_error_paths(monkeypatch): ) -def test_roster_list_and_invite_options(monkeypatch): +def test_roster_list_and_invite_options(monkeypatch: pytest.MonkeyPatch): created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) updated_at = datetime(2026, 1, 3, 3, 4, 5, tzinfo=UTC) version_created_at = datetime(2026, 1, 4, 3, 4, 5, tzinfo=UTC) @@ -790,7 +790,7 @@ def test_roster_list_and_invite_options(monkeypatch): assert invited["data"][0]["existing_node_ids"] == ["node-1"] -def test_invite_options_uses_db_filtered_pagination(monkeypatch): +def test_invite_options_uses_db_filtered_pagination(monkeypatch: pytest.MonkeyPatch): configured_agent = Agent( id="agent-2", tenant_id="tenant-1", @@ -915,7 +915,7 @@ def test_published_references_include_app_display_fields_and_sort_by_updated_at( assert references[0]["workflow_version"] == "published-recent" -def test_roster_update_archive_versions_and_detail(monkeypatch): +def test_roster_update_archive_versions_and_detail(monkeypatch: pytest.MonkeyPatch): listed_version = AgentConfigSnapshot(id="version-2", agent_id="agent-1", version=2) listed_version_created_at = datetime(2026, 1, 5, 3, 4, 5, tzinfo=UTC) listed_version.created_at = listed_version_created_at @@ -973,7 +973,7 @@ def test_roster_update_archive_versions_and_detail(monkeypatch): assert detail["revisions"][0]["created_at"] == int(revision_created_at.timestamp()) -def test_roster_create_detail_and_lookup_helpers(monkeypatch): +def test_roster_create_detail_and_lookup_helpers(monkeypatch: pytest.MonkeyPatch): fake_session = FakeSession( scalar=[ SimpleNamespace(id="agent-1"), @@ -1040,9 +1040,7 @@ def test_agent_app_visible_versions_exclude_draft_saves(): def test_app_list_all_excludes_agent_apps_by_default(): filters = AppService._build_app_list_filters( - "account-1", - "tenant-1", - AppListParams(mode="all"), + "account-1", "tenant-1", AppListParams(mode="all"), FakeSession(scalar=None, scalars=None) ) sql = " ".join(str(filter_) for filter_ in filters) @@ -1249,6 +1247,7 @@ class TestAgentAppBackingAgent: app_id="app-1", name="Iris", description="clarifier", + role="research assistant", ) # Agent is bound to the app and is a roster/agent_app entry. @@ -1258,6 +1257,7 @@ class TestAgentAppBackingAgent: assert agent.status == AgentStatus.ACTIVE assert agent.agent_kind == AgentKind.DIFY_AGENT assert agent.name == "Iris" + assert agent.role == "research assistant" # A v1 snapshot + revision are seeded and wired as the active version. snapshots = [a for a in session.added if isinstance(a, AgentConfigSnapshot)] assert len(snapshots) == 1 @@ -2173,7 +2173,7 @@ class TestWorkflowAgentDraftBindingSync: assert session.deleted == [stale_binding] -def test_dataset_rows_filters_malformed_ids(monkeypatch): +def test_dataset_rows_filters_malformed_ids(monkeypatch: pytest.MonkeyPatch): """Mention ids are user-editable text: a non-UUID id must read as missing (placeholder semantics), never reach the UUID-typed dataset query (E2E 500).""" captured = {} @@ -2197,7 +2197,7 @@ def test_dataset_rows_filters_malformed_ids(monkeypatch): assert captured == {} -def test_workspace_dify_tools_returns_provider_and_tool_granularities(monkeypatch): +def test_workspace_dify_tools_returns_provider_and_tool_granularities(monkeypatch: pytest.MonkeyPatch): """The slash-menu Tools tab needs both selection granularities: a provider hosts many tools (like an MCP server), so candidates return one provider-level entry (id = /*, = all tools) plus one per tool.""" @@ -2268,7 +2268,7 @@ def _patch_drive_keys(monkeypatch, existing_keys): return captured -def test_drive_ref_findings_reports_missing_keys(monkeypatch): +def test_drive_ref_findings_reports_missing_keys(monkeypatch: pytest.MonkeyPatch): _patch_drive_keys(monkeypatch, existing_keys=["tender-analyzer/SKILL.md"]) findings = AgentComposerService._drive_ref_findings( @@ -2279,7 +2279,7 @@ def test_drive_ref_findings_reports_missing_keys(monkeypatch): assert str(findings[0]["message"]).startswith("file_ref_dangling: ") -def test_drive_ref_findings_clean_when_all_keys_exist(monkeypatch): +def test_drive_ref_findings_clean_when_all_keys_exist(monkeypatch: pytest.MonkeyPatch): _patch_drive_keys(monkeypatch, existing_keys=["tender-analyzer/SKILL.md", "files/sample.pdf"]) assert ( @@ -2288,7 +2288,7 @@ def test_drive_ref_findings_clean_when_all_keys_exist(monkeypatch): ) -def test_drive_ref_findings_skips_refs_without_drive_keys(monkeypatch): +def test_drive_ref_findings_skips_refs_without_drive_keys(monkeypatch: pytest.MonkeyPatch): # No drive-backed ref at all -> no DB roundtrip, no findings. soul = _drive_soul( skills_files={"skills": [{"id": "legacy", "name": "Legacy"}], "files": [{"name": "u.pdf", "file_id": "u-1"}]} @@ -2297,7 +2297,7 @@ def test_drive_ref_findings_skips_refs_without_drive_keys(monkeypatch): assert findings == [] -def test_require_drive_refs_resolved_raises_with_stable_code(monkeypatch): +def test_require_drive_refs_resolved_raises_with_stable_code(monkeypatch: pytest.MonkeyPatch): from services.agent.errors import InvalidComposerConfigError _patch_drive_keys(monkeypatch, existing_keys=[]) @@ -2308,7 +2308,7 @@ def test_require_drive_refs_resolved_raises_with_stable_code(monkeypatch): ) -def test_collect_validation_findings_appends_drive_findings_with_agent_context(monkeypatch): +def test_collect_validation_findings_appends_drive_findings_with_agent_context(monkeypatch: pytest.MonkeyPatch): from services.entities.agent_entities import ComposerSavePayload _patch_drive_keys(monkeypatch, existing_keys=[]) @@ -2334,7 +2334,7 @@ def test_collect_validation_findings_appends_drive_findings_with_agent_context(m # ── ENG-625 D5: soul-first ref removal ─────────────────────────────────────── -def _patch_remove_drive_refs_env(monkeypatch, *, soul_dict): +def _patch_remove_drive_refs_env(monkeypatch: pytest.MonkeyPatch, *, soul_dict): """Wire the classmethod's collaborators so soul editing + versioning is observable.""" from types import SimpleNamespace @@ -2359,7 +2359,7 @@ def _patch_remove_drive_refs_env(monkeypatch, *, soul_dict): return agent, captured, committed -def test_remove_drive_refs_drops_skill_by_slug_and_versions(monkeypatch): +def test_remove_drive_refs_drops_skill_by_slug_and_versions(monkeypatch: pytest.MonkeyPatch): soul_dict = { "skills_files": { "skills": [ @@ -2383,7 +2383,7 @@ def test_remove_drive_refs_drops_skill_by_slug_and_versions(monkeypatch): assert committed.get("committed") is True -def test_remove_drive_refs_is_noop_when_ref_absent(monkeypatch): +def test_remove_drive_refs_is_noop_when_ref_absent(monkeypatch: pytest.MonkeyPatch): soul_dict = {"skills_files": {"skills": [], "files": []}} agent, captured, committed = _patch_remove_drive_refs_env(monkeypatch, soul_dict=soul_dict) @@ -2397,7 +2397,7 @@ def test_remove_drive_refs_is_noop_when_ref_absent(monkeypatch): assert committed == {} -def test_remove_drive_refs_drops_file_by_key(monkeypatch): +def test_remove_drive_refs_drops_file_by_key(monkeypatch: pytest.MonkeyPatch): soul_dict = { "skills_files": { "skills": [], @@ -2417,7 +2417,7 @@ def test_remove_drive_refs_drops_file_by_key(monkeypatch): assert [f.drive_key for f in captured["agent_soul"].skills_files.files] == ["files/keep.pdf"] -def test_add_drive_file_ref_adds_or_replaces_file_and_versions(monkeypatch): +def test_add_drive_file_ref_adds_or_replaces_file_and_versions(monkeypatch: pytest.MonkeyPatch): soul_dict = { "skills_files": { "skills": [], @@ -2444,7 +2444,7 @@ def test_add_drive_file_ref_adds_or_replaces_file_and_versions(monkeypatch): assert committed.get("committed") is True -def test_add_drive_file_ref_syncs_workflow_binding_snapshot(monkeypatch): +def test_add_drive_file_ref_syncs_workflow_binding_snapshot(monkeypatch: pytest.MonkeyPatch): binding = SimpleNamespace(agent_id="agent-1", current_snapshot_id="snap-1", updated_by=None) _patch_remove_drive_refs_env(monkeypatch, soul_dict={"skills_files": {"skills": [], "files": []}}) monkeypatch.setattr( @@ -2473,7 +2473,7 @@ def test_remove_drive_refs_requires_exactly_one_scope(): # ── ENG-623/625: resolver helpers + save-path drive guard ──────────────────── -def test_resolve_bound_agent_id_queries_active_roster_agent(monkeypatch): +def test_resolve_bound_agent_id_queries_active_roster_agent(monkeypatch: pytest.MonkeyPatch): from types import SimpleNamespace import services.agent.composer_service as module @@ -2482,7 +2482,7 @@ def test_resolve_bound_agent_id_queries_active_roster_agent(monkeypatch): assert AgentComposerService.resolve_bound_agent_id(tenant_id="t-1", app_id="app-1") == "agent-9" -def test_resolve_workflow_node_agent_id_degrades_without_workflow_or_binding(monkeypatch): +def test_resolve_workflow_node_agent_id_degrades_without_workflow_or_binding(monkeypatch: pytest.MonkeyPatch): from types import SimpleNamespace def boom(cls, **kwargs): @@ -2505,7 +2505,7 @@ def test_resolve_workflow_node_agent_id_degrades_without_workflow_or_binding(mon assert AgentComposerService.resolve_workflow_node_agent_id(tenant_id="t", app_id="a", node_id="n") == "agent-7" -def test_remove_drive_refs_returns_none_without_agent_or_snapshot(monkeypatch): +def test_remove_drive_refs_returns_none_without_agent_or_snapshot(monkeypatch: pytest.MonkeyPatch): from types import SimpleNamespace import services.agent.composer_service as module @@ -2518,7 +2518,7 @@ def test_remove_drive_refs_returns_none_without_agent_or_snapshot(monkeypatch): assert AgentComposerService.remove_drive_refs(tenant_id="t", agent_id="a", account_id="u", skill_slug="s") is None -def test_save_workflow_composer_guards_drive_refs_for_existing_agent_strategies(monkeypatch): +def test_save_workflow_composer_guards_drive_refs_for_existing_agent_strategies(monkeypatch: pytest.MonkeyPatch): from types import SimpleNamespace from services.entities.agent_entities import ComposerSavePayload diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index db79cf4cb55..c98b717a105 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -1752,13 +1752,15 @@ class TestRegisterService: mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, None, "add") mock_create_member.assert_called_once_with(mock_tenant, mock_new_account, "normal") mock_switch_tenant.assert_called_once_with(mock_new_account, mock_tenant.id) - mock_generate_token.assert_called_once_with(mock_tenant, mock_new_account) + mock_generate_token.assert_called_once_with( + mock_tenant, mock_new_account, "normal", requires_setup=True + ) mock_task_dependencies.delay.assert_called_once() def test_invite_new_member_existing_account( self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies ): - """Test inviting a new member who already has an account.""" + """Test inviting a pending account that is not in the tenant yet.""" # Setup test data mock_tenant = MagicMock() mock_tenant.id = "tenant-456" @@ -1796,10 +1798,51 @@ class TestRegisterService: # Verify results assert result == "invite-token-123" mock_create_member.assert_called_once_with(mock_tenant, mock_existing_account, "normal") - mock_generate_token.assert_called_once_with(mock_tenant, mock_existing_account) + mock_generate_token.assert_called_once_with( + mock_tenant, mock_existing_account, "normal", requires_setup=True + ) mock_task_dependencies.delay.assert_called_once() mock_lookup.assert_called_once_with("existing@example.com") + def test_invite_existing_active_account_requires_acceptance_before_joining( + self, mock_db_dependencies, mock_redis_dependencies, mock_task_dependencies + ): + """Existing active accounts outside the tenant receive an invite without immediate membership.""" + mock_tenant = MagicMock() + mock_tenant.id = "tenant-456" + mock_tenant.name = "Test Workspace" + mock_inviter = TestAccountAssociatedDataFactory.create_account_mock(account_id="inviter-123", name="Inviter") + mock_existing_account = TestAccountAssociatedDataFactory.create_account_mock( + account_id="existing-user-456", email="existing@example.com", status="active" + ) + + with patch("services.account_service.AccountService.get_account_by_email_with_case_fallback") as mock_lookup: + mock_lookup.return_value = mock_existing_account + mock_db_dependencies["db"].session.scalar.return_value = None + + with ( + patch("services.account_service.TenantService.check_member_permission") as mock_check_permission, + patch("services.account_service.TenantService.create_tenant_member") as mock_create_member, + patch("services.account_service.RegisterService.generate_invite_token") as mock_generate_token, + ): + mock_generate_token.return_value = "invite-token-123" + + result = RegisterService.invite_new_member( + tenant=mock_tenant, + email="existing@example.com", + language="en-US", + role="admin", + inviter=mock_inviter, + ) + + assert result == "invite-token-123" + mock_check_permission.assert_called_once_with(mock_tenant, mock_inviter, mock_existing_account, "add") + mock_create_member.assert_not_called() + mock_generate_token.assert_called_once_with( + mock_tenant, mock_existing_account, "admin", requires_setup=False + ) + mock_task_dependencies.delay.assert_called_once() + def test_invite_new_member_already_in_tenant(self, mock_db_dependencies, mock_redis_dependencies): """Test inviting a member who is already in the tenant.""" # Setup test data @@ -1864,7 +1907,7 @@ class TestRegisterService: mock_uuid.return_value = "test-uuid-123" # Execute test - result = RegisterService.generate_invite_token(mock_tenant, mock_account) + result = RegisterService.generate_invite_token(mock_tenant, mock_account, "admin", requires_setup=True) # Verify results assert result == "test-uuid-123" @@ -1877,6 +1920,8 @@ class TestRegisterService: assert stored_data["account_id"] == "user-123" assert stored_data["email"] == "test@example.com" assert stored_data["workspace_id"] == "tenant-456" + assert stored_data["role"] == "admin" + assert stored_data["requires_setup"] is True def test_is_valid_invite_token_valid(self, mock_redis_dependencies): """Test checking valid invite token.""" @@ -1943,9 +1988,8 @@ class TestRegisterService: } mock_get_invitation_by_token.return_value = invitation_data - # Mock scalar for tenant lookup, execute for account+role lookup - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, mock_account] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -2001,9 +2045,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock scalar for tenant, execute for account+role - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = None # No account found + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, None] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") @@ -2029,9 +2072,8 @@ class TestRegisterService: } mock_redis_dependencies.get.return_value = json.dumps(invitation_data).encode() - # Mock scalar for tenant, execute for account+role - mock_db_dependencies["db"].session.scalar.return_value = mock_tenant - mock_db_dependencies["db"].session.execute.return_value.first.return_value = (mock_account, "normal") + # Mock scalar for tenant lookup, then account lookup. + mock_db_dependencies["db"].session.scalar.side_effect = [mock_tenant, mock_account] # Execute test result = RegisterService.get_invitation_if_token_valid("tenant-456", "test@example.com", "token-123") diff --git a/api/tests/unit_tests/services/test_annotation_service.py b/api/tests/unit_tests/services/test_annotation_service.py index 5054010e89d..55912cc1c1f 100644 --- a/api/tests/unit_tests/services/test_annotation_service.py +++ b/api/tests/unit_tests/services/test_annotation_service.py @@ -2,6 +2,7 @@ Unit tests for services.annotation_service """ +import logging from io import BytesIO from types import SimpleNamespace from typing import Any, cast @@ -1049,7 +1050,9 @@ class TestAppAnnotationServiceBatchImport: mock_redis.setnx.assert_called_once_with("app_annotation_batch_import_uuid-3", "waiting") mock_task.delay.assert_called_once() - def test_batch_import_app_annotations_should_cleanup_active_job_on_unexpected_exception(self) -> None: + def test_batch_import_app_annotations_should_cleanup_active_job_on_unexpected_exception( + self, caplog: pytest.LogCaptureFixture + ) -> None: """Test unexpected runtime errors trigger cleanup and return wrapped error.""" # Arrange file = _make_file(b"question,answer\nq,a\n") @@ -1067,7 +1070,6 @@ class TestAppAnnotationServiceBatchImport: patch("services.annotation_service.redis_client") as mock_redis, patch("services.annotation_service.uuid.uuid4", return_value="uuid-4"), patch("services.annotation_service.naive_utc_now", return_value=SimpleNamespace(timestamp=lambda: 1)), - patch("services.annotation_service.logger") as mock_logger, patch( "configs.dify_config", new=SimpleNamespace(ANNOTATION_IMPORT_MAX_RECORDS=5, ANNOTATION_IMPORT_MIN_RECORDS=1), @@ -1078,12 +1080,15 @@ class TestAppAnnotationServiceBatchImport: mock_redis.zrem.side_effect = RuntimeError("cleanup-failed") # Act - result = AppAnnotationService.batch_import_app_annotations(app.id, file) + with caplog.at_level(logging.DEBUG): + result = AppAnnotationService.batch_import_app_annotations(app.id, file) # Assert assert result["error_msg"] == "An error occurred while processing the file: boom" mock_redis.zrem.assert_called_once_with(f"annotation_import_active:{tenant_id}", "uuid-4") - mock_logger.debug.assert_called_once() + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "DEBUG" + assert "Failed to clean up active job tracking during error handling" in caplog.records[0].message class TestAppAnnotationServiceHitHistoryAndSettings: diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py index bb764112640..e595721e169 100644 --- a/api/tests/unit_tests/services/test_app_service.py +++ b/api/tests/unit_tests/services/test_app_service.py @@ -168,6 +168,7 @@ class TestAgentAppType: mode=AppMode.AGENT, name="Old", description="old", + role="draft", icon_type=IconType.EMOJI, icon="robot", icon_background="#fff", @@ -178,6 +179,7 @@ class TestAgentAppType: backing_agent = SimpleNamespace( name="Old", description="old", + role="draft", icon_type=AgentIconType.EMOJI, icon="robot", icon_background="#fff", @@ -195,6 +197,7 @@ class TestAgentAppType: { "name": "Iris", "description": "agent app", + "role": "research assistant", "icon_type": "image", "icon": "file-id", "icon_background": "#123456", @@ -206,12 +209,114 @@ class TestAgentAppType: assert updated_app.name == "Iris" assert backing_agent.name == "Iris" assert backing_agent.description == "agent app" + assert backing_agent.role == "research assistant" assert backing_agent.icon_type == AgentIconType.IMAGE assert backing_agent.icon == "file-id" assert backing_agent.icon_background == "#123456" assert backing_agent.updated_by == "account-2" assert backing_agent.updated_at == updated_app.updated_at + def test_update_agent_app_preserves_role_when_args_omit_it(self): + from models.agent import AgentIconType + from models.model import AppMode, IconType + from services.app_service import AppService + + app = SimpleNamespace( + id="app-1", + tenant_id="tenant-1", + mode=AppMode.AGENT, + name="Old", + description="old", + role="draft", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + use_icon_as_answer_icon=False, + max_active_requests=None, + created_by="account-1", + ) + backing_agent = SimpleNamespace( + name="Old", + description="old", + role="research assistant", + icon_type=AgentIconType.EMOJI, + icon="robot", + icon_background="#fff", + updated_by=None, + updated_at=None, + ) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + ): + mock_db.session.scalar.return_value = backing_agent + AppService().update_app( + app, # type: ignore[arg-type] + { + "name": "Iris", + "description": "agent app", + "icon_type": "image", + "icon": "file-id", + "icon_background": "#123456", + "use_icon_as_answer_icon": False, + "max_active_requests": 0, + }, + ) + + assert backing_agent.role == "research assistant" + + def test_update_agent_app_clears_role_when_args_set_empty_string(self): + from models.agent import AgentIconType + from models.model import AppMode, IconType + from services.app_service import AppService + + app = SimpleNamespace( + id="app-1", + tenant_id="tenant-1", + mode=AppMode.AGENT, + name="Old", + description="old", + role="draft", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + use_icon_as_answer_icon=False, + max_active_requests=None, + created_by="account-1", + ) + backing_agent = SimpleNamespace( + name="Old", + description="old", + role="research assistant", + icon_type=AgentIconType.EMOJI, + icon="robot", + icon_background="#fff", + updated_by=None, + updated_at=None, + ) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + ): + mock_db.session.scalar.return_value = backing_agent + AppService().update_app( + app, # type: ignore[arg-type] + { + "name": "Iris", + "description": "agent app", + "role": "", + "icon_type": "image", + "icon": "file-id", + "icon_background": "#123456", + "use_icon_as_answer_icon": False, + "max_active_requests": 0, + }, + ) + + assert backing_agent.role == "" + def test_delete_agent_app_archives_backing_agent(self): from models.agent import AgentStatus from models.model import AppMode diff --git a/api/tests/unit_tests/services/test_model_provider_service.py b/api/tests/unit_tests/services/test_model_provider_service.py index 806be013497..a8a976f4b07 100644 --- a/api/tests/unit_tests/services/test_model_provider_service.py +++ b/api/tests/unit_tests/services/test_model_provider_service.py @@ -215,12 +215,13 @@ class TestModelProviderServiceDelegation: get_provider_config_mock.assert_called_once_with("tenant-1", "openai") provider_method = getattr(provider_configuration, provider_method_name) - if isinstance(provider_call_kwargs, tuple): - provider_method.assert_called_once_with(*provider_call_kwargs) - elif isinstance(provider_call_kwargs, dict): - provider_method.assert_called_once_with(**provider_call_kwargs) - else: - provider_method.assert_called_once_with(provider_call_kwargs) + match provider_call_kwargs: + case tuple(): + provider_method.assert_called_once_with(*provider_call_kwargs) + case dict(): + provider_method.assert_called_once_with(**provider_call_kwargs) + case _: + provider_method.assert_called_once_with(provider_call_kwargs) if method_name == "get_provider_credential": assert result == {"token": "abc"} diff --git a/api/tests/unit_tests/services/test_snippet_service.py b/api/tests/unit_tests/services/test_snippet_service.py index 7cbe773e419..008b6cfa418 100644 --- a/api/tests/unit_tests/services/test_snippet_service.py +++ b/api/tests/unit_tests/services/test_snippet_service.py @@ -94,14 +94,15 @@ def test_validate_snippet_graph_forbidden_nodes_raises_with_node_details() -> No def test_get_snippets_returns_empty_when_tag_filter_has_no_targets(monkeypatch: pytest.MonkeyPatch) -> None: + session = _SessionWithoutNameLookup() get_target_ids = Mock(return_value=[]) monkeypatch.setattr("services.snippet_service.TagService.get_target_ids_by_tag_ids", get_target_ids) service = SnippetService.__new__(SnippetService) - result = service.get_snippets(tenant_id="tenant-1", tag_ids=["tag-1"]) + result = service.get_snippets(tenant_id="tenant-1", session=session, tag_ids=["tag-1"]) assert result == ([], 0, False) - get_target_ids.assert_called_once_with("snippet", "tenant-1", ["tag-1"], match_all=True) + get_target_ids.assert_called_once_with("snippet", "tenant-1", ["tag-1"], session, match_all=True) def test_get_snippets_applies_filters_and_paginates(monkeypatch: pytest.MonkeyPatch) -> None: @@ -124,6 +125,7 @@ def test_get_snippets_applies_filters_and_paginates(monkeypatch: pytest.MonkeyPa result, total, has_more = service.get_snippets( tenant_id="tenant-1", + session=session, page=2, limit=2, keyword="search", @@ -135,7 +137,7 @@ def test_get_snippets_applies_filters_and_paginates(monkeypatch: pytest.MonkeyPa assert result == snippets[:2] assert total == 3 assert has_more is True - get_target_ids.assert_called_once_with("snippet", "tenant-1", ["tag-1"], match_all=True) + get_target_ids.assert_called_once_with("snippet", "tenant-1", ["tag-1"], session, match_all=True) session.scalar.assert_called_once() session.scalars.assert_called_once() diff --git a/api/tests/unit_tests/services/test_tag_service.py b/api/tests/unit_tests/services/test_tag_service.py index 282b32a7e55..73df7cc2673 100644 --- a/api/tests/unit_tests/services/test_tag_service.py +++ b/api/tests/unit_tests/services/test_tag_service.py @@ -1,6 +1,7 @@ from types import SimpleNamespace import pytest +from pytest_mock import MockerFixture from werkzeug.exceptions import NotFound from models.enums import TagType @@ -8,19 +9,19 @@ from services.tag_service import TagBindingCreatePayload, TagBindingDeletePayloa @pytest.fixture -def current_user(mocker): +def current_user(mocker: MockerFixture): user = SimpleNamespace(id="user-1", current_tenant_id="tenant-1") mocker.patch("services.tag_service.current_user", user) return user @pytest.fixture -def db_session(mocker): - mock_db = mocker.patch("services.tag_service.db") +def db_session(mocker: MockerFixture): + mock_db = mocker.Mock() return mock_db.session -def test_save_tag_binding_only_creates_bindings_for_valid_snippet_tags(mocker, current_user, db_session): +def test_save_tag_binding_only_creates_bindings_for_valid_snippet_tags(mocker: MockerFixture, current_user, db_session): mocker.patch("services.tag_service.TagService.check_target_exists") db_session.scalars.return_value.all.return_value = ["tag-1"] db_session.scalar.return_value = None @@ -30,7 +31,8 @@ def test_save_tag_binding_only_creates_bindings_for_valid_snippet_tags(mocker, c tag_ids=["tag-1", "tag-from-other-tenant"], target_id="snippet-1", type=TagType.SNIPPET, - ) + ), + db_session, ) db_session.add.assert_called_once() @@ -42,7 +44,7 @@ def test_save_tag_binding_only_creates_bindings_for_valid_snippet_tags(mocker, c db_session.commit.assert_called_once() -def test_delete_tag_binding_limits_deletion_to_valid_snippet_tags(mocker, current_user, db_session): +def test_delete_tag_binding_limits_deletion_to_valid_snippet_tags(mocker: MockerFixture, current_user, db_session): mocker.patch("services.tag_service.TagService.check_target_exists") db_session.execute.return_value = SimpleNamespace(rowcount=1) @@ -51,14 +53,15 @@ def test_delete_tag_binding_limits_deletion_to_valid_snippet_tags(mocker, curren tag_ids=["tag-1", "tag-from-other-tenant"], target_id="snippet-1", type=TagType.SNIPPET, - ) + ), + db_session, ) db_session.execute.assert_called_once() db_session.commit.assert_called_once() -def test_delete_tag_binding_does_not_commit_when_no_rows_deleted(mocker, current_user, db_session): +def test_delete_tag_binding_does_not_commit_when_no_rows_deleted(mocker: MockerFixture, current_user, db_session): mocker.patch("services.tag_service.TagService.check_target_exists") db_session.execute.return_value = SimpleNamespace(rowcount=0) @@ -67,7 +70,8 @@ def test_delete_tag_binding_does_not_commit_when_no_rows_deleted(mocker, current tag_ids=["tag-1"], target_id="snippet-1", type=TagType.SNIPPET, - ) + ), + db_session, ) db_session.execute.assert_called_once() @@ -75,7 +79,7 @@ def test_delete_tag_binding_does_not_commit_when_no_rows_deleted(mocker, current def test_get_target_ids_by_tag_ids_returns_empty_without_query_for_empty_input(db_session): - result = TagService.get_target_ids_by_tag_ids(TagType.SNIPPET, "tenant-1", []) + result = TagService.get_target_ids_by_tag_ids(TagType.SNIPPET, "tenant-1", [], db_session) assert result == [] db_session.scalars.assert_not_called() @@ -84,7 +88,7 @@ def test_get_target_ids_by_tag_ids_returns_empty_without_query_for_empty_input(d def test_check_target_exists_accepts_existing_snippet(current_user, db_session): db_session.scalar.return_value = SimpleNamespace(id="snippet-1") - TagService.check_target_exists("snippet", "snippet-1") + TagService.check_target_exists("snippet", "snippet-1", db_session) db_session.scalar.assert_called_once() @@ -93,11 +97,11 @@ def test_check_target_exists_raises_when_snippet_missing(current_user, db_sessio db_session.scalar.return_value = None with pytest.raises(NotFound, match="Snippet not found"): - TagService.check_target_exists("snippet", "missing-snippet") + TagService.check_target_exists("snippet", "missing-snippet", db_session) def test_check_target_exists_raises_for_invalid_binding_type(current_user, db_session): with pytest.raises(NotFound, match="Invalid binding type"): - TagService.check_target_exists("unknown", "target-1") + TagService.check_target_exists("unknown", "target-1", db_session) db_session.scalar.assert_not_called() diff --git a/api/tests/unit_tests/services/test_trigger_provider_service.py b/api/tests/unit_tests/services/test_trigger_provider_service.py index 4da4af2d939..184e1375ed9 100644 --- a/api/tests/unit_tests/services/test_trigger_provider_service.py +++ b/api/tests/unit_tests/services/test_trigger_provider_service.py @@ -2,6 +2,7 @@ from __future__ import annotations import contextlib import json +import logging from types import SimpleNamespace from typing import Any from unittest.mock import MagicMock @@ -252,27 +253,28 @@ def test_add_trigger_subscription_should_raise_error_when_provider_limit_reached mock_session: MagicMock, provider_id: TriggerProviderID, provider_controller: MagicMock, + caplog, ) -> None: # Arrange _patch_redis_lock(mocker) mock_session.scalar.return_value = TriggerProviderService.__MAX_TRIGGER_PROVIDER_COUNT__ _mock_get_trigger_provider(mocker, provider_controller) - mock_logger = mocker.patch("services.trigger.trigger_provider_service.logger") # Act + Assert - with pytest.raises(ValueError, match="Maximum number of providers"): - TriggerProviderService.add_trigger_subscription( - tenant_id="tenant-1", - user_id="user-1", - name="main", - provider_id=provider_id, - endpoint_id="endpoint-1", - credential_type=CredentialType.API_KEY, - parameters={}, - properties={}, - credentials={}, - ) - mock_logger.exception.assert_called_once() + with caplog.at_level(logging.ERROR, logger="services.trigger.trigger_provider_service"): + with pytest.raises(ValueError, match="Maximum number of providers"): + TriggerProviderService.add_trigger_subscription( + tenant_id="tenant-1", + user_id="user-1", + name="main", + provider_id=provider_id, + endpoint_id="endpoint-1", + credential_type=CredentialType.API_KEY, + parameters={}, + properties={}, + credentials={}, + ) + assert sum(1 for r in caplog.records if r.levelno >= logging.ERROR) == 1 def test_add_trigger_subscription_should_raise_error_when_name_exists( diff --git a/api/uv.lock b/api/uv.lock index 3445ec78321..048d991a49e 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1293,7 +1293,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "fastapi", marker = "extra == 'server'", specifier = "==0.136.0" }, - { name = "graphon", marker = "extra == 'server'", specifier = "==0.5.1" }, + { name = "graphon", marker = "extra == 'server'", specifier = "==0.5.2" }, { name = "grpclib", extras = ["protobuf"], marker = "extra == 'grpc'", specifier = ">=0.4.9,<0.5.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0,<5.0.0" }, @@ -1636,7 +1636,7 @@ requires-dist = [ { name = "gmpy2", specifier = ">=2.3.0,<3.0.0" }, { name = "google-api-python-client", specifier = ">=2.196.0,<3.0.0" }, { name = "google-cloud-aiplatform", specifier = ">=1.151.0,<2.0.0" }, - { name = "graphon", specifier = "==0.5.1" }, + { name = "graphon", specifier = "==0.5.2" }, { name = "gunicorn", specifier = ">=26.0.0,<27.0.0" }, { name = "httpx", extras = ["socks"], specifier = "==0.28.1" }, { name = "httpx-sse", specifier = "==0.4.3" }, @@ -2987,7 +2987,7 @@ httpx = [ [[package]] name = "graphon" -version = "0.5.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, @@ -3008,9 +3008,9 @@ dependencies = [ { name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] }, { name = "webvtt-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/fa/432fa802bcb13f7f51dc323ddef92594b15333eafef181d937ffa554116e/graphon-0.5.1.tar.gz", hash = "sha256:ca38cc62ef3fbc2f3072b68235bcb41e32a6369a1753b46418c1d761c57125fe", size = 269741, upload-time = "2026-06-11T03:01:38.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/16/f183da187414c335be67f52f6a1b7c2a33bf0b1d5090eda7e6c92d42d94a/graphon-0.5.2.tar.gz", hash = "sha256:d66a9edcd883766bd50e94f84a691c92ce536ea60e721552089e83ac8e94bf68", size = 269773, upload-time = "2026-06-16T04:06:22.074Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/c5/61e8634b89c320af9453083213e8be436071634dbc69cb14b5fe646763e4/graphon-0.5.1-py3-none-any.whl", hash = "sha256:70b49c244a46fb6e338905210cc895bd67584d9ab1412f6ba3cd4ed284010091", size = 381866, upload-time = "2026-06-11T03:01:36.693Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e6/36a3981cd44e7a40a7cd7d374e26f01e02dd49410c5fbbd7df248750d5fb/graphon-0.5.2-py3-none-any.whl", hash = "sha256:11f89399e67ed1ddd2ce1c336accd9c4ad5b8fe2741f9167e6085af0b325cd14", size = 381908, upload-time = "2026-06-16T04:06:20.453Z" }, ] [[package]] diff --git a/cli/test/e2e/suites/error-handling/error-messages.e2e.ts b/cli/test/e2e/suites/error-handling/error-messages.e2e.ts index 30a5d213b7d..932998d9afe 100644 --- a/cli/test/e2e/suites/error-handling/error-messages.e2e.ts +++ b/cli/test/e2e/suites/error-handling/error-messages.e2e.ts @@ -37,6 +37,7 @@ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest' import { ZERO } from '@/util/uuid.js' import { assertErrorEnvelope, + assertExitCode, assertNoAnsi, assertNonZeroExit, } from '../../helpers/assert.js' @@ -215,6 +216,147 @@ describe('E2E / error message standards (spec 5.3)', () => { expect(result.stderr).not.toContain(sentValue) }) + // ── 5.70d-h ErrorBody contract — error.server structure and rendering priorities ── + // PR #37285 introduces canonical ErrorBody on every /openapi/v1 non-2xx response. + // CLI strict-parses via zErrorBody.safeParse; success → full struct at error.server. + // + // V2 rendering priorities (format.ts, verified against codebase): + // header code : server?.code ?? cliCode — server wins, CLI fallback + // hint : cliHint ?? server?.hint — CLI wins, server fallback (V2 correction) + // details : server?.details[] — " - loc: msg (type)" per entry, no -v + + it('[P0] 5.70d JSON envelope contains error.server with canonical code/status/message', async () => { + // Trigger: describe app ZERO — server returns canonical 404 ErrorBody + // { code:"not_found", status:404, message:"app not found" }. + // zErrorBody.safeParse succeeds → error.server is populated on the current server. + const result = await fx.r(['describe', 'app', ZERO, '-o', 'json']) + assertNonZeroExit(result) + const envelope = JSON.parse(result.stderr.trim()) as { + error: { code: string, server?: { code: string, status: number, message: string } } + } + expect(envelope.error.server, 'error.server must be present when server returns canonical ErrorBody').toBeDefined() + expect(typeof envelope.error.server?.code, 'error.server.code must be a string').toBe('string') + expect(envelope.error.server?.code.length).toBeGreaterThan(0) + expect(typeof envelope.error.server?.status, 'error.server.status must be a number').toBe('number') + expect(typeof envelope.error.server?.message, 'error.server.message must be a string').toBe('string') + expect(envelope.error.server?.message.length).toBeGreaterThan(0) + }) + + it('[P1] 5.70e @accepts query validation returns canonical 422 with details array', async () => { + // Trigger: direct fetch to GET /apps?page=not-integer — @accepts(query=AppListQuery) + // validates page as int and emits canonical 422 ErrorBody with details[]. + // Direct fetch is used because the CLI validates --page as integer client-side + // (would exit 2 before hitting the server); this pins the server-side contract. + const res = await fetch( + `${E.host.replace(/\/$/, '')}/openapi/v1/apps?workspace_id=${E.workspaceId}&page=not-an-integer`, + { headers: { Authorization: `Bearer ${E.token}` }, signal: AbortSignal.timeout(8_000) }, + ) + expect(res.status).toBe(422) + const body = await res.json() as { + code?: string + status?: number + details?: Array<{ type: string, loc: Array, msg: string }> + } + expect(body.code).toBe('invalid_param') + expect(body.status).toBe(422) + expect(Array.isArray(body.details), 'details must be an array').toBe(true) + expect(body.details!.length).toBeGreaterThan(0) + const entry = body.details![0]! + expect(typeof entry.type).toBe('string') + expect(typeof entry.msg).toBe('string') + expect(Array.isArray(entry.loc)).toBe(true) + }) + + it('[P1] 5.70g rendering priority — header code: server code wins over CLI classification code', async () => { + // renderHuman: headerCode = server?.code ?? e.code (server wins, V2 unchanged) + // When canonical ErrorBody is parsed, the server semantic code replaces the CLI + // classification code ("server_4xx_other") in the human-readable output header. + // Trigger: describe app ZERO → canonical 404; header starts with "not_found:". + const result = await fx.r(['describe', 'app', ZERO]) + assertNonZeroExit(result) + expect(result.stderr.trimStart()).not.toMatch(/^server_4xx_other:/) + expect(result.stderr.trimStart()).toMatch(/^not_found:/) + }) + + it('[P1] 5.70g2 rendering priority — hint: CLI hint wins over server hint (V2 correction)', async () => { + // renderHuman: hint = cliHint ?? server?.hint (CLI wins — V2 spec correction) + // V1 incorrectly documented "server wins"; V2 aligns with codebase: CLI wins. + // Test: 401 AuthExpired — classifyResponse sets c.hint = AUTH_LOGIN_HINT before + // serverError is parsed; CLI hint takes precedence over any server-provided hint. + // Verified on current server (no @accepts deployment required). + const unauthTmp = await withTempConfig() + try { + const result = await run(['get', 'app', '-o', 'json'], { configDir: unauthTmp.configDir }) + assertExitCode(result, 4) + const envelope = JSON.parse(result.stderr.trim()) as { error: { hint?: string } } + expect(envelope.error.hint, 'CLI login hint must appear for auth error').toMatch(/auth login/i) + } + finally { + await unauthTmp.cleanup() + } + }) + + it('[P1] 5.70h JSON envelope: error.code = CLI classification; error.server.code = server semantic code', async () => { + // toEnvelope() sets error.code from HTTP status bucket (e.g. "server_4xx_other") + // while the server's semantic code is separate in error.server.code. + // Agents can branch on error.server.code without parsing human-readable text. + // Trigger: describe app ZERO → canonical 404; error.code="server_4xx_other", + // error.server.code="not_found" — always distinct when ErrorBody is present. + const result = await fx.r(['describe', 'app', ZERO, '-o', 'json']) + assertNonZeroExit(result) + const envelope = JSON.parse(result.stderr.trim()) as { + error: { code: string, server?: { code: string } } + } + expect(envelope.error.code).toBe('server_4xx_other') + expect(envelope.error.server?.code).toBeDefined() + expect(envelope.error.server?.code).not.toBe('server_4xx_other') + }) + // ── 5.70i / 5.70j PR #37285 boundary contract ─────────────────────────── + + it('[P1] 5.70i unknown /openapi/v1 route returns canonical 404 ErrorBody without route suggestions', async () => { + // PR #37285: ExternalApi._help_on_404 suppresses flask-restx route enumeration. + // Previously, an unknown path under /openapi/v1/ returned flask-restx's default + // 404 with a "Did you mean /openapi/v1/apps?" suggestion, leaking the route table. + // After the fix it must return a canonical ErrorBody and contain no suggestions. + const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/this-path-does-not-exist-e2e`, { + headers: { Authorization: `Bearer ${E.token}` }, + signal: AbortSignal.timeout(8_000), + }) + expect(res.status).toBe(404) + const body = await res.json() as Record + // canonical ErrorBody fields must be present + expect(typeof body.code, '404 body must have a string code field').toBe('string') + expect(body.status, '404 body must have status: 404').toBe(404) + // no flask-restx route enumeration in the response + const raw = JSON.stringify(body) + expect(raw).not.toMatch(/did you mean/i) + expect(raw).not.toMatch(/you might want/i) + }) + + it('[P1] 5.70j device-flow 4xx uses RFC 8628 format, not ErrorBody — zErrorBody parse fails gracefully', async () => { + // PR #37285 explicitly excludes RFC 8628 device-flow endpoints from the + // ErrorBody contract. This test pins that contract: + // - The device/token endpoint returns RFC 8628 {error: string} on failure, + // not the canonical {code, status, message} shape. + // - When the CLI's classifyResponse encounters this, zErrorBody.safeParse + // returns failure → serverError = undefined → generic status-based message, + // no error.server field, no crash. + const res = await fetch(`${E.host.replace(/\/$/, '')}/openapi/v1/oauth/device/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ device_code: 'fake-invalid-device-code-e2e-test', client_id: 'difyctl' }), + signal: AbortSignal.timeout(8_000), + }) + // device flow errors are 4xx (400 bad_request or 401 expired_token etc.) + expect(res.status).toBeGreaterThanOrEqual(400) + expect(res.status).toBeLessThan(500) + const body = await res.json() as Record + // RFC 8628 shape: has 'error' string, must NOT have ErrorBody 'code'/'status' pair + expect(typeof body.error, 'RFC 8628 body must have a string error field').toBe('string') + expect(body).not.toHaveProperty('status') + // zErrorBody.safeParse would fail → CLI sets serverError = undefined → generic message + }) + // ── 5.76 Failed command + -o yaml → stderr is still JSON envelope ──────── it('[P1] 5.76 failed command with -o yaml still outputs a JSON error envelope on stderr', async () => { diff --git a/dify-agent/Dockerfile b/dify-agent/Dockerfile new file mode 100644 index 00000000000..6d56811981d --- /dev/null +++ b/dify-agent/Dockerfile @@ -0,0 +1,68 @@ +# Dedicated image for the standalone Dify Agent backend server. +# +# It is laid out to match how the service is deployed today (see the +# agent_backend compose service): the virtualenv lives at /app/api/.venv and +# the server is started with +# cd /app/api && .venv/bin/uvicorn dify_agent.server.app:app --host 0.0.0.0 --port 5050 +# +# Unlike the dify-api image (which only installs the base `dify-agent` +# dependency), this image installs the `[server]` extra, so jwcrypto, +# shell-session-manager, fastapi, uvicorn, etc. are present and the server can +# actually start. dify-api is intentionally left lean. + +# base image +FROM python:3.12-slim-bookworm AS base + +WORKDIR /app/api + +# Install uv +ENV UV_VERSION=0.8.9 + +RUN pip install --no-cache-dir uv==${UV_VERSION} + + +FROM base AS packages + +# The build context is the repository root (see build-push.yml), so paths are +# repo-root relative. dify-agent ships its own uv.lock that resolves the server +# extra, so this builds standalone without the api project. +COPY dify-agent/pyproject.toml dify-agent/uv.lock dify-agent/README.md ./ +COPY dify-agent/src ./src +# Trust the checked-in lock during image builds and install the server extra. +RUN uv sync --frozen --no-dev --no-editable --extra server + + +# production stage +FROM base AS production + +ENV TZ=UTC +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 +ENV PYTHONIOENCODING=utf-8 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app/api + +# Create non-root user (uid matches the dify-api image convention) +ARG dify_uid=1001 +RUN groupadd -r -g ${dify_uid} dify && \ + useradd -r -u ${dify_uid} -g ${dify_uid} -s /bin/bash dify + +# Copy the resolved virtualenv. The dify-agent package is installed +# non-editable, so the source is already baked into site-packages. +ENV VIRTUAL_ENV=/app/api/.venv +COPY --from=packages --chown=dify:dify ${VIRTUAL_ENV} ${VIRTUAL_ENV} +ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" + +# storage is bind-mounted at runtime; pre-create it so the dify user can write. +RUN mkdir -p /app/api/storage && chown -R dify:dify /app/api + +ARG COMMIT_SHA +ENV COMMIT_SHA=${COMMIT_SHA} + +EXPOSE 5050 + +USER dify + +CMD ["uvicorn", "dify_agent.server.app:app", "--host", "0.0.0.0", "--port", "5050"] diff --git a/dify-agent/pyproject.toml b/dify-agent/pyproject.toml index 915114d2338..03f2525ace7 100644 --- a/dify-agent/pyproject.toml +++ b/dify-agent/pyproject.toml @@ -20,7 +20,7 @@ dify-agent-stub-server = "dify_agent.agent_stub.server.cli:main" grpc = ["grpclib[protobuf]>=0.4.9,<0.5.0", "protobuf>=6.33.5,<7.0.0"] server = [ "fastapi==0.136.0", - "graphon==0.5.1", + "graphon==0.5.2", "jsonschema>=4.23.0,<5.0.0", "jwcrypto>=1.5.6,<2", "pydantic-ai-slim[anthropic,google,openai]>=1.85.1,<2.0.0", diff --git a/dify-agent/src/dify_agent/agent_stub/cli/_drive.py b/dify-agent/src/dify_agent/agent_stub/cli/_drive.py new file mode 100644 index 00000000000..19611592c0e --- /dev/null +++ b/dify-agent/src/dify_agent/agent_stub/cli/_drive.py @@ -0,0 +1,380 @@ +"""CLI helpers for sandbox-visible Agent Stub drive commands. + +Drive commands stay in the sandbox-facing CLI because they orchestrate existing +control-plane and signed data-plane helpers. The Agent Stub server authenticates +and injects trusted drive scope; this module only formats manifest output, +downloads signed URLs into a local drive base (including safe auto-extraction of +downloaded skill archives), and uploads local files before committing their +ToolFile ids back into the drive. +""" + +from __future__ import annotations + +import stat +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from tempfile import TemporaryDirectory +from uuid import uuid4 +from zipfile import BadZipFile, ZIP_DEFLATED, ZipFile, ZipInfo + +from dify_agent.agent_stub.cli._env import read_agent_stub_environment +from dify_agent.agent_stub.cli._files import upload_tool_file_resource_from_environment +from dify_agent.agent_stub.client._agent_stub import ( + download_file_bytes_from_signed_url_sync, + request_agent_stub_drive_commit_sync, + request_agent_stub_drive_manifest_sync, +) +from dify_agent.agent_stub.client._errors import AgentStubTransferError, AgentStubValidationError +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitItem, + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveFileRef, + AgentStubDriveItem, + AgentStubDriveManifestResponse, +) + +_SKILL_MD_FILENAME = "SKILL.md" +_SKILL_ARCHIVE_FILENAME = ".DIFY-SKILL-FULL.zip" +_SKIP_DIR_NAMES = frozenset( + {".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".venv", "node_modules"} +) +_SKIP_FILE_NAMES = frozenset({".DS_Store", _SKILL_ARCHIVE_FILENAME}) + + +@dataclass(frozen=True, slots=True) +class _DriveUploadItem: + """Prepared local upload paired with its destination drive key.""" + + local_path: Path + drive_key: str + + +def list_drive_from_environment(prefix: str, json_output: bool) -> str | AgentStubDriveManifestResponse: + """List drive items through the Agent Stub using the current environment. + + Args: + prefix: Optional drive-key prefix forwarded to the manifest request. + json_output: When ``True``, return the validated manifest response model. + When ``False``, return one human-readable tab-separated line per item + containing size, mime type, hash, and key. + + Returns: + Either ``AgentStubDriveManifestResponse`` for JSON callers or a formatted + string for human-facing CLI output. + + Side effects: + Calls the Agent Stub drive manifest control-plane endpoint with + ``include_download_url=False`` so list output does not allocate signed + download URLs. + """ + + environment = read_agent_stub_environment() + response = request_agent_stub_drive_manifest_sync( + url=environment.url, + auth_jwe=environment.auth_jwe, + prefix=prefix, + include_download_url=False, + ) + if json_output: + return response + return _format_manifest(response) + + +def pull_drive_from_environment(prefix: str, drive_base: str = "/mnt/drive") -> list[Path]: + """Pull drive files into one local drive base via signed download URLs. + + Args: + prefix: Optional drive-key prefix forwarded to the manifest request. + drive_base: Local base directory that receives downloaded drive files. + + Returns: + A list of written local paths under ``drive_base``. + + Observable behavior: + Requests a manifest with ``include_download_url=True``, requires every + returned item to include ``download_url``, downloads bytes directly from + those signed URLs, blocks path traversal by resolving each destination + under the resolved drive base, writes through a temporary sibling file + before replacing the final path, validates byte length when the manifest + includes ``size``, and automatically extracts + ``.DIFY-SKILL-FULL.zip`` archives into their containing skill + directory with the same path-safety checks. Archive extraction is staged + under a temporary directory and only moved into place after the full + archive validates successfully. + + The return value remains the list of downloaded paths only; extracted + files are materialized on disk but are not added to the returned list. + + Raises: + AgentStubValidationError: if a manifest item omits ``download_url``, a + destination would escape the drive base, or a downloaded skill + archive contains unsafe entries such as absolute paths, traversal + entries, or symlink entries. + AgentStubTransferError: if a downloaded payload does not match declared + size metadata or a downloaded skill archive is corrupt / not a valid + zip file. + """ + + environment = read_agent_stub_environment() + response = request_agent_stub_drive_manifest_sync( + url=environment.url, + auth_jwe=environment.auth_jwe, + prefix=prefix, + include_download_url=True, + ) + base_path = Path(drive_base).expanduser().resolve() + base_path.mkdir(parents=True, exist_ok=True) + written_paths: list[Path] = [] + for item in response.items: + download_url = item.download_url + if not isinstance(download_url, str) or not download_url: + raise AgentStubValidationError(f"drive manifest item is missing download_url: {item.key}") + destination = _resolve_drive_destination(base_path, item.key) + payload = download_file_bytes_from_signed_url_sync(download_url=download_url) + if item.size is not None and len(payload) != item.size: + raise AgentStubTransferError(f"downloaded drive file size mismatch for {item.key}") + destination.parent.mkdir(parents=True, exist_ok=True) + temp_path = destination.with_name(f"{destination.name}.tmp-{uuid4().hex}") + temp_path.write_bytes(payload) + temp_path.replace(destination) + written_paths.append(destination) + if destination.name == _SKILL_ARCHIVE_FILENAME: + _extract_skill_archive(destination) + return written_paths + + +def push_drive_from_environment(local_path: str, drive_path: str, recursive: bool) -> AgentStubDriveCommitResponse: + """Upload local files through the Agent Stub and commit them into the drive. + + Args: + local_path: Source file or directory in the sandbox filesystem. + drive_path: Destination drive key or drive-key prefix. + recursive: Select directory mode. ``False`` standardizes skill + directories, while ``True`` uploads raw directory contents. + + Returns: + The validated drive commit response returned by the Agent Stub. + + Mode split: + * If ``local_path`` is a file, upload that file and commit exactly one + ``tool_file`` binding to ``drive_path``. + * If ``local_path`` is a directory and ``recursive`` is ``False``, + require ``SKILL.md`` and standardize the upload into + ``/SKILL.md`` plus ``/.DIFY-SKILL-FULL.zip``. + * If ``local_path`` is a directory and ``recursive`` is ``True``, upload + each regular file under ``drive_path/`` without skill + standardization. + + Observable safety behavior: + Rejects missing local paths, rejects recursive directory pushes with no + regular files, and rejects symlinked or escaping paths while preparing + directory uploads or skill archives. + """ + + source_path = Path(local_path).expanduser().resolve() + if source_path.is_file(): + return _commit_uploaded_items([_prepare_uploaded_file(source_path, drive_path)]) + if not source_path.is_dir(): + raise AgentStubValidationError(f"local path not found: {source_path}") + if recursive: + upload_items = [ + _prepare_uploaded_file(path, _join_drive_key(drive_path, relative_path)) + for path, relative_path in _iter_regular_files(source_path) + ] + if not upload_items: + raise AgentStubValidationError(f"directory has no regular files: {source_path}") + return _commit_uploaded_items(upload_items) + return _push_skill_directory(source_path, drive_path) + + +def _push_skill_directory(source_path: Path, drive_path: str) -> AgentStubDriveCommitResponse: + skill_md_path = source_path / _SKILL_MD_FILENAME + if not skill_md_path.is_file(): + raise AgentStubValidationError(f"non-recursive drive push requires {_SKILL_MD_FILENAME}: {source_path}") + with TemporaryDirectory() as temp_dir: + archive_path = Path(temp_dir) / _SKILL_ARCHIVE_FILENAME + _build_skill_archive(source_path, archive_path) + return _commit_uploaded_items( + [ + _prepare_uploaded_file(skill_md_path.resolve(), _join_drive_key(drive_path, _SKILL_MD_FILENAME)), + _prepare_uploaded_file(archive_path, _join_drive_key(drive_path, _SKILL_ARCHIVE_FILENAME)), + ] + ) + + +def _prepare_uploaded_file(local_path: Path, drive_key: str) -> _DriveUploadItem: + return _DriveUploadItem(local_path=local_path, drive_key=drive_key) + + +def _commit_uploaded_items(items: list[_DriveUploadItem]) -> AgentStubDriveCommitResponse: + environment = read_agent_stub_environment() + commit_items: list[AgentStubDriveCommitItem] = [] + for item in items: + uploaded_file = upload_tool_file_resource_from_environment(path=str(item.local_path)) + commit_items.append( + AgentStubDriveCommitItem( + key=item.drive_key, + file_ref=AgentStubDriveFileRef(kind="tool_file", id=uploaded_file.tool_file_id), + ) + ) + return request_agent_stub_drive_commit_sync( + url=environment.url, + auth_jwe=environment.auth_jwe, + request=AgentStubDriveCommitRequest(items=commit_items), + ) + + +def _format_manifest(response: AgentStubDriveManifestResponse) -> str: + return "\n".join(_format_manifest_item(item) for item in response.items) + + +def _format_manifest_item(item: AgentStubDriveItem) -> str: + size = str(item.size) if item.size is not None else "-" + mime_type = item.mime_type or "-" + item_hash = item.hash or "-" + return f"{size}\t{mime_type}\t{item_hash}\t{item.key}" + + +def _resolve_drive_destination(base_path: Path, drive_key: str) -> Path: + destination = (base_path / Path(drive_key)).resolve() + try: + destination.relative_to(base_path) + except ValueError as exc: + raise AgentStubValidationError(f"drive key resolves outside the drive base: {drive_key}") from exc + return destination + + +def _iter_regular_files(root_path: Path) -> list[tuple[Path, str]]: + """Return all regular files under one directory, rejecting unsafe symlinks.""" + + return _iter_regular_files_with_skip_filter(root_path, skip_filtered=False) + + +def _iter_skill_archive_files(root_path: Path) -> list[tuple[Path, str]]: + """Return regular files for skill packaging, excluding transient content.""" + + return _iter_regular_files_with_skip_filter(root_path, skip_filtered=True) + + +def _iter_regular_files_with_skip_filter(root_path: Path, *, skip_filtered: bool) -> list[tuple[Path, str]]: + root_resolved = root_path.resolve() + collected: list[tuple[Path, str]] = [] + for candidate in sorted(root_path.rglob("*")): + if skip_filtered and _should_skip_path(candidate, root_path): + continue + if candidate.is_symlink(): + raise AgentStubValidationError(f"drive push does not support symlinked files: {candidate}") + if not candidate.is_file(): + continue + resolved_candidate = candidate.resolve() + try: + relative_path = resolved_candidate.relative_to(root_resolved) + except ValueError as exc: + raise AgentStubValidationError( + f"drive push file resolves outside the source directory: {candidate}" + ) from exc + collected.append((resolved_candidate, relative_path.as_posix())) + return collected + + +def _should_skip_path(candidate: Path, root_path: Path) -> bool: + relative_path = candidate.relative_to(root_path) + if any(part in _SKIP_DIR_NAMES for part in relative_path.parts): + return True + return candidate.name in _SKIP_FILE_NAMES + + +def _build_skill_archive(source_path: Path, archive_path: Path) -> None: + with ZipFile(archive_path, mode="w", compression=ZIP_DEFLATED) as archive: + for file_path, relative_path in _iter_skill_archive_files(source_path): + archive.write(file_path, arcname=relative_path) + + +def _extract_skill_archive(archive_path: Path) -> None: + """Safely extract one downloaded skill archive into its containing directory. + + Extraction is staged under a temporary directory created inside the target + skill directory. Every entry is validated and materialized into staging + first, and only after the full archive succeeds are staged files moved into + their final locations under the skill directory. Existing files at those + final locations are overwritten in place by the extracted archive content. + + Error mapping is intentionally stable for CLI callers: unsafe archive entry + names raise ``AgentStubValidationError``, while malformed archives and zip + parsing / archive I/O failures are translated into ``AgentStubTransferError``. + """ + + target_dir = archive_path.parent.resolve() + try: + with TemporaryDirectory(dir=target_dir, prefix=".dify-skill-extract-") as staging_dir_name: + staging_dir = Path(staging_dir_name).resolve() + with ZipFile(archive_path) as archive: + for zip_info in archive.infolist(): + destination = _resolve_zip_entry_destination(staging_dir, zip_info.filename) + if _is_zip_symlink(zip_info): + raise AgentStubValidationError( + f"skill archive contains unsupported symlink entry: {zip_info.filename}" + ) + if zip_info.is_dir(): + destination.mkdir(parents=True, exist_ok=True) + continue + destination.parent.mkdir(parents=True, exist_ok=True) + with archive.open(zip_info) as source_file: + temp_path = destination.with_name(f"{destination.name}.tmp-{uuid4().hex}") + temp_path.write_bytes(source_file.read()) + temp_path.replace(destination) + for staged_path in sorted(staging_dir.rglob("*")): + if staged_path.is_dir(): + continue + relative_path = staged_path.relative_to(staging_dir) + destination = (target_dir / relative_path).resolve() + destination.parent.mkdir(parents=True, exist_ok=True) + staged_path.replace(destination) + except AgentStubValidationError: + raise + except (BadZipFile, OSError) as exc: + raise AgentStubTransferError(f"downloaded skill archive is invalid: {archive_path.name}") from exc + + +def _resolve_zip_entry_destination(target_dir: Path, entry_name: str) -> Path: + """Resolve one zip entry path under a target skill directory. + + Zip metadata may contain POSIX or backslash-separated names, so entry names + are normalized to forward slashes before validation. The resolved entry must + not be absolute, empty, ``.`` / ``..`` based, or otherwise escape the target + skill directory after resolution. + """ + + normalized_name = entry_name.replace("\\", "/") + pure_path = PurePosixPath(normalized_name) + if not normalized_name or normalized_name.startswith("/") or pure_path.is_absolute(): + raise AgentStubValidationError(f"skill archive contains unsafe absolute path: {entry_name}") + if any(part in {"", ".", ".."} for part in pure_path.parts): + raise AgentStubValidationError(f"skill archive contains unsafe path traversal entry: {entry_name}") + destination = (target_dir / Path(*pure_path.parts)).resolve() + try: + destination.relative_to(target_dir) + except ValueError as exc: + raise AgentStubValidationError( + f"skill archive entry resolves outside the skill directory: {entry_name}" + ) from exc + return destination + + +def _is_zip_symlink(zip_info: ZipInfo) -> bool: + file_mode = zip_info.external_attr >> 16 + return stat.S_ISLNK(file_mode) + + +def _join_drive_key(base_key: str, child_key: str) -> str: + stripped_base = base_key.rstrip("/") + stripped_child = child_key.lstrip("/") + return f"{stripped_base}/{stripped_child}" if stripped_base else stripped_child + + +__all__ = [ + "list_drive_from_environment", + "pull_drive_from_environment", + "push_drive_from_environment", +] diff --git a/dify-agent/src/dify_agent/agent_stub/cli/_files.py b/dify-agent/src/dify_agent/agent_stub/cli/_files.py index b46bc7409c2..dccc6d36c44 100644 --- a/dify-agent/src/dify_agent/agent_stub/cli/_files.py +++ b/dify-agent/src/dify_agent/agent_stub/cli/_files.py @@ -36,6 +36,14 @@ class DownloadedFileResult: path: Path +@dataclass(frozen=True, slots=True) +class UploadedToolFileResource: + """Lower-level upload result carrying both public mapping and ToolFile id.""" + + mapping: UploadedToolFileMapping + tool_file_id: str + + def upload_file_from_environment(*, path: str) -> UploadedToolFileMapping: """Upload one sandbox-local file through the Agent Stub control plane. @@ -44,6 +52,23 @@ def upload_file_from_environment(*, path: str) -> UploadedToolFileMapping: canonical Agent output file mapping without synthesizing reference format. """ + return upload_tool_file_resource_from_environment(path=path).mapping + + +def upload_tool_file_resource_from_environment(*, path: str) -> UploadedToolFileResource: + """Upload one sandbox-local file and preserve both reference and ToolFile id. + + This lower-level helper backs ``drive push``. The signed upload data-plane + response must include both the canonical Dify ``reference`` used by public + CLI output and the raw ToolFile ``id`` required by drive commit payloads. + + Raises: + AgentStubValidationError: if ``path`` does not resolve to a local file. + AgentStubTransferError: if the signed upload response omits either the + canonical ``reference`` or the raw ToolFile ``id``, or if the + canonical reference is malformed. + """ + source_path = Path(path).expanduser().resolve() if not source_path.is_file(): raise AgentStubValidationError(f"local file not found: {source_path}") @@ -64,7 +89,7 @@ def upload_file_from_environment(*, path: str) -> UploadedToolFileMapping: file_obj=file_obj, mimetype=mime_type, ) - return _normalize_uploaded_tool_file(payload) + return _normalize_uploaded_tool_file_resource(payload) def download_file_from_environment( @@ -101,13 +126,19 @@ def download_file_from_environment( return DownloadedFileResult(path=destination) -def _normalize_uploaded_tool_file(payload: dict[str, object]) -> UploadedToolFileMapping: +def _normalize_uploaded_tool_file_resource(payload: dict[str, object]) -> UploadedToolFileResource: reference = payload.get("reference") if not isinstance(reference, str) or not reference: raise AgentStubTransferError("signed file upload response is missing reference") if not is_canonical_dify_file_reference(reference): raise AgentStubTransferError("signed file upload response has invalid canonical reference") - return UploadedToolFileMapping(reference=reference) + tool_file_id = payload.get("id") + if not isinstance(tool_file_id, str) or not tool_file_id: + raise AgentStubTransferError("signed file upload response is missing id") + return UploadedToolFileResource( + mapping=UploadedToolFileMapping(reference=reference), + tool_file_id=tool_file_id, + ) def _deduplicate_destination_path(path: Path) -> Path: @@ -134,6 +165,8 @@ def _sanitize_download_filename(filename: str) -> str: __all__ = [ "DownloadedFileResult", "UploadedToolFileMapping", + "UploadedToolFileResource", "download_file_from_environment", "upload_file_from_environment", + "upload_tool_file_resource_from_environment", ] diff --git a/dify-agent/src/dify_agent/agent_stub/cli/main.py b/dify-agent/src/dify_agent/agent_stub/cli/main.py index 87bf870f7cd..466a53339e7 100644 --- a/dify-agent/src/dify_agent/agent_stub/cli/main.py +++ b/dify-agent/src/dify_agent/agent_stub/cli/main.py @@ -1,11 +1,11 @@ """Typer entry point for the client-safe ``dify-agent`` console script. -The CLI supports an explicit ``connect`` command and treats unknown bare -commands as Agent Stub forwards. When the injected Agent Stub environment -variables are missing, that path intentionally surfaces a clear missing-env -error instead of Typer's generic unknown-command message. The module depends -only on client-safe code so importing the console entry point does not pull in -FastAPI, Redis, shellctl, or JWE runtime dependencies. +The CLI supports explicit ``connect``, ``file``, and ``drive`` commands and +treats unknown bare commands as Agent Stub forwards. When the injected Agent +Stub environment variables are missing, that path intentionally surfaces a +clear missing-env error instead of Typer's generic unknown-command message. The +module depends only on client-safe code so importing the console entry point +does not pull in FastAPI, Redis, shellctl, or JWE runtime dependencies. """ from __future__ import annotations @@ -16,6 +16,11 @@ import typer from typer.main import get_command from dify_agent.agent_stub.cli._agent_stub import connect_from_environment +from dify_agent.agent_stub.cli._drive import ( + list_drive_from_environment, + pull_drive_from_environment, + push_drive_from_environment, +) from dify_agent.agent_stub.cli._env import MissingAgentStubEnvironmentError, has_agent_stub_environment from dify_agent.agent_stub.cli._files import download_file_from_environment, upload_file_from_environment from dify_agent.agent_stub.client._errors import AgentStubClientError @@ -27,8 +32,10 @@ app = typer.Typer( no_args_is_help=True, ) file_app = typer.Typer(help="Upload or download workflow files through the Agent Stub.") +drive_app = typer.Typer(help="List, pull, or push agent drive files through the Agent Stub.") app.add_typer(file_app, name="file") -_KNOWN_ROOT_COMMANDS = frozenset({"connect", "file"}) +app.add_typer(drive_app, name="drive") +_KNOWN_ROOT_COMMANDS = frozenset({"connect", "drive", "file"}) @app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) @@ -60,6 +67,34 @@ def download( ) +@drive_app.command("list") +def drive_list( + path_prefix: str = typer.Argument("", metavar="PATH_PREFIX"), + json_output: bool = typer.Option(False, "--json", help="Emit the drive manifest as JSON."), +) -> None: + """List drive files visible to the current sandbox execution.""" + _run_drive_list(path_prefix=path_prefix, json_output=json_output) + + +@drive_app.command("pull") +def drive_pull( + path_prefix: str = typer.Argument("", metavar="PATH_PREFIX"), + drive_base: str = typer.Option("/mnt/drive", "--drive-base", help="Local base directory for pulled drive files."), +) -> None: + """Pull drive files into one local directory tree.""" + _run_drive_pull(path_prefix=path_prefix, drive_base=drive_base) + + +@drive_app.command("push") +def drive_push( + local_path: str = typer.Argument(..., metavar="LOCAL_PATH"), + drive_path: str = typer.Argument(..., metavar="DRIVE_PATH"), + recursive: bool = typer.Option(False, "-r", "--recursive", help="Recursively upload directory contents."), +) -> None: + """Upload one local file or directory into the agent drive.""" + _run_drive_push(local_path=local_path, drive_path=drive_path, recursive=recursive) + + def main(argv: list[str] | None = None) -> None: """Run the ``dify-agent`` CLI with optional argv injection for tests.""" args = list(sys.argv[1:] if argv is None else argv) @@ -155,4 +190,46 @@ def _run_file_download(*, transfer_method: str, reference_or_url: str, directory typer.echo(str(response.path)) +def _run_drive_list(*, path_prefix: str, json_output: bool) -> None: + try: + response = list_drive_from_environment(prefix=path_prefix, json_output=json_output) + except MissingAgentStubEnvironmentError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(2) from exc + except AgentStubClientError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(1) from exc + if json_output: + if isinstance(response, str): + raise RuntimeError("drive list JSON output expected a manifest response") + typer.echo(response.model_dump_json()) + return + typer.echo(response) + + +def _run_drive_pull(*, path_prefix: str, drive_base: str) -> None: + try: + response = pull_drive_from_environment(prefix=path_prefix, drive_base=drive_base) + except MissingAgentStubEnvironmentError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(2) from exc + except AgentStubClientError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(1) from exc + for path in response: + typer.echo(str(path)) + + +def _run_drive_push(*, local_path: str, drive_path: str, recursive: bool) -> None: + try: + response = push_drive_from_environment(local_path=local_path, drive_path=drive_path, recursive=recursive) + except MissingAgentStubEnvironmentError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(2) from exc + except AgentStubClientError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(1) from exc + typer.echo(response.model_dump_json()) + + __all__ = ["app", "main"] diff --git a/dify-agent/src/dify_agent/agent_stub/client/_agent_stub.py b/dify-agent/src/dify_agent/agent_stub/client/_agent_stub.py index 9c546cdbd05..34b54cabaca 100644 --- a/dify-agent/src/dify_agent/agent_stub/client/_agent_stub.py +++ b/dify-agent/src/dify_agent/agent_stub/client/_agent_stub.py @@ -8,12 +8,18 @@ from pydantic import JsonValue from dify_agent.agent_stub.client._agent_stub_http import ( connect_agent_stub_http_sync, download_file_bytes_from_signed_url_sync, + request_agent_stub_drive_commit_http_sync, + request_agent_stub_drive_manifest_http_sync, request_agent_stub_file_download_http_sync, request_agent_stub_file_upload_http_sync, upload_file_to_signed_url_sync, ) from dify_agent.agent_stub.client._errors import AgentStubValidationError -from dify_agent.agent_stub.protocol.agent_stub import AgentStubFileMapping, parse_agent_stub_endpoint +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitRequest, + AgentStubFileMapping, + parse_agent_stub_endpoint, +) def connect_agent_stub_sync( @@ -106,6 +112,60 @@ def request_agent_stub_file_download_sync( ) +def request_agent_stub_drive_manifest_sync( + *, + url: str, + auth_jwe: str, + prefix: str, + include_download_url: bool, + timeout: float | httpx.Timeout = 30.0, + sync_http_client: httpx.Client | None = None, +): + """Request one drive manifest through the HTTP Agent Stub transport. + + Drive operations are intentionally HTTP-only in this stage. Callers must + provide an ``http://`` or ``https://`` Agent Stub URL; ``grpc://`` endpoints + raise ``AgentStubValidationError`` instead of attempting transport fallback. + """ + endpoint = _parse_endpoint(url) + if endpoint.is_grpc: + raise AgentStubValidationError("Agent Stub drive operations require an HTTP Agent Stub URL") + return request_agent_stub_drive_manifest_http_sync( + base_url=endpoint.url, + auth_jwe=auth_jwe, + prefix=prefix, + include_download_url=include_download_url, + timeout=timeout, + sync_http_client=sync_http_client, + ) + + +def request_agent_stub_drive_commit_sync( + *, + url: str, + auth_jwe: str, + request: AgentStubDriveCommitRequest, + timeout: float | httpx.Timeout = 30.0, + sync_http_client: httpx.Client | None = None, +): + """Commit one drive batch through the HTTP Agent Stub transport. + + Drive operations are intentionally HTTP-only in this stage. Callers must + provide an ``http://`` or ``https://`` Agent Stub URL; ``grpc://`` endpoints + raise ``AgentStubValidationError`` instead of attempting transport fallback. + """ + endpoint = _parse_endpoint(url) + if endpoint.is_grpc: + raise AgentStubValidationError("Agent Stub drive operations require an HTTP Agent Stub URL") + return request_agent_stub_drive_commit_http_sync( + base_url=endpoint.url, + auth_jwe=auth_jwe, + request=request, + timeout=timeout, + sync_http_client=sync_http_client, + ) + + def _parse_endpoint(url: str): try: return parse_agent_stub_endpoint(url) @@ -116,6 +176,8 @@ def _parse_endpoint(url: str): __all__ = [ "connect_agent_stub_sync", "download_file_bytes_from_signed_url_sync", + "request_agent_stub_drive_commit_sync", + "request_agent_stub_drive_manifest_sync", "request_agent_stub_file_download_sync", "request_agent_stub_file_upload_sync", "upload_file_to_signed_url_sync", diff --git a/dify-agent/src/dify_agent/agent_stub/client/_agent_stub_http.py b/dify-agent/src/dify_agent/agent_stub/client/_agent_stub_http.py index f3ea57d3cd6..9c80e2d060d 100644 --- a/dify-agent/src/dify_agent/agent_stub/client/_agent_stub_http.py +++ b/dify-agent/src/dify_agent/agent_stub/client/_agent_stub_http.py @@ -24,12 +24,17 @@ from dify_agent.agent_stub.client._errors import ( from dify_agent.agent_stub.protocol.agent_stub import ( AgentStubConnectRequest, AgentStubConnectResponse, + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveManifestResponse, AgentStubFileDownloadRequest, AgentStubFileDownloadResponse, AgentStubFileMapping, AgentStubFileUploadRequest, AgentStubFileUploadResponse, agent_stub_connections_url, + agent_stub_drive_commit_url, + agent_stub_drive_manifest_url, agent_stub_file_download_request_url, agent_stub_file_upload_request_url, ) @@ -124,6 +129,56 @@ def request_agent_stub_file_download_http_sync( ) +def request_agent_stub_drive_manifest_http_sync( + *, + base_url: str, + auth_jwe: str, + prefix: str, + include_download_url: bool, + timeout: float | httpx.Timeout = 30.0, + sync_http_client: httpx.Client | None = None, +) -> AgentStubDriveManifestResponse: + """Request one drive manifest from the HTTP Agent Stub endpoint.""" + + response = _get_agent_stub_json( + base_url=base_url, + auth_jwe=auth_jwe, + endpoint_name="drive manifest request", + endpoint_url_factory=agent_stub_drive_manifest_url, + params={ + "prefix": prefix, + "include_download_url": str(include_download_url).lower(), + }, + timeout=timeout, + sync_http_client=sync_http_client, + ) + return _parse_success_response( + response=response, response_model=AgentStubDriveManifestResponse, label="drive manifest" + ) + + +def request_agent_stub_drive_commit_http_sync( + *, + base_url: str, + auth_jwe: str, + request: AgentStubDriveCommitRequest, + timeout: float | httpx.Timeout = 30.0, + sync_http_client: httpx.Client | None = None, +) -> AgentStubDriveCommitResponse: + """Commit one drive batch through the HTTP Agent Stub endpoint.""" + + response = _post_agent_stub_json( + base_url=base_url, + auth_jwe=auth_jwe, + endpoint_name="drive commit request", + endpoint_url_factory=agent_stub_drive_commit_url, + request_body=request.model_dump_json(exclude_none=True), + timeout=timeout, + sync_http_client=sync_http_client, + ) + return _parse_success_response(response=response, response_model=AgentStubDriveCommitResponse, label="drive commit") + + def upload_file_to_signed_url_sync( *, upload_url: str, @@ -229,6 +284,38 @@ def _post_agent_stub_json( client.close() +def _get_agent_stub_json( + *, + base_url: str, + auth_jwe: str, + endpoint_name: str, + endpoint_url_factory: Callable[[str], str], + params: dict[str, str], + timeout: float | httpx.Timeout, + sync_http_client: httpx.Client | None, +) -> httpx.Response: + try: + endpoint_url = endpoint_url_factory(base_url) + except ValueError as exc: + raise AgentStubValidationError("invalid Agent Stub base URL") from exc + owns_client = sync_http_client is None + client = sync_http_client or httpx.Client(timeout=timeout, follow_redirects=True) + try: + return client.get( + endpoint_url, + params=params, + headers={"Authorization": f"Bearer {auth_jwe}"}, + timeout=timeout, + ) + except httpx.TimeoutException as exc: + raise AgentStubClientError(f"Agent Stub {endpoint_name} timed out") from exc + except httpx.RequestError as exc: + raise AgentStubClientError(f"Agent Stub {endpoint_name} request failed: {exc}") from exc + finally: + if owns_client: + client.close() + + def _parse_success_response[T: BaseModel]( *, response: httpx.Response, @@ -257,6 +344,8 @@ def _parse_json_payload(response: httpx.Response, *, invalid_json_message: str) __all__ = [ "connect_agent_stub_http_sync", "download_file_bytes_from_signed_url_sync", + "request_agent_stub_drive_commit_http_sync", + "request_agent_stub_drive_manifest_http_sync", "request_agent_stub_file_download_http_sync", "request_agent_stub_file_upload_http_sync", "upload_file_to_signed_url_sync", diff --git a/dify-agent/src/dify_agent/agent_stub/protocol/__init__.py b/dify-agent/src/dify_agent/agent_stub/protocol/__init__.py index c9b4d429e14..18ea874a2a6 100644 --- a/dify-agent/src/dify_agent/agent_stub/protocol/__init__.py +++ b/dify-agent/src/dify_agent/agent_stub/protocol/__init__.py @@ -6,6 +6,12 @@ from .agent_stub import ( AGENT_STUB_URL_ENV_VAR, AgentStubConnectRequest, AgentStubConnectResponse, + AgentStubDriveCommitItem, + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveFileRef, + AgentStubDriveItem, + AgentStubDriveManifestResponse, AgentStubEndpoint, AgentStubFileDownloadRequest, AgentStubFileDownloadResponse, @@ -14,6 +20,8 @@ from .agent_stub import ( AgentStubFileUploadResponse, AgentStubURLScheme, agent_stub_connections_url, + agent_stub_drive_commit_url, + agent_stub_drive_manifest_url, agent_stub_file_download_request_url, agent_stub_file_upload_request_url, is_canonical_dify_file_reference, @@ -27,6 +35,12 @@ __all__ = [ "AGENT_STUB_URL_ENV_VAR", "AgentStubConnectRequest", "AgentStubConnectResponse", + "AgentStubDriveCommitItem", + "AgentStubDriveCommitRequest", + "AgentStubDriveCommitResponse", + "AgentStubDriveFileRef", + "AgentStubDriveItem", + "AgentStubDriveManifestResponse", "AgentStubEndpoint", "AgentStubFileDownloadRequest", "AgentStubFileDownloadResponse", @@ -35,6 +49,8 @@ __all__ = [ "AgentStubFileUploadResponse", "AgentStubURLScheme", "agent_stub_connections_url", + "agent_stub_drive_commit_url", + "agent_stub_drive_manifest_url", "agent_stub_file_download_request_url", "agent_stub_file_upload_request_url", "is_canonical_dify_file_reference", diff --git a/dify-agent/src/dify_agent/agent_stub/protocol/agent_stub.py b/dify-agent/src/dify_agent/agent_stub/protocol/agent_stub.py index 1a98778f9e0..16d5fa79532 100644 --- a/dify-agent/src/dify_agent/agent_stub/protocol/agent_stub.py +++ b/dify-agent/src/dify_agent/agent_stub/protocol/agent_stub.py @@ -115,6 +115,16 @@ def agent_stub_file_download_request_url(base_url: str) -> str: return f"{_require_http_base_url(base_url)}/files/download-request" +def agent_stub_drive_manifest_url(base_url: str) -> str: + """Return the stable HTTP drive-manifest endpoint URL for one base URL.""" + return f"{_require_http_base_url(base_url)}/drive/manifest" + + +def agent_stub_drive_commit_url(base_url: str) -> str: + """Return the stable HTTP drive-commit endpoint URL for one base URL.""" + return f"{_require_http_base_url(base_url)}/drive/commit" + + def is_canonical_dify_file_reference(reference: str) -> bool: """Return whether one string matches Dify's opaque file reference format.""" prefix = "dify-file-ref:" @@ -210,6 +220,65 @@ class AgentStubFileDownloadResponse(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") +class AgentStubDriveFileRef(BaseModel): + """Trusted file reference used by Agent Stub drive commit requests.""" + + kind: Literal["upload_file", "tool_file"] + id: str + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class AgentStubDriveCommitItem(BaseModel): + """One drive key to file binding committed through the Agent Stub.""" + + key: str + file_ref: AgentStubDriveFileRef + value_owned_by_drive: bool = True + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class AgentStubDriveCommitRequest(BaseModel): + """Request body for one Agent Stub drive commit batch.""" + + items: list[AgentStubDriveCommitItem] + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class AgentStubDriveItem(BaseModel): + """One manifest or commit item returned by the Agent Stub drive API.""" + + key: str + size: int | None = None + hash: str | None = None + mime_type: str | None = None + file_kind: Literal["upload_file", "tool_file"] + file_id: str + created_at: int | None = None + download_url: str | None = None + value_owned_by_drive: bool | None = None + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class AgentStubDriveManifestResponse(BaseModel): + """Response body for one Agent Stub drive manifest request.""" + + items: list[AgentStubDriveItem] + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class AgentStubDriveCommitResponse(BaseModel): + """Response body for one Agent Stub drive commit request.""" + + items: list[AgentStubDriveItem] + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + def _require_http_base_url(base_url: str) -> str: endpoint = parse_agent_stub_endpoint(base_url) if not endpoint.is_http: @@ -228,6 +297,12 @@ __all__ = [ "AgentStubConnectRequest", "AgentStubConnectResponse", "AgentStubEndpoint", + "AgentStubDriveCommitItem", + "AgentStubDriveCommitRequest", + "AgentStubDriveCommitResponse", + "AgentStubDriveFileRef", + "AgentStubDriveItem", + "AgentStubDriveManifestResponse", "AgentStubFileDownloadRequest", "AgentStubFileDownloadResponse", "AgentStubFileMapping", @@ -235,6 +310,8 @@ __all__ = [ "AgentStubFileUploadResponse", "AgentStubURLScheme", "agent_stub_connections_url", + "agent_stub_drive_commit_url", + "agent_stub_drive_manifest_url", "agent_stub_file_download_request_url", "agent_stub_file_upload_request_url", "is_canonical_dify_file_reference", diff --git a/dify-agent/src/dify_agent/agent_stub/server/agent_stub_drive.py b/dify-agent/src/dify_agent/agent_stub/server/agent_stub_drive.py new file mode 100644 index 00000000000..463cbd4a706 --- /dev/null +++ b/dify-agent/src/dify_agent/agent_stub/server/agent_stub_drive.py @@ -0,0 +1,190 @@ +"""Server-side Dify API client for Agent Stub drive endpoints. + +The Agent Stub drive API is an HTTP-only control plane over the existing Dify +agent drive inner APIs. Sandbox callers never send trusted tenant, agent, or +user ids directly; this module receives an authenticated ``AgentStubPrincipal``, +derives ``agent-`` from execution context, injects trusted identity +fields into the Dify inner request, and normalizes transport, HTTP, JSON, and +schema failures into ``AgentStubDriveRequestError`` for the route layer. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Protocol + +import httpx +from pydantic import ValidationError + +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveManifestResponse, +) +from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubPrincipal +from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig + + +class AgentStubDriveRequestHandler(Protocol): + """Trusted control-plane bridge from sandbox drive calls to Dify inner APIs.""" + + async def get_manifest( + self, + *, + principal: AgentStubPrincipal, + prefix: str, + include_download_url: bool, + ) -> AgentStubDriveManifestResponse: ... + + async def commit( + self, + *, + principal: AgentStubPrincipal, + request: AgentStubDriveCommitRequest, + ) -> AgentStubDriveCommitResponse: ... + + +class AgentStubDriveRequestError(RuntimeError): + """Raised when the Agent Stub cannot complete one drive control-plane call.""" + + status_code: int + detail: object + + def __init__(self, status_code: int, detail: object) -> None: + self.status_code = status_code + self.detail = detail + super().__init__(str(detail)) + + +@dataclass(slots=True) +class DifyApiAgentStubDriveRequestHandler: + """Call Dify API inner drive endpoints on behalf of authenticated sandboxes. + + Manifest requests require ``tenant_id`` and ``agent_id`` from execution + context and forward query parameters to + ``/inner/api/drive/agent-/manifest``. Commit requests additionally + require ``user_id`` and post a raw JSON payload to + ``/inner/api/drive/agent-/commit``. Dify drive endpoints return + raw ``{"items": [...]}`` payloads instead of plugin-style ``data`` envelopes, + so this module validates the raw success payload directly. + """ + + dify_api_base_url: str + dify_api_inner_api_key: str + timeout: httpx.Timeout | float = 30.0 + + async def get_manifest( + self, + *, + principal: AgentStubPrincipal, + prefix: str, + include_download_url: bool, + ) -> AgentStubDriveManifestResponse: + """Request one drive manifest from Dify's inner drive manifest endpoint.""" + execution_context = self._require_agent_context(principal.execution_context) + payload = await self._get_inner_api( + f"/inner/api/drive/{self._drive_ref(execution_context)}/manifest", + { + "tenant_id": execution_context.tenant_id, + "prefix": prefix, + "include_download_url": str(include_download_url).lower(), + }, + ) + try: + return AgentStubDriveManifestResponse.model_validate(payload) + except ValidationError as exc: + raise AgentStubDriveRequestError(502, "Dify API drive manifest response is invalid") from exc + + async def commit( + self, + *, + principal: AgentStubPrincipal, + request: AgentStubDriveCommitRequest, + ) -> AgentStubDriveCommitResponse: + """Commit one drive batch through Dify's inner drive commit endpoint.""" + execution_context = self._require_user_context(self._require_agent_context(principal.execution_context)) + payload = await self._post_inner_api( + f"/inner/api/drive/{self._drive_ref(execution_context)}/commit", + { + "tenant_id": execution_context.tenant_id, + "user_id": execution_context.user_id, + "items": [item.model_dump(mode="json", exclude_none=True) for item in request.items], + }, + ) + try: + return AgentStubDriveCommitResponse.model_validate(payload) + except ValidationError as exc: + raise AgentStubDriveRequestError(502, "Dify API drive commit response is invalid") from exc + + def _require_agent_context( + self, execution_context: DifyExecutionContextLayerConfig + ) -> DifyExecutionContextLayerConfig: + if execution_context.agent_id is None: + raise AgentStubDriveRequestError(400, "execution context agent_id is required for drive operations") + return execution_context + + def _require_user_context( + self, execution_context: DifyExecutionContextLayerConfig + ) -> DifyExecutionContextLayerConfig: + if execution_context.user_id is None: + raise AgentStubDriveRequestError(400, "execution context user_id is required for drive commit") + return execution_context + + @staticmethod + def _drive_ref(execution_context: DifyExecutionContextLayerConfig) -> str: + agent_id = execution_context.agent_id + if agent_id is None: + raise AgentStubDriveRequestError(400, "execution context agent_id is required for drive operations") + return f"agent-{agent_id}" + + async def _get_inner_api(self, path: str, params: Mapping[str, str]) -> object: + url = f"{self.dify_api_base_url.rstrip('/')}{path}" + async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True, trust_env=False) as client: + try: + response = await client.get( + url, + params=dict(params), + headers={"X-Inner-Api-Key": self.dify_api_inner_api_key}, + ) + except httpx.TimeoutException as exc: + raise AgentStubDriveRequestError(504, "Dify API drive request timed out") from exc + except httpx.RequestError as exc: + raise AgentStubDriveRequestError(502, f"Dify API drive request failed: {exc}") from exc + return self._normalize_payload(response) + + async def _post_inner_api(self, path: str, payload: Mapping[str, Any]) -> object: + url = f"{self.dify_api_base_url.rstrip('/')}{path}" + async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True, trust_env=False) as client: + try: + response = await client.post( + url, + json=dict(payload), + headers={"X-Inner-Api-Key": self.dify_api_inner_api_key}, + ) + except httpx.TimeoutException as exc: + raise AgentStubDriveRequestError(504, "Dify API drive request timed out") from exc + except httpx.RequestError as exc: + raise AgentStubDriveRequestError(502, f"Dify API drive request failed: {exc}") from exc + return self._normalize_payload(response) + + def _normalize_payload(self, response: httpx.Response) -> object: + raw_payload = self._parse_json(response) + if response.is_error: + detail = raw_payload.get("detail", raw_payload) if isinstance(raw_payload, dict) else raw_payload + raise AgentStubDriveRequestError(response.status_code, detail) + return raw_payload + + @staticmethod + def _parse_json(response: httpx.Response) -> object: + try: + return response.json() + except ValueError as exc: + raise AgentStubDriveRequestError(502, "Dify API drive request returned invalid JSON") from exc + + +__all__ = [ + "AgentStubDriveRequestError", + "AgentStubDriveRequestHandler", + "DifyApiAgentStubDriveRequestHandler", +] diff --git a/dify-agent/src/dify_agent/agent_stub/server/app.py b/dify-agent/src/dify_agent/agent_stub/server/app.py index fa329cd4765..8467dea79f4 100644 --- a/dify-agent/src/dify_agent/agent_stub/server/app.py +++ b/dify-agent/src/dify_agent/agent_stub/server/app.py @@ -2,8 +2,9 @@ The standalone stub server is only a convenience wrapper around the shared router. It reuses the main ``ServerSettings`` model and derives the Agent Stub -token codec and optional file-request bridge from the same helper methods that -the standard run server uses before mounting ``create_agent_stub_router(...)``. +token codec plus optional file and drive request bridges from the same helper +methods that the standard run server uses before mounting +``create_agent_stub_router(...)``. """ from __future__ import annotations @@ -22,6 +23,7 @@ def create_agent_stub_app(settings: ServerSettings | None = None) -> FastAPI: create_agent_stub_router( token_codec=resolved_settings.create_agent_stub_token_codec(), file_request_handler=resolved_settings.create_agent_stub_file_request_handler(), + drive_request_handler=resolved_settings.create_agent_stub_drive_request_handler(), ) ) return app diff --git a/dify-agent/src/dify_agent/agent_stub/server/control_plane.py b/dify-agent/src/dify_agent/agent_stub/server/control_plane.py index 89c9b2e42da..ef747bd6d0e 100644 --- a/dify-agent/src/dify_agent/agent_stub/server/control_plane.py +++ b/dify-agent/src/dify_agent/agent_stub/server/control_plane.py @@ -8,11 +8,15 @@ from uuid import uuid4 from dify_agent.agent_stub.protocol.agent_stub import ( AgentStubConnectResponse, + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveManifestResponse, AgentStubFileDownloadRequest, AgentStubFileDownloadResponse, AgentStubFileUploadRequest, AgentStubFileUploadResponse, ) +from dify_agent.agent_stub.server.agent_stub_drive import AgentStubDriveRequestError, AgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import AgentStubFileRequestError, AgentStubFileRequestHandler from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubPrincipal, AgentStubTokenCodec, AgentStubTokenError @@ -43,11 +47,12 @@ class AgentStubControlPlaneService: HTTP and gRPC adapters validate or decode transport payloads before calling this service, so this layer focuses only on shared auth, connection-id - generation, and file-request delegation. + generation, plus file and drive request delegation. """ token_codec: AgentStubTokenCodec | None file_request_handler: AgentStubFileRequestHandler | None = None + drive_request_handler: AgentStubDriveRequestHandler | None = None connection_id_factory: Callable[[], str] = field(default=lambda: str(uuid4())) async def connect(self, *, authorization: str | None) -> AgentStubConnectResponse: @@ -83,6 +88,39 @@ class AgentStubControlPlaneService: except AgentStubFileRequestError as exc: raise AgentStubControlPlaneError(exc.status_code, exc.detail) from exc + async def get_drive_manifest( + self, + *, + prefix: str, + include_download_url: bool, + authorization: str | None, + ) -> AgentStubDriveManifestResponse: + """Authenticate and delegate one drive manifest request.""" + principal = self._authenticate(authorization) + handler = self._require_drive_request_handler() + try: + return await handler.get_manifest( + principal=principal, + prefix=prefix, + include_download_url=include_download_url, + ) + except AgentStubDriveRequestError as exc: + raise AgentStubControlPlaneError(exc.status_code, exc.detail) from exc + + async def commit_drive( + self, + *, + request: AgentStubDriveCommitRequest, + authorization: str | None, + ) -> AgentStubDriveCommitResponse: + """Authenticate and delegate one drive commit request.""" + principal = self._authenticate(authorization) + handler = self._require_drive_request_handler() + try: + return await handler.commit(principal=principal, request=request) + except AgentStubDriveRequestError as exc: + raise AgentStubControlPlaneError(exc.status_code, exc.detail) from exc + def _authenticate(self, authorization: str | None) -> AgentStubPrincipal: token_codec = self.token_codec if token_codec is None: @@ -97,6 +135,11 @@ class AgentStubControlPlaneService: raise AgentStubConfigurationError(503, "Agent Stub file API is not configured") return self.file_request_handler + def _require_drive_request_handler(self) -> AgentStubDriveRequestHandler: + if self.drive_request_handler is None: + raise AgentStubConfigurationError(503, "Agent Stub drive API is not configured") + return self.drive_request_handler + __all__ = [ "AgentStubAuthenticationError", diff --git a/dify-agent/src/dify_agent/agent_stub/server/router.py b/dify-agent/src/dify_agent/agent_stub/server/router.py index dadde9b053a..fa506ab0188 100644 --- a/dify-agent/src/dify_agent/agent_stub/server/router.py +++ b/dify-agent/src/dify_agent/agent_stub/server/router.py @@ -1,17 +1,18 @@ """Embeddable router factory for Dify Agent stub endpoints. Both the standalone stub server and the standard run server mount the same -router so the Agent Stub protocol, token validation, and file-control-plane -behavior stay identical regardless of hosting mode. The factory is intentionally -settings-agnostic: callers must pass already constructed token-codec and file -handler dependencies rather than having this module read environment variables -or import server settings directly. +router so the Agent Stub protocol, token validation, and file/drive +control-plane behavior stay identical regardless of hosting mode. The factory is +intentionally settings-agnostic: callers must pass already constructed +token-codec and request-handler dependencies rather than having this module read +environment variables or import server settings directly. """ from __future__ import annotations from fastapi import APIRouter +from dify_agent.agent_stub.server.agent_stub_drive import AgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import AgentStubFileRequestHandler from dify_agent.agent_stub.server.routes.agent_stub import create_agent_stub_http_router from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec @@ -21,9 +22,10 @@ def create_agent_stub_router( *, token_codec: AgentStubTokenCodec | None, file_request_handler: AgentStubFileRequestHandler | None = None, + drive_request_handler: AgentStubDriveRequestHandler | None = None, ) -> APIRouter: """Build the embeddable stub router from pre-built server dependencies.""" - return create_agent_stub_http_router(token_codec, file_request_handler) + return create_agent_stub_http_router(token_codec, file_request_handler, drive_request_handler) __all__ = ["create_agent_stub_router"] diff --git a/dify-agent/src/dify_agent/agent_stub/server/routes/agent_stub.py b/dify-agent/src/dify_agent/agent_stub/server/routes/agent_stub.py index cdadb738642..72077c88545 100644 --- a/dify-agent/src/dify_agent/agent_stub/server/routes/agent_stub.py +++ b/dify-agent/src/dify_agent/agent_stub/server/routes/agent_stub.py @@ -2,8 +2,8 @@ The router is a thin HTTP adapter around ``AgentStubControlPlaneService``. It keeps FastAPI-specific request parsing and HTTPException translation here while -sharing auth, DTO validation, connection-id generation, and file delegation with -the gRPC transport. +sharing auth, DTO validation, connection-id generation, and file/drive +delegation with the gRPC transport. """ from __future__ import annotations @@ -13,11 +13,15 @@ from fastapi import APIRouter, Header, HTTPException from dify_agent.agent_stub.protocol.agent_stub import ( AgentStubConnectRequest, AgentStubConnectResponse, + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveManifestResponse, AgentStubFileDownloadRequest, AgentStubFileDownloadResponse, AgentStubFileUploadRequest, AgentStubFileUploadResponse, ) +from dify_agent.agent_stub.server.agent_stub_drive import AgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import AgentStubFileRequestHandler from dify_agent.agent_stub.server.control_plane import AgentStubControlPlaneError, AgentStubControlPlaneService from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec @@ -26,10 +30,11 @@ from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec def create_agent_stub_http_router( token_codec: AgentStubTokenCodec | None, file_request_handler: AgentStubFileRequestHandler | None = None, + drive_request_handler: AgentStubDriveRequestHandler | None = None, ) -> APIRouter: """Create HTTP routes bound to the application's Agent Stub dependencies.""" router = APIRouter(prefix="/agent-stub", tags=["agent-stub"]) - service = AgentStubControlPlaneService(token_codec, file_request_handler) + service = AgentStubControlPlaneService(token_codec, file_request_handler, drive_request_handler) @router.post("/connections", response_model=AgentStubConnectResponse) async def create_connection( @@ -62,6 +67,31 @@ def create_agent_stub_http_router( except AgentStubControlPlaneError as exc: raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc + @router.get("/drive/manifest", response_model=AgentStubDriveManifestResponse) + async def get_drive_manifest( + prefix: str = "", + include_download_url: bool = False, + authorization: str | None = Header(default=None, alias="Authorization"), + ) -> AgentStubDriveManifestResponse: + try: + return await service.get_drive_manifest( + prefix=prefix, + include_download_url=include_download_url, + authorization=authorization, + ) + except AgentStubControlPlaneError as exc: + raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc + + @router.post("/drive/commit", response_model=AgentStubDriveCommitResponse) + async def commit_drive( + request: AgentStubDriveCommitRequest, + authorization: str | None = Header(default=None, alias="Authorization"), + ) -> AgentStubDriveCommitResponse: + try: + return await service.commit_drive(request=request, authorization=authorization) + except AgentStubControlPlaneError as exc: + raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc + return router diff --git a/dify-agent/src/dify_agent/client/_client.py b/dify-agent/src/dify_agent/client/_client.py index 0ab8dc0dcf3..7c9a76e604f 100644 --- a/dify-agent/src/dify_agent/client/_client.py +++ b/dify-agent/src/dify_agent/client/_client.py @@ -46,7 +46,7 @@ from dify_agent.protocol import ( _ResponseModelT = TypeVar("_ResponseModelT", bound=BaseModel) _TERMINAL_EVENT_TYPES = {"run_succeeded", "run_failed", "run_cancelled"} _TERMINAL_RUN_STATUSES = {"succeeded", "failed", "cancelled"} -_FUNCTION_TOOL_RESULT_PAYLOAD_KEY: str | None = None +_function_tool_result_payload_key_cache: str | None = None class DifyAgentClientError(RuntimeError): @@ -176,13 +176,13 @@ def _function_tool_result_payload_key() -> str: during local development or rolling deploys, so the client normalizes the remote frame into the local schema before Pydantic validation. """ - global _FUNCTION_TOOL_RESULT_PAYLOAD_KEY - if _FUNCTION_TOOL_RESULT_PAYLOAD_KEY is not None: - return _FUNCTION_TOOL_RESULT_PAYLOAD_KEY + global _function_tool_result_payload_key_cache + if _function_tool_result_payload_key_cache is not None: + return _function_tool_result_payload_key_cache parameters = list(inspect.signature(FunctionToolResultEvent).parameters) - _FUNCTION_TOOL_RESULT_PAYLOAD_KEY = "part" if parameters and parameters[0] == "part" else "result" - return _FUNCTION_TOOL_RESULT_PAYLOAD_KEY + _function_tool_result_payload_key_cache = "part" if parameters and parameters[0] == "part" else "result" + return _function_tool_result_payload_key_cache def _normalize_run_event_payload_for_local_pydantic_ai(payload: Any) -> Any: diff --git a/dify-agent/src/dify_agent/layers/dify_plugin/llm_layer.py b/dify-agent/src/dify_agent/layers/dify_plugin/llm_layer.py index 48e6c5508d8..bdd9d947565 100644 --- a/dify-agent/src/dify_agent/layers/dify_plugin/llm_layer.py +++ b/dify-agent/src/dify_agent/layers/dify_plugin/llm_layer.py @@ -32,7 +32,7 @@ class DifyPluginLLMDeps(LayerDeps): class DifyPluginLLMLayer(PlainLayer[DifyPluginLLMDeps, DifyPluginLLMLayerConfig]): """Layer that creates the Dify plugin-daemon Pydantic AI model.""" - type_id: ClassVar[str] = DIFY_PLUGIN_LLM_LAYER_TYPE_ID + type_id: ClassVar[str | None] = DIFY_PLUGIN_LLM_LAYER_TYPE_ID config: DifyPluginLLMLayerConfig diff --git a/dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py b/dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py index 5ed4a5ea330..dd6c0c3cfb2 100644 --- a/dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py +++ b/dify-agent/src/dify_agent/layers/dify_plugin/tools_layer.py @@ -58,7 +58,7 @@ class DifyPluginToolsDeps(LayerDeps): class DifyPluginToolsLayer(PlainLayer[DifyPluginToolsDeps, DifyPluginToolsLayerConfig]): """Layer that resolves Dify plugin tools into Pydantic AI tools.""" - type_id: ClassVar[str] = DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID + type_id: ClassVar[str | None] = DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID config: DifyPluginToolsLayerConfig diff --git a/dify-agent/src/dify_agent/layers/drive/layer.py b/dify-agent/src/dify_agent/layers/drive/layer.py index 3d5efb23d40..42f7011a51c 100644 --- a/dify-agent/src/dify_agent/layers/drive/layer.py +++ b/dify-agent/src/dify_agent/layers/drive/layer.py @@ -21,7 +21,7 @@ from dify_agent.layers.drive.configs import DIFY_DRIVE_LAYER_TYPE_ID, DifyDriveL class DifyDriveLayer(PlainLayer[NoLayerDeps, DifyDriveLayerConfig, EmptyRuntimeState]): """Config-only carrier of the drive Skills & Files manifest.""" - type_id: ClassVar[str] = DIFY_DRIVE_LAYER_TYPE_ID + type_id: ClassVar[str | None] = DIFY_DRIVE_LAYER_TYPE_ID config: DifyDriveLayerConfig diff --git a/dify-agent/src/dify_agent/server/app.py b/dify-agent/src/dify_agent/server/app.py index dab6caaca07..42b406799bb 100644 --- a/dify-agent/src/dify_agent/server/app.py +++ b/dify-agent/src/dify_agent/server/app.py @@ -37,6 +37,7 @@ def create_app(settings: ServerSettings | None = None) -> FastAPI: resolved_settings = settings or ServerSettings() agent_stub_token_codec = resolved_settings.create_agent_stub_token_codec() agent_stub_file_request_handler = resolved_settings.create_agent_stub_file_request_handler() + agent_stub_drive_request_handler = resolved_settings.create_agent_stub_drive_request_handler() layer_providers = create_default_layer_providers( plugin_daemon_url=resolved_settings.plugin_daemon_url, plugin_daemon_api_key=resolved_settings.plugin_daemon_api_key, @@ -106,6 +107,7 @@ def create_app(settings: ServerSettings | None = None) -> FastAPI: create_agent_stub_router( token_codec=agent_stub_token_codec, file_request_handler=agent_stub_file_request_handler, + drive_request_handler=agent_stub_drive_request_handler, ) ) return app @@ -135,12 +137,7 @@ def create_dify_api_inner_http_client(settings: ServerSettings) -> httpx.AsyncCl def _create_shared_http_client(settings: ServerSettings) -> httpx.AsyncClient: """Build one shared HTTP client using generic outbound timeout/pool settings.""" return httpx.AsyncClient( - timeout=httpx.Timeout( - connect=settings.outbound_http_connect_timeout, - read=settings.outbound_http_read_timeout, - write=settings.outbound_http_write_timeout, - pool=settings.outbound_http_pool_timeout, - ), + timeout=settings.create_outbound_http_timeout(), limits=httpx.Limits( max_connections=settings.outbound_http_max_connections, max_keepalive_connections=settings.outbound_http_max_keepalive_connections, diff --git a/dify-agent/src/dify_agent/server/settings.py b/dify-agent/src/dify_agent/server/settings.py index 2b4aff62e50..b85fcffdc36 100644 --- a/dify-agent/src/dify_agent/server/settings.py +++ b/dify-agent/src/dify_agent/server/settings.py @@ -6,16 +6,19 @@ Dify API inner calls. Layers and Agenton providers do not own those clients, so these settings are process resource limits rather than per-run lifecycle knobs. Endpoint URLs and API keys stay service-specific. The Agent Stub also uses this settings model directly: the public Agent Stub URL, server secret, optional gRPC -bind override, and optional Dify inner API file-request settings all live here -under the longstanding ``DIFY_AGENT_...`` environment-variable namespace. +bind override, and optional Dify inner API file/drive request settings all live +here under the longstanding ``DIFY_AGENT_...`` environment-variable namespace. """ +import httpx + from typing import ClassVar from pydantic import AnyHttpUrl, Field, TypeAdapter, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from dify_agent.agent_stub.protocol.agent_stub import normalize_agent_stub_url, parse_agent_stub_endpoint +from dify_agent.agent_stub.server.agent_stub_drive import DifyApiAgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler from dify_agent.agent_stub.server.grpc_bind import normalize_agent_stub_grpc_bind_address from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec, decode_server_secret_key @@ -145,5 +148,28 @@ class ServerSettings(BaseSettings): dify_api_inner_api_key=self.dify_api_inner_api_key, ) + def create_agent_stub_drive_request_handler(self) -> DifyApiAgentStubDriveRequestHandler | None: + """Return the Dify API drive bridge when both Dify API settings are configured. + + Drive manifest and commit requests should honor the same outbound timeout + settings as the server's other trusted Dify API HTTP calls. + """ + if self.dify_api_base_url is None or self.dify_api_inner_api_key is None: + return None + return DifyApiAgentStubDriveRequestHandler( + dify_api_base_url=self.dify_api_base_url, + dify_api_inner_api_key=self.dify_api_inner_api_key, + timeout=self.create_outbound_http_timeout(), + ) + + def create_outbound_http_timeout(self) -> httpx.Timeout: + """Build one shared outbound HTTP timeout object from server settings.""" + return httpx.Timeout( + connect=self.outbound_http_connect_timeout, + read=self.outbound_http_read_timeout, + write=self.outbound_http_write_timeout, + pool=self.outbound_http_pool_timeout, + ) + __all__ = ["DEFAULT_RUN_RETENTION_SECONDS", "ServerSettings"] diff --git a/dify-agent/tests/local/dify_agent/agent_stub/cli/test_drive.py b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_drive.py new file mode 100644 index 00000000000..ff52bb6b4f8 --- /dev/null +++ b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_drive.py @@ -0,0 +1,651 @@ +from __future__ import annotations + +from io import BytesIO +from pathlib import Path +import stat +from zipfile import ZipFile, ZipInfo + +import pytest + +from dify_agent.agent_stub.cli._drive import ( + list_drive_from_environment, + pull_drive_from_environment, + push_drive_from_environment, +) +from dify_agent.agent_stub.cli._files import UploadedToolFileMapping, UploadedToolFileResource +from dify_agent.agent_stub.client._errors import AgentStubTransferError, AgentStubValidationError +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitRequest, + AgentStubDriveCommitResponse, + AgentStubDriveItem, + AgentStubDriveManifestResponse, +) + + +def test_list_drive_from_environment_returns_manifest_json_model(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + captured: dict[str, object] = {} + + def fake_manifest(**kwargs): + captured.update(kwargs) + return AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=12, + hash="sha256:abc", + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + ) + ] + ) + + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + fake_manifest, + ) + + result = list_drive_from_environment(prefix="skills/", json_output=True) + + assert isinstance(result, AgentStubDriveManifestResponse) + assert result.items[0].key == "skills/example/SKILL.md" + assert captured["prefix"] == "skills/" + assert captured["include_download_url"] is False + + +def test_list_drive_from_environment_returns_human_readable_listing(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + captured: dict[str, object] = {} + + def fake_manifest(**kwargs): + captured.update(kwargs) + return AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=12, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + ), + AgentStubDriveItem( + key="skills/example/helper.py", + size=None, + hash="sha256:abc", + mime_type=None, + file_kind="tool_file", + file_id="tool-file-2", + ), + ] + ) + + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + fake_manifest, + ) + + result = list_drive_from_environment(prefix="skills/", json_output=False) + + assert result == ("12\ttext/markdown\t-\tskills/example/SKILL.md\n-\t-\tsha256:abc\tskills/example/helper.py") + assert captured["prefix"] == "skills/" + assert captured["include_download_url"] is False + + +def test_pull_drive_from_environment_writes_files_under_drive_base( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + captured: dict[str, object] = {} + + def fake_manifest(**kwargs): + captured.update(kwargs) + return AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=11, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ) + + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + fake_manifest, + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: b"hello world", + ) + + results = pull_drive_from_environment(prefix="skills/", drive_base=str(tmp_path)) + + assert results == [tmp_path / "skills" / "example" / "SKILL.md"] + assert results[0].read_bytes() == b"hello world" + assert captured["prefix"] == "skills/" + assert captured["include_download_url"] is True + + +def test_pull_drive_from_environment_auto_extracts_skill_archive( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + archive_buffer = BytesIO() + with ZipFile(archive_buffer, mode="w") as archive: + archive.writestr("SKILL.md", "# Example\n") + archive.writestr("nested/helper.py", "print('x')\n") + archive_bytes = archive_buffer.getvalue() + + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/foo/.DIFY-SKILL-FULL.zip", + size=len(archive_bytes), + hash=None, + mime_type="application/zip", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: archive_bytes, + ) + + results = pull_drive_from_environment(prefix="skills/foo", drive_base=str(tmp_path)) + + archive_path = tmp_path / "skills" / "foo" / ".DIFY-SKILL-FULL.zip" + assert results == [archive_path] + assert archive_path.read_bytes() == archive_bytes + assert (tmp_path / "skills" / "foo" / "SKILL.md").read_text(encoding="utf-8") == "# Example\n" + assert (tmp_path / "skills" / "foo" / "nested" / "helper.py").read_text(encoding="utf-8") == "print('x')\n" + + +def test_pull_drive_from_environment_rejects_traversal_keys( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="../escape.txt", + size=4, + hash=None, + mime_type="text/plain", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + + with pytest.raises(AgentStubValidationError, match="outside the drive base"): + _ = pull_drive_from_environment(prefix="", drive_base=str(tmp_path)) + + +def test_pull_drive_from_environment_rejects_skill_archive_path_traversal( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + archive_buffer = BytesIO() + with ZipFile(archive_buffer, mode="w") as archive: + archive.writestr("SKILL.md", "# Example\n") + archive.writestr("../escape.txt", "escape") + archive_bytes = archive_buffer.getvalue() + + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/foo/.DIFY-SKILL-FULL.zip", + size=len(archive_bytes), + hash=None, + mime_type="application/zip", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: archive_bytes, + ) + + with pytest.raises(AgentStubValidationError, match="path traversal"): + _ = pull_drive_from_environment(prefix="skills/foo", drive_base=str(tmp_path)) + assert not (tmp_path / "skills" / "foo" / "SKILL.md").exists() + + +def test_pull_drive_from_environment_rejects_skill_archive_absolute_entry( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + archive_buffer = BytesIO() + with ZipFile(archive_buffer, mode="w") as archive: + archive.writestr("/escape.txt", "escape") + archive_bytes = archive_buffer.getvalue() + + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/foo/.DIFY-SKILL-FULL.zip", + size=len(archive_bytes), + hash=None, + mime_type="application/zip", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: archive_bytes, + ) + + with pytest.raises(AgentStubValidationError, match="absolute path"): + _ = pull_drive_from_environment(prefix="skills/foo", drive_base=str(tmp_path)) + + +def test_pull_drive_from_environment_rejects_skill_archive_symlink_entry( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + archive_buffer = BytesIO() + with ZipFile(archive_buffer, mode="w") as archive: + symlink_info = ZipInfo("linked.txt") + symlink_info.external_attr = (stat.S_IFLNK | 0o777) << 16 + archive.writestr(symlink_info, "outside.txt") + archive_bytes = archive_buffer.getvalue() + + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/foo/.DIFY-SKILL-FULL.zip", + size=len(archive_bytes), + hash=None, + mime_type="application/zip", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: archive_bytes, + ) + + with pytest.raises(AgentStubValidationError, match="symlink entry"): + _ = pull_drive_from_environment(prefix="skills/foo", drive_base=str(tmp_path)) + + +def test_pull_drive_from_environment_rejects_invalid_skill_archive( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + archive_bytes = b"not-a-zip" + + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/foo/.DIFY-SKILL-FULL.zip", + size=len(archive_bytes), + hash=None, + mime_type="application/zip", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: archive_bytes, + ) + + with pytest.raises(AgentStubTransferError, match="downloaded skill archive is invalid"): + _ = pull_drive_from_environment(prefix="skills/foo", drive_base=str(tmp_path)) + + +def test_pull_drive_from_environment_rejects_missing_download_url( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=11, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + ) + ] + ), + ) + + with pytest.raises(AgentStubValidationError, match="missing download_url"): + _ = pull_drive_from_environment(prefix="skills/", drive_base=str(tmp_path)) + + +def test_pull_drive_from_environment_rejects_size_mismatch( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_manifest_sync", + lambda **_kwargs: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=99, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + download_url="https://files.example.com/download", + ) + ] + ), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.download_file_bytes_from_signed_url_sync", + lambda **_kwargs: b"hello world", + ) + + with pytest.raises(AgentStubTransferError, match="size mismatch"): + _ = pull_drive_from_environment(prefix="skills/", drive_base=str(tmp_path)) + + +def test_push_drive_from_environment_commits_single_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + source = tmp_path / "report.pdf" + source.write_bytes(b"report") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.upload_tool_file_resource_from_environment", + lambda *, path: UploadedToolFileResource( + mapping=UploadedToolFileMapping(reference="dify-file-ref:tool-file-1"), + tool_file_id="tool-file-1", + ), + ) + captured: dict[str, object] = {} + + def fake_commit(**kwargs): + captured.update(kwargs) + return AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key="files/report.pdf", + size=6, + hash=None, + mime_type="application/pdf", + file_kind="tool_file", + file_id="tool-file-1", + value_owned_by_drive=True, + ) + ] + ) + + monkeypatch.setattr("dify_agent.agent_stub.cli._drive.request_agent_stub_drive_commit_sync", fake_commit) + + response = push_drive_from_environment(local_path=str(source), drive_path="files/report.pdf", recursive=False) + + assert response.items[0].key == "files/report.pdf" + request = captured["request"] + assert isinstance(request, AgentStubDriveCommitRequest) + assert request.items[0].model_dump(mode="json") == { + "key": "files/report.pdf", + "file_ref": {"kind": "tool_file", "id": "tool-file-1"}, + "value_owned_by_drive": True, + } + + +def test_push_drive_from_environment_requires_skill_md_for_non_recursive_directory( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + with pytest.raises(AgentStubValidationError, match="SKILL.md"): + _ = push_drive_from_environment(local_path=str(skill_dir), drive_path="skills/example", recursive=False) + + +def test_push_drive_from_environment_standardizes_non_recursive_skill_directory( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + (skill_dir / "SKILL.md").write_text("# Example\n", encoding="utf-8") + (skill_dir / "helper.py").write_text("print('x')\n", encoding="utf-8") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + uploaded_paths: list[str] = [] + + def fake_upload(*, path: str) -> UploadedToolFileResource: + uploaded_paths.append(Path(path).name) + return UploadedToolFileResource( + mapping=UploadedToolFileMapping(reference=f"dify-file-ref:{Path(path).name}"), + tool_file_id=Path(path).name, + ) + + monkeypatch.setattr("dify_agent.agent_stub.cli._drive.upload_tool_file_resource_from_environment", fake_upload) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_commit_sync", + lambda **kwargs: AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key=item.key, + size=None, + hash=None, + mime_type=None, + file_kind=item.file_ref.kind, + file_id=item.file_ref.id, + value_owned_by_drive=item.value_owned_by_drive, + ) + for item in kwargs["request"].items + ] + ), + ) + + response = push_drive_from_environment(local_path=str(skill_dir), drive_path="skills/example", recursive=False) + + assert set(uploaded_paths) == {"SKILL.md", ".DIFY-SKILL-FULL.zip"} + assert {item.key for item in response.items} == { + "skills/example/SKILL.md", + "skills/example/.DIFY-SKILL-FULL.zip", + } + + +def test_push_drive_from_environment_non_recursive_archive_excludes_transient_entries( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + (skill_dir / "SKILL.md").write_text("# Example\n", encoding="utf-8") + (skill_dir / "helper.py").write_text("print('x')\n", encoding="utf-8") + (skill_dir / ".DIFY-SKILL-FULL.zip").write_bytes(b"old-archive") + git_dir = skill_dir / ".git" + git_dir.mkdir() + (git_dir / "config").write_text("[core]\n", encoding="utf-8") + pycache_dir = skill_dir / "__pycache__" + pycache_dir.mkdir() + (pycache_dir / "helper.pyc").write_bytes(b"compiled") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + archive_entries: list[str] = [] + + def fake_upload(*, path: str) -> UploadedToolFileResource: + if Path(path).name == ".DIFY-SKILL-FULL.zip": + with ZipFile(path) as archive: + archive_entries.extend(sorted(archive.namelist())) + return UploadedToolFileResource( + mapping=UploadedToolFileMapping(reference=f"dify-file-ref:{Path(path).name}"), + tool_file_id=Path(path).name, + ) + + monkeypatch.setattr("dify_agent.agent_stub.cli._drive.upload_tool_file_resource_from_environment", fake_upload) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_commit_sync", + lambda **kwargs: AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key=item.key, + size=None, + hash=None, + mime_type=None, + file_kind=item.file_ref.kind, + file_id=item.file_ref.id, + value_owned_by_drive=item.value_owned_by_drive, + ) + for item in kwargs["request"].items + ] + ), + ) + + _ = push_drive_from_environment(local_path=str(skill_dir), drive_path="skills/example", recursive=False) + + assert {"SKILL.md", "helper.py"}.issubset(archive_entries) + assert ".git/config" not in archive_entries + assert "__pycache__/helper.pyc" not in archive_entries + assert ".DIFY-SKILL-FULL.zip" not in archive_entries + + +def test_push_drive_from_environment_non_recursive_rejects_symlinked_archive_entries( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + skill_dir = tmp_path / "skill" + skill_dir.mkdir() + (skill_dir / "SKILL.md").write_text("# Example\n", encoding="utf-8") + outside = tmp_path / "outside.txt" + outside.write_text("outside", encoding="utf-8") + (skill_dir / "linked.txt").symlink_to(outside) + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + with pytest.raises(AgentStubValidationError, match="symlink"): + _ = push_drive_from_environment(local_path=str(skill_dir), drive_path="skills/example", recursive=False) + + +def test_push_drive_from_environment_rejects_symlinked_recursive_files( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + root = tmp_path / "skill" + root.mkdir() + outside = tmp_path / "outside.txt" + outside.write_text("outside", encoding="utf-8") + (root / "linked.txt").symlink_to(outside) + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + with pytest.raises(AgentStubValidationError, match="symlink"): + _ = push_drive_from_environment(local_path=str(root), drive_path="skills/example", recursive=True) + + +def test_push_drive_from_environment_recursive_keeps_user_files_that_skill_packaging_skips( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + root = tmp_path / "skill" + root.mkdir() + (root / ".DIFY-SKILL-FULL.zip").write_bytes(b"archive") + node_modules_dir = root / "node_modules" + node_modules_dir.mkdir() + (node_modules_dir / "module.js").write_text("export default 1\n", encoding="utf-8") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + uploaded_paths: list[str] = [] + + def fake_upload(*, path: str) -> UploadedToolFileResource: + uploaded_paths.append(Path(path).relative_to(root).as_posix()) + return UploadedToolFileResource( + mapping=UploadedToolFileMapping(reference=f"dify-file-ref:{Path(path).name}"), + tool_file_id=Path(path).name, + ) + + monkeypatch.setattr("dify_agent.agent_stub.cli._drive.upload_tool_file_resource_from_environment", fake_upload) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._drive.request_agent_stub_drive_commit_sync", + lambda **kwargs: AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key=item.key, + size=None, + hash=None, + mime_type=None, + file_kind=item.file_ref.kind, + file_id=item.file_ref.id, + value_owned_by_drive=item.value_owned_by_drive, + ) + for item in kwargs["request"].items + ] + ), + ) + + response = push_drive_from_environment(local_path=str(root), drive_path="skills/example", recursive=True) + + assert set(uploaded_paths) == {".DIFY-SKILL-FULL.zip", "node_modules/module.js"} + assert {item.key for item in response.items} == { + "skills/example/.DIFY-SKILL-FULL.zip", + "skills/example/node_modules/module.js", + } diff --git a/dify-agent/tests/local/dify_agent/agent_stub/cli/test_files.py b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_files.py index 3f2e044be5d..c5b093128fd 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/cli/test_files.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_files.py @@ -6,7 +6,11 @@ from pathlib import Path import pytest -from dify_agent.agent_stub.cli._files import download_file_from_environment, upload_file_from_environment +from dify_agent.agent_stub.cli._files import ( + download_file_from_environment, + upload_file_from_environment, + upload_tool_file_resource_from_environment, +) from dify_agent.agent_stub.client._errors import AgentStubTransferError @@ -36,6 +40,7 @@ def test_upload_file_from_environment_requests_signed_url_and_normalizes_output( captured["file_bytes"] = kwargs["file_obj"].read() kwargs["file_obj"].seek(0) return { + "id": "tool-file-1", "reference": _reference("tool-file-1"), } @@ -57,6 +62,33 @@ def test_upload_file_from_environment_requests_signed_url_and_normalizes_output( } +def test_upload_tool_file_resource_from_environment_preserves_tool_file_id( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + source = tmp_path / "report.pdf" + source.write_bytes(b"report-bytes") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + monkeypatch.setattr( + "dify_agent.agent_stub.cli._files.request_agent_stub_file_upload_sync", + lambda **_kwargs: type("Response", (), {"upload_url": "https://files.example.com/upload"})(), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._files.upload_file_to_signed_url_sync", + lambda **_kwargs: {"id": "tool-file-1", "reference": _reference("tool-file-1")}, + ) + + result = upload_tool_file_resource_from_environment(path=str(source)) + + assert result.mapping.model_dump() == { + "transfer_method": "tool_file", + "reference": _reference("tool-file-1"), + } + assert result.tool_file_id == "tool-file-1" + + def test_download_file_from_environment_saves_bytes_and_renames_on_collision( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, @@ -148,8 +180,30 @@ def test_upload_file_from_environment_rejects_non_canonical_reference( ) monkeypatch.setattr( "dify_agent.agent_stub.cli._files.upload_file_to_signed_url_sync", - lambda **_kwargs: {"reference": "raw-tool-file-uuid"}, + lambda **_kwargs: {"id": "tool-file-1", "reference": "raw-tool-file-uuid"}, ) with pytest.raises(AgentStubTransferError, match="invalid canonical reference"): _ = upload_file_from_environment(path=str(source)) + + +def test_upload_tool_file_resource_from_environment_rejects_missing_id( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + source = tmp_path / "report.pdf" + source.write_bytes(b"report-bytes") + monkeypatch.setenv("DIFY_AGENT_STUB_URL", "https://agent.example.com/agent-stub") + monkeypatch.setenv("DIFY_AGENT_STUB_AUTH_JWE", "test-jwe") + + monkeypatch.setattr( + "dify_agent.agent_stub.cli._files.request_agent_stub_file_upload_sync", + lambda **_kwargs: type("Response", (), {"upload_url": "https://files.example.com/upload"})(), + ) + monkeypatch.setattr( + "dify_agent.agent_stub.cli._files.upload_file_to_signed_url_sync", + lambda **_kwargs: {"reference": _reference("tool-file-1")}, + ) + + with pytest.raises(AgentStubTransferError, match="missing id"): + _ = upload_tool_file_resource_from_environment(path=str(source)) diff --git a/dify-agent/tests/local/dify_agent/agent_stub/cli/test_main.py b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_main.py index b6fa85049b3..8699ac99e13 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/cli/test_main.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/cli/test_main.py @@ -7,6 +7,11 @@ from pathlib import Path import pytest from dify_agent.agent_stub.cli.main import main +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitResponse, + AgentStubDriveItem, + AgentStubDriveManifestResponse, +) from dify_agent.agent_stub.protocol.agent_stub import AgentStubConnectResponse @@ -194,3 +199,97 @@ def test_cli_file_download_prints_saved_path( captured = capsys.readouterr() assert exc_info.value.code == 0 assert captured.out.strip() == "/tmp/report.pdf" + + +def test_cli_drive_list_prints_manifest_json( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setattr( + "dify_agent.agent_stub.cli.main.list_drive_from_environment", + lambda *, prefix, json_output: AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key=prefix + "example/SKILL.md", + size=12, + hash="sha256:abc", + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + ) + ] + ), + ) + + with pytest.raises(SystemExit) as exc_info: + main(["drive", "list", "skills/", "--json"]) + + captured = capsys.readouterr() + assert exc_info.value.code == 0 + assert json.loads(captured.out)["items"][0]["key"] == "skills/example/SKILL.md" + + +def test_cli_drive_list_prints_human_readable_listing( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setattr( + "dify_agent.agent_stub.cli.main.list_drive_from_environment", + lambda *, prefix, json_output: f"12\ttext/markdown\t-\t{prefix}example/SKILL.md", + ) + + with pytest.raises(SystemExit) as exc_info: + main(["drive", "list", "skills/"]) + + captured = capsys.readouterr() + assert exc_info.value.code == 0 + assert captured.out.strip() == "12\ttext/markdown\t-\tskills/example/SKILL.md" + + +def test_cli_drive_pull_prints_downloaded_paths( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setattr( + "dify_agent.agent_stub.cli.main.pull_drive_from_environment", + lambda *, prefix, drive_base: [Path(drive_base) / prefix / "SKILL.md", Path(drive_base) / prefix / "helper.py"], + ) + + with pytest.raises(SystemExit) as exc_info: + main(["drive", "pull", "skills/example", "--drive-base", "/tmp/drive"]) + + captured = capsys.readouterr() + assert exc_info.value.code == 0 + assert captured.out.strip().splitlines() == [ + "/tmp/drive/skills/example/SKILL.md", + "/tmp/drive/skills/example/helper.py", + ] + + +def test_cli_drive_push_prints_commit_json( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + monkeypatch.setattr( + "dify_agent.agent_stub.cli.main.push_drive_from_environment", + lambda *, local_path, drive_path, recursive: AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key=drive_path, + size=12, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id=Path(local_path).name, + value_owned_by_drive=recursive is False, + ) + ] + ), + ) + + with pytest.raises(SystemExit) as exc_info: + main(["drive", "push", "/tmp/report.md", "skills/example/SKILL.md"]) + + captured = capsys.readouterr() + assert exc_info.value.code == 0 + assert json.loads(captured.out)["items"][0]["key"] == "skills/example/SKILL.md" diff --git a/dify-agent/tests/local/dify_agent/agent_stub/client/test_agent_stub_client.py b/dify-agent/tests/local/dify_agent/agent_stub/client/test_agent_stub_client.py index b97dff3e3ec..f62cc4564e9 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/client/test_agent_stub_client.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/client/test_agent_stub_client.py @@ -11,6 +11,8 @@ import pytest from dify_agent.agent_stub.client._agent_stub import ( connect_agent_stub_sync, download_file_bytes_from_signed_url_sync, + request_agent_stub_drive_commit_sync, + request_agent_stub_drive_manifest_sync, request_agent_stub_file_download_sync, request_agent_stub_file_upload_sync, upload_file_to_signed_url_sync, @@ -23,7 +25,12 @@ from dify_agent.agent_stub.client._errors import ( AgentStubTransferError, AgentStubValidationError, ) -from dify_agent.agent_stub.protocol.agent_stub import AgentStubFileMapping +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitItem, + AgentStubDriveCommitRequest, + AgentStubDriveFileRef, + AgentStubFileMapping, +) def _reference(record_id: str) -> str: @@ -174,6 +181,140 @@ def test_request_agent_stub_file_download_sync_posts_download_request() -> None: assert response.download_url == "https://files.example.com/download" +def test_request_agent_stub_drive_manifest_sync_gets_manifest_request() -> None: + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert str(request.url) == ( + "https://agent.example.com/agent-stub/drive/manifest?prefix=skills%2F&include_download_url=true" + ) + assert request.headers["Authorization"] == "Bearer test-jwe" + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "hash": "sha256:abc", + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + "created_at": 123, + } + ] + }, + ) + + http_client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + response = request_agent_stub_drive_manifest_sync( + url="https://agent.example.com/agent-stub", + auth_jwe="test-jwe", + prefix="skills/", + include_download_url=True, + sync_http_client=http_client, + ) + finally: + http_client.close() + + assert response.items[0].key == "skills/example/SKILL.md" + + +def test_request_agent_stub_drive_commit_sync_posts_commit_request() -> None: + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert str(request.url) == "https://agent.example.com/agent-stub/drive/commit" + assert request.headers["Authorization"] == "Bearer test-jwe" + assert json.loads(request.content) == { + "items": [ + { + "key": "skills/example/SKILL.md", + "file_ref": {"kind": "tool_file", "id": "tool-file-1"}, + "value_owned_by_drive": True, + } + ] + } + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + "value_owned_by_drive": True, + } + ] + }, + ) + + http_client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + response = request_agent_stub_drive_commit_sync( + url="https://agent.example.com/agent-stub", + auth_jwe="test-jwe", + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + sync_http_client=http_client, + ) + finally: + http_client.close() + + assert response.items[0].file_id == "tool-file-1" + + +def test_request_agent_stub_drive_manifest_sync_maps_invalid_json_to_client_error() -> None: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(200, text="not-json", headers={"Content-Type": "application/json"}) + + http_client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + with pytest.raises(AgentStubClientError, match="invalid JSON"): + _ = request_agent_stub_drive_manifest_sync( + url="https://agent.example.com/agent-stub", + auth_jwe="test-jwe", + prefix="", + include_download_url=False, + sync_http_client=http_client, + ) + finally: + http_client.close() + + +def test_request_agent_stub_drive_commit_sync_maps_non_2xx_to_http_error() -> None: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(403, json={"detail": "forbidden"}) + + http_client = httpx.Client(transport=httpx.MockTransport(handler)) + try: + with pytest.raises(AgentStubHTTPError, match="403") as exc_info: + _ = request_agent_stub_drive_commit_sync( + url="https://agent.example.com/agent-stub", + auth_jwe="test-jwe", + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + sync_http_client=http_client, + ) + finally: + http_client.close() + + assert exc_info.value.detail == "forbidden" + + def test_upload_file_to_signed_url_sync_posts_multipart_file() -> None: def handler(request: httpx.Request) -> httpx.Response: assert request.method == "POST" @@ -322,6 +463,32 @@ def test_request_agent_stub_file_download_sync_dispatches_grpc_urls(monkeypatch: assert response.download_url == "https://files.example.com/download" +def test_request_agent_stub_drive_manifest_sync_rejects_grpc_urls() -> None: + with pytest.raises(AgentStubValidationError, match="require an HTTP Agent Stub URL"): + _ = request_agent_stub_drive_manifest_sync( + url="grpc://agent.example.com:9091", + auth_jwe="token", + prefix="skills/", + include_download_url=False, + ) + + +def test_request_agent_stub_drive_commit_sync_rejects_grpc_urls() -> None: + with pytest.raises(AgentStubValidationError, match="require an HTTP Agent Stub URL"): + _ = request_agent_stub_drive_commit_sync( + url="grpc://agent.example.com:9091", + auth_jwe="token", + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + ) + + def test_request_agent_stub_file_upload_grpc_sync_attaches_bearer_metadata(monkeypatch: pytest.MonkeyPatch) -> None: import dify_agent.agent_stub.client._agent_stub_grpc as grpc_module diff --git a/dify-agent/tests/local/dify_agent/agent_stub/protocol/test_agent_stub_protocol.py b/dify-agent/tests/local/dify_agent/agent_stub/protocol/test_agent_stub_protocol.py index b2dd3629c10..dccde1b42b5 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/protocol/test_agent_stub_protocol.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/protocol/test_agent_stub_protocol.py @@ -5,10 +5,16 @@ import json from typing import Literal import pytest +from pydantic import ValidationError from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitItem, + AgentStubDriveCommitRequest, + AgentStubDriveFileRef, AgentStubFileMapping, agent_stub_connections_url, + agent_stub_drive_commit_url, + agent_stub_drive_manifest_url, agent_stub_file_download_request_url, agent_stub_file_upload_request_url, normalize_agent_stub_url, @@ -39,6 +45,15 @@ def test_agent_stub_file_request_urls_handle_trailing_slash() -> None: ) +def test_agent_stub_drive_request_urls_handle_trailing_slash() -> None: + assert agent_stub_drive_manifest_url("https://agent.example.com/agent-stub/") == ( + "https://agent.example.com/agent-stub/drive/manifest" + ) + assert agent_stub_drive_commit_url("https://agent.example.com/agent-stub") == ( + "https://agent.example.com/agent-stub/drive/commit" + ) + + def test_normalize_agent_stub_url_rejects_query_and_fragment() -> None: with pytest.raises(ValueError, match="query string or fragment"): _ = normalize_agent_stub_url("https://agent.example.com/agent-stub?x=1") @@ -98,6 +113,25 @@ def test_agent_stub_file_mapping_rejects_remote_url_with_reference() -> None: ) +def test_agent_stub_drive_commit_request_validates_file_refs() -> None: + request = AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ) + + assert request.items[0].file_ref.kind == "tool_file" + + with pytest.raises(ValidationError, match="tool_file"): + _ = AgentStubDriveFileRef(kind="bad_kind", id="tool-file-1") # pyright: ignore[reportArgumentType] + + with pytest.raises(ValidationError, match="file_ref"): + _ = AgentStubDriveCommitItem.model_validate({"key": "skills/example/SKILL.md"}) + + @pytest.mark.parametrize("transfer_method", ["tool_file", "local_file", "datasource_file"]) def test_agent_stub_file_mapping_rejects_non_remote_with_url( transfer_method: Literal["tool_file", "local_file", "datasource_file"], diff --git a/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_app.py b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_app.py index be8d9e4031c..e1a53a11bb4 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_app.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_app.py @@ -39,6 +39,8 @@ def test_create_agent_stub_app_exposes_same_stub_routes_as_module_app() -> None: assert "/agent-stub/connections" in created_paths assert "/agent-stub/files/upload-request" in created_paths assert "/agent-stub/files/download-request" in created_paths + assert "/agent-stub/drive/manifest" in created_paths + assert "/agent-stub/drive/commit" in created_paths assert created_paths == module_paths @@ -88,3 +90,56 @@ def test_create_agent_stub_app_wires_configured_file_handler_for_upload_requests assert response.status_code == 200 assert response.json() == {"upload_url": "https://files.example.com/upload"} + + +def test_create_agent_stub_app_wires_configured_drive_handler_for_manifest_requests(monkeypatch) -> None: + settings = ServerSettings( + agent_stub_url="https://agent.example.com/agent-stub", + server_secret_key=_base64url_secret(b"1" * 32), + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + token_codec = settings.create_agent_stub_token_codec() + assert token_codec is not None + token = token_codec.encode_connection_token( + _execution_context().model_copy(update={"agent_id": "agent-1"}), now=int(time.time()) - 1 + ) + + original_async_client = httpx.AsyncClient + + def handler(request: httpx.Request) -> httpx.Response: + assert str(request.url) == ( + "https://api.example.com/inner/api/drive/agent-agent-1/manifest" + "?tenant_id=tenant-1&prefix=skills%2F&include_download_url=false" + ) + assert request.headers["X-Inner-Api-Key"] == "inner-secret" + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "hash": "sha256:abc", + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + } + ] + }, + ) + + monkeypatch.setattr( + "dify_agent.agent_stub.server.agent_stub_drive.httpx.AsyncClient", + lambda **kwargs: original_async_client(transport=httpx.MockTransport(handler), **kwargs), + ) + + client = TestClient(create_agent_stub_app(settings)) + response = client.get( + "/agent-stub/drive/manifest", + headers={"Authorization": f"Bearer {token}"}, + params={"prefix": "skills/"}, + ) + + assert response.status_code == 200 + assert response.json()["items"][0]["key"] == "skills/example/SKILL.md" diff --git a/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_drive.py b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_drive.py new file mode 100644 index 00000000000..b0d07f68e00 --- /dev/null +++ b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_drive.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import asyncio +import json + +import httpx + +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitItem, + AgentStubDriveCommitRequest, + AgentStubDriveFileRef, +) +from dify_agent.agent_stub.server.agent_stub_drive import ( + AgentStubDriveRequestError, + DifyApiAgentStubDriveRequestHandler, +) +from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubPrincipal +from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig + + +def _principal() -> AgentStubPrincipal: + return AgentStubPrincipal( + execution_context=DifyExecutionContextLayerConfig( + tenant_id="tenant-1", + user_id="user-1", + user_from="account", + workflow_id="workflow-1", + agent_id="agent-1", + agent_mode="workflow_run", + invoke_from="service-api", + ), + session_id="session-1", + scope=["agent_stub:connect"], + token_id="token-1", + ) + + +def _patch_async_client(monkeypatch, handler) -> None: + original_async_client = httpx.AsyncClient + monkeypatch.setattr( + "dify_agent.agent_stub.server.agent_stub_drive.httpx.AsyncClient", + lambda **kwargs: original_async_client(transport=httpx.MockTransport(handler), **kwargs), + ) + + +def test_dify_api_agent_stub_drive_handler_injects_execution_context_for_manifest(monkeypatch) -> None: + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "GET" + assert str(request.url) == ( + "https://api.example.com/inner/api/drive/agent-agent-1/manifest" + "?tenant_id=tenant-1&prefix=skills%2F&include_download_url=true" + ) + assert request.headers["X-Inner-Api-Key"] == "inner-secret" + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "hash": "sha256:abc", + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + "created_at": 123, + "download_url": "https://files.example.com/download", + } + ] + }, + ) + + _patch_async_client(monkeypatch, handler) + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + + async def scenario() -> None: + response = await drive_handler.get_manifest( + principal=_principal(), + prefix="skills/", + include_download_url=True, + ) + assert response.items[0].download_url == "https://files.example.com/download" + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_injects_execution_context_for_commit(monkeypatch) -> None: + def handler(request: httpx.Request) -> httpx.Response: + assert request.method == "POST" + assert str(request.url) == "https://api.example.com/inner/api/drive/agent-agent-1/commit" + assert json.loads(request.content) == { + "tenant_id": "tenant-1", + "user_id": "user-1", + "items": [ + { + "key": "skills/example/SKILL.md", + "file_ref": {"kind": "tool_file", "id": "tool-file-1"}, + "value_owned_by_drive": True, + } + ], + } + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + "value_owned_by_drive": True, + } + ] + }, + ) + + _patch_async_client(monkeypatch, handler) + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + + async def scenario() -> None: + response = await drive_handler.commit( + principal=_principal(), + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + ) + assert response.items[0].value_owned_by_drive is True + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_rejects_missing_agent_id() -> None: + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + principal = _principal() + principal.execution_context = principal.execution_context.model_copy(update={"agent_id": None}) + + async def scenario() -> None: + try: + await drive_handler.get_manifest(principal=principal, prefix="", include_download_url=False) + except AgentStubDriveRequestError as exc: + assert exc.status_code == 400 + assert "agent_id" in str(exc) + else: + raise AssertionError("expected AgentStubDriveRequestError") + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_rejects_missing_user_id_for_commit() -> None: + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + principal = _principal() + principal.execution_context = principal.execution_context.model_copy(update={"user_id": None}) + + async def scenario() -> None: + try: + await drive_handler.commit( + principal=principal, + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + ) + except AgentStubDriveRequestError as exc: + assert exc.status_code == 400 + assert "user_id" in str(exc) + else: + raise AssertionError("expected AgentStubDriveRequestError") + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_maps_invalid_json_response(monkeypatch) -> None: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(200, text="not-json", headers={"Content-Type": "application/json"}) + + _patch_async_client(monkeypatch, handler) + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + + async def scenario() -> None: + try: + await drive_handler.get_manifest(principal=_principal(), prefix="skills/", include_download_url=False) + except AgentStubDriveRequestError as exc: + assert exc.status_code == 502 + assert exc.detail == "Dify API drive request returned invalid JSON" + else: + raise AssertionError("expected AgentStubDriveRequestError") + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_rejects_malformed_success_payload(monkeypatch) -> None: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json={"unexpected": []}) + + _patch_async_client(monkeypatch, handler) + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + + async def scenario() -> None: + try: + await drive_handler.get_manifest(principal=_principal(), prefix="skills/", include_download_url=False) + except AgentStubDriveRequestError as exc: + assert exc.status_code == 502 + assert exc.detail == "Dify API drive manifest response is invalid" + else: + raise AssertionError("expected AgentStubDriveRequestError") + + asyncio.run(scenario()) + + +def test_dify_api_agent_stub_drive_handler_preserves_non_2xx_detail(monkeypatch) -> None: + def handler(_request: httpx.Request) -> httpx.Response: + return httpx.Response(404, json={"code": "source_not_found", "message": "missing file"}) + + _patch_async_client(monkeypatch, handler) + drive_handler = DifyApiAgentStubDriveRequestHandler( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + + async def scenario() -> None: + try: + await drive_handler.commit( + principal=_principal(), + request=AgentStubDriveCommitRequest( + items=[ + AgentStubDriveCommitItem( + key="skills/example/SKILL.md", + file_ref=AgentStubDriveFileRef(kind="tool_file", id="tool-file-1"), + ) + ] + ), + ) + except AgentStubDriveRequestError as exc: + assert exc.status_code == 404 + assert exc.detail == {"code": "source_not_found", "message": "missing file"} + else: + raise AssertionError("expected AgentStubDriveRequestError") + + asyncio.run(scenario()) diff --git a/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_routes.py b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_routes.py index fb3fd264f29..ab285cc681a 100644 --- a/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_routes.py +++ b/dify-agent/tests/local/dify_agent/agent_stub/server/test_agent_stub_routes.py @@ -8,7 +8,14 @@ from typing import cast from fastapi import FastAPI from fastapi.testclient import TestClient -from dify_agent.agent_stub.protocol.agent_stub import AgentStubFileDownloadResponse, AgentStubFileUploadResponse +from dify_agent.agent_stub.protocol.agent_stub import ( + AgentStubDriveCommitResponse, + AgentStubDriveItem, + AgentStubDriveManifestResponse, + AgentStubFileDownloadResponse, + AgentStubFileUploadResponse, +) +from dify_agent.agent_stub.server.agent_stub_drive import AgentStubDriveRequestError, AgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import AgentStubFileRequestError, AgentStubFileRequestHandler from dify_agent.agent_stub.server.routes.agent_stub import create_agent_stub_http_router from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec @@ -261,3 +268,137 @@ def test_agent_stub_file_route_preserves_structured_handler_error_details() -> N assert response.status_code == 400 assert response.json()["detail"] == {"detail": "bad request", "code": "inner_api_error"} + + +def test_agent_stub_drive_manifest_route_forwards_authenticated_request() -> None: + codec = _token_codec() + token = codec.encode_connection_token(_execution_context(), now=int(time.time()) - 1) + + class FakeDriveHandler: + async def get_manifest(self, *, principal, prefix, include_download_url): + assert principal.execution_context.user_id == "user-1" + assert prefix == "skills/" + assert include_download_url is True + return AgentStubDriveManifestResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=12, + hash="sha256:abc", + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + created_at=123, + download_url="https://files.example.com/download", + ) + ] + ) + + async def commit(self, *, principal, request): + del principal, request + raise AssertionError("unexpected commit request") + + drive_handler = cast(AgentStubDriveRequestHandler, cast(object, FakeDriveHandler())) + app = FastAPI() + app.include_router(create_agent_stub_http_router(codec, None, drive_handler)) + client = TestClient(app) + + response = client.get( + "/agent-stub/drive/manifest", + headers={"Authorization": f"Bearer {token}"}, + params={"prefix": "skills/", "include_download_url": "true"}, + ) + + assert response.status_code == 200 + assert response.json()["items"][0]["key"] == "skills/example/SKILL.md" + + +def test_agent_stub_drive_commit_route_forwards_authenticated_request() -> None: + codec = _token_codec() + token = codec.encode_connection_token(_execution_context(), now=int(time.time()) - 1) + + class FakeDriveHandler: + async def commit(self, *, principal, request): + assert principal.execution_context.user_id == "user-1" + assert request.items[0].file_ref.id == "tool-file-1" + return AgentStubDriveCommitResponse( + items=[ + AgentStubDriveItem( + key="skills/example/SKILL.md", + size=12, + hash=None, + mime_type="text/markdown", + file_kind="tool_file", + file_id="tool-file-1", + value_owned_by_drive=True, + ) + ] + ) + + async def get_manifest(self, *, principal, prefix, include_download_url): + del principal, prefix, include_download_url + raise AssertionError("unexpected manifest request") + + drive_handler = cast(AgentStubDriveRequestHandler, cast(object, FakeDriveHandler())) + app = FastAPI() + app.include_router(create_agent_stub_http_router(codec, None, drive_handler)) + client = TestClient(app) + + response = client.post( + "/agent-stub/drive/commit", + headers={"Authorization": f"Bearer {token}"}, + json={"items": [{"key": "skills/example/SKILL.md", "file_ref": {"kind": "tool_file", "id": "tool-file-1"}}]}, + ) + + assert response.status_code == 200 + assert response.json()["items"][0]["file_id"] == "tool-file-1" + + +def test_agent_stub_drive_routes_return_503_when_drive_api_is_unconfigured() -> None: + codec = _token_codec() + token = codec.encode_connection_token(_execution_context(), now=int(time.time()) - 1) + app = FastAPI() + app.include_router(create_agent_stub_http_router(codec, None, None)) + client = TestClient(app) + + manifest_response = client.get( + "/agent-stub/drive/manifest", + headers={"Authorization": f"Bearer {token}"}, + ) + commit_response = client.post( + "/agent-stub/drive/commit", + headers={"Authorization": f"Bearer {token}"}, + json={"items": [{"key": "skills/example/SKILL.md", "file_ref": {"kind": "tool_file", "id": "tool-file-1"}}]}, + ) + + assert manifest_response.status_code == 503 + assert commit_response.status_code == 503 + assert manifest_response.json()["detail"] == "Agent Stub drive API is not configured" + assert commit_response.json()["detail"] == "Agent Stub drive API is not configured" + + +def test_agent_stub_drive_route_preserves_structured_handler_error_details() -> None: + codec = _token_codec() + token = codec.encode_connection_token(_execution_context(), now=int(time.time()) - 1) + + class FakeDriveHandler: + async def get_manifest(self, *, principal, prefix, include_download_url): + del principal, prefix, include_download_url + raise AgentStubDriveRequestError(400, {"code": "invalid_key", "message": "bad request"}) + + async def commit(self, *, principal, request): + del principal, request + raise AssertionError("unexpected commit request") + + drive_handler = cast(AgentStubDriveRequestHandler, cast(object, FakeDriveHandler())) + app = FastAPI() + app.include_router(create_agent_stub_http_router(codec, None, drive_handler)) + client = TestClient(app) + + response = client.get( + "/agent-stub/drive/manifest", + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == {"code": "invalid_key", "message": "bad request"} diff --git a/dify-agent/tests/local/dify_agent/server/test_app.py b/dify-agent/tests/local/dify_agent/server/test_app.py index b12a636381e..3983ef43506 100644 --- a/dify-agent/tests/local/dify_agent/server/test_app.py +++ b/dify-agent/tests/local/dify_agent/server/test_app.py @@ -262,6 +262,10 @@ def test_create_app_creates_scheduler_and_closes_after_shutdown(monkeypatch: pyt getattr(route, "path", None) == "/agent-stub/files/download-request" for route in create_app(settings).routes ) + assert any( + getattr(route, "path", None) == "/agent-stub/drive/manifest" for route in create_app(settings).routes + ) + assert any(getattr(route, "path", None) == "/agent-stub/drive/commit" for route in create_app(settings).routes) assert FakeRunScheduler.created[0].shutdown_called is True assert FakeRunScheduler.created[0].dify_api_http_client.is_closed is True @@ -334,6 +338,64 @@ def test_create_app_wires_authenticated_agent_stub_file_upload_route(monkeypatch assert fake_redis.closed is True +def test_create_app_wires_authenticated_agent_stub_drive_manifest_route(monkeypatch: pytest.MonkeyPatch) -> None: + fake_redis, fake_http_client = _patch_app_lifecycle(monkeypatch) + settings = ServerSettings( + redis_url="redis://example.invalid/0", + agent_stub_url="https://agent.example.com/agent-stub", + server_secret_key=_base64url_secret(b"1" * 32), + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + ) + token_codec = settings.create_agent_stub_token_codec() + assert token_codec is not None + token = token_codec.encode_connection_token( + _execution_context().model_copy(update={"agent_id": "agent-1"}), now=int(time.time()) - 1 + ) + + original_async_client = httpx.AsyncClient + + def handler(request: httpx.Request) -> httpx.Response: + assert str(request.url) == ( + "https://api.example.com/inner/api/drive/agent-agent-1/manifest" + "?tenant_id=tenant-1&prefix=skills%2F&include_download_url=false" + ) + assert request.headers["X-Inner-Api-Key"] == "inner-secret" + return httpx.Response( + 200, + json={ + "items": [ + { + "key": "skills/example/SKILL.md", + "size": 12, + "hash": "sha256:abc", + "mime_type": "text/markdown", + "file_kind": "tool_file", + "file_id": "tool-file-1", + } + ] + }, + ) + + monkeypatch.setattr( + "dify_agent.agent_stub.server.agent_stub_drive.httpx.AsyncClient", + lambda **kwargs: original_async_client(transport=httpx.MockTransport(handler), **kwargs), + ) + + with TestClient(create_app(settings)) as client: + response = client.get( + "/agent-stub/drive/manifest", + headers={"Authorization": f"Bearer {token}"}, + params={"prefix": "skills/"}, + ) + + assert response.status_code == 200 + assert response.json()["items"][0]["key"] == "skills/example/SKILL.md" + assert FakeRunScheduler.created[0].shutdown_called is True + assert fake_http_client.is_closed is True + assert fake_redis.closed is True + + def test_create_app_starts_and_stops_agent_stub_grpc_server_for_grpc_url(monkeypatch: pytest.MonkeyPatch) -> None: fake_redis, fake_http_client = _patch_app_lifecycle(monkeypatch) started: dict[str, object] = {} @@ -380,7 +442,6 @@ def test_create_plugin_daemon_http_client_uses_generic_outbound_httpx_constructi ) assert isinstance(client, FakePluginDaemonHttpClient) - assert isinstance(client.timeout, FakeTimeout) assert client.timeout.connect == 1 assert client.timeout.read == 2 assert client.timeout.write == 3 @@ -410,7 +471,6 @@ def test_create_dify_api_inner_http_client_uses_generic_outbound_httpx_construct ) assert isinstance(client, FakePluginDaemonHttpClient) - assert isinstance(client.timeout, FakeTimeout) assert client.timeout.connect == 1 assert client.timeout.read == 2 assert client.timeout.write == 3 diff --git a/dify-agent/tests/local/dify_agent/server/test_settings.py b/dify-agent/tests/local/dify_agent/server/test_settings.py index fb444f840c2..1862e5995e2 100644 --- a/dify-agent/tests/local/dify_agent/server/test_settings.py +++ b/dify-agent/tests/local/dify_agent/server/test_settings.py @@ -2,10 +2,13 @@ from __future__ import annotations from pathlib import Path import secrets +from typing import cast +import httpx import pytest from pydantic import ValidationError +from dify_agent.agent_stub.server.agent_stub_drive import DifyApiAgentStubDriveRequestHandler from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec from dify_agent.server.settings import ServerSettings @@ -179,3 +182,29 @@ def test_server_settings_create_agent_stub_file_request_handler_returns_handler_ assert isinstance(handler, DifyApiAgentStubFileRequestHandler) assert handler.dify_api_base_url == "https://api.example.com" assert handler.dify_api_inner_api_key == "inner-secret" + + +def test_server_settings_create_agent_stub_drive_request_handler_returns_none_without_full_settings() -> None: + assert ServerSettings().create_agent_stub_drive_request_handler() is None + + +def test_server_settings_create_agent_stub_drive_request_handler_returns_handler_when_configured() -> None: + settings = ServerSettings( + dify_api_base_url="https://api.example.com", + dify_api_inner_api_key="inner-secret", + outbound_http_connect_timeout=11, + outbound_http_read_timeout=22, + outbound_http_write_timeout=33, + outbound_http_pool_timeout=44, + ) + + handler = settings.create_agent_stub_drive_request_handler() + + assert isinstance(handler, DifyApiAgentStubDriveRequestHandler) + assert handler.dify_api_base_url == "https://api.example.com" + assert handler.dify_api_inner_api_key == "inner-secret" + timeout = cast(httpx.Timeout, handler.timeout) + assert timeout.connect == 11 + assert timeout.read == 22 + assert timeout.write == 33 + assert timeout.pool == 44 diff --git a/dify-agent/tests/local/test_packaging.py b/dify-agent/tests/local/test_packaging.py index 23ae6e65e8b..bc76ede8ed7 100644 --- a/dify-agent/tests/local/test_packaging.py +++ b/dify-agent/tests/local/test_packaging.py @@ -16,7 +16,7 @@ CLIENT_SHARED_DTO_DEPENDENCIES = { SERVER_RUNTIME_DEPENDENCIES = { "fastapi==0.136.0", - "graphon==0.5.1", + "graphon==0.5.2", "jsonschema>=4.23.0,<5.0.0", "jwcrypto>=1.5.6,<2", "pydantic-ai-slim[anthropic,google,openai]>=1.85.1,<2.0.0", diff --git a/dify-agent/uv.lock b/dify-agent/uv.lock index 0ee1bf4f8bf..bd99e89a439 100644 --- a/dify-agent/uv.lock +++ b/dify-agent/uv.lock @@ -628,7 +628,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "fastapi", marker = "extra == 'server'", specifier = "==0.136.0" }, - { name = "graphon", marker = "extra == 'server'", specifier = "==0.5.1" }, + { name = "graphon", marker = "extra == 'server'", specifier = "==0.5.2" }, { name = "grpclib", extras = ["protobuf"], marker = "extra == 'grpc'", specifier = ">=0.4.9,<0.5.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0,<5.0.0" }, @@ -808,7 +808,7 @@ wheels = [ [[package]] name = "graphon" -version = "0.5.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, @@ -829,9 +829,9 @@ dependencies = [ { name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] }, { name = "webvtt-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/fa/432fa802bcb13f7f51dc323ddef92594b15333eafef181d937ffa554116e/graphon-0.5.1.tar.gz", hash = "sha256:ca38cc62ef3fbc2f3072b68235bcb41e32a6369a1753b46418c1d761c57125fe", size = 269741, upload-time = "2026-06-11T03:01:38.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/16/f183da187414c335be67f52f6a1b7c2a33bf0b1d5090eda7e6c92d42d94a/graphon-0.5.2.tar.gz", hash = "sha256:d66a9edcd883766bd50e94f84a691c92ce536ea60e721552089e83ac8e94bf68", size = 269773, upload-time = "2026-06-16T04:06:22.074Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/c5/61e8634b89c320af9453083213e8be436071634dbc69cb14b5fe646763e4/graphon-0.5.1-py3-none-any.whl", hash = "sha256:70b49c244a46fb6e338905210cc895bd67584d9ab1412f6ba3cd4ed284010091", size = 381866, upload-time = "2026-06-11T03:01:36.693Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e6/36a3981cd44e7a40a7cd7d374e26f01e02dd49410c5fbbd7df248750d5fb/graphon-0.5.2-py3-none-any.whl", hash = "sha256:11f89399e67ed1ddd2ce1c336accd9c4ad5b8fe2741f9167e6085af0b325cd14", size = 381908, upload-time = "2026-06-16T04:06:20.453Z" }, ] [[package]] diff --git a/docker/.env.example b/docker/.env.example index 8daa82d05a1..5e13db9cbc4 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # Essential defaults for Docker Compose deployments. # Only include variables required for services to start. +# Do not add optional variables to this file. # # For a default deployment, copy this file to .env and run: # docker compose up -d @@ -150,6 +151,7 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false # Enable preview features still in development (currently the /create and # /refine slash commands in the "Go to Anything" command palette). NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +ENABLE_AGENT_V2=false EXPERIMENTAL_ENABLE_VINEXT=false # Storage and default vector store @@ -199,8 +201,6 @@ SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128 SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128 SSRF_HTTP_PORT=3128 SSRF_COREDUMP_DIR=/var/spool/squid -SSRF_REVERSE_PROXY_PORT=8194 -SSRF_SANDBOX_HOST=sandbox SSRF_DEFAULT_TIME_OUT=5 SSRF_DEFAULT_CONNECT_TIME_OUT=5 SSRF_DEFAULT_READ_TIME_OUT=5 diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 9987483156f..1fbd0ec1169 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -622,9 +622,8 @@ services: # pls clearly modify the squid env vars to fit your network environment. HTTP_PORT: ${SSRF_HTTP_PORT:-3128} COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} - REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} - SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} - SANDBOX_PORT: ${SANDBOX_PORT:-8194} + SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-} + SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-} networks: - ssrf_proxy_network - default diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 170e1718565..f9d15675aa0 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -212,12 +212,13 @@ services: # pls clearly modify the squid env vars to fit your network environment. HTTP_PORT: ${SSRF_HTTP_PORT:-3128} COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} - REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} - SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} - SANDBOX_PORT: ${SANDBOX_PORT:-8194} + SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-} + SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-} + SSRF_SANDBOX_PROXY_PORT: ${SSRF_SANDBOX_PROXY_PORT:-8194} + SSRF_SANDBOX_PROXY_HOST: ${SSRF_SANDBOX_PROXY_HOST:-sandbox} ports: - "${EXPOSE_SSRF_PROXY_PORT:-3128}:${SSRF_HTTP_PORT:-3128}" - - "${EXPOSE_SANDBOX_PORT:-8194}:${SANDBOX_PORT:-8194}" + - "${EXPOSE_SANDBOX_PORT:-8194}:${SSRF_SANDBOX_PROXY_PORT:-8194}" networks: - ssrf_proxy_network - default diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 2b9f91492eb..af9ccdfd9c2 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -628,9 +628,8 @@ services: # pls clearly modify the squid env vars to fit your network environment. HTTP_PORT: ${SSRF_HTTP_PORT:-3128} COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} - REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} - SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} - SANDBOX_PORT: ${SANDBOX_PORT:-8194} + SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-} + SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-} networks: - ssrf_proxy_network - default diff --git a/docker/envs/core-services/shared.env.example b/docker/envs/core-services/shared.env.example index 0cc840d2a4d..26274fe87d2 100644 --- a/docker/envs/core-services/shared.env.example +++ b/docker/envs/core-services/shared.env.example @@ -188,8 +188,6 @@ WEBHOOK_REQUEST_BODY_MAX_SIZE=10485760 RESPECT_XFORWARD_HEADERS_ENABLED=false SSRF_HTTP_PORT=3128 SSRF_COREDUMP_DIR=/var/spool/squid -SSRF_REVERSE_PROXY_PORT=8194 -SSRF_SANDBOX_HOST=sandbox SSRF_DEFAULT_TIME_OUT=5 SSRF_DEFAULT_CONNECT_TIME_OUT=5 SSRF_DEFAULT_READ_TIME_OUT=5 diff --git a/docker/envs/core-services/web.env.example b/docker/envs/core-services/web.env.example index 4c119106316..bd788a1b16c 100644 --- a/docker/envs/core-services/web.env.example +++ b/docker/envs/core-services/web.env.example @@ -25,6 +25,7 @@ ENABLE_WEBSITE_FIRECRAWL=true ENABLE_WEBSITE_WATERCRAWL=true NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +ENABLE_AGENT_V2=false NEXT_PUBLIC_COOKIE_DOMAIN= NEXT_PUBLIC_BATCH_CONCURRENCY=5 CSP_WHITELIST= diff --git a/docker/envs/infrastructure/ssrf-proxy.env.example b/docker/envs/infrastructure/ssrf-proxy.env.example index 210a7824944..3624bd9fbb4 100644 --- a/docker/envs/infrastructure/ssrf-proxy.env.example +++ b/docker/envs/infrastructure/ssrf-proxy.env.example @@ -6,8 +6,8 @@ SSRF_PROXY_HTTP_URL=http://ssrf_proxy:3128 SSRF_PROXY_HTTPS_URL=http://ssrf_proxy:3128 SSRF_HTTP_PORT=3128 SSRF_COREDUMP_DIR=/var/spool/squid -SSRF_REVERSE_PROXY_PORT=8194 -SSRF_SANDBOX_HOST=sandbox +SSRF_PROXY_ALLOW_PRIVATE_IPS= +SSRF_PROXY_ALLOW_PRIVATE_DOMAINS= SSRF_DEFAULT_TIME_OUT=5 SSRF_DEFAULT_CONNECT_TIME_OUT=5 SSRF_DEFAULT_READ_TIME_OUT=5 diff --git a/docker/envs/middleware.env.example b/docker/envs/middleware.env.example index 7b28a77fe3e..3ff8139ad16 100644 --- a/docker/envs/middleware.env.example +++ b/docker/envs/middleware.env.example @@ -111,8 +111,10 @@ SANDBOX_PORT=8194 # ------------------------------ SSRF_HTTP_PORT=3128 SSRF_COREDUMP_DIR=/var/spool/squid -SSRF_REVERSE_PROXY_PORT=8194 -SSRF_SANDBOX_HOST=sandbox +SSRF_PROXY_ALLOW_PRIVATE_IPS= +SSRF_PROXY_ALLOW_PRIVATE_DOMAINS= +SSRF_SANDBOX_PROXY_PORT=8194 +SSRF_SANDBOX_PROXY_HOST=sandbox # ------------------------------ # Environment Variables for weaviate Service @@ -240,4 +242,4 @@ LOGSTORE_DUAL_READ_ENABLED=true # Control flag for whether to write the `graph` field to LogStore. # If LOGSTORE_ENABLE_PUT_GRAPH_FIELD is "true", write the full `graph` field; # otherwise write an empty {} instead. Defaults to writing the `graph` field. -LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true \ No newline at end of file +LOGSTORE_ENABLE_PUT_GRAPH_FIELD=true diff --git a/docker/ssrf_proxy/docker-entrypoint.sh b/docker/ssrf_proxy/docker-entrypoint.sh index 613897bb7db..a19f9818b24 100755 --- a/docker/ssrf_proxy/docker-entrypoint.sh +++ b/docker/ssrf_proxy/docker-entrypoint.sh @@ -26,6 +26,54 @@ tail -F /var/log/squid/error.log 2>/dev/null & tail -F /var/log/squid/store.log 2>/dev/null & tail -F /var/log/squid/cache.log 2>/dev/null & +ALLOW_PRIVATE_CONF=/etc/squid/dify_allow_private.conf +SANDBOX_PROXY_CONF=/etc/squid/dify_sandbox_proxy.conf + +write_optional_private_allowlist() { + local env_name="$1" + local acl_name="$2" + local acl_type="$3" + local raw_values="${!env_name:-}" + + raw_values="${raw_values//,/ }" + + if [ -z "${raw_values//[[:space:]]/}" ]; then + return + fi + + printf 'acl %s %s' "$acl_name" "$acl_type" >> "$ALLOW_PRIVATE_CONF" + for value in $raw_values; do + printf ' %s' "$value" >> "$ALLOW_PRIVATE_CONF" + done + printf '\nhttp_access allow client_localnet %s\n' "$acl_name" >> "$ALLOW_PRIVATE_CONF" +} + +{ + echo "# Generated by docker-entrypoint.sh." + echo "# Allows selected private targets before the default private-network deny rule." +} > "$ALLOW_PRIVATE_CONF" +write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_IPS" "dify_allowed_private_networks" "dst" +write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS" "dify_allowed_private_domains" "dstdomain" + +{ + echo "# Generated by docker-entrypoint.sh." + echo "# Enables the middleware-only sandbox host bridge when configured." +} > "$SANDBOX_PROXY_CONF" + +if [ -n "${SSRF_SANDBOX_PROXY_PORT:-}" ]; then + sandbox_proxy_host="${SSRF_SANDBOX_PROXY_HOST:-sandbox}" + sandbox_proxy_target_port="${SANDBOX_PORT:-8194}" + + { + printf 'http_port %s accel vhost\n' "$SSRF_SANDBOX_PROXY_PORT" + printf 'cache_peer %s parent %s 0 no-query originserver name=dify_sandbox\n' \ + "$sandbox_proxy_host" \ + "$sandbox_proxy_target_port" + printf 'acl dify_sandbox_proxy_port localport %s\n' "$SSRF_SANDBOX_PROXY_PORT" + printf 'http_access allow dify_sandbox_proxy_port\n' + } >> "$SANDBOX_PROXY_CONF" +fi + # Replace environment variables in the template and output to the squid.conf echo "[ENTRYPOINT] replacing environment variables in the template" awk '{ diff --git a/docker/ssrf_proxy/squid.conf.template b/docker/ssrf_proxy/squid.conf.template index fbe9ebc448b..6e30cdba928 100644 --- a/docker/ssrf_proxy/squid.conf.template +++ b/docker/ssrf_proxy/squid.conf.template @@ -1,11 +1,26 @@ -acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) -acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) -acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) -acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines -acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) -acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) -acl localnet src fc00::/7 # RFC 4193 local private network range -acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines +acl client_localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) +acl client_localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) +acl client_localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) +acl client_localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines +acl client_localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) +acl client_localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) +acl client_localnet src fc00::/7 # RFC 4193 local private network range +acl client_localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines +acl to_private_networks dst 0.0.0.0/8 +acl to_private_networks dst 10.0.0.0/8 +acl to_private_networks dst 100.64.0.0/10 +acl to_private_networks dst 127.0.0.0/8 +acl to_private_networks dst 169.254.0.0/16 +acl to_private_networks dst 172.16.0.0/12 +acl to_private_networks dst 192.168.0.0/16 +acl to_private_networks dst 224.0.0.0/4 +acl to_private_networks dst 240.0.0.0/4 +acl to_private_networks dst ::/128 +acl to_private_networks dst ::1/128 +acl to_private_networks dst ::ffff:0:0/96 # IPv4-mapped +acl to_private_networks dst ::/96 # deprecated IPv4-compatible +acl to_private_networks dst fc00::/7 +acl to_private_networks dst fe80::/10 acl SSL_ports port 443 # acl SSL_ports port 1025-65535 # Enable the configuration to resolve this issue: https://github.com/langgenius/dify/issues/12792 acl Safe_ports port 80 # http @@ -20,18 +35,23 @@ acl Safe_ports port 591 # filemaker acl Safe_ports port 777 # multiling http acl CONNECT method CONNECT acl allowed_domains dstdomain .marketplace.dify.ai -http_access allow allowed_domains + +http_port ${HTTP_PORT} + http_access deny !Safe_ports http_access deny CONNECT !SSL_ports http_access allow localhost manager http_access deny manager +include /etc/squid/dify_sandbox_proxy.conf +include /etc/squid/dify_allow_private.conf +http_access deny to_private_networks +http_access allow allowed_domains +http_access allow client_localnet http_access allow localhost -include /etc/squid/conf.d/*.conf http_access deny all tcp_outgoing_address 0.0.0.0 ################################## Proxy Server ################################ -http_port ${HTTP_PORT} coredump_dir ${COREDUMP_DIR} refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 @@ -47,11 +67,7 @@ refresh_pattern . 0 20% 4320 # upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks # cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default -################################## Reverse Proxy To Sandbox ################################ -http_port ${REVERSE_PROXY_PORT} accel vhost -cache_peer ${SANDBOX_HOST} parent ${SANDBOX_PORT} 0 no-query originserver -acl src_all src all -http_access allow src_all +################################## Request Buffer ################################ # Unless the option's size is increased, an error will occur when uploading more than two files. client_request_buffer_max_size 100 MB @@ -103,4 +119,3 @@ access_log daemon:/var/log/squid/access.log dify_log # Access log to track concurrent requests and timeouts logfile_rotate 10 - diff --git a/docker/ssrf_proxy/test_ssrf_proxy_config.sh b/docker/ssrf_proxy/test_ssrf_proxy_config.sh new file mode 100755 index 00000000000..21206a96b7f --- /dev/null +++ b/docker/ssrf_proxy/test_ssrf_proxy_config.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMAGE="${SSRF_PROXY_TEST_IMAGE:-ubuntu/squid:latest}" +CLIENT_IMAGE="${SSRF_PROXY_TEST_CLIENT_IMAGE:-busybox:latest}" +CONTAINER_NAME="${SSRF_PROXY_TEST_CONTAINER:-dify-ssrf-proxy-test-$$}" +SANDBOX_CONTAINER_NAME="${CONTAINER_NAME}-sandbox" +NETWORK_NAME="${SSRF_PROXY_TEST_NETWORK:-dify-ssrf-proxy-test-$$}" +RUN_PUBLIC_CHECK="${SSRF_PROXY_TEST_PUBLIC_CHECK:-true}" + +cleanup() { + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker rm -f "$SANDBOX_CONTAINER_NAME" >/dev/null 2>&1 || true + docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true +} + +http_code_for() { + local proxy_url="$1" + local target_url="$2" + local output + + output="$( + docker run \ + --rm \ + --network "$NETWORK_NAME" \ + --env "http_proxy=$proxy_url" \ + --env "https_proxy=$proxy_url" \ + "$CLIENT_IMAGE" \ + wget -S -O /dev/null -T 10 "$target_url" 2>&1 || true + )" + + printf '%s\n' "$output" | awk '$1 ~ /^HTTP\// { code = $2 } END { print code }' +} + +direct_http_code_for() { + local target_url="$1" + local output + + output="$( + docker run \ + --rm \ + --network "$NETWORK_NAME" \ + "$CLIENT_IMAGE" \ + wget -S -O /dev/null -T 10 "$target_url" 2>&1 || true + )" + + printf '%s\n' "$output" | awk '$1 ~ /^HTTP\// { code = $2 } END { print code }' +} + +assert_private_target_blocked() { + local proxy_url="$1" + local target_url="$2" + local status_code + + status_code="$(http_code_for "$proxy_url" "$target_url")" + if [[ "$status_code" != "403" ]]; then + echo "Expected $target_url to be blocked with HTTP 403, got ${status_code:-no response}." + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 + fi +} + +assert_public_target_allowed() { + local proxy_url="$1" + local target_url="$2" + local status_code + + status_code="$(http_code_for "$proxy_url" "$target_url")" + if [[ ! "$status_code" =~ ^[234][0-9][0-9]$ || "$status_code" == "403" ]]; then + echo "Expected $target_url to remain reachable, got ${status_code:-no response}." + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 + fi +} + +assert_sandbox_bridge_allowed() { + local target_url="$1" + local status_code + + status_code="$(direct_http_code_for "$target_url")" + if [[ ! "$status_code" =~ ^2[0-9][0-9]$ ]]; then + echo "Expected sandbox host bridge $target_url to remain reachable, got ${status_code:-no response}." + docker logs "$CONTAINER_NAME" >&2 || true + docker logs "$SANDBOX_CONTAINER_NAME" >&2 || true + exit 1 + fi +} + +trap cleanup EXIT +cleanup +docker network create "$NETWORK_NAME" >/dev/null + +docker run \ + --detach \ + --name "$SANDBOX_CONTAINER_NAME" \ + --network "$NETWORK_NAME" \ + --network-alias sandbox \ + "$CLIENT_IMAGE" \ + sh -c "mkdir -p /www && echo ok > /www/health && httpd -f -p 8194 -h /www" \ + >/dev/null + +docker run \ + --detach \ + --name "$CONTAINER_NAME" \ + --entrypoint sh \ + --network "$NETWORK_NAME" \ + --volume "$ROOT_DIR/docker/ssrf_proxy/squid.conf.template:/etc/squid/squid.conf.template:ro" \ + --volume "$ROOT_DIR/docker/ssrf_proxy/docker-entrypoint.sh:/docker-entrypoint-mount.sh:ro" \ + --env HTTP_PORT=3128 \ + --env COREDUMP_DIR=/var/spool/squid \ + --env SSRF_SANDBOX_PROXY_PORT=8194 \ + --env SSRF_SANDBOX_PROXY_HOST=sandbox \ + --env "SSRF_PROXY_ALLOW_PRIVATE_IPS=${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}" \ + --env "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}" \ + "$IMAGE" \ + -c "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" \ + >/dev/null + +proxy_url="http://$CONTAINER_NAME:3128" +for _ in {1..30}; do + probe_status="$(http_code_for "$proxy_url" "http://127.0.0.1:80/")" + if [[ -n "$probe_status" ]]; then + break + fi + sleep 1 +done + +if [[ -z "${probe_status:-}" ]]; then + echo "Squid proxy did not respond to probes." + docker logs "$CONTAINER_NAME" >&2 || true + exit 1 +fi + +assert_private_target_blocked "$proxy_url" "http://127.0.0.1:80/" +assert_private_target_blocked "$proxy_url" "http://0.1.2.3:80/" +assert_private_target_blocked "$proxy_url" "http://169.254.169.254/latest/meta-data/" + +if [[ "$RUN_PUBLIC_CHECK" == "true" ]]; then + assert_public_target_allowed "$proxy_url" "http://example.com/" +fi + +assert_sandbox_bridge_allowed "http://$CONTAINER_NAME:8194/health" diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 8a75a82a9da..263481bc9d1 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1089,14 +1089,6 @@ "count": 1 } }, - "web/app/components/base/app-icon/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/base/audio-btn/audio.ts": { "node/prefer-global/buffer": { "count": 1 @@ -1529,17 +1521,6 @@ "count": 1 } }, - "web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/base/features/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -2502,11 +2483,6 @@ "count": 2 } }, - "web/app/components/base/prompt-editor/utils.ts": { - "ts/no-explicit-any": { - "count": 4 - } - }, "web/app/components/base/prompt-log-modal/index.stories.tsx": { "no-console": { "count": 1 @@ -5012,14 +4988,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/blocks.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/featured-tools.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -5152,11 +5120,6 @@ "count": 1 } }, - "web/app/components/workflow/candidate-node-main.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/workflow/comment/comment-input.tsx": { "jsx-a11y/no-autofocus": { "count": 1 @@ -5300,11 +5263,6 @@ "count": 1 } }, - "web/app/components/workflow/hooks/use-nodes-interactions.ts": { - "ts/no-explicit-any": { - "count": 8 - } - }, "web/app/components/workflow/hooks/use-serial-async-callback.ts": { "ts/no-explicit-any": { "count": 1 @@ -5704,7 +5662,7 @@ "count": 2 }, "ts/no-explicit-any": { - "count": 6 + "count": 4 } }, "web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": { @@ -5885,11 +5843,6 @@ "count": 5 } }, - "web/app/components/workflow/nodes/components.ts": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/workflow/nodes/data-source-empty/hooks.ts": { "ts/no-explicit-any": { "count": 1 @@ -7060,19 +7013,6 @@ "count": 1 } }, - "web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/app/components/workflow/run/agent-log/index.tsx": { - "no-barrel-files/no-barrel-files": { - "count": 2 - } - }, "web/app/components/workflow/run/hooks.ts": { "ts/no-explicit-any": { "count": 1 @@ -7505,24 +7445,11 @@ "count": 1 } }, - "web/app/signin/invite-settings/page.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/signin/layout.tsx": { "ts/no-explicit-any": { "count": 1 } }, - "web/app/signin/normal-form.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - } - }, "web/app/signin/one-more-step.tsx": { "no-restricted-imports": { "count": 1 @@ -7750,17 +7677,12 @@ "count": 1 } }, - "web/service/client.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/common.ts": { "no-restricted-imports": { "count": 1 }, "ts/no-explicit-any": { - "count": 27 + "count": 26 } }, "web/service/datasets.ts": { diff --git a/packages/contracts/generated/api/console/activate/types.gen.ts b/packages/contracts/generated/api/console/activate/types.gen.ts index 92370b5a75c..6591aba3996 100644 --- a/packages/contracts/generated/api/console/activate/types.gen.ts +++ b/packages/contracts/generated/api/console/activate/types.gen.ts @@ -6,9 +6,9 @@ export type ClientOptions = { export type ActivatePayload = { email?: string | null - interface_language: string - name: string - timezone: string + interface_language?: string | null + name?: string | null + timezone?: string | null token: string workspace_id?: string | null } @@ -23,7 +23,9 @@ export type ActivationCheckResponse = { } export type ActivationCheckData = { + account_status?: string | null email: string | null + requires_setup?: boolean | null workspace_id: string | null workspace_name: string | null } diff --git a/packages/contracts/generated/api/console/activate/zod.gen.ts b/packages/contracts/generated/api/console/activate/zod.gen.ts index 00f85767b7c..40aff1f934f 100644 --- a/packages/contracts/generated/api/console/activate/zod.gen.ts +++ b/packages/contracts/generated/api/console/activate/zod.gen.ts @@ -7,9 +7,9 @@ import * as z from 'zod' */ export const zActivatePayload = z.object({ email: z.string().nullish(), - interface_language: z.string(), - name: z.string().max(30), - timezone: z.string(), + interface_language: z.string().nullish(), + name: z.string().max(30).nullish(), + timezone: z.string().nullish(), token: z.string(), workspace_id: z.string().nullish(), }) @@ -25,7 +25,9 @@ export const zActivationResponse = z.object({ * ActivationCheckData */ export const zActivationCheckData = z.object({ + account_status: z.string().nullish(), email: z.string().nullable(), + requires_setup: z.boolean().nullish(), workspace_id: z.string().nullable(), workspace_name: z.string().nullable(), }) diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index 13522677abe..fbfca3be118 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -29,6 +29,11 @@ import { zGetAgentByAgentIdDriveFilesPreviewResponse, zGetAgentByAgentIdDriveFilesQuery, zGetAgentByAgentIdDriveFilesResponse, + zGetAgentByAgentIdLogsByConversationIdMessagesPath, + zGetAgentByAgentIdLogsByConversationIdMessagesQuery, + zGetAgentByAgentIdLogsByConversationIdMessagesResponse, + zGetAgentByAgentIdLogSourcesPath, + zGetAgentByAgentIdLogSourcesResponse, zGetAgentByAgentIdLogsPath, zGetAgentByAgentIdLogsQuery, zGetAgentByAgentIdLogsResponse, @@ -78,8 +83,7 @@ import { zPostAgentByAgentIdSandboxFilesUploadResponse, zPostAgentByAgentIdSkillsBySlugInferToolsPath, zPostAgentByAgentIdSkillsBySlugInferToolsResponse, - zPostAgentByAgentIdSkillsStandardizePath, - zPostAgentByAgentIdSkillsStandardizeResponse, + zPostAgentByAgentIdSkillsUploadBody, zPostAgentByAgentIdSkillsUploadPath, zPostAgentByAgentIdSkillsUploadResponse, zPostAgentResponse, @@ -417,6 +421,45 @@ export const files2 = { } export const get9 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAgentByAgentIdLogSources', + path: '/agent/{agent_id}/log-sources', + tags: ['console'], + }) + .input(z.object({ params: zGetAgentByAgentIdLogSourcesPath })) + .output(zGetAgentByAgentIdLogSourcesResponse) + +export const logSources = { + get: get9, +} + +export const get10 = oc + .route({ + inputStructure: 'detailed', + method: 'GET', + operationId: 'getAgentByAgentIdLogsByConversationIdMessages', + path: '/agent/{agent_id}/logs/{conversation_id}/messages', + tags: ['console'], + }) + .input( + z.object({ + params: zGetAgentByAgentIdLogsByConversationIdMessagesPath, + query: zGetAgentByAgentIdLogsByConversationIdMessagesQuery.optional(), + }), + ) + .output(zGetAgentByAgentIdLogsByConversationIdMessagesResponse) + +export const messages = { + get: get10, +} + +export const byConversationId = { + messages, +} + +export const get11 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -430,13 +473,14 @@ export const get9 = oc .output(zGetAgentByAgentIdLogsResponse) export const logs = { - get: get9, + get: get11, + byConversationId, } /** * Get Agent App message details by ID */ -export const get10 = oc +export const get12 = oc .route({ description: 'Get Agent App message details by ID', inputStructure: 'detailed', @@ -449,17 +493,17 @@ export const get10 = oc .output(zGetAgentByAgentIdMessagesByMessageIdResponse) export const byMessageId2 = { - get: get10, + get: get12, } -export const messages = { +export const messages2 = { byMessageId: byMessageId2, } /** * List workflow apps that reference this Agent App's bound Agent (read-only) */ -export const get11 = oc +export const get13 = oc .route({ description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)', inputStructure: 'detailed', @@ -472,13 +516,13 @@ export const get11 = oc .output(zGetAgentByAgentIdReferencingWorkflowsResponse) export const referencingWorkflows = { - get: get11, + get: get13, } /** * Read a text/binary preview file in an Agent App conversation sandbox */ -export const get12 = oc +export const get14 = oc .route({ description: 'Read a text/binary preview file in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -496,7 +540,7 @@ export const get12 = oc .output(zGetAgentByAgentIdSandboxFilesReadResponse) export const read = { - get: get12, + get: get14, } /** @@ -526,7 +570,7 @@ export const upload = { /** * List a directory in an Agent App conversation sandbox */ -export const get13 = oc +export const get15 = oc .route({ description: 'List a directory in an Agent App conversation sandbox', inputStructure: 'detailed', @@ -544,7 +588,7 @@ export const get13 = oc .output(zGetAgentByAgentIdSandboxFilesResponse) export const files3 = { - get: get13, + get: get15, read, upload, } @@ -554,31 +598,11 @@ export const sandbox = { } /** - * Validate + standardize a Skill into an Agent App drive + * Upload + standardize a Skill into an Agent App drive */ export const post8 = oc .route({ - description: 'Validate + standardize a Skill into an Agent App drive', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAgentByAgentIdSkillsStandardize', - path: '/agent/{agent_id}/skills/standardize', - successStatus: 201, - tags: ['console'], - }) - .input(z.object({ params: zPostAgentByAgentIdSkillsStandardizePath })) - .output(zPostAgentByAgentIdSkillsStandardizeResponse) - -export const standardize = { - post: post8, -} - -/** - * Upload + validate a Skill package for an Agent App - */ -export const post9 = oc - .route({ - description: 'Upload + validate a Skill package for an Agent App', + description: 'Upload + standardize a Skill into an Agent App drive', inputStructure: 'detailed', method: 'POST', operationId: 'postAgentByAgentIdSkillsUpload', @@ -586,17 +610,22 @@ export const post9 = oc successStatus: 201, tags: ['console'], }) - .input(z.object({ params: zPostAgentByAgentIdSkillsUploadPath })) + .input( + z.object({ + body: zPostAgentByAgentIdSkillsUploadBody, + params: zPostAgentByAgentIdSkillsUploadPath, + }), + ) .output(zPostAgentByAgentIdSkillsUploadResponse) export const upload2 = { - post: post9, + post: post8, } /** * Infer CLI tool + ENV suggestions from a standardized Agent App skill */ -export const post10 = oc +export const post9 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill', inputStructure: 'detailed', @@ -609,7 +638,7 @@ export const post10 = oc .output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse) export const inferTools = { - post: post10, + post: post9, } /** @@ -633,12 +662,11 @@ export const bySlug = { } export const skills = { - standardize, upload: upload2, bySlug, } -export const get14 = oc +export const get16 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -655,14 +683,14 @@ export const get14 = oc .output(zGetAgentByAgentIdStatisticsSummaryResponse) export const summary = { - get: get14, + get: get16, } export const statistics = { summary, } -export const get15 = oc +export const get17 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -674,10 +702,10 @@ export const get15 = oc .output(zGetAgentByAgentIdVersionsByVersionIdResponse) export const byVersionId = { - get: get15, + get: get17, } -export const get16 = oc +export const get18 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -689,7 +717,7 @@ export const get16 = oc .output(zGetAgentByAgentIdVersionsResponse) export const versions = { - get: get16, + get: get18, byVersionId, } @@ -705,7 +733,7 @@ export const delete3 = oc .input(z.object({ params: zDeleteAgentByAgentIdPath })) .output(zDeleteAgentByAgentIdResponse) -export const get17 = oc +export const get19 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -729,7 +757,7 @@ export const put2 = oc export const byAgentId = { delete: delete3, - get: get17, + get: get19, put: put2, chatMessages, composer, @@ -738,8 +766,9 @@ export const byAgentId = { features, feedbacks, files: files2, + logSources, logs, - messages, + messages: messages2, referencingWorkflows, sandbox, skills, @@ -747,7 +776,7 @@ export const byAgentId = { versions, } -export const get18 = oc +export const get20 = oc .route({ inputStructure: 'detailed', method: 'GET', @@ -758,7 +787,7 @@ export const get18 = oc .input(z.object({ query: zGetAgentQuery.optional() })) .output(zGetAgentResponse) -export const post11 = oc +export const post10 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -771,8 +800,8 @@ export const post11 = oc .output(zPostAgentResponse) export const agent = { - get: get18, - post: post11, + get: get20, + post: post10, inviteOptions, byAgentId, } diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 2373233d063..19a818cf9a6 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -18,10 +18,10 @@ export type AgentAppCreatePayload = { icon_background?: string | null icon_type?: IconType | null name: string - role?: string + role: string } -export type AppDetailWithSite = { +export type AgentAppDetailWithSite = { access_mode?: string | null active_config_is_published?: boolean api_base_url?: string | null @@ -67,7 +67,7 @@ export type AgentAppUpdatePayload = { icon_type?: IconType | null max_active_requests?: number | null name: string - role?: string | null + role: string use_icon_as_answer_icon?: boolean | null } @@ -177,8 +177,21 @@ export type AgentDriveFileCommitResponse = { file: AgentDriveFileResponse } +export type AgentLogSourceListResponse = { + data: Array + groups: Array +} + export type AgentLogListResponse = { - data: Array + data: Array + has_more: boolean + limit: number + page: number + total: number +} + +export type AgentLogMessageListResponse = { + data: Array has_more: boolean limit: number page: number @@ -242,11 +255,6 @@ export type SandboxUploadResponse = { path: string } -export type AgentSkillStandardizeResponse = { - manifest: SkillManifest - skill: AgentSkillRefConfig -} - export type AgentSkillUploadResponse = { manifest: SkillManifest skill: AgentSkillRefConfig @@ -554,23 +562,54 @@ export type AgentDriveFileResponse = { size?: number | null } -export type AgentLogItemResponse = { +export type AgentLogSourceResponse = { + app_icon?: string | null + app_icon_background?: string | null + app_icon_type?: string | null + app_id: string + app_name: string + id: string + node_id?: string | null + type: 'webapp' | 'workflow' + workflow_id?: string | null + workflow_version?: string | null +} + +export type AgentLogSourceGroupResponse = { + label: string + sources?: Array + type: 'webapp' | 'workflow' +} + +export type AgentLogConversationItemResponse = { + conversation_id: string + created_at?: number | null + end_user_id?: string | null + id: string + message_count: number + operation_rate?: number | null + source?: AgentLogSourceResponse | null + status: 'failed' | 'paused' | 'success' + title?: string | null + unread: boolean + updated_at?: number | null + user_rate?: number | null +} + +export type AgentLogMessageItemResponse = { answer: string answer_tokens: number conversation_id: string - conversation_name?: string | null created_at?: number | null currency: string error?: string | null from_account_id?: string | null from_end_user_id?: string | null - from_source?: string | null id: string latency: number message_id: string message_tokens: number query: string - source?: string | null status: string total_price: string total_tokens: number @@ -1387,7 +1426,7 @@ export type AgentAppPaginationWritable = { total: number } -export type AppDetailWithSiteWritable = { +export type AgentAppDetailWithSiteWritable = { access_mode?: string | null active_config_is_published?: boolean api_base_url?: string | null @@ -1506,7 +1545,7 @@ export type PostAgentErrors = { } export type PostAgentResponses = { - 201: AppDetailWithSite + 201: AgentAppDetailWithSite } export type PostAgentResponse = PostAgentResponses[keyof PostAgentResponses] @@ -1560,7 +1599,7 @@ export type GetAgentByAgentIdData = { } export type GetAgentByAgentIdResponses = { - 200: AppDetailWithSite + 200: AgentAppDetailWithSite } export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses] @@ -1580,7 +1619,7 @@ export type PutAgentByAgentIdErrors = { } export type PutAgentByAgentIdResponses = { - 200: AppDetailWithSite + 200: AgentAppDetailWithSite } export type PutAgentByAgentIdResponse = PutAgentByAgentIdResponses[keyof PutAgentByAgentIdResponses] @@ -1726,7 +1765,7 @@ export type PostAgentByAgentIdCopyErrors = { } export type PostAgentByAgentIdCopyResponses = { - 201: AppDetailWithSite + 201: AgentAppDetailWithSite } export type PostAgentByAgentIdCopyResponse @@ -1861,6 +1900,22 @@ export type PostAgentByAgentIdFilesResponses = { export type PostAgentByAgentIdFilesResponse = PostAgentByAgentIdFilesResponses[keyof PostAgentByAgentIdFilesResponses] +export type GetAgentByAgentIdLogSourcesData = { + body?: never + path: { + agent_id: string + } + query?: never + url: '/agent/{agent_id}/log-sources' +} + +export type GetAgentByAgentIdLogSourcesResponses = { + 200: AgentLogSourceListResponse +} + +export type GetAgentByAgentIdLogSourcesResponse + = GetAgentByAgentIdLogSourcesResponses[keyof GetAgentByAgentIdLogSourcesResponses] + export type GetAgentByAgentIdLogsData = { body?: never path: { @@ -1885,6 +1940,31 @@ export type GetAgentByAgentIdLogsResponses = { export type GetAgentByAgentIdLogsResponse = GetAgentByAgentIdLogsResponses[keyof GetAgentByAgentIdLogsResponses] +export type GetAgentByAgentIdLogsByConversationIdMessagesData = { + body?: never + path: { + agent_id: string + conversation_id: string + } + query?: { + end?: string + keyword?: string + limit?: number + page?: number + source?: string + start?: string + status?: string + } + url: '/agent/{agent_id}/logs/{conversation_id}/messages' +} + +export type GetAgentByAgentIdLogsByConversationIdMessagesResponses = { + 200: AgentLogMessageListResponse +} + +export type GetAgentByAgentIdLogsByConversationIdMessagesResponse + = GetAgentByAgentIdLogsByConversationIdMessagesResponses[keyof GetAgentByAgentIdLogsByConversationIdMessagesResponses] + export type GetAgentByAgentIdMessagesByMessageIdData = { body?: never path: { @@ -1980,28 +2060,10 @@ export type PostAgentByAgentIdSandboxFilesUploadResponses = { export type PostAgentByAgentIdSandboxFilesUploadResponse = PostAgentByAgentIdSandboxFilesUploadResponses[keyof PostAgentByAgentIdSandboxFilesUploadResponses] -export type PostAgentByAgentIdSkillsStandardizeData = { - body?: never - path: { - agent_id: string - } - query?: never - url: '/agent/{agent_id}/skills/standardize' -} - -export type PostAgentByAgentIdSkillsStandardizeErrors = { - 400: unknown -} - -export type PostAgentByAgentIdSkillsStandardizeResponses = { - 201: AgentSkillStandardizeResponse -} - -export type PostAgentByAgentIdSkillsStandardizeResponse - = PostAgentByAgentIdSkillsStandardizeResponses[keyof PostAgentByAgentIdSkillsStandardizeResponses] - export type PostAgentByAgentIdSkillsUploadData = { - body?: never + body: { + file: Blob | File + } path: { agent_id: string } diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index dec6a250798..fad0a3f0048 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -92,7 +92,7 @@ export const zAgentAppCreatePayload = z.object({ icon_background: z.string().nullish(), icon_type: zIconType.nullish(), name: z.string().min(1), - role: z.string().max(255).optional().default(''), + role: z.string().min(1).max(255), }) /** @@ -105,7 +105,7 @@ export const zAgentAppUpdatePayload = z.object({ icon_type: zIconType.nullish(), max_active_requests: z.int().nullish(), name: z.string().min(1), - role: z.string().max(255).nullish(), + role: z.string().min(1).max(255), use_icon_as_answer_icon: z.boolean().nullish(), }) @@ -333,25 +333,84 @@ export const zAgentDriveFileCommitResponse = z.object({ }) /** - * AgentLogItemResponse + * AgentLogSourceResponse */ -export const zAgentLogItemResponse = z.object({ +export const zAgentLogSourceResponse = z.object({ + app_icon: z.string().nullish(), + app_icon_background: z.string().nullish(), + app_icon_type: z.string().nullish(), + app_id: z.string(), + app_name: z.string(), + id: z.string(), + node_id: z.string().nullish(), + type: z.enum(['webapp', 'workflow']), + workflow_id: z.string().nullish(), + workflow_version: z.string().nullish(), +}) + +/** + * AgentLogSourceGroupResponse + */ +export const zAgentLogSourceGroupResponse = z.object({ + label: z.string(), + sources: z.array(zAgentLogSourceResponse).optional(), + type: z.enum(['webapp', 'workflow']), +}) + +/** + * AgentLogSourceListResponse + */ +export const zAgentLogSourceListResponse = z.object({ + data: z.array(zAgentLogSourceResponse), + groups: z.array(zAgentLogSourceGroupResponse), +}) + +/** + * AgentLogConversationItemResponse + */ +export const zAgentLogConversationItemResponse = z.object({ + conversation_id: z.string(), + created_at: z.int().nullish(), + end_user_id: z.string().nullish(), + id: z.string(), + message_count: z.int(), + operation_rate: z.number().nullish(), + source: zAgentLogSourceResponse.nullish(), + status: z.enum(['failed', 'paused', 'success']), + title: z.string().nullish(), + unread: z.boolean(), + updated_at: z.int().nullish(), + user_rate: z.number().nullish(), +}) + +/** + * AgentLogListResponse + */ +export const zAgentLogListResponse = z.object({ + data: z.array(zAgentLogConversationItemResponse), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + +/** + * AgentLogMessageItemResponse + */ +export const zAgentLogMessageItemResponse = z.object({ answer: z.string(), answer_tokens: z.int(), conversation_id: z.string(), - conversation_name: z.string().nullish(), created_at: z.int().nullish(), currency: z.string(), error: z.string().nullish(), from_account_id: z.string().nullish(), from_end_user_id: z.string().nullish(), - from_source: z.string().nullish(), id: z.string(), latency: z.number(), message_id: z.string(), message_tokens: z.int(), query: z.string(), - source: z.string().nullish(), status: z.string(), total_price: z.string(), total_tokens: z.int(), @@ -359,10 +418,10 @@ export const zAgentLogItemResponse = z.object({ }) /** - * AgentLogListResponse + * AgentLogMessageListResponse */ -export const zAgentLogListResponse = z.object({ - data: z.array(zAgentLogItemResponse), +export const zAgentLogMessageListResponse = z.object({ + data: z.array(zAgentLogMessageItemResponse), has_more: z.boolean(), limit: z.int(), page: z.int(), @@ -490,14 +549,6 @@ export const zAgentSkillRefConfig = z.object({ skill_md_key: z.string().max(512).nullish(), }) -/** - * AgentSkillStandardizeResponse - */ -export const zAgentSkillStandardizeResponse = z.object({ - manifest: zSkillManifest, - skill: zAgentSkillRefConfig, -}) - /** * AgentSkillUploadResponse */ @@ -608,9 +659,9 @@ export const zModelConfig = z.object({ }) /** - * AppDetailWithSite + * AgentAppDetailWithSite */ -export const zAppDetailWithSite = z.object({ +export const zAgentAppDetailWithSite = z.object({ access_mode: z.string().nullish(), active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), @@ -2004,9 +2055,9 @@ export const zSiteWritable = z.object({ }) /** - * AppDetailWithSite + * AgentAppDetailWithSite */ -export const zAppDetailWithSiteWritable = z.object({ +export const zAgentAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), @@ -2072,7 +2123,7 @@ export const zPostAgentBody = zAgentAppCreatePayload /** * Agent app created successfully */ -export const zPostAgentResponse = zAppDetailWithSite +export const zPostAgentResponse = zAgentAppDetailWithSite export const zGetAgentInviteOptionsQuery = z.object({ app_id: z.string().optional(), @@ -2087,7 +2138,7 @@ export const zGetAgentInviteOptionsQuery = z.object({ export const zGetAgentInviteOptionsResponse = zAgentInviteOptionsResponse export const zDeleteAgentByAgentIdPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2096,27 +2147,27 @@ export const zDeleteAgentByAgentIdPath = z.object({ export const zDeleteAgentByAgentIdResponse = z.void() export const zGetAgentByAgentIdPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** * Agent app detail */ -export const zGetAgentByAgentIdResponse = zAppDetailWithSite +export const zGetAgentByAgentIdResponse = zAgentAppDetailWithSite export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload export const zPutAgentByAgentIdPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** * Agent app updated successfully */ -export const zPutAgentByAgentIdResponse = zAppDetailWithSite +export const zPutAgentByAgentIdResponse = zAgentAppDetailWithSite export const zGetAgentByAgentIdChatMessagesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdChatMessagesQuery = z.object({ @@ -2131,8 +2182,8 @@ export const zGetAgentByAgentIdChatMessagesQuery = z.object({ export const zGetAgentByAgentIdChatMessagesResponse = zMessageInfiniteScrollPaginationResponse export const zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsPath = z.object({ - agent_id: z.string(), - message_id: z.string(), + agent_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -2142,7 +2193,7 @@ export const zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponse = zSuggestedQuestionsResponse export const zPostAgentByAgentIdChatMessagesByTaskIdStopPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), task_id: z.string(), }) @@ -2152,7 +2203,7 @@ export const zPostAgentByAgentIdChatMessagesByTaskIdStopPath = z.object({ export const zPostAgentByAgentIdChatMessagesByTaskIdStopResponse = zSimpleResultResponse export const zGetAgentByAgentIdComposerPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2163,7 +2214,7 @@ export const zGetAgentByAgentIdComposerResponse = zAgentAppComposerResponse export const zPutAgentByAgentIdComposerBody = zComposerSavePayload export const zPutAgentByAgentIdComposerPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2172,7 +2223,7 @@ export const zPutAgentByAgentIdComposerPath = z.object({ export const zPutAgentByAgentIdComposerResponse = zAgentAppComposerResponse export const zGetAgentByAgentIdComposerCandidatesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2183,7 +2234,7 @@ export const zGetAgentByAgentIdComposerCandidatesResponse = zAgentComposerCandid export const zPostAgentByAgentIdComposerValidateBody = zComposerSavePayload export const zPostAgentByAgentIdComposerValidatePath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2194,16 +2245,16 @@ export const zPostAgentByAgentIdComposerValidateResponse = zAgentComposerValidat export const zPostAgentByAgentIdCopyBody = zCopyAppPayload export const zPostAgentByAgentIdCopyPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** * Agent app copied successfully */ -export const zPostAgentByAgentIdCopyResponse = zAppDetailWithSite +export const zPostAgentByAgentIdCopyResponse = zAgentAppDetailWithSite export const zGetAgentByAgentIdDriveFilesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdDriveFilesQuery = z.object({ @@ -2216,7 +2267,7 @@ export const zGetAgentByAgentIdDriveFilesQuery = z.object({ export const zGetAgentByAgentIdDriveFilesResponse = zAgentDriveListResponse export const zGetAgentByAgentIdDriveFilesDownloadPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdDriveFilesDownloadQuery = z.object({ @@ -2229,7 +2280,7 @@ export const zGetAgentByAgentIdDriveFilesDownloadQuery = z.object({ export const zGetAgentByAgentIdDriveFilesDownloadResponse = zAgentDriveDownloadResponse export const zGetAgentByAgentIdDriveFilesPreviewPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdDriveFilesPreviewQuery = z.object({ @@ -2244,7 +2295,7 @@ export const zGetAgentByAgentIdDriveFilesPreviewResponse = zAgentDrivePreviewRes export const zPostAgentByAgentIdFeaturesBody = zAgentAppFeaturesPayload export const zPostAgentByAgentIdFeaturesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2255,7 +2306,7 @@ export const zPostAgentByAgentIdFeaturesResponse = zSimpleResultResponse export const zPostAgentByAgentIdFeedbacksBody = zMessageFeedbackPayload export const zPostAgentByAgentIdFeedbacksPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2264,7 +2315,7 @@ export const zPostAgentByAgentIdFeedbacksPath = z.object({ export const zPostAgentByAgentIdFeedbacksResponse = zSimpleResultResponse export const zDeleteAgentByAgentIdFilesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zDeleteAgentByAgentIdFilesQuery = z.object({ @@ -2279,7 +2330,7 @@ export const zDeleteAgentByAgentIdFilesResponse = zAgentDriveDeleteResponse export const zPostAgentByAgentIdFilesBody = zAgentDriveFilePayload export const zPostAgentByAgentIdFilesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2287,8 +2338,17 @@ export const zPostAgentByAgentIdFilesPath = z.object({ */ export const zPostAgentByAgentIdFilesResponse = zAgentDriveFileCommitResponse +export const zGetAgentByAgentIdLogSourcesPath = z.object({ + agent_id: z.uuid(), +}) + +/** + * Agent log sources + */ +export const zGetAgentByAgentIdLogSourcesResponse = zAgentLogSourceListResponse + export const zGetAgentByAgentIdLogsPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdLogsQuery = z.object({ @@ -2306,9 +2366,29 @@ export const zGetAgentByAgentIdLogsQuery = z.object({ */ export const zGetAgentByAgentIdLogsResponse = zAgentLogListResponse +export const zGetAgentByAgentIdLogsByConversationIdMessagesPath = z.object({ + agent_id: z.uuid(), + conversation_id: z.uuid(), +}) + +export const zGetAgentByAgentIdLogsByConversationIdMessagesQuery = z.object({ + end: z.string().optional(), + keyword: z.string().optional(), + limit: z.int().gte(1).lte(100).optional().default(20), + page: z.int().gte(1).optional().default(1), + source: z.string().optional(), + start: z.string().optional(), + status: z.string().optional(), +}) + +/** + * Agent log messages + */ +export const zGetAgentByAgentIdLogsByConversationIdMessagesResponse = zAgentLogMessageListResponse + export const zGetAgentByAgentIdMessagesByMessageIdPath = z.object({ - agent_id: z.string(), - message_id: z.string(), + agent_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -2317,7 +2397,7 @@ export const zGetAgentByAgentIdMessagesByMessageIdPath = z.object({ export const zGetAgentByAgentIdMessagesByMessageIdResponse = zMessageDetailResponse export const zGetAgentByAgentIdReferencingWorkflowsPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2326,7 +2406,7 @@ export const zGetAgentByAgentIdReferencingWorkflowsPath = z.object({ export const zGetAgentByAgentIdReferencingWorkflowsResponse = zAgentReferencingWorkflowsResponse export const zGetAgentByAgentIdSandboxFilesPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdSandboxFilesQuery = z.object({ @@ -2340,7 +2420,7 @@ export const zGetAgentByAgentIdSandboxFilesQuery = z.object({ export const zGetAgentByAgentIdSandboxFilesResponse = zSandboxListResponse export const zGetAgentByAgentIdSandboxFilesReadPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdSandboxFilesReadQuery = z.object({ @@ -2356,7 +2436,7 @@ export const zGetAgentByAgentIdSandboxFilesReadResponse = zSandboxReadResponse export const zPostAgentByAgentIdSandboxFilesUploadBody = zAgentSandboxUploadPayload export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2364,26 +2444,21 @@ export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({ */ export const zPostAgentByAgentIdSandboxFilesUploadResponse = zSandboxUploadResponse -export const zPostAgentByAgentIdSkillsStandardizePath = z.object({ - agent_id: z.string(), +export const zPostAgentByAgentIdSkillsUploadBody = z.object({ + file: z.custom(), }) -/** - * Skill standardized into drive - */ -export const zPostAgentByAgentIdSkillsStandardizeResponse = zAgentSkillStandardizeResponse - export const zPostAgentByAgentIdSkillsUploadPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** - * Skill validated + * Skill uploaded into drive */ export const zPostAgentByAgentIdSkillsUploadResponse = zAgentSkillUploadResponse export const zDeleteAgentByAgentIdSkillsBySlugPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), slug: z.string(), }) @@ -2393,7 +2468,7 @@ export const zDeleteAgentByAgentIdSkillsBySlugPath = z.object({ export const zDeleteAgentByAgentIdSkillsBySlugResponse = zAgentDriveDeleteResponse export const zPostAgentByAgentIdSkillsBySlugInferToolsPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), slug: z.string(), }) @@ -2403,7 +2478,7 @@ export const zPostAgentByAgentIdSkillsBySlugInferToolsPath = z.object({ export const zPostAgentByAgentIdSkillsBySlugInferToolsResponse = zSkillToolInferenceResult export const zGetAgentByAgentIdStatisticsSummaryPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) export const zGetAgentByAgentIdStatisticsSummaryQuery = z.object({ @@ -2418,7 +2493,7 @@ export const zGetAgentByAgentIdStatisticsSummaryQuery = z.object({ export const zGetAgentByAgentIdStatisticsSummaryResponse = zAgentStatisticSummaryEnvelopeResponse export const zGetAgentByAgentIdVersionsPath = z.object({ - agent_id: z.string(), + agent_id: z.uuid(), }) /** @@ -2427,8 +2502,8 @@ export const zGetAgentByAgentIdVersionsPath = z.object({ export const zGetAgentByAgentIdVersionsResponse = zAgentConfigSnapshotListResponse export const zGetAgentByAgentIdVersionsByVersionIdPath = z.object({ - agent_id: z.string(), - version_id: z.string(), + agent_id: z.uuid(), + version_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts b/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts index 9bb0f67728f..468e9d09efc 100644 --- a/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts +++ b/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts @@ -37,7 +37,7 @@ export const zPostApiBasedExtensionBody = zApiBasedExtensionPayload export const zPostApiBasedExtensionResponse = zApiBasedExtensionResponse export const zDeleteApiBasedExtensionByIdPath = z.object({ - id: z.string(), + id: z.uuid(), }) /** @@ -46,7 +46,7 @@ export const zDeleteApiBasedExtensionByIdPath = z.object({ export const zDeleteApiBasedExtensionByIdResponse = z.void() export const zGetApiBasedExtensionByIdPath = z.object({ - id: z.string(), + id: z.uuid(), }) /** @@ -57,7 +57,7 @@ export const zGetApiBasedExtensionByIdResponse = zApiBasedExtensionResponse export const zPostApiBasedExtensionByIdBody = zApiBasedExtensionPayload export const zPostApiBasedExtensionByIdPath = z.object({ - id: z.string(), + id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts b/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts index 0ebff4365b3..a55ba309823 100644 --- a/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts +++ b/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts @@ -50,7 +50,7 @@ export const zPostApiKeyAuthDataSourceBindingBody = zApiKeyAuthBindingPayload export const zPostApiKeyAuthDataSourceBindingResponse = zSimpleResultResponse export const zDeleteApiKeyAuthDataSourceByBindingIdPath = z.object({ - binding_id: z.string(), + binding_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index 3952812a5a8..eab3c17eb43 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -271,10 +271,9 @@ import { zPostAppsByAppIdAgentSkillsBySlugInferToolsPath, zPostAppsByAppIdAgentSkillsBySlugInferToolsQuery, zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse, - zPostAppsByAppIdAgentSkillsStandardizePath, - zPostAppsByAppIdAgentSkillsStandardizeQuery, - zPostAppsByAppIdAgentSkillsStandardizeResponse, + zPostAppsByAppIdAgentSkillsUploadBody, zPostAppsByAppIdAgentSkillsUploadPath, + zPostAppsByAppIdAgentSkillsUploadQuery, zPostAppsByAppIdAgentSkillsUploadResponse, zPostAppsByAppIdAnnotationReplyByActionBody, zPostAppsByAppIdAnnotationReplyByActionPath, @@ -939,57 +938,32 @@ export const logs = { } /** - * Upload a Skill, validate it, and standardize it into the app agent's drive + * Upload a Skill, validate it, and commit drive-backed skill files * - * Validate + standardize a Skill into the agent drive (ENG-594) + * Upload + standardize a Skill into the agent drive */ export const post10 = oc .route({ - description: 'Validate + standardize a Skill into the agent drive (ENG-594)', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAppsByAppIdAgentSkillsStandardize', - path: '/apps/{app_id}/agent/skills/standardize', - successStatus: 201, - summary: 'Upload a Skill, validate it, and standardize it into the app agent\'s drive', - tags: ['console'], - }) - .input( - z.object({ - params: zPostAppsByAppIdAgentSkillsStandardizePath, - query: zPostAppsByAppIdAgentSkillsStandardizeQuery.optional(), - }), - ) - .output(zPostAppsByAppIdAgentSkillsStandardizeResponse) - -export const standardize = { - post: post10, -} - -/** - * Validate an uploaded Skill package and persist the archive - * - * Upload + validate a Skill package (.zip/.skill) and extract its manifest - * Returns a validated skill ref (to bind into the Agent soul config on save) - * plus its manifest. Standardizing into the agent drive is ENG-594. - */ -export const post11 = oc - .route({ - description: - 'Upload + validate a Skill package (.zip/.skill) and extract its manifest\nReturns a validated skill ref (to bind into the Agent soul config on save)\nplus its manifest. Standardizing into the agent drive is ENG-594.', + description: 'Upload + standardize a Skill into the agent drive', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdAgentSkillsUpload', path: '/apps/{app_id}/agent/skills/upload', successStatus: 201, - summary: 'Validate an uploaded Skill package and persist the archive', + summary: 'Upload a Skill, validate it, and commit drive-backed skill files', tags: ['console'], }) - .input(z.object({ params: zPostAppsByAppIdAgentSkillsUploadPath })) + .input( + z.object({ + body: zPostAppsByAppIdAgentSkillsUploadBody, + params: zPostAppsByAppIdAgentSkillsUploadPath, + query: zPostAppsByAppIdAgentSkillsUploadQuery.optional(), + }), + ) .output(zPostAppsByAppIdAgentSkillsUploadResponse) export const upload = { - post: post11, + post: post10, } /** @@ -998,7 +972,7 @@ export const upload = { * Infer CLI tool + ENV suggestions from a standardized skill's SKILL.md (draft only, ENG-371) * Saving still goes through composer validation. */ -export const post12 = oc +export const post11 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized skill\'s SKILL.md (draft only, ENG-371)\nSaving still goes through composer validation.', @@ -1018,7 +992,7 @@ export const post12 = oc .output(zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse) export const inferTools = { - post: post12, + post: post11, } /** @@ -1048,7 +1022,6 @@ export const bySlug = { } export const skills = { - standardize, upload, bySlug, } @@ -1086,7 +1059,7 @@ export const status = { /** * Enable or disable annotation reply for an app */ -export const post13 = oc +export const post12 = oc .route({ description: 'Enable or disable annotation reply for an app', inputStructure: 'detailed', @@ -1104,7 +1077,7 @@ export const post13 = oc .output(zPostAppsByAppIdAnnotationReplyByActionResponse) export const byAction = { - post: post13, + post: post12, status, } @@ -1134,7 +1107,7 @@ export const annotationSetting = { /** * Update annotation settings for an app */ -export const post14 = oc +export const post13 = oc .route({ description: 'Update annotation settings for an app', inputStructure: 'detailed', @@ -1152,7 +1125,7 @@ export const post14 = oc .output(zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdResponse) export const byAnnotationSettingId = { - post: post14, + post: post13, } export const annotationSettings = { @@ -1162,7 +1135,7 @@ export const annotationSettings = { /** * Batch import annotations from CSV file with rate limiting and security checks */ -export const post15 = oc +export const post14 = oc .route({ description: 'Batch import annotations from CSV file with rate limiting and security checks', inputStructure: 'detailed', @@ -1175,7 +1148,7 @@ export const post15 = oc .output(zPostAppsByAppIdAnnotationsBatchImportResponse) export const batchImport = { - post: post15, + post: post14, } /** @@ -1278,7 +1251,7 @@ export const delete3 = oc /** * Update or delete an annotation */ -export const post16 = oc +export const post15 = oc .route({ description: 'Update or delete an annotation', inputStructure: 'detailed', @@ -1297,7 +1270,7 @@ export const post16 = oc export const byAnnotationId = { delete: delete3, - post: post16, + post: post15, hitHistories, } @@ -1336,7 +1309,7 @@ export const get15 = oc /** * Create a new annotation for an app */ -export const post17 = oc +export const post16 = oc .route({ description: 'Create a new annotation for an app', inputStructure: 'detailed', @@ -1354,7 +1327,7 @@ export const post17 = oc export const annotations = { delete: delete4, get: get15, - post: post17, + post: post16, batchImport, batchImportStatus, count: count2, @@ -1365,7 +1338,7 @@ export const annotations = { /** * Enable or disable app API */ -export const post18 = oc +export const post17 = oc .route({ description: 'Enable or disable app API', inputStructure: 'detailed', @@ -1378,13 +1351,13 @@ export const post18 = oc .output(zPostAppsByAppIdApiEnableResponse) export const apiEnable = { - post: post18, + post: post17, } /** * Transcript audio to text for chat messages */ -export const post19 = oc +export const post18 = oc .route({ description: 'Transcript audio to text for chat messages', inputStructure: 'detailed', @@ -1397,7 +1370,7 @@ export const post19 = oc .output(zPostAppsByAppIdAudioToTextResponse) export const audioToText = { - post: post19, + post: post18, } /** @@ -1487,7 +1460,7 @@ export const byMessageId = { /** * Stop a running chat message generation */ -export const post20 = oc +export const post19 = oc .route({ description: 'Stop a running chat message generation', inputStructure: 'detailed', @@ -1500,7 +1473,7 @@ export const post20 = oc .output(zPostAppsByAppIdChatMessagesByTaskIdStopResponse) export const stop = { - post: post20, + post: post19, } export const byTaskId = { @@ -1594,7 +1567,7 @@ export const completionConversations = { /** * Stop a running completion message generation */ -export const post21 = oc +export const post20 = oc .route({ description: 'Stop a running completion message generation', inputStructure: 'detailed', @@ -1607,7 +1580,7 @@ export const post21 = oc .output(zPostAppsByAppIdCompletionMessagesByTaskIdStopResponse) export const stop2 = { - post: post21, + post: post20, } export const byTaskId2 = { @@ -1617,7 +1590,7 @@ export const byTaskId2 = { /** * Generate completion message for debugging */ -export const post22 = oc +export const post21 = oc .route({ description: 'Generate completion message for debugging', inputStructure: 'detailed', @@ -1635,7 +1608,7 @@ export const post22 = oc .output(zPostAppsByAppIdCompletionMessagesResponse) export const completionMessages = { - post: post22, + post: post21, byTaskId: byTaskId2, } @@ -1670,7 +1643,7 @@ export const conversationVariables = { * Convert expert mode of chatbot app to workflow mode * Convert Completion App to Workflow App */ -export const post23 = oc +export const post22 = oc .route({ description: 'Convert application to workflow mode\nConvert expert mode of chatbot app to workflow mode\nConvert Completion App to Workflow App', @@ -1690,7 +1663,7 @@ export const post23 = oc .output(zPostAppsByAppIdConvertToWorkflowResponse) export const convertToWorkflow = { - post: post23, + post: post22, } /** @@ -1698,7 +1671,7 @@ export const convertToWorkflow = { * * Create a copy of an existing application */ -export const post24 = oc +export const post23 = oc .route({ description: 'Create a copy of an existing application', inputStructure: 'detailed', @@ -1713,7 +1686,7 @@ export const post24 = oc .output(zPostAppsByAppIdCopyResponse) export const copy = { - post: post24, + post: post23, } /** @@ -1767,7 +1740,7 @@ export const export3 = { /** * Create or update message feedback (like/dislike) */ -export const post25 = oc +export const post24 = oc .route({ description: 'Create or update message feedback (like/dislike)', inputStructure: 'detailed', @@ -1780,14 +1753,14 @@ export const post25 = oc .output(zPostAppsByAppIdFeedbacksResponse) export const feedbacks = { - post: post25, + post: post24, export: export3, } /** * Update application icon */ -export const post26 = oc +export const post25 = oc .route({ description: 'Update application icon', inputStructure: 'detailed', @@ -1800,7 +1773,7 @@ export const post26 = oc .output(zPostAppsByAppIdIconResponse) export const icon = { - post: post26, + post: post25, } /** @@ -1831,7 +1804,7 @@ export const messages = { * * Update application model configuration */ -export const post27 = oc +export const post26 = oc .route({ description: 'Update application model configuration', inputStructure: 'detailed', @@ -1847,13 +1820,13 @@ export const post27 = oc .output(zPostAppsByAppIdModelConfigResponse) export const modelConfig = { - post: post27, + post: post26, } /** * Check if app name is available */ -export const post28 = oc +export const post27 = oc .route({ description: 'Check if app name is available', inputStructure: 'detailed', @@ -1866,13 +1839,13 @@ export const post28 = oc .output(zPostAppsByAppIdNameResponse) export const name = { - post: post28, + post: post27, } /** * Publish app to Creators Platform */ -export const post29 = oc +export const post28 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1885,7 +1858,7 @@ export const post29 = oc .output(zPostAppsByAppIdPublishToCreatorsPlatformResponse) export const publishToCreatorsPlatform = { - post: post29, + post: post28, } /** @@ -1906,7 +1879,7 @@ export const get26 = oc /** * Create MCP server configuration for an application */ -export const post30 = oc +export const post29 = oc .route({ description: 'Create MCP server configuration for an application', inputStructure: 'detailed', @@ -1936,14 +1909,14 @@ export const put = oc export const server = { get: get26, - post: post30, + post: post29, put, } /** * Reset access token for application site */ -export const post31 = oc +export const post30 = oc .route({ description: 'Reset access token for application site', inputStructure: 'detailed', @@ -1956,13 +1929,13 @@ export const post31 = oc .output(zPostAppsByAppIdSiteAccessTokenResetResponse) export const accessTokenReset = { - post: post31, + post: post30, } /** * Update application site configuration */ -export const post32 = oc +export const post31 = oc .route({ description: 'Update application site configuration', inputStructure: 'detailed', @@ -1975,14 +1948,14 @@ export const post32 = oc .output(zPostAppsByAppIdSiteResponse) export const site = { - post: post32, + post: post31, accessTokenReset, } /** * Enable or disable app site */ -export const post33 = oc +export const post32 = oc .route({ description: 'Enable or disable app site', inputStructure: 'detailed', @@ -1995,7 +1968,7 @@ export const post33 = oc .output(zPostAppsByAppIdSiteEnableResponse) export const siteEnable = { - post: post33, + post: post32, } /** @@ -2016,7 +1989,7 @@ export const delete7 = oc /** * Star an application for the current account */ -export const post34 = oc +export const post33 = oc .route({ description: 'Star an application for the current account', inputStructure: 'detailed', @@ -2030,7 +2003,7 @@ export const post34 = oc export const star = { delete: delete7, - post: post34, + post: post33, } /** @@ -2263,7 +2236,7 @@ export const voices = { /** * Convert text to speech for chat messages */ -export const post35 = oc +export const post34 = oc .route({ description: 'Convert text to speech for chat messages', inputStructure: 'detailed', @@ -2278,7 +2251,7 @@ export const post35 = oc .output(zPostAppsByAppIdTextToAudioResponse) export const textToAudio = { - post: post35, + post: post34, voices, } @@ -2303,7 +2276,7 @@ export const get36 = oc /** * Update app tracing configuration */ -export const post36 = oc +export const post35 = oc .route({ description: 'Update app tracing configuration', inputStructure: 'detailed', @@ -2317,7 +2290,7 @@ export const post36 = oc export const trace = { get: get36, - post: post36, + post: post35, } /** @@ -2386,7 +2359,7 @@ export const patch = oc * * Create a new tracing configuration for an application */ -export const post37 = oc +export const post36 = oc .route({ description: 'Create a new tracing configuration for an application', inputStructure: 'detailed', @@ -2406,13 +2379,13 @@ export const traceConfig = { delete: delete8, get: get37, patch, - post: post37, + post: post36, } /** * Update app trigger (enable/disable) */ -export const post38 = oc +export const post37 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -2430,7 +2403,7 @@ export const post38 = oc .output(zPostAppsByAppIdTriggerEnableResponse) export const triggerEnable = { - post: post38, + post: post37, } /** @@ -2538,7 +2511,7 @@ export const count3 = { * * Stop running workflow task */ -export const post39 = oc +export const post38 = oc .route({ description: 'Stop running workflow task', inputStructure: 'detailed', @@ -2552,7 +2525,7 @@ export const post39 = oc .output(zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopResponse) export const stop3 = { - post: post39, + post: post38, } export const byTaskId3 = { @@ -2655,7 +2628,7 @@ export const read = { /** * Upload one workflow Agent sandbox file as a Dify ToolFile mapping */ -export const post40 = oc +export const post39 = oc .route({ description: 'Upload one workflow Agent sandbox file as a Dify ToolFile mapping', inputStructure: 'detailed', @@ -2673,7 +2646,7 @@ export const post40 = oc .output(zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesUploadResponse) export const upload2 = { - post: post40, + post: post39, } /** @@ -2824,7 +2797,7 @@ export const byReplyId = { * * Add a reply to a workflow comment */ -export const post41 = oc +export const post40 = oc .route({ description: 'Add a reply to a workflow comment', inputStructure: 'detailed', @@ -2844,7 +2817,7 @@ export const post41 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesResponse) export const replies = { - post: post41, + post: post40, byReplyId, } @@ -2853,7 +2826,7 @@ export const replies = { * * Resolve a workflow comment */ -export const post42 = oc +export const post41 = oc .route({ description: 'Resolve a workflow comment', inputStructure: 'detailed', @@ -2867,7 +2840,7 @@ export const post42 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdResolveResponse) export const resolve = { - post: post42, + post: post41, } /** @@ -2961,7 +2934,7 @@ export const get50 = oc * * Create a new workflow comment */ -export const post43 = oc +export const post42 = oc .route({ description: 'Create a new workflow comment', inputStructure: 'detailed', @@ -2982,7 +2955,7 @@ export const post43 = oc export const comments = { get: get50, - post: post43, + post: post42, mentionUsers, byCommentId, } @@ -3163,7 +3136,7 @@ export const get57 = oc /** * Update conversation variables for workflow draft */ -export const post44 = oc +export const post43 = oc .route({ description: 'Update conversation variables for workflow draft', inputStructure: 'detailed', @@ -3182,7 +3155,7 @@ export const post44 = oc export const conversationVariables2 = { get: get57, - post: post44, + post: post43, } /** @@ -3206,7 +3179,7 @@ export const get58 = oc /** * Update environment variables for workflow draft */ -export const post45 = oc +export const post44 = oc .route({ description: 'Update environment variables for workflow draft', inputStructure: 'detailed', @@ -3225,13 +3198,13 @@ export const post45 = oc export const environmentVariables = { get: get58, - post: post45, + post: post44, } /** * Update draft workflow features */ -export const post46 = oc +export const post45 = oc .route({ description: 'Update draft workflow features', inputStructure: 'detailed', @@ -3249,7 +3222,7 @@ export const post46 = oc .output(zPostAppsByAppIdWorkflowsDraftFeaturesResponse) export const features = { - post: post46, + post: post45, } /** @@ -3257,7 +3230,7 @@ export const features = { * * Test human input delivery for workflow */ -export const post47 = oc +export const post46 = oc .route({ description: 'Test human input delivery for workflow', inputStructure: 'detailed', @@ -3276,7 +3249,7 @@ export const post47 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestResponse) export const deliveryTest = { - post: post47, + post: post46, } /** @@ -3284,7 +3257,7 @@ export const deliveryTest = { * * Get human input form preview for workflow */ -export const post48 = oc +export const post47 = oc .route({ description: 'Get human input form preview for workflow', inputStructure: 'detailed', @@ -3303,7 +3276,7 @@ export const post48 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponse) export const preview3 = { - post: post48, + post: post47, } /** @@ -3311,7 +3284,7 @@ export const preview3 = { * * Submit human input form preview for workflow */ -export const post49 = oc +export const post48 = oc .route({ description: 'Submit human input form preview for workflow', inputStructure: 'detailed', @@ -3330,7 +3303,7 @@ export const post49 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunResponse) export const run5 = { - post: post49, + post: post48, } export const form2 = { @@ -3356,7 +3329,7 @@ export const humanInput2 = { * * Run draft workflow iteration node */ -export const post50 = oc +export const post49 = oc .route({ description: 'Run draft workflow iteration node', inputStructure: 'detailed', @@ -3375,7 +3348,7 @@ export const post50 = oc .output(zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunResponse) export const run6 = { - post: post50, + post: post49, } export const byNodeId6 = { @@ -3395,7 +3368,7 @@ export const iteration2 = { * * Run draft workflow loop node */ -export const post51 = oc +export const post50 = oc .route({ description: 'Run draft workflow loop node', inputStructure: 'detailed', @@ -3414,7 +3387,7 @@ export const post51 = oc .output(zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse) export const run7 = { - post: post51, + post: post50, } export const byNodeId7 = { @@ -3446,7 +3419,7 @@ export const candidates = { get: get59, } -export const post52 = oc +export const post51 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3463,10 +3436,10 @@ export const post52 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse) export const impact = { - post: post52, + post: post51, } -export const post53 = oc +export const post52 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3483,10 +3456,10 @@ export const post53 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterResponse) export const saveToRoster = { - post: post53, + post: post52, } -export const post54 = oc +export const post53 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3503,7 +3476,7 @@ export const post54 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateResponse) export const validate = { - post: post54, + post: post53, } export const get60 = oc @@ -3566,7 +3539,7 @@ export const lastRun = { * * Run draft workflow node */ -export const post55 = oc +export const post54 = oc .route({ description: 'Run draft workflow node', inputStructure: 'detailed', @@ -3585,7 +3558,7 @@ export const post55 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse) export const run8 = { - post: post55, + post: post54, } /** @@ -3593,7 +3566,7 @@ export const run8 = { * * Poll for trigger events and execute single node when event arrives */ -export const post56 = oc +export const post55 = oc .route({ description: 'Poll for trigger events and execute single node when event arrives', inputStructure: 'detailed', @@ -3607,7 +3580,7 @@ export const post56 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse) export const run9 = { - post: post56, + post: post55, } export const trigger = { @@ -3667,7 +3640,7 @@ export const nodes7 = { * * Run draft workflow */ -export const post57 = oc +export const post56 = oc .route({ description: 'Run draft workflow', inputStructure: 'detailed', @@ -3686,7 +3659,7 @@ export const post57 = oc .output(zPostAppsByAppIdWorkflowsDraftRunResponse) export const run10 = { - post: post57, + post: post56, } /** @@ -3808,7 +3781,7 @@ export const systemVariables = { * * Poll for trigger events and execute full workflow when event arrives */ -export const post58 = oc +export const post57 = oc .route({ description: 'Poll for trigger events and execute full workflow when event arrives', inputStructure: 'detailed', @@ -3827,7 +3800,7 @@ export const post58 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunResponse) export const run11 = { - post: post58, + post: post57, } /** @@ -3835,7 +3808,7 @@ export const run11 = { * * Full workflow debug when the start node is a trigger */ -export const post59 = oc +export const post58 = oc .route({ description: 'Full workflow debug when the start node is a trigger', inputStructure: 'detailed', @@ -3854,7 +3827,7 @@ export const post59 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse) export const runAll = { - post: post59, + post: post58, } export const trigger2 = { @@ -4007,7 +3980,7 @@ export const get70 = oc * * Sync draft workflow configuration */ -export const post60 = oc +export const post59 = oc .route({ description: 'Sync draft workflow configuration', inputStructure: 'detailed', @@ -4027,7 +4000,7 @@ export const post60 = oc export const draft2 = { get: get70, - post: post60, + post: post59, conversationVariables: conversationVariables2, environmentVariables, features, @@ -4063,7 +4036,7 @@ export const get71 = oc /** * Publish workflow */ -export const post61 = oc +export const post60 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -4082,7 +4055,7 @@ export const post61 = oc export const publish = { get: get71, - post: post61, + post: post60, } /** @@ -4219,7 +4192,7 @@ export const triggers2 = { /** * Restore a published workflow version into the draft workflow */ -export const post62 = oc +export const post61 = oc .route({ description: 'Restore a published workflow version into the draft workflow', inputStructure: 'detailed', @@ -4232,7 +4205,7 @@ export const post62 = oc .output(zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse) export const restore = { - post: post62, + post: post61, } /** @@ -4457,7 +4430,7 @@ export const get79 = oc * * Create a new API key for an app */ -export const post63 = oc +export const post62 = oc .route({ description: 'Create a new API key for an app', inputStructure: 'detailed', @@ -4473,7 +4446,7 @@ export const post63 = oc export const apiKeys = { get: get79, - post: post63, + post: post62, byApiKeyId, } @@ -4531,7 +4504,7 @@ export const get81 = oc * * Create a new application */ -export const post64 = oc +export const post63 = oc .route({ description: 'Create a new application', inputStructure: 'detailed', @@ -4547,7 +4520,7 @@ export const post64 = oc export const apps = { get: get81, - post: post64, + post: post63, imports, starred, workflows, diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 8ccf899b451..debd7ff1991 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -5,10 +5,10 @@ export type ClientOptions = { } export type AppPagination = { - has_next: boolean - items: Array + data: Array + has_more: boolean + limit: number page: number - per_page: number total: number } @@ -23,7 +23,6 @@ export type CreateAppPayload = { export type AppDetailWithSite = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -42,7 +41,6 @@ export type AppDetailWithSite = { mode: string model_config?: ModelConfig | null name: string - role?: string | null site?: Site | null tags?: Array tracing?: JsonValue | null @@ -213,11 +211,6 @@ export type AgentLogResponse = { meta: AgentLogMetaResponse } -export type AgentSkillStandardizeResponse = { - manifest: SkillManifest - skill: AgentSkillRefConfig -} - export type AgentSkillUploadResponse = { manifest: SkillManifest skill: AgentSkillRefConfig @@ -1156,25 +1149,24 @@ export type ApiKeyItem = { export type AppPartial = { access_mode?: string | null - active_config_is_published?: boolean app_id?: string | null - app_model_config?: ModelConfigPartial | null author_name?: string | null bound_agent_id?: string | null create_user_name?: string | null created_at?: number | null created_by?: string | null - desc_or_prompt?: string | null + description?: string | null has_draft_trigger?: boolean | null icon?: string | null icon_background?: string | null icon_type?: string | null + readonly icon_url: string | null id: string is_starred?: boolean max_active_requests?: number | null - mode_compatible_with_agent: string + mode: string + model_config?: ModelConfigPartial | null name: string - role?: string | null tags?: Array updated_at?: number | null updated_by?: string | null @@ -2575,9 +2567,16 @@ export type AgentModerationIoConfig = { export type ValueSourceType = 'constant' | 'variable' +export type AppPaginationWritable = { + data: Array + has_more: boolean + limit: number + page: number + total: number +} + export type AppDetailWithSiteWritable = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -2595,7 +2594,6 @@ export type AppDetailWithSiteWritable = { mode: string model_config?: ModelConfig | null name: string - role?: string | null site?: SiteWritable | null tags?: Array tracing?: JsonValue | null @@ -2628,6 +2626,32 @@ export type WorkflowCommentDetailWritable = { updated_at?: number | null } +export type AppPartialWritable = { + access_mode?: string | null + app_id?: string | null + author_name?: string | null + bound_agent_id?: string | null + create_user_name?: string | null + created_at?: number | null + created_by?: string | null + description?: string | null + has_draft_trigger?: boolean | null + icon?: string | null + icon_background?: string | null + icon_type?: string | null + id: string + is_starred?: boolean + max_active_requests?: number | null + mode: string + model_config?: ModelConfigPartial | null + name: string + tags?: Array + updated_at?: number | null + updated_by?: string | null + use_icon_as_answer_icon?: boolean | null + workflow?: WorkflowPartial | null +} + export type SiteWritable = { chat_color_theme?: string | null chat_color_theme_inverted: boolean @@ -3144,34 +3168,16 @@ export type GetAppsByAppIdAgentLogsResponses = { export type GetAppsByAppIdAgentLogsResponse = GetAppsByAppIdAgentLogsResponses[keyof GetAppsByAppIdAgentLogsResponses] -export type PostAppsByAppIdAgentSkillsStandardizeData = { - body?: never +export type PostAppsByAppIdAgentSkillsUploadData = { + body: { + file: Blob | File + } path: { app_id: string } query?: { node_id?: string } - url: '/apps/{app_id}/agent/skills/standardize' -} - -export type PostAppsByAppIdAgentSkillsStandardizeErrors = { - 400: unknown -} - -export type PostAppsByAppIdAgentSkillsStandardizeResponses = { - 201: AgentSkillStandardizeResponse -} - -export type PostAppsByAppIdAgentSkillsStandardizeResponse - = PostAppsByAppIdAgentSkillsStandardizeResponses[keyof PostAppsByAppIdAgentSkillsStandardizeResponses] - -export type PostAppsByAppIdAgentSkillsUploadData = { - body?: never - path: { - app_id: string - } - query?: never url: '/apps/{app_id}/agent/skills/upload' } diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 556f11f5521..83a0b79c624 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -990,14 +990,6 @@ export const zAgentSkillRefConfig = z.object({ skill_md_key: z.string().max(512).nullish(), }) -/** - * AgentSkillStandardizeResponse - */ -export const zAgentSkillStandardizeResponse = z.object({ - manifest: zSkillManifest, - skill: zAgentSkillRefConfig, -}) - /** * AgentSkillUploadResponse */ @@ -1946,25 +1938,24 @@ export const zModelConfigPartial = z.object({ */ export const zAppPartial = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), - app_model_config: zModelConfigPartial.nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), create_user_name: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), - desc_or_prompt: z.string().nullish(), + description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), + icon_url: z.string().nullable(), id: z.string(), is_starred: z.boolean().optional().default(false), max_active_requests: z.int().nullish(), - mode_compatible_with_agent: z.string(), + mode: z.string(), + model_config: zModelConfigPartial.nullish(), name: z.string(), - role: z.string().nullish(), tags: z.array(zTag).optional(), updated_at: z.int().nullish(), updated_by: z.string().nullish(), @@ -1976,10 +1967,10 @@ export const zAppPartial = z.object({ * AppPagination */ export const zAppPagination = z.object({ - has_next: z.boolean(), - items: z.array(zAppPartial), + data: z.array(zAppPartial), + has_more: z.boolean(), + limit: z.int(), page: z.int(), - per_page: z.int(), total: z.int(), }) @@ -2005,7 +1996,6 @@ export const zModelConfig = z.object({ */ export const zAppDetailWithSite = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -2024,7 +2014,6 @@ export const zAppDetailWithSite = z.object({ mode: z.string(), model_config: zModelConfig.nullish(), name: z.string(), - role: z.string().nullish(), site: zSite.nullish(), tags: z.array(zTag).optional(), tracing: zJsonValue.nullish(), @@ -3472,6 +3461,46 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({ */ export const zGeneratedAppResponseWritable = zJsonValue +/** + * AppPartial + */ +export const zAppPartialWritable = z.object({ + access_mode: z.string().nullish(), + app_id: z.string().nullish(), + author_name: z.string().nullish(), + bound_agent_id: z.string().nullish(), + create_user_name: z.string().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + description: z.string().nullish(), + has_draft_trigger: z.boolean().nullish(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + id: z.string(), + is_starred: z.boolean().optional().default(false), + max_active_requests: z.int().nullish(), + mode: z.string(), + model_config: zModelConfigPartial.nullish(), + name: z.string(), + tags: z.array(zTag).optional(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + +/** + * AppPagination + */ +export const zAppPaginationWritable = z.object({ + data: z.array(zAppPartialWritable), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * Site */ @@ -3496,7 +3525,6 @@ export const zSiteWritable = z.object({ */ export const zAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), app_id: z.string().nullish(), bound_agent_id: z.string().nullish(), @@ -3514,7 +3542,6 @@ export const zAppDetailWithSiteWritable = z.object({ mode: z.string(), model_config: zModelConfig.nullish(), name: z.string(), - role: z.string().nullish(), site: zSiteWritable.nullish(), tags: z.array(zTag).optional(), tracing: zJsonValue.nullish(), @@ -3703,7 +3730,7 @@ export const zPostAppsWorkflowsOnlineUsersBody = zWorkflowOnlineUsersPayload export const zPostAppsWorkflowsOnlineUsersResponse = zWorkflowOnlineUsersResponse export const zDeleteAppsByAppIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -3712,7 +3739,7 @@ export const zDeleteAppsByAppIdPath = z.object({ export const zDeleteAppsByAppIdResponse = z.void() export const zGetAppsByAppIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -3723,7 +3750,7 @@ export const zGetAppsByAppIdResponse = zAppDetailWithSite export const zPutAppsByAppIdBody = zUpdateAppPayload export const zPutAppsByAppIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -3732,7 +3759,7 @@ export const zPutAppsByAppIdPath = z.object({ export const zPutAppsByAppIdResponse = zAppDetailWithSite export const zGetAppsByAppIdAdvancedChatWorkflowRunsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAdvancedChatWorkflowRunsQuery = z.object({ @@ -3749,7 +3776,7 @@ export const zGetAppsByAppIdAdvancedChatWorkflowRunsResponse = zAdvancedChatWorkflowRunPaginationResponse export const zGetAppsByAppIdAdvancedChatWorkflowRunsCountPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAdvancedChatWorkflowRunsCountQuery = z.object({ @@ -3768,7 +3795,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFo export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormPreviewPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -3783,7 +3810,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFo export const zPostAppsByAppIdAdvancedChatWorkflowsDraftHumanInputNodesByNodeIdFormRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -3797,7 +3824,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftIterationNodesByNodeIdRun = zIterationNodeRunPayload export const zPostAppsByAppIdAdvancedChatWorkflowsDraftIterationNodesByNodeIdRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -3811,7 +3838,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftLoopNodesByNodeIdRunBody = zLoopNodeRunPayload export const zPostAppsByAppIdAdvancedChatWorkflowsDraftLoopNodesByNodeIdRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -3824,7 +3851,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftLoopNodesByNodeIdRunRespo export const zPostAppsByAppIdAdvancedChatWorkflowsDraftRunBody = zAdvancedChatWorkflowRunPayload export const zPostAppsByAppIdAdvancedChatWorkflowsDraftRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -3833,7 +3860,7 @@ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftRunPath = z.object({ export const zPostAppsByAppIdAdvancedChatWorkflowsDraftRunResponse = zGeneratedAppResponse export const zGetAppsByAppIdAgentDriveFilesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAgentDriveFilesQuery = z.object({ @@ -3847,7 +3874,7 @@ export const zGetAppsByAppIdAgentDriveFilesQuery = z.object({ export const zGetAppsByAppIdAgentDriveFilesResponse = zAgentDriveListResponse export const zGetAppsByAppIdAgentDriveFilesDownloadPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAgentDriveFilesDownloadQuery = z.object({ @@ -3861,7 +3888,7 @@ export const zGetAppsByAppIdAgentDriveFilesDownloadQuery = z.object({ export const zGetAppsByAppIdAgentDriveFilesDownloadResponse = zAgentDriveDownloadResponse export const zGetAppsByAppIdAgentDriveFilesPreviewPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAgentDriveFilesPreviewQuery = z.object({ @@ -3875,7 +3902,7 @@ export const zGetAppsByAppIdAgentDriveFilesPreviewQuery = z.object({ export const zGetAppsByAppIdAgentDriveFilesPreviewResponse = zAgentDrivePreviewResponse export const zDeleteAppsByAppIdAgentFilesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zDeleteAppsByAppIdAgentFilesQuery = z.object({ @@ -3891,7 +3918,7 @@ export const zDeleteAppsByAppIdAgentFilesResponse = zAgentDriveDeleteResponse export const zPostAppsByAppIdAgentFilesBody = zAgentDriveFilePayload export const zPostAppsByAppIdAgentFilesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zPostAppsByAppIdAgentFilesQuery = z.object({ @@ -3904,7 +3931,7 @@ export const zPostAppsByAppIdAgentFilesQuery = z.object({ export const zPostAppsByAppIdAgentFilesResponse = zAgentDriveFileCommitResponse export const zGetAppsByAppIdAgentLogsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAgentLogsQuery = z.object({ @@ -3917,30 +3944,25 @@ export const zGetAppsByAppIdAgentLogsQuery = z.object({ */ export const zGetAppsByAppIdAgentLogsResponse = zAgentLogResponse -export const zPostAppsByAppIdAgentSkillsStandardizePath = z.object({ - app_id: z.string(), +export const zPostAppsByAppIdAgentSkillsUploadBody = z.object({ + file: z.custom(), }) -export const zPostAppsByAppIdAgentSkillsStandardizeQuery = z.object({ +export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({ + app_id: z.uuid(), +}) + +export const zPostAppsByAppIdAgentSkillsUploadQuery = z.object({ node_id: z.string().optional(), }) /** - * Skill standardized into drive - */ -export const zPostAppsByAppIdAgentSkillsStandardizeResponse = zAgentSkillStandardizeResponse - -export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({ - app_id: z.string(), -}) - -/** - * Skill validated + * Skill uploaded into drive */ export const zPostAppsByAppIdAgentSkillsUploadResponse = zAgentSkillUploadResponse export const zDeleteAppsByAppIdAgentSkillsBySlugPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), slug: z.string(), }) @@ -3954,7 +3976,7 @@ export const zDeleteAppsByAppIdAgentSkillsBySlugQuery = z.object({ export const zDeleteAppsByAppIdAgentSkillsBySlugResponse = zAgentDriveDeleteResponse export const zPostAppsByAppIdAgentSkillsBySlugInferToolsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), slug: z.string(), }) @@ -3971,7 +3993,7 @@ export const zPostAppsByAppIdAnnotationReplyByActionBody = zAnnotationReplyPaylo export const zPostAppsByAppIdAnnotationReplyByActionPath = z.object({ action: z.string(), - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -3981,8 +4003,8 @@ export const zPostAppsByAppIdAnnotationReplyByActionResponse = zAnnotationJobSta export const zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdPath = z.object({ action: z.string(), - app_id: z.string(), - job_id: z.string(), + app_id: z.uuid(), + job_id: z.uuid(), }) /** @@ -3992,7 +4014,7 @@ export const zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponse = zAnnotationJobStatusResponse export const zGetAppsByAppIdAnnotationSettingPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4004,8 +4026,8 @@ export const zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdBody = zAnnotationSettingUpdatePayload export const zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdPath = z.object({ - annotation_setting_id: z.string(), - app_id: z.string(), + annotation_setting_id: z.uuid(), + app_id: z.uuid(), }) /** @@ -4015,7 +4037,7 @@ export const zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdResponse = zAnnotationSettingResponse export const zDeleteAppsByAppIdAnnotationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4024,7 +4046,7 @@ export const zDeleteAppsByAppIdAnnotationsPath = z.object({ export const zDeleteAppsByAppIdAnnotationsResponse = z.void() export const zGetAppsByAppIdAnnotationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAnnotationsQuery = z.object({ @@ -4041,7 +4063,7 @@ export const zGetAppsByAppIdAnnotationsResponse = zAnnotationList export const zPostAppsByAppIdAnnotationsBody = zCreateAnnotationPayload export const zPostAppsByAppIdAnnotationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4050,7 +4072,7 @@ export const zPostAppsByAppIdAnnotationsPath = z.object({ export const zPostAppsByAppIdAnnotationsResponse = zAnnotation export const zPostAppsByAppIdAnnotationsBatchImportPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4059,8 +4081,8 @@ export const zPostAppsByAppIdAnnotationsBatchImportPath = z.object({ export const zPostAppsByAppIdAnnotationsBatchImportResponse = zAnnotationJobStatusResponse export const zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdPath = z.object({ - app_id: z.string(), - job_id: z.string(), + app_id: z.uuid(), + job_id: z.uuid(), }) /** @@ -4070,7 +4092,7 @@ export const zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponse = zAnnotationJobStatusResponse export const zGetAppsByAppIdAnnotationsCountPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4079,7 +4101,7 @@ export const zGetAppsByAppIdAnnotationsCountPath = z.object({ export const zGetAppsByAppIdAnnotationsCountResponse = zAnnotationCountResponse export const zGetAppsByAppIdAnnotationsExportPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4088,8 +4110,8 @@ export const zGetAppsByAppIdAnnotationsExportPath = z.object({ export const zGetAppsByAppIdAnnotationsExportResponse = zAnnotationExportList export const zDeleteAppsByAppIdAnnotationsByAnnotationIdPath = z.object({ - annotation_id: z.string(), - app_id: z.string(), + annotation_id: z.uuid(), + app_id: z.uuid(), }) /** @@ -4100,15 +4122,15 @@ export const zDeleteAppsByAppIdAnnotationsByAnnotationIdResponse = z.void() export const zPostAppsByAppIdAnnotationsByAnnotationIdBody = zUpdateAnnotationPayload export const zPostAppsByAppIdAnnotationsByAnnotationIdPath = z.object({ - annotation_id: z.string(), - app_id: z.string(), + annotation_id: z.uuid(), + app_id: z.uuid(), }) export const zPostAppsByAppIdAnnotationsByAnnotationIdResponse = z.union([zAnnotation, z.void()]) export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesPath = z.object({ - annotation_id: z.string(), - app_id: z.string(), + annotation_id: z.uuid(), + app_id: z.uuid(), }) export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesQuery = z.object({ @@ -4125,7 +4147,7 @@ export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesResponse export const zPostAppsByAppIdApiEnableBody = zAppApiStatusPayload export const zPostAppsByAppIdApiEnablePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4134,7 +4156,7 @@ export const zPostAppsByAppIdApiEnablePath = z.object({ export const zPostAppsByAppIdApiEnableResponse = zAppDetail export const zPostAppsByAppIdAudioToTextPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4143,7 +4165,7 @@ export const zPostAppsByAppIdAudioToTextPath = z.object({ export const zPostAppsByAppIdAudioToTextResponse = zAudioTranscriptResponse export const zGetAppsByAppIdChatConversationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdChatConversationsQuery = z.object({ @@ -4165,8 +4187,8 @@ export const zGetAppsByAppIdChatConversationsQuery = z.object({ export const zGetAppsByAppIdChatConversationsResponse = zConversationWithSummaryPagination export const zDeleteAppsByAppIdChatConversationsByConversationIdPath = z.object({ - app_id: z.string(), - conversation_id: z.string(), + app_id: z.uuid(), + conversation_id: z.uuid(), }) /** @@ -4175,8 +4197,8 @@ export const zDeleteAppsByAppIdChatConversationsByConversationIdPath = z.object( export const zDeleteAppsByAppIdChatConversationsByConversationIdResponse = z.void() export const zGetAppsByAppIdChatConversationsByConversationIdPath = z.object({ - app_id: z.string(), - conversation_id: z.string(), + app_id: z.uuid(), + conversation_id: z.uuid(), }) /** @@ -4185,7 +4207,7 @@ export const zGetAppsByAppIdChatConversationsByConversationIdPath = z.object({ export const zGetAppsByAppIdChatConversationsByConversationIdResponse = zConversationDetail export const zGetAppsByAppIdChatMessagesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdChatMessagesQuery = z.object({ @@ -4200,8 +4222,8 @@ export const zGetAppsByAppIdChatMessagesQuery = z.object({ export const zGetAppsByAppIdChatMessagesResponse = zMessageInfiniteScrollPaginationResponse export const zGetAppsByAppIdChatMessagesByMessageIdSuggestedQuestionsPath = z.object({ - app_id: z.string(), - message_id: z.string(), + app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -4211,7 +4233,7 @@ export const zGetAppsByAppIdChatMessagesByMessageIdSuggestedQuestionsResponse = zSuggestedQuestionsResponse export const zPostAppsByAppIdChatMessagesByTaskIdStopPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), task_id: z.string(), }) @@ -4221,7 +4243,7 @@ export const zPostAppsByAppIdChatMessagesByTaskIdStopPath = z.object({ export const zPostAppsByAppIdChatMessagesByTaskIdStopResponse = zSimpleResultResponse export const zGetAppsByAppIdCompletionConversationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdCompletionConversationsQuery = z.object({ @@ -4239,8 +4261,8 @@ export const zGetAppsByAppIdCompletionConversationsQuery = z.object({ export const zGetAppsByAppIdCompletionConversationsResponse = zConversationPagination export const zDeleteAppsByAppIdCompletionConversationsByConversationIdPath = z.object({ - app_id: z.string(), - conversation_id: z.string(), + app_id: z.uuid(), + conversation_id: z.uuid(), }) /** @@ -4249,8 +4271,8 @@ export const zDeleteAppsByAppIdCompletionConversationsByConversationIdPath = z.o export const zDeleteAppsByAppIdCompletionConversationsByConversationIdResponse = z.void() export const zGetAppsByAppIdCompletionConversationsByConversationIdPath = z.object({ - app_id: z.string(), - conversation_id: z.string(), + app_id: z.uuid(), + conversation_id: z.uuid(), }) /** @@ -4262,7 +4284,7 @@ export const zGetAppsByAppIdCompletionConversationsByConversationIdResponse export const zPostAppsByAppIdCompletionMessagesBody = zCompletionMessagePayload export const zPostAppsByAppIdCompletionMessagesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4271,7 +4293,7 @@ export const zPostAppsByAppIdCompletionMessagesPath = z.object({ export const zPostAppsByAppIdCompletionMessagesResponse = zGeneratedAppResponse export const zPostAppsByAppIdCompletionMessagesByTaskIdStopPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), task_id: z.string(), }) @@ -4281,7 +4303,7 @@ export const zPostAppsByAppIdCompletionMessagesByTaskIdStopPath = z.object({ export const zPostAppsByAppIdCompletionMessagesByTaskIdStopResponse = zSimpleResultResponse export const zGetAppsByAppIdConversationVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdConversationVariablesQuery = z.object({ @@ -4296,7 +4318,7 @@ export const zGetAppsByAppIdConversationVariablesResponse = zPaginatedConversati export const zPostAppsByAppIdConvertToWorkflowBody = zConvertToWorkflowPayload export const zPostAppsByAppIdConvertToWorkflowPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4307,7 +4329,7 @@ export const zPostAppsByAppIdConvertToWorkflowResponse = zNewAppResponse export const zPostAppsByAppIdCopyBody = zCopyAppPayload export const zPostAppsByAppIdCopyPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4316,7 +4338,7 @@ export const zPostAppsByAppIdCopyPath = z.object({ export const zPostAppsByAppIdCopyResponse = zAppDetailWithSite export const zGetAppsByAppIdExportPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdExportQuery = z.object({ @@ -4332,7 +4354,7 @@ export const zGetAppsByAppIdExportResponse = zAppExportResponse export const zPostAppsByAppIdFeedbacksBody = zMessageFeedbackPayload export const zPostAppsByAppIdFeedbacksPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4341,7 +4363,7 @@ export const zPostAppsByAppIdFeedbacksPath = z.object({ export const zPostAppsByAppIdFeedbacksResponse = zSimpleResultResponse export const zGetAppsByAppIdFeedbacksExportPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdFeedbacksExportQuery = z.object({ @@ -4361,7 +4383,7 @@ export const zGetAppsByAppIdFeedbacksExportResponse = zTextFileResponse export const zPostAppsByAppIdIconBody = zAppIconPayload export const zPostAppsByAppIdIconPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4370,8 +4392,8 @@ export const zPostAppsByAppIdIconPath = z.object({ export const zPostAppsByAppIdIconResponse = zAppDetail export const zGetAppsByAppIdMessagesByMessageIdPath = z.object({ - app_id: z.string(), - message_id: z.string(), + app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -4382,7 +4404,7 @@ export const zGetAppsByAppIdMessagesByMessageIdResponse = zMessageDetailResponse export const zPostAppsByAppIdModelConfigBody = zModelConfigRequest export const zPostAppsByAppIdModelConfigPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4393,7 +4415,7 @@ export const zPostAppsByAppIdModelConfigResponse = zSimpleResultResponse export const zPostAppsByAppIdNameBody = zAppNamePayload export const zPostAppsByAppIdNamePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4402,7 +4424,7 @@ export const zPostAppsByAppIdNamePath = z.object({ export const zPostAppsByAppIdNameResponse = zAppDetail export const zPostAppsByAppIdPublishToCreatorsPlatformPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4411,7 +4433,7 @@ export const zPostAppsByAppIdPublishToCreatorsPlatformPath = z.object({ export const zPostAppsByAppIdPublishToCreatorsPlatformResponse = zRedirectUrlResponse export const zGetAppsByAppIdServerPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4422,7 +4444,7 @@ export const zGetAppsByAppIdServerResponse = zAppMcpServerResponse export const zPostAppsByAppIdServerBody = zMcpServerCreatePayload export const zPostAppsByAppIdServerPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4433,7 +4455,7 @@ export const zPostAppsByAppIdServerResponse = zAppMcpServerResponse export const zPutAppsByAppIdServerBody = zMcpServerUpdatePayload export const zPutAppsByAppIdServerPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4444,7 +4466,7 @@ export const zPutAppsByAppIdServerResponse = zAppMcpServerResponse export const zPostAppsByAppIdSiteBody = zAppSiteUpdatePayload export const zPostAppsByAppIdSitePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4455,7 +4477,7 @@ export const zPostAppsByAppIdSiteResponse = zAppSiteResponse export const zPostAppsByAppIdSiteEnableBody = zAppSiteStatusPayload export const zPostAppsByAppIdSiteEnablePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4464,7 +4486,7 @@ export const zPostAppsByAppIdSiteEnablePath = z.object({ export const zPostAppsByAppIdSiteEnableResponse = zAppDetail export const zPostAppsByAppIdSiteAccessTokenResetPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4473,7 +4495,7 @@ export const zPostAppsByAppIdSiteAccessTokenResetPath = z.object({ export const zPostAppsByAppIdSiteAccessTokenResetResponse = zAppSiteResponse export const zDeleteAppsByAppIdStarPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4482,7 +4504,7 @@ export const zDeleteAppsByAppIdStarPath = z.object({ export const zDeleteAppsByAppIdStarResponse = zSimpleResultResponse export const zPostAppsByAppIdStarPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4491,7 +4513,7 @@ export const zPostAppsByAppIdStarPath = z.object({ export const zPostAppsByAppIdStarResponse = zSimpleResultResponse export const zGetAppsByAppIdStatisticsAverageResponseTimePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsAverageResponseTimeQuery = z.object({ @@ -4506,7 +4528,7 @@ export const zGetAppsByAppIdStatisticsAverageResponseTimeResponse = zAverageResponseTimeStatisticResponse export const zGetAppsByAppIdStatisticsAverageSessionInteractionsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsAverageSessionInteractionsQuery = z.object({ @@ -4521,7 +4543,7 @@ export const zGetAppsByAppIdStatisticsAverageSessionInteractionsResponse = zAverageSessionInteractionStatisticResponse export const zGetAppsByAppIdStatisticsDailyConversationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsDailyConversationsQuery = z.object({ @@ -4536,7 +4558,7 @@ export const zGetAppsByAppIdStatisticsDailyConversationsResponse = zDailyConversationStatisticResponse export const zGetAppsByAppIdStatisticsDailyEndUsersPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsDailyEndUsersQuery = z.object({ @@ -4550,7 +4572,7 @@ export const zGetAppsByAppIdStatisticsDailyEndUsersQuery = z.object({ export const zGetAppsByAppIdStatisticsDailyEndUsersResponse = zDailyTerminalStatisticResponse export const zGetAppsByAppIdStatisticsDailyMessagesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsDailyMessagesQuery = z.object({ @@ -4564,7 +4586,7 @@ export const zGetAppsByAppIdStatisticsDailyMessagesQuery = z.object({ export const zGetAppsByAppIdStatisticsDailyMessagesResponse = zDailyMessageStatisticResponse export const zGetAppsByAppIdStatisticsTokenCostsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsTokenCostsQuery = z.object({ @@ -4578,7 +4600,7 @@ export const zGetAppsByAppIdStatisticsTokenCostsQuery = z.object({ export const zGetAppsByAppIdStatisticsTokenCostsResponse = zDailyTokenCostStatisticResponse export const zGetAppsByAppIdStatisticsTokensPerSecondPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsTokensPerSecondQuery = z.object({ @@ -4592,7 +4614,7 @@ export const zGetAppsByAppIdStatisticsTokensPerSecondQuery = z.object({ export const zGetAppsByAppIdStatisticsTokensPerSecondResponse = zTokensPerSecondStatisticResponse export const zGetAppsByAppIdStatisticsUserSatisfactionRatePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdStatisticsUserSatisfactionRateQuery = z.object({ @@ -4609,7 +4631,7 @@ export const zGetAppsByAppIdStatisticsUserSatisfactionRateResponse export const zPostAppsByAppIdTextToAudioBody = zTextToSpeechPayload export const zPostAppsByAppIdTextToAudioPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4618,7 +4640,7 @@ export const zPostAppsByAppIdTextToAudioPath = z.object({ export const zPostAppsByAppIdTextToAudioResponse = zAudioBinaryResponse export const zGetAppsByAppIdTextToAudioVoicesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdTextToAudioVoicesQuery = z.object({ @@ -4631,7 +4653,7 @@ export const zGetAppsByAppIdTextToAudioVoicesQuery = z.object({ export const zGetAppsByAppIdTextToAudioVoicesResponse = zTextToSpeechVoiceListResponse export const zGetAppsByAppIdTracePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4642,7 +4664,7 @@ export const zGetAppsByAppIdTraceResponse = zAppTraceResponse export const zPostAppsByAppIdTraceBody = zAppTracePayload export const zPostAppsByAppIdTracePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4651,7 +4673,7 @@ export const zPostAppsByAppIdTracePath = z.object({ export const zPostAppsByAppIdTraceResponse = zSimpleResultResponse export const zDeleteAppsByAppIdTraceConfigPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zDeleteAppsByAppIdTraceConfigQuery = z.object({ @@ -4664,7 +4686,7 @@ export const zDeleteAppsByAppIdTraceConfigQuery = z.object({ export const zDeleteAppsByAppIdTraceConfigResponse = z.void() export const zGetAppsByAppIdTraceConfigPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdTraceConfigQuery = z.object({ @@ -4679,7 +4701,7 @@ export const zGetAppsByAppIdTraceConfigResponse = zTraceAppConfigResponse export const zPatchAppsByAppIdTraceConfigBody = zTraceConfigPayload export const zPatchAppsByAppIdTraceConfigPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4690,7 +4712,7 @@ export const zPatchAppsByAppIdTraceConfigResponse = zTraceAppConfigResponse export const zPostAppsByAppIdTraceConfigBody = zTraceConfigPayload export const zPostAppsByAppIdTraceConfigPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4701,7 +4723,7 @@ export const zPostAppsByAppIdTraceConfigResponse = zTraceAppConfigResponse export const zPostAppsByAppIdTriggerEnableBody = zParserEnable export const zPostAppsByAppIdTriggerEnablePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4710,7 +4732,7 @@ export const zPostAppsByAppIdTriggerEnablePath = z.object({ export const zPostAppsByAppIdTriggerEnableResponse = zWorkflowTriggerResponse export const zGetAppsByAppIdTriggersPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4719,7 +4741,7 @@ export const zGetAppsByAppIdTriggersPath = z.object({ export const zGetAppsByAppIdTriggersResponse = zWorkflowTriggerListResponse export const zGetAppsByAppIdWorkflowAppLogsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowAppLogsQuery = z.object({ @@ -4742,7 +4764,7 @@ export const zGetAppsByAppIdWorkflowAppLogsQuery = z.object({ export const zGetAppsByAppIdWorkflowAppLogsResponse = zWorkflowAppLogPaginationResponse export const zGetAppsByAppIdWorkflowArchivedLogsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowArchivedLogsQuery = z.object({ @@ -4765,7 +4787,7 @@ export const zGetAppsByAppIdWorkflowArchivedLogsQuery = z.object({ export const zGetAppsByAppIdWorkflowArchivedLogsResponse = zWorkflowArchivedLogPaginationResponse export const zGetAppsByAppIdWorkflowRunsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowRunsQuery = z.object({ @@ -4781,7 +4803,7 @@ export const zGetAppsByAppIdWorkflowRunsQuery = z.object({ export const zGetAppsByAppIdWorkflowRunsResponse = zWorkflowRunPaginationResponse export const zGetAppsByAppIdWorkflowRunsCountPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowRunsCountQuery = z.object({ @@ -4796,7 +4818,7 @@ export const zGetAppsByAppIdWorkflowRunsCountQuery = z.object({ export const zGetAppsByAppIdWorkflowRunsCountResponse = zWorkflowRunCountResponse export const zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), task_id: z.string(), }) @@ -4806,8 +4828,8 @@ export const zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopPath = z.object({ export const zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopResponse = zSimpleResultResponse export const zGetAppsByAppIdWorkflowRunsByRunIdPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -4816,8 +4838,8 @@ export const zGetAppsByAppIdWorkflowRunsByRunIdPath = z.object({ export const zGetAppsByAppIdWorkflowRunsByRunIdResponse = zWorkflowRunDetailResponse export const zGetAppsByAppIdWorkflowRunsByRunIdExportPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -4826,8 +4848,8 @@ export const zGetAppsByAppIdWorkflowRunsByRunIdExportPath = z.object({ export const zGetAppsByAppIdWorkflowRunsByRunIdExportResponse = zWorkflowRunExportResponse export const zGetAppsByAppIdWorkflowRunsByRunIdNodeExecutionsPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -4838,9 +4860,9 @@ export const zGetAppsByAppIdWorkflowRunsByRunIdNodeExecutionsResponse export const zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), - workflow_run_id: z.string(), + workflow_run_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesQuery @@ -4857,9 +4879,9 @@ export const zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandbox export const zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesReadPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), - workflow_run_id: z.string(), + workflow_run_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesReadQuery @@ -4879,9 +4901,9 @@ export const zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandbo export const zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesUploadPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), - workflow_run_id: z.string(), + workflow_run_id: z.uuid(), }) /** @@ -4891,7 +4913,7 @@ export const zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandbo = zSandboxUploadResponse export const zGetAppsByAppIdWorkflowCommentsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4902,7 +4924,7 @@ export const zGetAppsByAppIdWorkflowCommentsResponse = zWorkflowCommentBasicList export const zPostAppsByAppIdWorkflowCommentsBody = zWorkflowCommentCreatePayload export const zPostAppsByAppIdWorkflowCommentsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4911,7 +4933,7 @@ export const zPostAppsByAppIdWorkflowCommentsPath = z.object({ export const zPostAppsByAppIdWorkflowCommentsResponse = zWorkflowCommentCreate export const zGetAppsByAppIdWorkflowCommentsMentionUsersPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -4921,7 +4943,7 @@ export const zGetAppsByAppIdWorkflowCommentsMentionUsersResponse = zWorkflowCommentMentionUsersPayload export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), }) @@ -4931,7 +4953,7 @@ export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdResponse = z.void() export const zGetAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), }) @@ -4943,7 +4965,7 @@ export const zGetAppsByAppIdWorkflowCommentsByCommentIdResponse = zWorkflowComme export const zPutAppsByAppIdWorkflowCommentsByCommentIdBody = zWorkflowCommentUpdatePayload export const zPutAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), }) @@ -4955,7 +4977,7 @@ export const zPutAppsByAppIdWorkflowCommentsByCommentIdResponse = zWorkflowComme export const zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesBody = zWorkflowCommentReplyPayload export const zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), }) @@ -4966,7 +4988,7 @@ export const zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesResponse = zWorkflowCommentReplyCreate export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), reply_id: z.string(), }) @@ -4980,7 +5002,7 @@ export const zPutAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdBody = zWorkflowCommentReplyPayload export const zPutAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), reply_id: z.string(), }) @@ -4992,7 +5014,7 @@ export const zPutAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponse = zWorkflowCommentReplyUpdate export const zPostAppsByAppIdWorkflowCommentsByCommentIdResolvePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), comment_id: z.string(), }) @@ -5002,7 +5024,7 @@ export const zPostAppsByAppIdWorkflowCommentsByCommentIdResolvePath = z.object({ export const zPostAppsByAppIdWorkflowCommentsByCommentIdResolveResponse = zWorkflowCommentResolve export const zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsQuery = z.object({ @@ -5017,7 +5039,7 @@ export const zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsResponse = zWorkflowAverageAppInteractionStatisticResponse export const zGetAppsByAppIdWorkflowStatisticsDailyConversationsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowStatisticsDailyConversationsQuery = z.object({ @@ -5032,7 +5054,7 @@ export const zGetAppsByAppIdWorkflowStatisticsDailyConversationsResponse = zWorkflowDailyRunsStatisticResponse export const zGetAppsByAppIdWorkflowStatisticsDailyTerminalsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowStatisticsDailyTerminalsQuery = z.object({ @@ -5047,7 +5069,7 @@ export const zGetAppsByAppIdWorkflowStatisticsDailyTerminalsResponse = zWorkflowDailyTerminalsStatisticResponse export const zGetAppsByAppIdWorkflowStatisticsTokenCostsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowStatisticsTokenCostsQuery = z.object({ @@ -5062,7 +5084,7 @@ export const zGetAppsByAppIdWorkflowStatisticsTokenCostsResponse = zWorkflowDailyTokenCostStatisticResponse export const zGetAppsByAppIdWorkflowsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowsQuery = z.object({ @@ -5078,7 +5100,7 @@ export const zGetAppsByAppIdWorkflowsQuery = z.object({ export const zGetAppsByAppIdWorkflowsResponse = zWorkflowPaginationResponse export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5088,7 +5110,7 @@ export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsResponse = zDefaultBlockConfigsResponse export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), block_type: z.string(), }) @@ -5103,7 +5125,7 @@ export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeRespo = zDefaultBlockConfigResponse export const zGetAppsByAppIdWorkflowsDraftPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5114,7 +5136,7 @@ export const zGetAppsByAppIdWorkflowsDraftResponse = zWorkflowResponse export const zPostAppsByAppIdWorkflowsDraftBody = zSyncDraftWorkflowPayload export const zPostAppsByAppIdWorkflowsDraftPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5123,7 +5145,7 @@ export const zPostAppsByAppIdWorkflowsDraftPath = z.object({ export const zPostAppsByAppIdWorkflowsDraftResponse = zSyncDraftWorkflowResponse export const zGetAppsByAppIdWorkflowsDraftConversationVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5135,7 +5157,7 @@ export const zPostAppsByAppIdWorkflowsDraftConversationVariablesBody = zConversationVariableUpdatePayload export const zPostAppsByAppIdWorkflowsDraftConversationVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5144,7 +5166,7 @@ export const zPostAppsByAppIdWorkflowsDraftConversationVariablesPath = z.object( export const zPostAppsByAppIdWorkflowsDraftConversationVariablesResponse = zSimpleResultResponse export const zGetAppsByAppIdWorkflowsDraftEnvironmentVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5157,7 +5179,7 @@ export const zPostAppsByAppIdWorkflowsDraftEnvironmentVariablesBody = zEnvironmentVariableUpdatePayload export const zPostAppsByAppIdWorkflowsDraftEnvironmentVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5168,7 +5190,7 @@ export const zPostAppsByAppIdWorkflowsDraftEnvironmentVariablesResponse = zSimpl export const zPostAppsByAppIdWorkflowsDraftFeaturesBody = zWorkflowFeaturesPayload export const zPostAppsByAppIdWorkflowsDraftFeaturesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5180,7 +5202,7 @@ export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestBo = zHumanInputDeliveryTestPayload export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5194,7 +5216,7 @@ export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewBod = zHumanInputFormPreviewPayload export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5208,7 +5230,7 @@ export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunBody = zHumanInputFormSubmitPayload export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5221,7 +5243,7 @@ export const zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunRespons export const zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunBody = zIterationNodeRunPayload export const zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5233,7 +5255,7 @@ export const zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunResponse = z export const zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunBody = zLoopNodeRunPayload export const zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5243,7 +5265,7 @@ export const zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunPath = z.object({ export const zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse = zGeneratedAppResponse export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5256,7 +5278,7 @@ export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerResponse export const zPutAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerBody = zComposerSavePayload export const zPutAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5267,7 +5289,7 @@ export const zPutAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerResponse = zWorkflowAgentComposerResponse export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerCandidatesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5281,7 +5303,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactBody = zComposerSavePayload export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5295,7 +5317,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRoste = zComposerSavePayload export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5309,7 +5331,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateBod = zComposerSavePayload export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidatePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5320,7 +5342,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateRes = zAgentComposerValidateResponse export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdLastRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5333,7 +5355,7 @@ export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdLastRunResponse export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunBody = zDraftWorkflowNodeRunPayload export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5344,7 +5366,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse = zWorkflowRunNodeExecutionResponse export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5354,7 +5376,7 @@ export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunPath = z.objec export const zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse = zGeneratedAppResponse export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5364,7 +5386,7 @@ export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesPath = z.obje export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), }) @@ -5377,7 +5399,7 @@ export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse export const zPostAppsByAppIdWorkflowsDraftRunBody = zDraftWorkflowRunPayload export const zPostAppsByAppIdWorkflowsDraftRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5386,8 +5408,8 @@ export const zPostAppsByAppIdWorkflowsDraftRunPath = z.object({ export const zPostAppsByAppIdWorkflowsDraftRunResponse = zGeneratedAppResponse export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -5396,8 +5418,8 @@ export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsPath = z.object( export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsResponse = zWorkflowRunSnapshotView export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsEventsPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -5407,9 +5429,9 @@ export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsEventsResponse = zEventStreamResponse export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), - run_id: z.string(), + run_id: z.uuid(), }) /** @@ -5419,10 +5441,10 @@ export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdResponse export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdByOutputNamePreviewPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), output_name: z.string(), - run_id: z.string(), + run_id: z.uuid(), }) /** @@ -5432,7 +5454,7 @@ export const zGetAppsByAppIdWorkflowsDraftRunsByRunIdNodeOutputsByNodeIdByOutput = zOutputPreviewView export const zGetAppsByAppIdWorkflowsDraftSystemVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5443,7 +5465,7 @@ export const zGetAppsByAppIdWorkflowsDraftSystemVariablesResponse = zWorkflowDra export const zPostAppsByAppIdWorkflowsDraftTriggerRunBody = zDraftWorkflowTriggerRunRequest export const zPostAppsByAppIdWorkflowsDraftTriggerRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5454,7 +5476,7 @@ export const zPostAppsByAppIdWorkflowsDraftTriggerRunResponse = zGeneratedAppRes export const zPostAppsByAppIdWorkflowsDraftTriggerRunAllBody = zDraftWorkflowTriggerRunAllPayload export const zPostAppsByAppIdWorkflowsDraftTriggerRunAllPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5463,7 +5485,7 @@ export const zPostAppsByAppIdWorkflowsDraftTriggerRunAllPath = z.object({ export const zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse = zGeneratedAppResponse export const zDeleteAppsByAppIdWorkflowsDraftVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5472,7 +5494,7 @@ export const zDeleteAppsByAppIdWorkflowsDraftVariablesPath = z.object({ export const zDeleteAppsByAppIdWorkflowsDraftVariablesResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftVariablesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowsDraftVariablesQuery = z.object({ @@ -5486,8 +5508,8 @@ export const zGetAppsByAppIdWorkflowsDraftVariablesQuery = z.object({ export const zGetAppsByAppIdWorkflowsDraftVariablesResponse = zWorkflowDraftVariableListWithoutValue export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - app_id: z.string(), - variable_id: z.string(), + app_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -5496,8 +5518,8 @@ export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.objec export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - app_id: z.string(), - variable_id: z.string(), + app_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -5509,8 +5531,8 @@ export const zPatchAppsByAppIdWorkflowsDraftVariablesByVariableIdBody = zWorkflowDraftVariableUpdatePayload export const zPatchAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - app_id: z.string(), - variable_id: z.string(), + app_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -5519,8 +5541,8 @@ export const zPatchAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.object export const zPatchAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse = zWorkflowDraftVariable export const zPutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetPath = z.object({ - app_id: z.string(), - variable_id: z.string(), + app_id: z.uuid(), + variable_id: z.uuid(), }) export const zPutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetResponse = z.union([ @@ -5529,7 +5551,7 @@ export const zPutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetResponse = z ]) export const zGetAppsByAppIdWorkflowsPublishPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5540,7 +5562,7 @@ export const zGetAppsByAppIdWorkflowsPublishResponse = zWorkflowResponse export const zPostAppsByAppIdWorkflowsPublishBody = zPublishWorkflowPayload export const zPostAppsByAppIdWorkflowsPublishPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -5549,8 +5571,8 @@ export const zPostAppsByAppIdWorkflowsPublishPath = z.object({ export const zPostAppsByAppIdWorkflowsPublishResponse = zWorkflowPublishResponse export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -5560,8 +5582,8 @@ export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsResponse = zWorkflowRunSnapshotView export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsEventsPath = z.object({ - app_id: z.string(), - run_id: z.string(), + app_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -5571,9 +5593,9 @@ export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsEventsRespon = zEventStreamResponse export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), - run_id: z.string(), + run_id: z.uuid(), }) /** @@ -5584,10 +5606,10 @@ export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdResp export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdByOutputNamePreviewPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), node_id: z.string(), output_name: z.string(), - run_id: z.string(), + run_id: z.uuid(), }) /** @@ -5597,7 +5619,7 @@ export const zGetAppsByAppIdWorkflowsPublishedRunsByRunIdNodeOutputsByNodeIdByOu = zOutputPreviewView export const zGetAppsByAppIdWorkflowsTriggersWebhookPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetAppsByAppIdWorkflowsTriggersWebhookQuery = z.object({ @@ -5610,7 +5632,7 @@ export const zGetAppsByAppIdWorkflowsTriggersWebhookQuery = z.object({ export const zGetAppsByAppIdWorkflowsTriggersWebhookResponse = zWebhookTriggerResponse export const zDeleteAppsByAppIdWorkflowsByWorkflowIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), workflow_id: z.string(), }) @@ -5622,7 +5644,7 @@ export const zDeleteAppsByAppIdWorkflowsByWorkflowIdResponse = z.void() export const zPatchAppsByAppIdWorkflowsByWorkflowIdBody = zWorkflowUpdatePayload export const zPatchAppsByAppIdWorkflowsByWorkflowIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), workflow_id: z.string(), }) @@ -5632,7 +5654,7 @@ export const zPatchAppsByAppIdWorkflowsByWorkflowIdPath = z.object({ export const zPatchAppsByAppIdWorkflowsByWorkflowIdResponse = zWorkflowResponse export const zPostAppsByAppIdWorkflowsByWorkflowIdRestorePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), workflow_id: z.string(), }) @@ -5642,7 +5664,7 @@ export const zPostAppsByAppIdWorkflowsByWorkflowIdRestorePath = z.object({ export const zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse = zWorkflowRestoreResponse export const zGetAppsByResourceIdApiKeysPath = z.object({ - resource_id: z.string(), + resource_id: z.uuid(), }) /** @@ -5651,7 +5673,7 @@ export const zGetAppsByResourceIdApiKeysPath = z.object({ export const zGetAppsByResourceIdApiKeysResponse = zApiKeyList export const zPostAppsByResourceIdApiKeysPath = z.object({ - resource_id: z.string(), + resource_id: z.uuid(), }) /** @@ -5660,8 +5682,8 @@ export const zPostAppsByResourceIdApiKeysPath = z.object({ export const zPostAppsByResourceIdApiKeysResponse = zApiKeyItem export const zDeleteAppsByResourceIdApiKeysByApiKeyIdPath = z.object({ - api_key_id: z.string(), - resource_id: z.string(), + api_key_id: z.uuid(), + resource_id: z.uuid(), }) /** @@ -5670,7 +5692,7 @@ export const zDeleteAppsByResourceIdApiKeysByApiKeyIdPath = z.object({ export const zDeleteAppsByResourceIdApiKeysByApiKeyIdResponse = z.void() export const zGetAppsByServerIdServerRefreshPath = z.object({ - server_id: z.string(), + server_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/data-source/zod.gen.ts b/packages/contracts/generated/api/console/data-source/zod.gen.ts index e5cf1735c3f..e2774d10f42 100644 --- a/packages/contracts/generated/api/console/data-source/zod.gen.ts +++ b/packages/contracts/generated/api/console/data-source/zod.gen.ts @@ -72,7 +72,7 @@ export const zPatchDataSourceIntegratesResponse = zSimpleResultResponse export const zGetDataSourceIntegratesByBindingIdByActionPath = z.object({ action: z.string(), - binding_id: z.string(), + binding_id: z.uuid(), }) /** @@ -82,7 +82,7 @@ export const zGetDataSourceIntegratesByBindingIdByActionResponse = zDataSourceIn export const zPatchDataSourceIntegratesByBindingIdByActionPath = z.object({ action: z.string(), - binding_id: z.string(), + binding_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/datasets/zod.gen.ts b/packages/contracts/generated/api/console/datasets/zod.gen.ts index 5a5f9282794..9609f8ad310 100644 --- a/packages/contracts/generated/api/console/datasets/zod.gen.ts +++ b/packages/contracts/generated/api/console/datasets/zod.gen.ts @@ -1502,7 +1502,7 @@ export const zGetDatasetsApiKeysResponse = zApiKeyList export const zPostDatasetsApiKeysResponse = zApiKeyItem export const zDeleteDatasetsApiKeysByApiKeyIdPath = z.object({ - api_key_id: z.string(), + api_key_id: z.uuid(), }) /** @@ -1511,7 +1511,7 @@ export const zDeleteDatasetsApiKeysByApiKeyIdPath = z.object({ export const zDeleteDatasetsApiKeysByApiKeyIdResponse = z.void() export const zGetDatasetsBatchImportStatusByJobIdPath = z.object({ - job_id: z.string(), + job_id: z.uuid(), }) /** @@ -1522,7 +1522,7 @@ export const zGetDatasetsBatchImportStatusByJobIdResponse = zSegmentBatchImportS export const zPostDatasetsBatchImportStatusByJobIdBody = zBatchImportPayload export const zPostDatasetsBatchImportStatusByJobIdPath = z.object({ - job_id: z.string(), + job_id: z.uuid(), }) /** @@ -1556,7 +1556,7 @@ export const zPostDatasetsExternalKnowledgeApiBody = zExternalKnowledgeApiPayloa export const zPostDatasetsExternalKnowledgeApiResponse = zExternalKnowledgeApiResponse export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z.object({ - external_knowledge_api_id: z.string(), + external_knowledge_api_id: z.uuid(), }) /** @@ -1565,7 +1565,7 @@ export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse = z.void() export const zGetDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z.object({ - external_knowledge_api_id: z.string(), + external_knowledge_api_id: z.uuid(), }) /** @@ -1578,7 +1578,7 @@ export const zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdBody = zExternalKnowledgeApiPayload export const zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z.object({ - external_knowledge_api_id: z.string(), + external_knowledge_api_id: z.uuid(), }) /** @@ -1588,7 +1588,7 @@ export const zPatchDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse = zExternalKnowledgeApiResponse export const zGetDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdUseCheckPath = z.object({ - external_knowledge_api_id: z.string(), + external_knowledge_api_id: z.uuid(), }) /** @@ -1647,7 +1647,7 @@ export const zGetDatasetsRetrievalSettingByVectorTypePath = z.object({ export const zGetDatasetsRetrievalSettingByVectorTypeResponse = zRetrievalSettingResponse export const zDeleteDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1656,7 +1656,7 @@ export const zDeleteDatasetsByDatasetIdPath = z.object({ export const zDeleteDatasetsByDatasetIdResponse = z.void() export const zGetDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1667,7 +1667,7 @@ export const zGetDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMembersR export const zPatchDatasetsByDatasetIdBody = zDatasetUpdatePayload export const zPatchDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1676,7 +1676,7 @@ export const zPatchDatasetsByDatasetIdPath = z.object({ export const zPatchDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMembersResponse export const zPostDatasetsByDatasetIdApiKeysByStatusPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), status: z.string(), }) @@ -1686,7 +1686,7 @@ export const zPostDatasetsByDatasetIdApiKeysByStatusPath = z.object({ export const zPostDatasetsByDatasetIdApiKeysByStatusResponse = zSimpleResultResponse export const zGetDatasetsByDatasetIdAutoDisableLogsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1696,7 +1696,7 @@ export const zGetDatasetsByDatasetIdAutoDisableLogsResponse = zAutoDisableLogsRe export const zGetDatasetsByDatasetIdBatchByBatchIndexingEstimatePath = z.object({ batch: z.string(), - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1706,7 +1706,7 @@ export const zGetDatasetsByDatasetIdBatchByBatchIndexingEstimateResponse = zOpaq export const zGetDatasetsByDatasetIdBatchByBatchIndexingStatusPath = z.object({ batch: z.string(), - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1715,7 +1715,7 @@ export const zGetDatasetsByDatasetIdBatchByBatchIndexingStatusPath = z.object({ export const zGetDatasetsByDatasetIdBatchByBatchIndexingStatusResponse = zDocumentStatusListResponse export const zDeleteDatasetsByDatasetIdDocumentsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1724,7 +1724,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsPath = z.object({ export const zDeleteDatasetsByDatasetIdDocumentsResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsQuery = z.object({ @@ -1744,7 +1744,7 @@ export const zGetDatasetsByDatasetIdDocumentsResponse = zDocumentWithSegmentsLis export const zPostDatasetsByDatasetIdDocumentsBody = zKnowledgeConfig export const zPostDatasetsByDatasetIdDocumentsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1755,7 +1755,7 @@ export const zPostDatasetsByDatasetIdDocumentsResponse = zDatasetAndDocumentResp export const zPostDatasetsByDatasetIdDocumentsDownloadZipBody = zDocumentBatchDownloadZipPayload export const zPostDatasetsByDatasetIdDocumentsDownloadZipPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1766,7 +1766,7 @@ export const zPostDatasetsByDatasetIdDocumentsDownloadZipResponse = zBinaryFileR export const zPostDatasetsByDatasetIdDocumentsGenerateSummaryBody = zGenerateSummaryPayload export const zPostDatasetsByDatasetIdDocumentsGenerateSummaryPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1777,7 +1777,7 @@ export const zPostDatasetsByDatasetIdDocumentsGenerateSummaryResponse = zSimpleR export const zPostDatasetsByDatasetIdDocumentsMetadataBody = zMetadataOperationData export const zPostDatasetsByDatasetIdDocumentsMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1787,7 +1787,7 @@ export const zPostDatasetsByDatasetIdDocumentsMetadataResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath = z.object({ action: z.string(), - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -1796,8 +1796,8 @@ export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath = z.objec export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchResponse = zSimpleResultResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1806,8 +1806,8 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdQuery = z.object({ @@ -1820,8 +1820,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdQuery = z.object({ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdResponse = zOpaqueObjectResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1830,8 +1830,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadPath = z.object export const zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadResponse = zUrlResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdIndexingEstimatePath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1841,8 +1841,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdIndexingEstimateRespons = zOpaqueObjectResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdIndexingStatusPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1855,8 +1855,8 @@ export const zPutDatasetsByDatasetIdDocumentsByDocumentIdMetadataBody = zDocumentMetadataUpdatePayload export const zPutDatasetsByDatasetIdDocumentsByDocumentIdMetadataPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1866,8 +1866,8 @@ export const zPutDatasetsByDatasetIdDocumentsByDocumentIdMetadataResponse = zSimpleResultMessageResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdNotionSyncPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1876,8 +1876,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdNotionSyncPath = z.obje export const zGetDatasetsByDatasetIdDocumentsByDocumentIdNotionSyncResponse = zSimpleResultResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPipelineExecutionLogPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1887,8 +1887,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPipelineExecutionLogRes = zOpaqueObjectResponse export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPausePath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1897,8 +1897,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPausePath = export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumePath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1908,8 +1908,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeRespo export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingByActionPath = z.object({ action: z.string(), - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1921,8 +1921,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingByActionRes export const zPostDatasetsByDatasetIdDocumentsByDocumentIdRenameBody = zDocumentRenamePayload export const zPostDatasetsByDatasetIdDocumentsByDocumentIdRenamePath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1933,8 +1933,8 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdRenameResponse = zDocu export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentBody = zSegmentCreatePayload export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -1944,8 +1944,8 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentResponse = zSeg export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentByActionPath = z.object({ action: z.string(), - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentByActionQuery = z.object({ @@ -1959,8 +1959,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentByActionRespon = zSimpleResultResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.object({ @@ -1973,8 +1973,8 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.ob export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.object({ @@ -1993,8 +1993,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = zConsoleSegmentListResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBatchImportPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2007,8 +2007,8 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBatchImportBod = zBatchImportPayload export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBatchImportPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2018,9 +2018,9 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBatchImportRes = zSegmentBatchImportStatusResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2032,9 +2032,9 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdBo = zSegmentUpdatePayload export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2045,9 +2045,9 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdRe export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksQuery @@ -2068,9 +2068,9 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2084,9 +2084,9 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2097,10 +2097,10 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdPath = z.object({ - child_chunk_id: z.string(), - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + child_chunk_id: z.uuid(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2114,10 +2114,10 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdPath = z.object({ - child_chunk_id: z.string(), - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + child_chunk_id: z.uuid(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** @@ -2127,8 +2127,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh = zChildChunkDetailResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSummaryStatusPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2138,8 +2138,8 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSummaryStatusResponse = zOpaqueObjectResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdWebsiteSyncPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2148,7 +2148,7 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdWebsiteSyncPath = z.obj export const zGetDatasetsByDatasetIdDocumentsByDocumentIdWebsiteSyncResponse = zSimpleResultResponse export const zGetDatasetsByDatasetIdErrorDocsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2159,7 +2159,7 @@ export const zGetDatasetsByDatasetIdErrorDocsResponse = zErrorDocsResponse export const zPostDatasetsByDatasetIdExternalHitTestingBody = zExternalHitTestingPayload export const zPostDatasetsByDatasetIdExternalHitTestingPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2170,7 +2170,7 @@ export const zPostDatasetsByDatasetIdExternalHitTestingResponse = zExternalRetri export const zPostDatasetsByDatasetIdHitTestingBody = zHitTestingPayload export const zPostDatasetsByDatasetIdHitTestingPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2179,7 +2179,7 @@ export const zPostDatasetsByDatasetIdHitTestingPath = z.object({ export const zPostDatasetsByDatasetIdHitTestingResponse = zHitTestingResponse export const zGetDatasetsByDatasetIdIndexingStatusPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2188,7 +2188,7 @@ export const zGetDatasetsByDatasetIdIndexingStatusPath = z.object({ export const zGetDatasetsByDatasetIdIndexingStatusResponse = zDocumentStatusListResponse export const zGetDatasetsByDatasetIdMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2199,7 +2199,7 @@ export const zGetDatasetsByDatasetIdMetadataResponse = zDatasetMetadataListRespo export const zPostDatasetsByDatasetIdMetadataBody = zMetadataArgs export const zPostDatasetsByDatasetIdMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2209,7 +2209,7 @@ export const zPostDatasetsByDatasetIdMetadataResponse = zDatasetMetadataResponse export const zPostDatasetsByDatasetIdMetadataBuiltInByActionPath = z.object({ action: z.string(), - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2218,8 +2218,8 @@ export const zPostDatasetsByDatasetIdMetadataBuiltInByActionPath = z.object({ export const zPostDatasetsByDatasetIdMetadataBuiltInByActionResponse = z.void() export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ - dataset_id: z.string(), - metadata_id: z.string(), + dataset_id: z.uuid(), + metadata_id: z.uuid(), }) /** @@ -2230,8 +2230,8 @@ export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.void() export const zPatchDatasetsByDatasetIdMetadataByMetadataIdBody = zMetadataUpdatePayload export const zPatchDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ - dataset_id: z.string(), - metadata_id: z.string(), + dataset_id: z.uuid(), + metadata_id: z.uuid(), }) /** @@ -2240,7 +2240,7 @@ export const zPatchDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ export const zPatchDatasetsByDatasetIdMetadataByMetadataIdResponse = zDatasetMetadataResponse export const zGetDatasetsByDatasetIdNotionSyncPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2249,7 +2249,7 @@ export const zGetDatasetsByDatasetIdNotionSyncPath = z.object({ export const zGetDatasetsByDatasetIdNotionSyncResponse = zSimpleResultResponse export const zGetDatasetsByDatasetIdPermissionPartUsersPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2258,7 +2258,7 @@ export const zGetDatasetsByDatasetIdPermissionPartUsersPath = z.object({ export const zGetDatasetsByDatasetIdPermissionPartUsersResponse = zPartialMemberListResponse export const zGetDatasetsByDatasetIdQueriesPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2267,7 +2267,7 @@ export const zGetDatasetsByDatasetIdQueriesPath = z.object({ export const zGetDatasetsByDatasetIdQueriesResponse = zDatasetQueryListResponse export const zGetDatasetsByDatasetIdRelatedAppsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2278,7 +2278,7 @@ export const zGetDatasetsByDatasetIdRelatedAppsResponse = zRelatedAppListRespons export const zPostDatasetsByDatasetIdRetryBody = zDocumentRetryPayload export const zPostDatasetsByDatasetIdRetryPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2287,7 +2287,7 @@ export const zPostDatasetsByDatasetIdRetryPath = z.object({ export const zPostDatasetsByDatasetIdRetryResponse = z.void() export const zGetDatasetsByDatasetIdUseCheckPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2296,7 +2296,7 @@ export const zGetDatasetsByDatasetIdUseCheckPath = z.object({ export const zGetDatasetsByDatasetIdUseCheckResponse = zUsageCheckResponse export const zGetDatasetsByResourceIdApiKeysPath = z.object({ - resource_id: z.string(), + resource_id: z.uuid(), }) /** @@ -2305,7 +2305,7 @@ export const zGetDatasetsByResourceIdApiKeysPath = z.object({ export const zGetDatasetsByResourceIdApiKeysResponse = zApiKeyList export const zPostDatasetsByResourceIdApiKeysPath = z.object({ - resource_id: z.string(), + resource_id: z.uuid(), }) /** @@ -2314,8 +2314,8 @@ export const zPostDatasetsByResourceIdApiKeysPath = z.object({ export const zPostDatasetsByResourceIdApiKeysResponse = zApiKeyItem export const zDeleteDatasetsByResourceIdApiKeysByApiKeyIdPath = z.object({ - api_key_id: z.string(), - resource_id: z.string(), + api_key_id: z.uuid(), + resource_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index 9346796a86b..fa29f0ac13e 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -86,7 +86,7 @@ export const zGetExploreAppsLearnDifyQuery = z.object({ export const zGetExploreAppsLearnDifyResponse = zLearnDifyAppListResponse export const zGetExploreAppsByAppIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/files/zod.gen.ts b/packages/contracts/generated/api/console/files/zod.gen.ts index 389c79558e3..4454afcdc86 100644 --- a/packages/contracts/generated/api/console/files/zod.gen.ts +++ b/packages/contracts/generated/api/console/files/zod.gen.ts @@ -69,7 +69,7 @@ export const zGetFilesUploadResponse = zUploadConfig export const zPostFilesUploadResponse = zFileResponse export const zGetFilesByFileIdPreviewPath = z.object({ - file_id: z.string(), + file_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/installed-apps/types.gen.ts b/packages/contracts/generated/api/console/installed-apps/types.gen.ts index 6111cf7e104..9070a3c3d9c 100644 --- a/packages/contracts/generated/api/console/installed-apps/types.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/types.gen.ts @@ -68,7 +68,16 @@ export type ConversationInfiniteScrollPagination = { limit: number } -export type ConversationRenamePayload = { +export type ConversationRenamePayload = ( + | { + auto_generate: true + name?: string | null + } + | { + auto_generate?: false + name: string + } +) & { auto_generate?: boolean name?: string | null } diff --git a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts index 055b53936bf..bdffbfb6d4d 100644 --- a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts @@ -70,13 +70,22 @@ export const zCompletionMessageExplorePayload = z.object({ retriever_from: z.string().optional().default('explore_app'), }) -/** - * ConversationRenamePayload - */ -export const zConversationRenamePayload = z.object({ - auto_generate: z.boolean().optional().default(false), - name: z.string().nullish(), -}) +export const zConversationRenamePayload = z.intersection( + z.union([ + z.object({ + auto_generate: z.literal(true), + name: z.string().nullish(), + }), + z.object({ + auto_generate: z.literal(false).optional().default(false), + name: z.string().regex(/.*\S.*/), + }), + ]), + z.object({ + auto_generate: z.boolean().optional().default(false), + name: z.string().nullish(), + }), +) /** * ResultResponse @@ -530,7 +539,7 @@ export const zPostInstalledAppsBody = zInstalledAppCreatePayload export const zPostInstalledAppsResponse = zSimpleMessageResponse export const zDeleteInstalledAppsByInstalledAppIdPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -541,7 +550,7 @@ export const zDeleteInstalledAppsByInstalledAppIdResponse = z.void() export const zPatchInstalledAppsByInstalledAppIdBody = zInstalledAppUpdatePayload export const zPatchInstalledAppsByInstalledAppIdPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -550,7 +559,7 @@ export const zPatchInstalledAppsByInstalledAppIdPath = z.object({ export const zPatchInstalledAppsByInstalledAppIdResponse = zSimpleResultMessageResponse export const zPostInstalledAppsByInstalledAppIdAudioToTextPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -561,7 +570,7 @@ export const zPostInstalledAppsByInstalledAppIdAudioToTextResponse = zAudioTrans export const zPostInstalledAppsByInstalledAppIdChatMessagesBody = zChatMessagePayload export const zPostInstalledAppsByInstalledAppIdChatMessagesPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -570,7 +579,7 @@ export const zPostInstalledAppsByInstalledAppIdChatMessagesPath = z.object({ export const zPostInstalledAppsByInstalledAppIdChatMessagesResponse = zGeneratedAppResponse export const zPostInstalledAppsByInstalledAppIdChatMessagesByTaskIdStopPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), task_id: z.string(), }) @@ -584,7 +593,7 @@ export const zPostInstalledAppsByInstalledAppIdCompletionMessagesBody = zCompletionMessageExplorePayload export const zPostInstalledAppsByInstalledAppIdCompletionMessagesPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -593,7 +602,7 @@ export const zPostInstalledAppsByInstalledAppIdCompletionMessagesPath = z.object export const zPostInstalledAppsByInstalledAppIdCompletionMessagesResponse = zGeneratedAppResponse export const zPostInstalledAppsByInstalledAppIdCompletionMessagesByTaskIdStopPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), task_id: z.string(), }) @@ -604,7 +613,7 @@ export const zPostInstalledAppsByInstalledAppIdCompletionMessagesByTaskIdStopRes = zSimpleResultResponse export const zGetInstalledAppsByInstalledAppIdConversationsPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) export const zGetInstalledAppsByInstalledAppIdConversationsQuery = z.object({ @@ -620,8 +629,8 @@ export const zGetInstalledAppsByInstalledAppIdConversationsResponse = zConversationInfiniteScrollPagination export const zDeleteInstalledAppsByInstalledAppIdConversationsByCIdPath = z.object({ - c_id: z.string(), - installed_app_id: z.string(), + c_id: z.uuid(), + installed_app_id: z.uuid(), }) /** @@ -633,8 +642,8 @@ export const zPostInstalledAppsByInstalledAppIdConversationsByCIdNameBody = zConversationRenamePayload export const zPostInstalledAppsByInstalledAppIdConversationsByCIdNamePath = z.object({ - c_id: z.string(), - installed_app_id: z.string(), + c_id: z.uuid(), + installed_app_id: z.uuid(), }) /** @@ -643,8 +652,8 @@ export const zPostInstalledAppsByInstalledAppIdConversationsByCIdNamePath = z.ob export const zPostInstalledAppsByInstalledAppIdConversationsByCIdNameResponse = zSimpleConversation export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdPinPath = z.object({ - c_id: z.string(), - installed_app_id: z.string(), + c_id: z.uuid(), + installed_app_id: z.uuid(), }) /** @@ -653,8 +662,8 @@ export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdPinPath = z.ob export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdPinResponse = zResultResponse export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdUnpinPath = z.object({ - c_id: z.string(), - installed_app_id: z.string(), + c_id: z.uuid(), + installed_app_id: z.uuid(), }) /** @@ -663,7 +672,7 @@ export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdUnpinPath = z. export const zPatchInstalledAppsByInstalledAppIdConversationsByCIdUnpinResponse = zResultResponse export const zGetInstalledAppsByInstalledAppIdMessagesPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) export const zGetInstalledAppsByInstalledAppIdMessagesQuery = z.object({ @@ -681,8 +690,8 @@ export const zPostInstalledAppsByInstalledAppIdMessagesByMessageIdFeedbacksBody = zMessageFeedbackPayload export const zPostInstalledAppsByInstalledAppIdMessagesByMessageIdFeedbacksPath = z.object({ - installed_app_id: z.string(), - message_id: z.string(), + installed_app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -692,8 +701,8 @@ export const zPostInstalledAppsByInstalledAppIdMessagesByMessageIdFeedbacksRespo = zResultResponse export const zGetInstalledAppsByInstalledAppIdMessagesByMessageIdMoreLikeThisPath = z.object({ - installed_app_id: z.string(), - message_id: z.string(), + installed_app_id: z.uuid(), + message_id: z.uuid(), }) export const zGetInstalledAppsByInstalledAppIdMessagesByMessageIdMoreLikeThisQuery = z.object({ @@ -707,8 +716,8 @@ export const zGetInstalledAppsByInstalledAppIdMessagesByMessageIdMoreLikeThisRes = zGeneratedAppResponse export const zGetInstalledAppsByInstalledAppIdMessagesByMessageIdSuggestedQuestionsPath = z.object({ - installed_app_id: z.string(), - message_id: z.string(), + installed_app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -718,7 +727,7 @@ export const zGetInstalledAppsByInstalledAppIdMessagesByMessageIdSuggestedQuesti = zSuggestedQuestionsResponse export const zGetInstalledAppsByInstalledAppIdMetaPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -727,7 +736,7 @@ export const zGetInstalledAppsByInstalledAppIdMetaPath = z.object({ export const zGetInstalledAppsByInstalledAppIdMetaResponse = zExploreAppMetaResponse export const zGetInstalledAppsByInstalledAppIdParametersPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -736,7 +745,7 @@ export const zGetInstalledAppsByInstalledAppIdParametersPath = z.object({ export const zGetInstalledAppsByInstalledAppIdParametersResponse = zParameters export const zGetInstalledAppsByInstalledAppIdSavedMessagesPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) export const zGetInstalledAppsByInstalledAppIdSavedMessagesQuery = z.object({ @@ -753,7 +762,7 @@ export const zGetInstalledAppsByInstalledAppIdSavedMessagesResponse export const zPostInstalledAppsByInstalledAppIdSavedMessagesBody = zSavedMessageCreatePayload export const zPostInstalledAppsByInstalledAppIdSavedMessagesPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -762,8 +771,8 @@ export const zPostInstalledAppsByInstalledAppIdSavedMessagesPath = z.object({ export const zPostInstalledAppsByInstalledAppIdSavedMessagesResponse = zResultResponse export const zDeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdPath = z.object({ - installed_app_id: z.string(), - message_id: z.string(), + installed_app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -774,7 +783,7 @@ export const zDeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdRespons export const zPostInstalledAppsByInstalledAppIdTextToAudioBody = zTextToAudioPayload export const zPostInstalledAppsByInstalledAppIdTextToAudioPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -785,7 +794,7 @@ export const zPostInstalledAppsByInstalledAppIdTextToAudioResponse = zAudioBinar export const zPostInstalledAppsByInstalledAppIdWorkflowsRunBody = zWorkflowRunPayload export const zPostInstalledAppsByInstalledAppIdWorkflowsRunPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), }) /** @@ -794,7 +803,7 @@ export const zPostInstalledAppsByInstalledAppIdWorkflowsRunPath = z.object({ export const zPostInstalledAppsByInstalledAppIdWorkflowsRunResponse = zGeneratedAppResponse export const zPostInstalledAppsByInstalledAppIdWorkflowsTasksByTaskIdStopPath = z.object({ - installed_app_id: z.string(), + installed_app_id: z.uuid(), task_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/notion/zod.gen.ts b/packages/contracts/generated/api/console/notion/zod.gen.ts index 633ae90be35..7f228414f19 100644 --- a/packages/contracts/generated/api/console/notion/zod.gen.ts +++ b/packages/contracts/generated/api/console/notion/zod.gen.ts @@ -48,7 +48,7 @@ export const zNotionIntegrateInfoListResponse = z.object({ }) export const zGetNotionPagesByPageIdByPageTypePreviewPath = z.object({ - page_id: z.string(), + page_id: z.uuid(), page_type: z.string(), }) diff --git a/packages/contracts/generated/api/console/oauth/zod.gen.ts b/packages/contracts/generated/api/console/oauth/zod.gen.ts index 569b35ec4d0..f38227c2b26 100644 --- a/packages/contracts/generated/api/console/oauth/zod.gen.ts +++ b/packages/contracts/generated/api/console/oauth/zod.gen.ts @@ -117,7 +117,7 @@ export const zGetOauthDataSourceByProviderPath = z.object({ export const zGetOauthDataSourceByProviderResponse = zOAuthDataSourceResponse export const zGetOauthDataSourceByProviderByBindingIdSyncPath = z.object({ - binding_id: z.string(), + binding_id: z.uuid(), provider: z.string(), }) diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index 31120fab148..71ed29509a5 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -777,7 +777,7 @@ export const zGetRagPipelinesRecommendedPluginsQuery = z.object({ export const zGetRagPipelinesRecommendedPluginsResponse = zRagPipelineOpaqueResponse export const zPostRagPipelinesTransformDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -810,7 +810,7 @@ export const zGetRagPipelinesByPipelineIdExportsQuery = z.object({ export const zGetRagPipelinesByPipelineIdExportsResponse = zSimpleDataResponse export const zGetRagPipelinesByPipelineIdWorkflowRunsPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowRunsQuery = z.object({ @@ -824,7 +824,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowRunsQuery = z.object({ export const zGetRagPipelinesByPipelineIdWorkflowRunsResponse = zWorkflowRunPaginationResponse export const zPostRagPipelinesByPipelineIdWorkflowRunsTasksByTaskIdStopPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), task_id: z.string(), }) @@ -835,8 +835,8 @@ export const zPostRagPipelinesByPipelineIdWorkflowRunsTasksByTaskIdStopResponse = zSimpleResultResponse export const zGetRagPipelinesByPipelineIdWorkflowRunsByRunIdPath = z.object({ - pipeline_id: z.string(), - run_id: z.string(), + pipeline_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -845,8 +845,8 @@ export const zGetRagPipelinesByPipelineIdWorkflowRunsByRunIdPath = z.object({ export const zGetRagPipelinesByPipelineIdWorkflowRunsByRunIdResponse = zWorkflowRunDetailResponse export const zGetRagPipelinesByPipelineIdWorkflowRunsByRunIdNodeExecutionsPath = z.object({ - pipeline_id: z.string(), - run_id: z.string(), + pipeline_id: z.uuid(), + run_id: z.uuid(), }) /** @@ -856,7 +856,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowRunsByRunIdNodeExecutionsRespon = zWorkflowRunNodeExecutionListResponse export const zGetRagPipelinesByPipelineIdWorkflowsPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsQuery = z.object({ @@ -872,7 +872,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsQuery = z.object({ export const zGetRagPipelinesByPipelineIdWorkflowsResponse = zWorkflowPaginationResponse export const zGetRagPipelinesByPipelineIdWorkflowsDefaultWorkflowBlockConfigsPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -884,7 +884,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDefaultWorkflowBlockConfigsRes export const zGetRagPipelinesByPipelineIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypePath = z.object({ block_type: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeQuery @@ -899,7 +899,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDefaultWorkflowBlockConfigsByB = zDefaultBlockConfigResponse export const zGetRagPipelinesByPipelineIdWorkflowsDraftPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -910,7 +910,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftResponse = zWorkflowRespo export const zPostRagPipelinesByPipelineIdWorkflowsDraftBody = zDraftWorkflowSyncPayload export const zPostRagPipelinesByPipelineIdWorkflowsDraftPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -923,7 +923,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdR export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceNodesByNodeIdRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -936,7 +936,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceVariablesInspe = zDatasourceVariablesPayload export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceVariablesInspectPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -946,7 +946,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftDatasourceVariablesInspe = zWorkflowRunNodeExecutionResponse export const zGetRagPipelinesByPipelineIdWorkflowsDraftEnvironmentVariablesPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -960,7 +960,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftIterationNodesByNodeIdRu export const zPostRagPipelinesByPipelineIdWorkflowsDraftIterationNodesByNodeIdRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -973,7 +973,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftLoopNodesByNodeIdRunBody export const zPostRagPipelinesByPipelineIdWorkflowsDraftLoopNodesByNodeIdRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -984,7 +984,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftLoopNodesByNodeIdRunResp export const zGetRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdLastRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -998,7 +998,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdRunBody export const zPostRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1009,7 +1009,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdRunResponse export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1019,7 +1019,7 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdVariables export const zGetRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1029,7 +1029,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftNodesByNodeIdVariablesRes = zWorkflowDraftVariableList export const zGetRagPipelinesByPipelineIdWorkflowsDraftPreProcessingParametersPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsDraftPreProcessingParametersQuery = z.object({ @@ -1043,7 +1043,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftPreProcessingParametersRe = zRagPipelineStepParametersResponse export const zGetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersQuery = z.object({ @@ -1059,7 +1059,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftProcessingParametersRespo export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunBody = zDraftWorkflowRunPayload export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1068,7 +1068,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunPath = z.object({ export const zPostRagPipelinesByPipelineIdWorkflowsDraftRunResponse = zRagPipelineOpaqueResponse export const zGetRagPipelinesByPipelineIdWorkflowsDraftSystemVariablesPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1078,7 +1078,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftSystemVariablesResponse = zWorkflowDraftVariableList export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1087,7 +1087,7 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesPath = z.obje export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesResponse = z.void() export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesQuery = z.object({ @@ -1102,8 +1102,8 @@ export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesResponse = zWorkflowDraftVariableListWithoutValue export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - pipeline_id: z.string(), - variable_id: z.string(), + pipeline_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -1112,8 +1112,8 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdP export const zDeleteRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResponse = z.void() export const zGetRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - pipeline_id: z.string(), - variable_id: z.string(), + pipeline_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -1126,8 +1126,8 @@ export const zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdBo = zWorkflowDraftVariablePatchPayload export const zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - pipeline_id: z.string(), - variable_id: z.string(), + pipeline_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -1137,8 +1137,8 @@ export const zPatchRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdRe = zWorkflowDraftVariable export const zPutRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResetPath = z.object({ - pipeline_id: z.string(), - variable_id: z.string(), + pipeline_id: z.uuid(), + variable_id: z.uuid(), }) export const zPutRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdResetResponse = z.union( @@ -1146,7 +1146,7 @@ export const zPutRagPipelinesByPipelineIdWorkflowsDraftVariablesByVariableIdRese ) export const zGetRagPipelinesByPipelineIdWorkflowsPublishPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1155,7 +1155,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsPublishPath = z.object({ export const zGetRagPipelinesByPipelineIdWorkflowsPublishResponse = zWorkflowResponse export const zPostRagPipelinesByPipelineIdWorkflowsPublishPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1170,7 +1170,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNod export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdPreviewPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1185,7 +1185,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNod export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNodeIdRunPath = z.object({ node_id: z.string(), - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1195,7 +1195,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedDatasourceNodesByNod = zRagPipelineOpaqueResponse export const zGetRagPipelinesByPipelineIdWorkflowsPublishedPreProcessingParametersPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsPublishedPreProcessingParametersQuery = z.object({ @@ -1209,7 +1209,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsPublishedPreProcessingParamete = zRagPipelineStepParametersResponse export const zGetRagPipelinesByPipelineIdWorkflowsPublishedProcessingParametersPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) export const zGetRagPipelinesByPipelineIdWorkflowsPublishedProcessingParametersQuery = z.object({ @@ -1225,7 +1225,7 @@ export const zGetRagPipelinesByPipelineIdWorkflowsPublishedProcessingParametersR export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunBody = zPublishedWorkflowRunPayload export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), }) /** @@ -1234,7 +1234,7 @@ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunPath = z.object({ export const zPostRagPipelinesByPipelineIdWorkflowsPublishedRunResponse = zRagPipelineOpaqueResponse export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), workflow_id: z.string(), }) @@ -1246,7 +1246,7 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = z.vo export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdBody = zWorkflowUpdatePayload export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), workflow_id: z.string(), }) @@ -1256,7 +1256,7 @@ export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object( export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = zWorkflowResponse export const zPostRagPipelinesByPipelineIdWorkflowsByWorkflowIdRestorePath = z.object({ - pipeline_id: z.string(), + pipeline_id: z.uuid(), workflow_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 8f1756ba499..1c861084434 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -387,7 +387,7 @@ export const zWorkflowDraftVariableListWithoutValue = z.object({ }) export const zGetSnippetsBySnippetIdWorkflowRunsPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) export const zGetSnippetsBySnippetIdWorkflowRunsQuery = z.object({ @@ -401,7 +401,7 @@ export const zGetSnippetsBySnippetIdWorkflowRunsQuery = z.object({ export const zGetSnippetsBySnippetIdWorkflowRunsResponse = zWorkflowRunPaginationResponse export const zPostSnippetsBySnippetIdWorkflowRunsTasksByTaskIdStopPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), task_id: z.string(), }) @@ -411,8 +411,8 @@ export const zPostSnippetsBySnippetIdWorkflowRunsTasksByTaskIdStopPath = z.objec export const zPostSnippetsBySnippetIdWorkflowRunsTasksByTaskIdStopResponse = zSimpleResultResponse export const zGetSnippetsBySnippetIdWorkflowRunsByRunIdPath = z.object({ - run_id: z.string(), - snippet_id: z.string(), + run_id: z.uuid(), + snippet_id: z.uuid(), }) /** @@ -421,8 +421,8 @@ export const zGetSnippetsBySnippetIdWorkflowRunsByRunIdPath = z.object({ export const zGetSnippetsBySnippetIdWorkflowRunsByRunIdResponse = zWorkflowRunDetailResponse export const zGetSnippetsBySnippetIdWorkflowRunsByRunIdNodeExecutionsPath = z.object({ - run_id: z.string(), - snippet_id: z.string(), + run_id: z.uuid(), + snippet_id: z.uuid(), }) /** @@ -432,7 +432,7 @@ export const zGetSnippetsBySnippetIdWorkflowRunsByRunIdNodeExecutionsResponse = zWorkflowRunNodeExecutionListResponse export const zGetSnippetsBySnippetIdWorkflowsPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) export const zGetSnippetsBySnippetIdWorkflowsQuery = z.object({ @@ -446,7 +446,7 @@ export const zGetSnippetsBySnippetIdWorkflowsQuery = z.object({ export const zGetSnippetsBySnippetIdWorkflowsResponse = zWorkflowPaginationResponse export const zGetSnippetsBySnippetIdWorkflowsDefaultWorkflowBlockConfigsPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -456,7 +456,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDefaultWorkflowBlockConfigsResponse = zDefaultBlockConfigsResponse export const zGetSnippetsBySnippetIdWorkflowsDraftPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -467,7 +467,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftResponse = zSnippetWorkflowRes export const zPostSnippetsBySnippetIdWorkflowsDraftBody = zSnippetDraftSyncPayload export const zPostSnippetsBySnippetIdWorkflowsDraftPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -476,7 +476,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftPath = z.object({ export const zPostSnippetsBySnippetIdWorkflowsDraftResponse = zWorkflowRestoreResponse export const zGetSnippetsBySnippetIdWorkflowsDraftConfigPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -485,7 +485,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftConfigPath = z.object({ export const zGetSnippetsBySnippetIdWorkflowsDraftConfigResponse = zSnippetDraftConfigResponse export const zGetSnippetsBySnippetIdWorkflowsDraftConversationVariablesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -495,7 +495,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftConversationVariablesResponse = zWorkflowDraftVariableList export const zGetSnippetsBySnippetIdWorkflowsDraftEnvironmentVariablesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -509,7 +509,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftIterationNodesByNodeIdRunBody export const zPostSnippetsBySnippetIdWorkflowsDraftIterationNodesByNodeIdRunPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -523,7 +523,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftLoopNodesByNodeIdRunBody export const zPostSnippetsBySnippetIdWorkflowsDraftLoopNodesByNodeIdRunPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -534,7 +534,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftLoopNodesByNodeIdRunResponse export const zGetSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdLastRunPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -548,7 +548,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdRunBody export const zPostSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdRunPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -559,7 +559,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdRunResponse export const zDeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -569,7 +569,7 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesRespo export const zGetSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ node_id: z.string(), - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -581,7 +581,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesResponse export const zPostSnippetsBySnippetIdWorkflowsDraftRunBody = zSnippetDraftRunPayload export const zPostSnippetsBySnippetIdWorkflowsDraftRunPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -590,7 +590,7 @@ export const zPostSnippetsBySnippetIdWorkflowsDraftRunPath = z.object({ export const zPostSnippetsBySnippetIdWorkflowsDraftRunResponse = zGeneratedAppResponse export const zGetSnippetsBySnippetIdWorkflowsDraftSystemVariablesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -600,7 +600,7 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftSystemVariablesResponse = zWorkflowDraftVariableList export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -609,7 +609,7 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesPath = z.object({ export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesResponse = z.void() export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesQuery = z.object({ @@ -624,8 +624,8 @@ export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesResponse = zWorkflowDraftVariableListWithoutValue export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - snippet_id: z.string(), - variable_id: z.string(), + snippet_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -634,8 +634,8 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResponse = z.void() export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - snippet_id: z.string(), - variable_id: z.string(), + snippet_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -648,8 +648,8 @@ export const zPatchSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdBody = zWorkflowDraftVariableUpdatePayload export const zPatchSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = z.object({ - snippet_id: z.string(), - variable_id: z.string(), + snippet_id: z.uuid(), + variable_id: z.uuid(), }) /** @@ -659,8 +659,8 @@ export const zPatchSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdRespons = zWorkflowDraftVariable export const zPutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetPath = z.object({ - snippet_id: z.string(), - variable_id: z.string(), + snippet_id: z.uuid(), + variable_id: z.uuid(), }) export const zPutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetResponse = z.union([ @@ -669,7 +669,7 @@ export const zPutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetResp ]) export const zGetSnippetsBySnippetIdWorkflowsPublishPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -680,7 +680,7 @@ export const zGetSnippetsBySnippetIdWorkflowsPublishResponse = zSnippetWorkflowR export const zPostSnippetsBySnippetIdWorkflowsPublishBody = zPublishWorkflowPayload export const zPostSnippetsBySnippetIdWorkflowsPublishPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -689,7 +689,7 @@ export const zPostSnippetsBySnippetIdWorkflowsPublishPath = z.object({ export const zPostSnippetsBySnippetIdWorkflowsPublishResponse = zWorkflowPublishResponse export const zPostSnippetsBySnippetIdWorkflowsByWorkflowIdRestorePath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), workflow_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/tags/zod.gen.ts b/packages/contracts/generated/api/console/tags/zod.gen.ts index 0e7d7bd1c60..20f28eaa059 100644 --- a/packages/contracts/generated/api/console/tags/zod.gen.ts +++ b/packages/contracts/generated/api/console/tags/zod.gen.ts @@ -52,7 +52,7 @@ export const zPostTagsBody = zTagBasePayload export const zPostTagsResponse = zTagResponse export const zDeleteTagsByTagIdPath = z.object({ - tag_id: z.string(), + tag_id: z.uuid(), }) /** @@ -63,7 +63,7 @@ export const zDeleteTagsByTagIdResponse = z.void() export const zPatchTagsByTagIdBody = zTagUpdateRequestPayload export const zPatchTagsByTagIdPath = z.object({ - tag_id: z.string(), + tag_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/console/trial-apps/zod.gen.ts b/packages/contracts/generated/api/console/trial-apps/zod.gen.ts index 8f284cda862..7e6b5fbb6d4 100644 --- a/packages/contracts/generated/api/console/trial-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/trial-apps/zod.gen.ts @@ -435,7 +435,7 @@ export const zSiteWritable = z.object({ }) export const zGetTrialAppsByAppIdPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -444,7 +444,7 @@ export const zGetTrialAppsByAppIdPath = z.object({ export const zGetTrialAppsByAppIdResponse = zTrialAppDetailWithSite export const zPostTrialAppsByAppIdAudioToTextPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -455,7 +455,7 @@ export const zPostTrialAppsByAppIdAudioToTextResponse = zAudioTranscriptResponse export const zPostTrialAppsByAppIdChatMessagesBody = zChatRequest export const zPostTrialAppsByAppIdChatMessagesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -466,7 +466,7 @@ export const zPostTrialAppsByAppIdChatMessagesResponse = zGeneratedAppResponse export const zPostTrialAppsByAppIdCompletionMessagesBody = zCompletionRequest export const zPostTrialAppsByAppIdCompletionMessagesPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -475,7 +475,7 @@ export const zPostTrialAppsByAppIdCompletionMessagesPath = z.object({ export const zPostTrialAppsByAppIdCompletionMessagesResponse = zGeneratedAppResponse export const zGetTrialAppsByAppIdDatasetsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) export const zGetTrialAppsByAppIdDatasetsQuery = z.object({ @@ -490,8 +490,8 @@ export const zGetTrialAppsByAppIdDatasetsQuery = z.object({ export const zGetTrialAppsByAppIdDatasetsResponse = zTrialDatasetList export const zGetTrialAppsByAppIdMessagesByMessageIdSuggestedQuestionsPath = z.object({ - app_id: z.string(), - message_id: z.string(), + app_id: z.uuid(), + message_id: z.uuid(), }) /** @@ -501,7 +501,7 @@ export const zGetTrialAppsByAppIdMessagesByMessageIdSuggestedQuestionsResponse = zSuggestedQuestionsResponse export const zGetTrialAppsByAppIdParametersPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -510,7 +510,7 @@ export const zGetTrialAppsByAppIdParametersPath = z.object({ export const zGetTrialAppsByAppIdParametersResponse = zParameters export const zGetTrialAppsByAppIdSitePath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -521,7 +521,7 @@ export const zGetTrialAppsByAppIdSiteResponse = zSite export const zPostTrialAppsByAppIdTextToAudioBody = zTextToSpeechRequest export const zPostTrialAppsByAppIdTextToAudioPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -530,7 +530,7 @@ export const zPostTrialAppsByAppIdTextToAudioPath = z.object({ export const zPostTrialAppsByAppIdTextToAudioResponse = zAudioBinaryResponse export const zGetTrialAppsByAppIdWorkflowsPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -541,7 +541,7 @@ export const zGetTrialAppsByAppIdWorkflowsResponse = zTrialWorkflow export const zPostTrialAppsByAppIdWorkflowsRunBody = zWorkflowRunRequest export const zPostTrialAppsByAppIdWorkflowsRunPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), }) /** @@ -550,7 +550,7 @@ export const zPostTrialAppsByAppIdWorkflowsRunPath = z.object({ export const zPostTrialAppsByAppIdWorkflowsRunResponse = zGeneratedAppResponse export const zPostTrialAppsByAppIdWorkflowsTasksByTaskIdStopPath = z.object({ - app_id: z.string(), + app_id: z.uuid(), task_id: z.string(), }) diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 8b6f34b18ff..59ee3242e75 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -32,7 +32,7 @@ export type AgentProviderListResponse = Array<{ }> export type SnippetPagination = { - data?: Array + data?: Array has_more?: boolean limit?: number page?: number @@ -769,7 +769,7 @@ export type WorkspaceCustomConfigResponse = { replace_webapp_logo?: string | null } -export type AnonymousInlineModelEfd591151Ea9 = { +export type AnonymousInlineModel744Ff9Cc03E6 = { author_name?: string created_at?: number created_by?: string @@ -1238,7 +1238,7 @@ export type CustomModelConfiguration = { current_credential_name?: string | null model: string model_type: ModelType - unadded_to_model_list?: boolean | null + unadded_to_model_list?: boolean } export type CredentialFormSchema = { diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index b342c586594..8c430ddec8b 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -776,7 +776,7 @@ export const zSnippet = z.object({ version: z.int().optional(), }) -export const zAnonymousInlineModelEfd591151Ea9 = z.object({ +export const zAnonymousInlineModel744Ff9Cc03E6 = z.object({ author_name: z.string().optional(), created_at: z.coerce .bigint() @@ -810,7 +810,7 @@ export const zAnonymousInlineModelEfd591151Ea9 = z.object({ }) export const zSnippetPagination = z.object({ - data: z.array(zAnonymousInlineModelEfd591151Ea9).optional(), + data: z.array(zAnonymousInlineModel744Ff9Cc03E6).optional(), has_more: z.boolean().optional(), limit: z.int().optional(), page: z.int().optional(), @@ -1531,7 +1531,7 @@ export const zCustomModelConfiguration = z.object({ current_credential_name: z.string().nullish(), model: z.string(), model_type: zModelType, - unadded_to_model_list: z.boolean().nullish().default(false), + unadded_to_model_list: z.boolean().optional().default(false), }) /** @@ -1927,7 +1927,7 @@ export const zPostWorkspacesCurrentCustomizedSnippetsImportsByImportIdConfirmRes = zSnippetImportResponse export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -1936,7 +1936,7 @@ export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.objec export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = z.void() export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -1947,7 +1947,7 @@ export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnipp export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdBody = zUpdateSnippetPayload export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -1956,7 +1956,7 @@ export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object export const zPatchWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = zSnippet export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdCheckDependenciesPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -1966,7 +1966,7 @@ export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdCheckDependencies = zSnippetDependencyCheckResponse export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdExportPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdExportQuery = z.object({ @@ -1979,7 +1979,7 @@ export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdExportQuery = z.o export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdExportResponse = zTextFileResponse export const zPostWorkspacesCurrentCustomizedSnippetsBySnippetIdUseCountIncrementPath = z.object({ - snippet_id: z.string(), + snippet_id: z.uuid(), }) /** @@ -2121,7 +2121,7 @@ export const zPostWorkspacesCurrentMembersSendOwnerTransferConfirmEmailResponse = zSimpleResultDataResponse export const zDeleteWorkspacesCurrentMembersByMemberIdPath = z.object({ - member_id: z.string(), + member_id: z.uuid(), }) /** @@ -2132,7 +2132,7 @@ export const zDeleteWorkspacesCurrentMembersByMemberIdResponse = zMemberActionTe export const zPostWorkspacesCurrentMembersByMemberIdOwnerTransferBody = zOwnerTransferPayload export const zPostWorkspacesCurrentMembersByMemberIdOwnerTransferPath = z.object({ - member_id: z.string(), + member_id: z.uuid(), }) /** @@ -2143,7 +2143,7 @@ export const zPostWorkspacesCurrentMembersByMemberIdOwnerTransferResponse = zSim export const zPutWorkspacesCurrentMembersByMemberIdUpdateRoleBody = zMemberRoleUpdatePayload export const zPutWorkspacesCurrentMembersByMemberIdUpdateRolePath = z.object({ - member_id: z.string(), + member_id: z.uuid(), }) /** diff --git a/packages/contracts/generated/api/service/orpc.gen.ts b/packages/contracts/generated/api/service/orpc.gen.ts index 518b44e06ea..b15cc78d07d 100644 --- a/packages/contracts/generated/api/service/orpc.gen.ts +++ b/packages/contracts/generated/api/service/orpc.gen.ts @@ -6,6 +6,7 @@ import * as z from 'zod' import { zDeleteAppsAnnotationsByAnnotationIdPath, zDeleteAppsAnnotationsByAnnotationIdResponse, + zDeleteConversationsByCIdBody, zDeleteConversationsByCIdPath, zDeleteConversationsByCIdResponse, zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath, @@ -72,6 +73,7 @@ import { zGetFormHumanInputByFormTokenResponse, zGetInfoResponse, zGetMessagesByMessageIdSuggestedPath, + zGetMessagesByMessageIdSuggestedQuery, zGetMessagesByMessageIdSuggestedResponse, zGetMessagesQuery, zGetMessagesResponse, @@ -110,12 +112,15 @@ import { zPostAppsAnnotationReplyByActionResponse, zPostAppsAnnotationsBody, zPostAppsAnnotationsResponse, + zPostAudioToTextBody, zPostAudioToTextResponse, zPostChatMessagesBody, + zPostChatMessagesByTaskIdStopBody, zPostChatMessagesByTaskIdStopPath, zPostChatMessagesByTaskIdStopResponse, zPostChatMessagesResponse, zPostCompletionMessagesBody, + zPostCompletionMessagesByTaskIdStopBody, zPostCompletionMessagesByTaskIdStopPath, zPostCompletionMessagesByTaskIdStopResponse, zPostCompletionMessagesResponse, @@ -179,6 +184,7 @@ import { zPostDatasetsByDatasetIdRetrieveBody, zPostDatasetsByDatasetIdRetrievePath, zPostDatasetsByDatasetIdRetrieveResponse, + zPostDatasetsPipelineFileUploadBody, zPostDatasetsPipelineFileUploadResponse, zPostDatasetsResponse, zPostDatasetsTagsBindingBody, @@ -187,6 +193,7 @@ import { zPostDatasetsTagsResponse, zPostDatasetsTagsUnbindingBody, zPostDatasetsTagsUnbindingResponse, + zPostFilesUploadBody, zPostFilesUploadResponse, zPostFormHumanInputByFormTokenBody, zPostFormHumanInputByFormTokenPath, @@ -201,6 +208,7 @@ import { zPostWorkflowsByWorkflowIdRunResponse, zPostWorkflowsRunBody, zPostWorkflowsRunResponse, + zPostWorkflowsTasksByTaskIdStopBody, zPostWorkflowsTasksByTaskIdStopPath, zPostWorkflowsTasksByTaskIdStopResponse, zPutAppsAnnotationsByAnnotationIdBody, @@ -226,21 +234,20 @@ export const root = { } /** - * Get all feedbacks for the application + * List App Feedbacks * - * Get all feedbacks for the application - * Returns paginated list of all feedback submitted for messages in this app. + * Retrieve a paginated list of all feedback submitted for messages in this application, including both end-user and admin feedback. */ export const get2 = oc .route({ description: - 'Get all feedbacks for the application\nReturns paginated list of all feedback submitted for messages in this app.', + 'Retrieve a paginated list of all feedback submitted for messages in this application, including both end-user and admin feedback.', inputStructure: 'detailed', method: 'GET', operationId: 'getAppFeedbacks', path: '/app/feedbacks', - summary: 'Get all feedbacks for the application', - tags: ['service_api'], + summary: 'List App Feedbacks', + tags: ['Feedback'], }) .input(z.object({ query: zGetAppFeedbacksQuery.optional() })) .output(zGetAppFeedbacksResponse) @@ -254,19 +261,20 @@ export const app = { } /** - * Get the status of an annotation reply action job + * Get Annotation Reply Job Status * - * Get the status of an annotation reply action job + * Retrieves the status of an asynchronous annotation reply configuration job started by [Configure Annotation Reply](/api-reference/annotations/configure-annotation-reply). */ export const get3 = oc .route({ - description: 'Get the status of an annotation reply action job', + description: + 'Retrieves the status of an asynchronous annotation reply configuration job started by [Configure Annotation Reply](/api-reference/annotations/configure-annotation-reply).', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsAnnotationReplyByActionStatusByJobId', path: '/apps/annotation-reply/{action}/status/{job_id}', - summary: 'Get the status of an annotation reply action job', - tags: ['service_api'], + summary: 'Get Annotation Reply Job Status', + tags: ['Annotations'], }) .input(z.object({ params: zGetAppsAnnotationReplyByActionStatusByJobIdPath })) .output(zGetAppsAnnotationReplyByActionStatusByJobIdResponse) @@ -280,19 +288,20 @@ export const status = { } /** - * Enable or disable annotation reply feature + * Configure Annotation Reply * - * Enable or disable annotation reply feature + * Enables or disables the annotation reply feature. Requires embedding model configuration when enabling. Executes asynchronously — use [Get Annotation Reply Job Status](/api-reference/annotations/get-annotation-reply-job-status) to track progress. */ export const post = oc .route({ - description: 'Enable or disable annotation reply feature', + description: + 'Enables or disables the annotation reply feature. Requires embedding model configuration when enabling. Executes asynchronously — use [Get Annotation Reply Job Status](/api-reference/annotations/get-annotation-reply-job-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsAnnotationReplyByAction', path: '/apps/annotation-reply/{action}', - summary: 'Enable or disable annotation reply feature', - tags: ['service_api'], + summary: 'Configure Annotation Reply', + tags: ['Annotations'], }) .input( z.object({ @@ -312,38 +321,38 @@ export const annotationReply = { } /** - * Delete an annotation + * Delete Annotation * - * Delete an annotation + * Deletes an annotation and its associated hit history. */ export const delete_ = oc .route({ - description: 'Delete an annotation', + description: 'Deletes an annotation and its associated hit history.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteAppsAnnotationsByAnnotationId', path: '/apps/annotations/{annotation_id}', successStatus: 204, - summary: 'Delete an annotation', - tags: ['service_api'], + summary: 'Delete Annotation', + tags: ['Annotations'], }) .input(z.object({ params: zDeleteAppsAnnotationsByAnnotationIdPath })) .output(zDeleteAppsAnnotationsByAnnotationIdResponse) /** - * Update an existing annotation + * Update Annotation * - * Update an existing annotation + * Updates the question and answer of an existing annotation. */ export const put = oc .route({ - description: 'Update an existing annotation', + description: 'Updates the question and answer of an existing annotation.', inputStructure: 'detailed', method: 'PUT', operationId: 'putAppsAnnotationsByAnnotationId', path: '/apps/annotations/{annotation_id}', - summary: 'Update an existing annotation', - tags: ['service_api'], + summary: 'Update Annotation', + tags: ['Annotations'], }) .input( z.object({ @@ -359,38 +368,40 @@ export const byAnnotationId = { } /** - * List annotations for the application + * List Annotations * - * List annotations for the application + * Retrieves a paginated list of annotations for the application. Supports keyword search filtering. */ export const get4 = oc .route({ - description: 'List annotations for the application', + description: + 'Retrieves a paginated list of annotations for the application. Supports keyword search filtering.', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsAnnotations', path: '/apps/annotations', - summary: 'List annotations for the application', - tags: ['service_api'], + summary: 'List Annotations', + tags: ['Annotations'], }) .input(z.object({ query: zGetAppsAnnotationsQuery.optional() })) .output(zGetAppsAnnotationsResponse) /** - * Create a new annotation + * Create Annotation * - * Create a new annotation + * Creates a new annotation. Annotations provide predefined question-answer pairs that the app can match and return directly instead of generating a response. */ export const post2 = oc .route({ - description: 'Create a new annotation', + description: + 'Creates a new annotation. Annotations provide predefined question-answer pairs that the app can match and return directly instead of generating a response.', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsAnnotations', path: '/apps/annotations', successStatus: 201, - summary: 'Create a new annotation', - tags: ['service_api'], + summary: 'Create Annotation', + tags: ['Annotations'], }) .input(z.object({ body: zPostAppsAnnotationsBody })) .output(zPostAppsAnnotationsResponse) @@ -407,22 +418,22 @@ export const apps = { } /** - * Convert audio to text using speech-to-text + * Convert Audio to Text * - * Convert audio to text using speech-to-text - * Accepts an audio file upload and returns the transcribed text. + * Convert audio file to text. Supported MIME types: `audio/mp3`, `audio/mpga`, `audio/m4a`, `audio/wav`, and `audio/amr`. File size limit is `30 MB`. */ export const post3 = oc .route({ description: - 'Convert audio to text using speech-to-text\nAccepts an audio file upload and returns the transcribed text.', + 'Convert audio file to text. Supported MIME types: `audio/mp3`, `audio/mpga`, `audio/m4a`, `audio/wav`, and `audio/amr`. File size limit is `30 MB`.', inputStructure: 'detailed', method: 'POST', operationId: 'postAudioToText', path: '/audio-to-text', - summary: 'Convert audio to text using speech-to-text', - tags: ['service_api'], + summary: 'Convert Audio to Text', + tags: ['TTS'], }) + .input(z.object({ body: zPostAudioToTextBody })) .output(zPostAudioToTextResponse) export const audioToText = { @@ -430,21 +441,26 @@ export const audioToText = { } /** - * Stop a running chat message generation + * Stop Chat Message Generation * - * Stop a running chat message generation + * Stops a chat message generation task. Only supported in `streaming` mode. */ export const post4 = oc .route({ - description: 'Stop a running chat message generation', + description: 'Stops a chat message generation task. Only supported in `streaming` mode.', inputStructure: 'detailed', method: 'POST', operationId: 'postChatMessagesByTaskIdStop', path: '/chat-messages/{task_id}/stop', - summary: 'Stop a running chat message generation', - tags: ['service_api'], + summary: 'Stop Chat Message Generation', + tags: ['Chatflows', 'Chats'], }) - .input(z.object({ params: zPostChatMessagesByTaskIdStopPath })) + .input( + z.object({ + body: zPostChatMessagesByTaskIdStopBody, + params: zPostChatMessagesByTaskIdStopPath, + }), + ) .output(zPostChatMessagesByTaskIdStopResponse) export const stop = { @@ -456,22 +472,19 @@ export const byTaskId = { } /** - * Send a message in a chat conversation + * Send Chat Message * - * Send a message in a chat conversation - * This endpoint handles chat messages for chat, agent chat, and advanced chat applications. - * Supports conversation management and both blocking and streaming response modes. + * Send a request to the chat application. */ export const post5 = oc .route({ - description: - 'Send a message in a chat conversation\nThis endpoint handles chat messages for chat, agent chat, and advanced chat applications.\nSupports conversation management and both blocking and streaming response modes.', + description: 'Send a request to the chat application.', inputStructure: 'detailed', method: 'POST', operationId: 'postChatMessages', path: '/chat-messages', - summary: 'Send a message in a chat conversation', - tags: ['service_api'], + summary: 'Send Chat Message', + tags: ['Chatflows', 'Chats'], }) .input(z.object({ body: zPostChatMessagesBody })) .output(zPostChatMessagesResponse) @@ -482,21 +495,26 @@ export const chatMessages = { } /** - * Stop a running completion task + * Stop Completion Message Generation * - * Stop a running completion task + * Stops a completion message generation task. Only supported in `streaming` mode. */ export const post6 = oc .route({ - description: 'Stop a running completion task', + description: 'Stops a completion message generation task. Only supported in `streaming` mode.', inputStructure: 'detailed', method: 'POST', operationId: 'postCompletionMessagesByTaskIdStop', path: '/completion-messages/{task_id}/stop', - summary: 'Stop a running completion task', - tags: ['service_api'], + summary: 'Stop Completion Message Generation', + tags: ['Completions'], }) - .input(z.object({ params: zPostCompletionMessagesByTaskIdStopPath })) + .input( + z.object({ + body: zPostCompletionMessagesByTaskIdStopBody, + params: zPostCompletionMessagesByTaskIdStopPath, + }), + ) .output(zPostCompletionMessagesByTaskIdStopResponse) export const stop2 = { @@ -508,22 +526,19 @@ export const byTaskId2 = { } /** - * Create a completion for the given prompt + * Send Completion Message * - * Create a completion for the given prompt - * This endpoint generates a completion based on the provided inputs and query. - * Supports both blocking and streaming response modes. + * Send a request to the text generation application. */ export const post7 = oc .route({ - description: - 'Create a completion for the given prompt\nThis endpoint generates a completion based on the provided inputs and query.\nSupports both blocking and streaming response modes.', + description: 'Send a request to the text generation application.', inputStructure: 'detailed', method: 'POST', operationId: 'postCompletionMessages', path: '/completion-messages', - summary: 'Create a completion for the given prompt', - tags: ['service_api'], + summary: 'Send Completion Message', + tags: ['Completions'], }) .input(z.object({ body: zPostCompletionMessagesBody })) .output(zPostCompletionMessagesResponse) @@ -534,19 +549,20 @@ export const completionMessages = { } /** - * Rename a conversation or auto-generate a name + * Rename Conversation * - * Rename a conversation or auto-generate a name + * Rename a conversation or auto-generate a name. The conversation name is used for display on clients that support multiple conversations. */ export const post8 = oc .route({ - description: 'Rename a conversation or auto-generate a name', + description: + 'Rename a conversation or auto-generate a name. The conversation name is used for display on clients that support multiple conversations.', inputStructure: 'detailed', method: 'POST', operationId: 'postConversationsByCIdName', path: '/conversations/{c_id}/name', - summary: 'Rename a conversation or auto-generate a name', - tags: ['service_api'], + summary: 'Rename Conversation', + tags: ['Conversations'], }) .input( z.object({ body: zPostConversationsByCIdNameBody, params: zPostConversationsByCIdNamePath }), @@ -558,22 +574,20 @@ export const name = { } /** - * Update a conversation variable's value + * Update Conversation Variable * - * Update a conversation variable's value - * Allows updating the value of a specific conversation variable. - * The value must match the variable's expected type. + * Update the value of a specific conversation variable. The value must match the expected type. */ export const put2 = oc .route({ description: - 'Update a conversation variable\'s value\nAllows updating the value of a specific conversation variable.\nThe value must match the variable\'s expected type.', + 'Update the value of a specific conversation variable. The value must match the expected type.', inputStructure: 'detailed', method: 'PUT', operationId: 'putConversationsByCIdVariablesByVariableId', path: '/conversations/{c_id}/variables/{variable_id}', - summary: 'Update a conversation variable\'s value', - tags: ['service_api'], + summary: 'Update Conversation Variable', + tags: ['Conversations'], }) .input( z.object({ @@ -588,21 +602,19 @@ export const byVariableId = { } /** - * List all variables for a conversation + * List Conversation Variables * - * List all variables for a conversation - * Conversational variables are only available for chat applications. + * Retrieve variables from a specific conversation. */ export const get5 = oc .route({ - description: - 'List all variables for a conversation\nConversational variables are only available for chat applications.', + description: 'Retrieve variables from a specific conversation.', inputStructure: 'detailed', method: 'GET', operationId: 'getConversationsByCIdVariables', path: '/conversations/{c_id}/variables', - summary: 'List all variables for a conversation', - tags: ['service_api'], + summary: 'List Conversation Variables', + tags: ['Conversations'], }) .input( z.object({ @@ -618,22 +630,22 @@ export const variables = { } /** - * Delete a specific conversation + * Delete Conversation * - * Delete a specific conversation + * Delete a conversation. */ export const delete2 = oc .route({ - description: 'Delete a specific conversation', + description: 'Delete a conversation.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteConversationsByCId', path: '/conversations/{c_id}', successStatus: 204, - summary: 'Delete a specific conversation', - tags: ['service_api'], + summary: 'Delete Conversation', + tags: ['Conversations'], }) - .input(z.object({ params: zDeleteConversationsByCIdPath })) + .input(z.object({ body: zDeleteConversationsByCIdBody, params: zDeleteConversationsByCIdPath })) .output(zDeleteConversationsByCIdResponse) export const byCId = { @@ -643,21 +655,20 @@ export const byCId = { } /** - * List all conversations for the current user + * List Conversations * - * List all conversations for the current user - * Supports pagination using last_id and limit parameters. + * Retrieve the conversation list for the current user, ordered by most recently active. */ export const get6 = oc .route({ description: - 'List all conversations for the current user\nSupports pagination using last_id and limit parameters.', + 'Retrieve the conversation list for the current user, ordered by most recently active.', inputStructure: 'detailed', method: 'GET', operationId: 'getConversations', path: '/conversations', - summary: 'List all conversations for the current user', - tags: ['service_api'], + summary: 'List Conversations', + tags: ['Conversations'], }) .input(z.object({ query: zGetConversationsQuery.optional() })) .output(zGetConversationsResponse) @@ -668,23 +679,23 @@ export const conversations = { } /** - * Upload a file for use in conversations + * Upload Pipeline File * - * Upload a file to a knowledgebase pipeline - * Accepts a single file upload via multipart/form-data. + * Upload a file for use in a knowledge pipeline. Accepts a single file via `multipart/form-data`. */ export const post9 = oc .route({ description: - 'Upload a file to a knowledgebase pipeline\nAccepts a single file upload via multipart/form-data.', + 'Upload a file for use in a knowledge pipeline. Accepts a single file via `multipart/form-data`.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsPipelineFileUpload', path: '/datasets/pipeline/file-upload', successStatus: 201, - summary: 'Upload a file for use in conversations', - tags: ['service_api'], + summary: 'Upload Pipeline File', + tags: ['Knowledge Pipeline'], }) + .input(z.object({ body: zPostDatasetsPipelineFileUploadBody })) .output(zPostDatasetsPipelineFileUploadResponse) export const fileUpload = { @@ -696,17 +707,21 @@ export const pipeline = { } /** - * Bind tags to a dataset + * Create Tag Binding + * + * Bind one or more tags to a knowledge base. A knowledge base can have multiple tags. */ export const post10 = oc .route({ - description: 'Bind tags to a dataset', + description: + 'Bind one or more tags to a knowledge base. A knowledge base can have multiple tags.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsTagsBinding', path: '/datasets/tags/binding', successStatus: 204, - tags: ['service_api'], + summary: 'Create Tag Binding', + tags: ['Tags'], }) .input(z.object({ body: zPostDatasetsTagsBindingBody })) .output(zPostDatasetsTagsBindingResponse) @@ -716,17 +731,20 @@ export const binding = { } /** - * Unbind tags from a dataset + * Delete Tag Binding + * + * Remove one or more tags from a knowledge base. */ export const post11 = oc .route({ - description: 'Unbind tags from a dataset', + description: 'Remove one or more tags from a knowledge base.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsTagsUnbinding', path: '/datasets/tags/unbinding', successStatus: 204, - tags: ['service_api'], + summary: 'Delete Tag Binding', + tags: ['Tags'], }) .input(z.object({ body: zPostDatasetsTagsUnbindingBody })) .output(zPostDatasetsTagsUnbindingResponse) @@ -736,70 +754,74 @@ export const unbinding = { } /** - * Delete a knowledge type tag + * Delete Knowledge Tag * - * Delete a knowledge type tag + * Permanently delete a knowledge base tag. Does not delete the knowledge bases that were tagged. */ export const delete3 = oc .route({ - description: 'Delete a knowledge type tag', + description: + 'Permanently delete a knowledge base tag. Does not delete the knowledge bases that were tagged.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsTags', path: '/datasets/tags', successStatus: 204, - summary: 'Delete a knowledge type tag', - tags: ['service_api'], + summary: 'Delete Knowledge Tag', + tags: ['Tags'], }) .input(z.object({ body: zDeleteDatasetsTagsBody })) .output(zDeleteDatasetsTagsResponse) /** - * Get all knowledge type tags + * List Knowledge Tags * - * Get all knowledge type tags + * Returns the list of all knowledge base tags in the workspace. */ export const get7 = oc .route({ - description: 'Get all knowledge type tags', + description: 'Returns the list of all knowledge base tags in the workspace.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsTags', path: '/datasets/tags', - summary: 'Get all knowledge type tags', - tags: ['service_api'], + summary: 'List Knowledge Tags', + tags: ['Tags'], }) .output(zGetDatasetsTagsResponse) /** - * Update a knowledge type tag + * Update Knowledge Tag + * + * Rename an existing knowledge base tag. */ export const patch = oc .route({ - description: 'Update a knowledge type tag', + description: 'Rename an existing knowledge base tag.', inputStructure: 'detailed', method: 'PATCH', operationId: 'patchDatasetsTags', path: '/datasets/tags', - tags: ['service_api'], + summary: 'Update Knowledge Tag', + tags: ['Tags'], }) .input(z.object({ body: zPatchDatasetsTagsBody })) .output(zPatchDatasetsTagsResponse) /** - * Add a knowledge type tag + * Create Knowledge Tag * - * Add a knowledge type tag + * Create a new tag for organizing knowledge bases. */ export const post12 = oc .route({ - description: 'Add a knowledge type tag', + description: 'Create a new tag for organizing knowledge bases.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsTags', path: '/datasets/tags', - summary: 'Add a knowledge type tag', - tags: ['service_api'], + summary: 'Create Knowledge Tag', + tags: ['Tags'], }) .input(z.object({ body: zPostDatasetsTagsBody })) .output(zPostDatasetsTagsResponse) @@ -814,16 +836,20 @@ export const tags = { } /** - * Create a new document by uploading a file + * Create Document by File + * + * Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. */ export const post13 = oc .route({ - description: 'Create a new document by uploading a file', + description: + 'Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentCreateByFile', path: '/datasets/{dataset_id}/document/create-by-file', - tags: ['service_api'], + summary: 'Create Document by File', + tags: ['Documents'], }) .input( z.object({ @@ -834,16 +860,23 @@ export const post13 = oc .output(zPostDatasetsByDatasetIdDocumentCreateByFileResponse) /** - * Create a new document by uploading a file + * Create Document by File + * + * Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. + * + * @deprecated */ export const post14 = oc .route({ - description: 'Create a new document by uploading a file', + deprecated: true, + description: + 'Create a document by uploading a file. Supports common document formats (PDF, TXT, DOCX, etc.). Processing is asynchronous — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentCreateByFile', path: '/datasets/{dataset_id}/document/create_by_file', - tags: ['service_api'], + summary: 'Create Document by File', + tags: ['Documents'], }) .input( z.object({ @@ -858,16 +891,20 @@ export const createByFile = { } /** - * Create a new document by providing text content + * Create Document by Text + * + * Create a document from raw text content. The document is processed asynchronously — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. */ export const post15 = oc .route({ - description: 'Create a new document by providing text content', + description: + 'Create a document from raw text content. The document is processed asynchronously — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentCreateByText', path: '/datasets/{dataset_id}/document/create-by-text', - tags: ['service_api'], + summary: 'Create Document by Text', + tags: ['Documents'], }) .input( z.object({ @@ -911,16 +948,20 @@ export const document_ = { } /** - * Download selected uploaded documents as a single ZIP archive + * Download Documents as ZIP + * + * Download multiple uploaded-file documents as a single ZIP archive. Accepts up to `100` document IDs. */ export const post17 = oc .route({ - description: 'Download selected uploaded documents as a single ZIP archive', + description: + 'Download multiple uploaded-file documents as a single ZIP archive. Accepts up to `100` document IDs.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsDownloadZip', path: '/datasets/{dataset_id}/documents/download-zip', - tags: ['service_api'], + summary: 'Download Documents as ZIP', + tags: ['Documents'], }) .input( z.object({ @@ -935,19 +976,20 @@ export const downloadZip = { } /** - * Update metadata for multiple documents + * Update Document Metadata in Batch * - * Update metadata for multiple documents + * Update metadata values for multiple documents at once. Each document in the request receives the specified metadata key-value pairs. */ export const post18 = oc .route({ - description: 'Update metadata for multiple documents', + description: + 'Update metadata values for multiple documents at once. Each document in the request receives the specified metadata key-value pairs.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsMetadata', path: '/datasets/{dataset_id}/documents/metadata', - summary: 'Update metadata for multiple documents', - tags: ['service_api'], + summary: 'Update Document Metadata in Batch', + tags: ['Metadata'], }) .input( z.object({ @@ -962,33 +1004,19 @@ export const metadata = { } /** - * Batch update document status + * Update Document Status in Batch * - * Batch update document status - * Args: - * tenant_id: tenant id - * dataset_id: dataset id - * action: action to perform (Literal["enable", "disable", "archive", "un_archive"]) - * - * Returns: - * dict: A dictionary with a key 'result' and a value 'success' - * int: HTTP status code 200 indicating that the operation was successful. - * - * Raises: - * NotFound: If the dataset with the given ID does not exist. - * Forbidden: If the user does not have permission. - * InvalidActionError: If the action is invalid or cannot be performed. + * Enable, disable, archive, or unarchive multiple documents at once. */ export const patch2 = oc .route({ - description: - 'Batch update document status\nArgs:\n tenant_id: tenant id\n dataset_id: dataset id\n action: action to perform (Literal["enable", "disable", "archive", "un_archive"])\n\nReturns:\n dict: A dictionary with a key \'result\' and a value \'success\'\n int: HTTP status code 200 indicating that the operation was successful.\n\nRaises:\n NotFound: If the dataset with the given ID does not exist.\n Forbidden: If the user does not have permission.\n InvalidActionError: If the action is invalid or cannot be performed.', + description: 'Enable, disable, archive, or unarchive multiple documents at once.', inputStructure: 'detailed', method: 'PATCH', operationId: 'patchDatasetsByDatasetIdDocumentsStatusByAction', path: '/datasets/{dataset_id}/documents/status/{action}', - summary: 'Batch update document status', - tags: ['service_api'], + summary: 'Update Document Status in Batch', + tags: ['Documents'], }) .input( z.object({ @@ -1007,16 +1035,20 @@ export const status2 = { } /** - * Get indexing status for documents in a batch + * Get Document Indexing Status + * + * Check the indexing progress of documents in a batch. Returns the current processing stage and chunk completion counts for each document. Poll this endpoint until `indexing_status` reaches `completed` or `error`. The status progresses through: `waiting` → `parsing` → `cleaning` → `splitting` → `indexing` → `completed`. */ export const get8 = oc .route({ - description: 'Get indexing status for documents in a batch', + description: + 'Check the indexing progress of documents in a batch. Returns the current processing stage and chunk completion counts for each document. Poll this endpoint until `indexing_status` reaches `completed` or `error`. The status progresses through: `waiting` → `parsing` → `cleaning` → `splitting` → `indexing` → `completed`.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByBatchIndexingStatus', path: '/datasets/{dataset_id}/documents/{batch}/indexing-status', - tags: ['service_api'], + summary: 'Get Document Indexing Status', + tags: ['Documents'], }) .input(z.object({ params: zGetDatasetsByDatasetIdDocumentsByBatchIndexingStatusPath })) .output(zGetDatasetsByDatasetIdDocumentsByBatchIndexingStatusResponse) @@ -1030,16 +1062,19 @@ export const byBatch = { } /** - * Get a signed download URL for a document's original uploaded file + * Download Document + * + * Get a signed download URL for a document's original uploaded file. */ export const get9 = oc .route({ - description: 'Get a signed download URL for a document\'s original uploaded file', + description: 'Get a signed download URL for a document\'s original uploaded file.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByDocumentIdDownload', path: '/datasets/{dataset_id}/documents/{document_id}/download', - tags: ['service_api'], + summary: 'Download Document', + tags: ['Documents'], }) .input(z.object({ params: zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadPath })) .output(zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadResponse) @@ -1049,18 +1084,21 @@ export const download = { } /** - * Delete a specific child chunk + * Delete Child Chunk + * + * Permanently delete a child chunk from its parent chunk. */ export const delete4 = oc .route({ - description: 'Delete a specific child chunk', + description: 'Permanently delete a child chunk from its parent chunk.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkId', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}', successStatus: 204, - tags: ['service_api'], + summary: 'Delete Child Chunk', + tags: ['Chunks'], }) .input( z.object({ @@ -1073,17 +1111,20 @@ export const delete4 = oc ) /** - * Update a specific child chunk + * Update Child Chunk + * + * Update the content of an existing child chunk. */ export const patch3 = oc .route({ - description: 'Update a specific child chunk', + description: 'Update the content of an existing child chunk.', inputStructure: 'detailed', method: 'PATCH', operationId: 'patchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkId', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}', - tags: ['service_api'], + summary: 'Update Child Chunk', + tags: ['Chunks'], }) .input( z.object({ @@ -1102,16 +1143,19 @@ export const byChildChunkId = { } /** - * List child chunks for a segment + * List Child Chunks + * + * Returns a paginated list of child chunks under a specific parent chunk. */ export const get10 = oc .route({ - description: 'List child chunks for a segment', + description: 'Returns a paginated list of child chunks under a specific parent chunk.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunks', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks', - tags: ['service_api'], + summary: 'List Child Chunks', + tags: ['Chunks'], }) .input( z.object({ @@ -1123,16 +1167,19 @@ export const get10 = oc .output(zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksResponse) /** - * Create a new child chunk for a segment + * Create Child Chunk + * + * Create a child chunk under the specified segment. */ export const post19 = oc .route({ - description: 'Create a new child chunk for a segment', + description: 'Create a child chunk under the specified segment.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunks', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks', - tags: ['service_api'], + summary: 'Create Child Chunk', + tags: ['Chunks'], }) .input( z.object({ @@ -1149,17 +1196,20 @@ export const childChunks = { } /** - * Delete a specific segment + * Delete Chunk + * + * Permanently delete a chunk from the document. */ export const delete5 = oc .route({ - description: 'Delete a specific segment', + description: 'Permanently delete a chunk from the document.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentId', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}', successStatus: 204, - tags: ['service_api'], + summary: 'Delete Chunk', + tags: ['Chunks'], }) .input( z.object({ params: zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath }), @@ -1167,31 +1217,39 @@ export const delete5 = oc .output(zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse) /** - * Get a specific segment by ID + * Get Chunk + * + * Retrieve detailed information about a specific chunk, including its content, keywords, and indexing status. */ export const get11 = oc .route({ - description: 'Get a specific segment by ID', + description: + 'Retrieve detailed information about a specific chunk, including its content, keywords, and indexing status.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentId', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}', - tags: ['service_api'], + summary: 'Get Chunk', + tags: ['Chunks'], }) .input(z.object({ params: zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath })) .output(zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse) /** - * Update a specific segment + * Update Chunk + * + * Update a chunk's content, keywords, or answer. Re-triggers indexing for the modified chunk. */ export const post20 = oc .route({ - description: 'Update a specific segment', + description: + 'Update a chunk\'s content, keywords, or answer. Re-triggers indexing for the modified chunk.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentId', path: '/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}', - tags: ['service_api'], + summary: 'Update Chunk', + tags: ['Chunks'], }) .input( z.object({ @@ -1209,16 +1267,20 @@ export const bySegmentId = { } /** - * List segments in a document + * List Chunks + * + * Returns a paginated list of chunks within a document. Supports filtering by keyword and status. */ export const get12 = oc .route({ - description: 'List segments in a document', + description: + 'Returns a paginated list of chunks within a document. Supports filtering by keyword and status.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByDocumentIdSegments', path: '/datasets/{dataset_id}/documents/{document_id}/segments', - tags: ['service_api'], + summary: 'List Chunks', + tags: ['Chunks'], }) .input( z.object({ @@ -1229,16 +1291,20 @@ export const get12 = oc .output(zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse) /** - * Create segments in a document + * Create Chunks + * + * Create one or more chunks within a document. Each chunk can include optional keywords and an answer field (for QA-mode documents). */ export const post21 = oc .route({ - description: 'Create segments in a document', + description: + 'Create one or more chunks within a document. Each chunk can include optional keywords and an answer field (for QA-mode documents).', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdSegments', path: '/datasets/{dataset_id}/documents/{document_id}/segments', - tags: ['service_api'], + summary: 'Create Chunks', + tags: ['Chunks'], }) .input( z.object({ @@ -1255,7 +1321,9 @@ export const segments = { } /** - * Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. + * Update Document by File + * + * Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. * * @deprecated */ @@ -1263,12 +1331,13 @@ export const post22 = oc .route({ deprecated: true, description: - 'Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead.', + 'Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile', path: '/datasets/{dataset_id}/documents/{document_id}/update-by-file', - tags: ['service_api'], + summary: 'Update Document by File', + tags: ['Documents'], }) .input( z.object({ @@ -1279,7 +1348,9 @@ export const post22 = oc .output(zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileResponse) /** - * Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. + * Update Document by File + * + * Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. * * @deprecated */ @@ -1287,12 +1358,13 @@ export const post23 = oc .route({ deprecated: true, description: - 'Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead.', + 'Update an existing document by uploading a new file. Re-triggers indexing — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile', path: '/datasets/{dataset_id}/documents/{document_id}/update_by_file', - tags: ['service_api'], + summary: 'Update Document by File', + tags: ['Documents'], }) .input( z.object({ @@ -1307,16 +1379,20 @@ export const updateByFile = { } /** - * Update an existing document by providing text content + * Update Document by Text + * + * Update an existing document's text content, name, or processing configuration. Re-triggers indexing if content changes — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress. */ export const post24 = oc .route({ - description: 'Update an existing document by providing text content', + description: + 'Update an existing document\'s text content, name, or processing configuration. Re-triggers indexing if content changes — use the returned `batch` ID with [Get Document Indexing Status](/api-reference/documents/get-document-indexing-status) to track progress.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText', path: '/datasets/{dataset_id}/documents/{document_id}/update-by-text', - tags: ['service_api'], + summary: 'Update Document by Text', + tags: ['Documents'], }) .input( z.object({ @@ -1355,35 +1431,39 @@ export const updateByText = { } /** - * Delete document + * Delete Document * - * Delete a document + * Permanently delete a document and all its chunks from the knowledge base. */ export const delete6 = oc .route({ - description: 'Delete a document', + description: 'Permanently delete a document and all its chunks from the knowledge base.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsByDatasetIdDocumentsByDocumentId', path: '/datasets/{dataset_id}/documents/{document_id}', successStatus: 204, - summary: 'Delete document', - tags: ['service_api'], + summary: 'Delete Document', + tags: ['Documents'], }) .input(z.object({ params: zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath })) .output(zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse) /** - * Get a specific document by ID + * Get Document + * + * Retrieve detailed information about a specific document, including its indexing status, metadata, and processing statistics. */ export const get13 = oc .route({ - description: 'Get a specific document by ID', + description: + 'Retrieve detailed information about a specific document, including its indexing status, metadata, and processing statistics.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocumentsByDocumentId', path: '/datasets/{dataset_id}/documents/{document_id}', - tags: ['service_api'], + summary: 'Get Document', + tags: ['Documents'], }) .input( z.object({ @@ -1424,16 +1504,20 @@ export const byDocumentId = { } /** - * List all documents in a dataset + * List Documents + * + * Returns a paginated list of documents in the knowledge base. Supports filtering by keyword and indexing status. */ export const get14 = oc .route({ - description: 'List all documents in a dataset', + description: + 'Returns a paginated list of documents in the knowledge base. Supports filtering by keyword and indexing status.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdDocuments', path: '/datasets/{dataset_id}/documents', - tags: ['service_api'], + summary: 'List Documents', + tags: ['Documents'], }) .input( z.object({ @@ -1453,21 +1537,20 @@ export const documents = { } /** - * Perform hit testing on a dataset + * Retrieve Chunks from a Knowledge Base / Test Retrieval * - * Perform hit testing on a dataset - * Tests retrieval performance for the specified dataset. + * Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval. */ export const post26 = oc .route({ description: - 'Perform hit testing on a dataset\nTests retrieval performance for the specified dataset.', + 'Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdHitTesting', path: '/datasets/{dataset_id}/hit-testing', - summary: 'Perform hit testing on a dataset', - tags: ['service_api'], + summary: 'Retrieve Chunks from a Knowledge Base / Test Retrieval', + tags: ['Knowledge Bases'], }) .input( z.object({ @@ -1482,19 +1565,19 @@ export const hitTesting = { } /** - * Enable or disable built-in metadata field + * Update Built-in Metadata Field * - * Enable or disable built-in metadata field + * Enable or disable built-in metadata fields for the knowledge base. */ export const post27 = oc .route({ - description: 'Enable or disable built-in metadata field', + description: 'Enable or disable built-in metadata fields for the knowledge base.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdMetadataBuiltInByAction', path: '/datasets/{dataset_id}/metadata/built-in/{action}', - summary: 'Enable or disable built-in metadata field', - tags: ['service_api'], + summary: 'Update Built-in Metadata Field', + tags: ['Metadata'], }) .input(z.object({ params: zPostDatasetsByDatasetIdMetadataBuiltInByActionPath })) .output(zPostDatasetsByDatasetIdMetadataBuiltInByActionResponse) @@ -1504,19 +1587,20 @@ export const byAction3 = { } /** - * Get all built-in metadata fields + * Get Built-in Metadata Fields * - * Get all built-in metadata fields + * Returns the list of built-in metadata fields provided by the system (e.g., document type, source URL). */ export const get15 = oc .route({ - description: 'Get all built-in metadata fields', + description: + 'Returns the list of built-in metadata fields provided by the system (e.g., document type, source URL).', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdMetadataBuiltIn', path: '/datasets/{dataset_id}/metadata/built-in', - summary: 'Get all built-in metadata fields', - tags: ['service_api'], + summary: 'Get Built-in Metadata Fields', + tags: ['Metadata'], }) .input(z.object({ params: zGetDatasetsByDatasetIdMetadataBuiltInPath })) .output(zGetDatasetsByDatasetIdMetadataBuiltInResponse) @@ -1527,38 +1611,39 @@ export const builtIn = { } /** - * Delete metadata + * Delete Metadata Field * - * Delete metadata + * Permanently delete a custom metadata field. Documents using this field will lose their metadata values for it. */ export const delete7 = oc .route({ - description: 'Delete metadata', + description: + 'Permanently delete a custom metadata field. Documents using this field will lose their metadata values for it.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsByDatasetIdMetadataByMetadataId', path: '/datasets/{dataset_id}/metadata/{metadata_id}', successStatus: 204, - summary: 'Delete metadata', - tags: ['service_api'], + summary: 'Delete Metadata Field', + tags: ['Metadata'], }) .input(z.object({ params: zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath })) .output(zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse) /** - * Update metadata name + * Update Metadata Field * - * Update metadata name + * Rename a custom metadata field. */ export const patch5 = oc .route({ - description: 'Update metadata name', + description: 'Rename a custom metadata field.', inputStructure: 'detailed', method: 'PATCH', operationId: 'patchDatasetsByDatasetIdMetadataByMetadataId', path: '/datasets/{dataset_id}/metadata/{metadata_id}', - summary: 'Update metadata name', - tags: ['service_api'], + summary: 'Update Metadata Field', + tags: ['Metadata'], }) .input( z.object({ @@ -1574,38 +1659,40 @@ export const byMetadataId = { } /** - * Get all metadata for a dataset + * List Metadata Fields * - * Get all metadata for a dataset + * Returns the list of all metadata fields (both custom and built-in) for the knowledge base, along with the count of documents using each field. */ export const get16 = oc .route({ - description: 'Get all metadata for a dataset', + description: + 'Returns the list of all metadata fields (both custom and built-in) for the knowledge base, along with the count of documents using each field.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdMetadata', path: '/datasets/{dataset_id}/metadata', - summary: 'Get all metadata for a dataset', - tags: ['service_api'], + summary: 'List Metadata Fields', + tags: ['Metadata'], }) .input(z.object({ params: zGetDatasetsByDatasetIdMetadataPath })) .output(zGetDatasetsByDatasetIdMetadataResponse) /** - * Create metadata for a dataset + * Create Metadata Field * - * Create metadata for a dataset + * Create a custom metadata field for the knowledge base. Metadata fields can be used to annotate documents with structured information. */ export const post28 = oc .route({ - description: 'Create metadata for a dataset', + description: + 'Create a custom metadata field for the knowledge base. Metadata fields can be used to annotate documents with structured information.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdMetadata', path: '/datasets/{dataset_id}/metadata', successStatus: 201, - summary: 'Create metadata for a dataset', - tags: ['service_api'], + summary: 'Create Metadata Field', + tags: ['Metadata'], }) .input( z.object({ @@ -1623,19 +1710,20 @@ export const metadata2 = { } /** - * Resource for getting datasource plugins + * List Datasource Plugins * - * List all datasource plugins for a rag pipeline + * List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses plus the metadata needed to run it. */ export const get17 = oc .route({ - description: 'List all datasource plugins for a rag pipeline', + description: + 'List the datasource nodes configured in the knowledge pipeline. Each node includes the plugin it uses plus the metadata needed to run it.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdPipelineDatasourcePlugins', path: '/datasets/{dataset_id}/pipeline/datasource-plugins', - summary: 'Resource for getting datasource plugins', - tags: ['service_api'], + summary: 'List Datasource Plugins', + tags: ['Knowledge Pipeline'], }) .input( z.object({ @@ -1650,19 +1738,20 @@ export const datasourcePlugins = { } /** - * Resource for getting datasource plugins + * Run Datasource Node * - * Run a datasource node for a rag pipeline + * Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the node execution results. */ export const post29 = oc .route({ - description: 'Run a datasource node for a rag pipeline', + description: + 'Execute a single datasource node within the knowledge pipeline. Returns a streaming response with the node execution results.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRun', path: '/datasets/{dataset_id}/pipeline/datasource/nodes/{node_id}/run', - summary: 'Resource for getting datasource plugins', - tags: ['service_api'], + summary: 'Run Datasource Node', + tags: ['Knowledge Pipeline'], }) .input( z.object({ @@ -1689,19 +1778,20 @@ export const datasource = { } /** - * Resource for running a rag pipeline + * Run Pipeline * - * Run a datasource node for a rag pipeline + * Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response modes. */ export const post30 = oc .route({ - description: 'Run a datasource node for a rag pipeline', + description: + 'Execute the full knowledge pipeline for a knowledge base. Supports both streaming and blocking response modes.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdPipelineRun', path: '/datasets/{dataset_id}/pipeline/run', - summary: 'Resource for running a rag pipeline', - tags: ['service_api'], + summary: 'Run Pipeline', + tags: ['Knowledge Pipeline'], }) .input( z.object({ @@ -1722,21 +1812,20 @@ export const pipeline2 = { } /** - * Perform hit testing on a dataset + * Retrieve Chunks from a Knowledge Base / Test Retrieval * - * Perform hit testing on a dataset - * Tests retrieval performance for the specified dataset. + * Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval. */ export const post31 = oc .route({ description: - 'Perform hit testing on a dataset\nTests retrieval performance for the specified dataset.', + 'Performs a search query against a knowledge base to retrieve the most relevant chunks. This endpoint can be used for both production retrieval and test retrieval.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasetsByDatasetIdRetrieve', path: '/datasets/{dataset_id}/retrieve', - summary: 'Perform hit testing on a dataset', - tags: ['service_api'], + summary: 'Retrieve Chunks from a Knowledge Base / Test Retrieval', + tags: ['Knowledge Bases'], }) .input( z.object({ @@ -1751,19 +1840,19 @@ export const retrieve = { } /** - * Get all knowledge type tags + * Get Knowledge Base Tags * - * Get tags bound to a specific dataset + * Returns the list of tags bound to a specific knowledge base. */ export const get18 = oc .route({ - description: 'Get tags bound to a specific dataset', + description: 'Returns the list of tags bound to a specific knowledge base.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetIdTags', path: '/datasets/{dataset_id}/tags', - summary: 'Get all knowledge type tags', - tags: ['service_api'], + summary: 'Get Knowledge Base Tags', + tags: ['Tags'], }) .input(z.object({ params: zGetDatasetsByDatasetIdTagsPath })) .output(zGetDatasetsByDatasetIdTagsResponse) @@ -1773,62 +1862,59 @@ export const tags2 = { } /** - * Deletes a dataset given its ID + * Delete Knowledge Base * - * Delete a dataset - * Args: - * _: ignore - * dataset_id (UUID): The ID of the dataset to be deleted. - * - * Returns: - * dict: A dictionary with a key 'result' and a value 'success' - * if the dataset was successfully deleted. Omitted in HTTP response. - * int: HTTP status code 204 indicating that the operation was successful. - * - * Raises: - * NotFound: If the dataset with the given ID does not exist. + * Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application. */ export const delete8 = oc .route({ description: - 'Delete a dataset\nArgs:\n _: ignore\n dataset_id (UUID): The ID of the dataset to be deleted.\n\nReturns:\n dict: A dictionary with a key \'result\' and a value \'success\'\n if the dataset was successfully deleted. Omitted in HTTP response.\n int: HTTP status code 204 indicating that the operation was successful.\n\nRaises:\n NotFound: If the dataset with the given ID does not exist.', + 'Permanently delete a knowledge base and all its documents. The knowledge base must not be in use by any application.', inputStructure: 'detailed', method: 'DELETE', operationId: 'deleteDatasetsByDatasetId', path: '/datasets/{dataset_id}', successStatus: 204, - summary: 'Deletes a dataset given its ID', - tags: ['service_api'], + summary: 'Delete Knowledge Base', + tags: ['Knowledge Bases'], }) .input(z.object({ params: zDeleteDatasetsByDatasetIdPath })) .output(zDeleteDatasetsByDatasetIdResponse) /** - * Get a specific dataset by ID + * Get Knowledge Base + * + * Retrieve detailed information about a specific knowledge base, including its embedding model, retrieval configuration, and document statistics. */ export const get19 = oc .route({ - description: 'Get a specific dataset by ID', + description: + 'Retrieve detailed information about a specific knowledge base, including its embedding model, retrieval configuration, and document statistics.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasetsByDatasetId', path: '/datasets/{dataset_id}', - tags: ['service_api'], + summary: 'Get Knowledge Base', + tags: ['Knowledge Bases'], }) .input(z.object({ params: zGetDatasetsByDatasetIdPath })) .output(zGetDatasetsByDatasetIdResponse) /** - * Update an existing dataset + * Update Knowledge Base + * + * Update the name, description, permissions, or retrieval settings of an existing knowledge base. Only the fields provided in the request body are updated. */ export const patch6 = oc .route({ - description: 'Update an existing dataset', + description: + 'Update the name, description, permissions, or retrieval settings of an existing knowledge base. Only the fields provided in the request body are updated.', inputStructure: 'detailed', method: 'PATCH', operationId: 'patchDatasetsByDatasetId', path: '/datasets/{dataset_id}', - tags: ['service_api'], + summary: 'Update Knowledge Base', + tags: ['Knowledge Bases'], }) .input(z.object({ body: zPatchDatasetsByDatasetIdBody, params: zPatchDatasetsByDatasetIdPath })) .output(zPatchDatasetsByDatasetIdResponse) @@ -1847,37 +1933,39 @@ export const byDatasetId = { } /** - * Resource for getting datasets + * List Knowledge Bases * - * List all datasets + * Returns a paginated list of knowledge bases. Supports filtering by keyword and tags. */ export const get20 = oc .route({ - description: 'List all datasets', + description: + 'Returns a paginated list of knowledge bases. Supports filtering by keyword and tags.', inputStructure: 'detailed', method: 'GET', operationId: 'getDatasets', path: '/datasets', - summary: 'Resource for getting datasets', - tags: ['service_api'], + summary: 'List Knowledge Bases', + tags: ['Knowledge Bases'], }) .input(z.object({ query: zGetDatasetsQuery.optional() })) .output(zGetDatasetsResponse) /** - * Resource for creating datasets + * Create an Empty Knowledge Base * - * Create a new dataset + * Create a new empty knowledge base. After creation, use [Create Document by Text](/api-reference/documents/create-document-by-text) or [Create Document by File](/api-reference/documents/create-document-by-file) to add documents. */ export const post32 = oc .route({ - description: 'Create a new dataset', + description: + 'Create a new empty knowledge base. After creation, use [Create Document by Text](/api-reference/documents/create-document-by-text) or [Create Document by File](/api-reference/documents/create-document-by-file) to add documents.', inputStructure: 'detailed', method: 'POST', operationId: 'postDatasets', path: '/datasets', - summary: 'Resource for creating datasets', - tags: ['service_api'], + summary: 'Create an Empty Knowledge Base', + tags: ['Knowledge Bases'], }) .input(z.object({ body: zPostDatasetsBody })) .output(zPostDatasetsResponse) @@ -1891,22 +1979,20 @@ export const datasets = { } /** - * Get end user detail + * Get End User Info * - * Get an end user by ID - * This endpoint is scoped to the current app token's tenant/app to prevent - * cross-tenant/app access when an end-user ID is known. + * Retrieve an end user by ID. Useful when other APIs return an end-user ID (e.g., `created_by` from [Upload File](/api-reference/files/upload-file)). */ export const get21 = oc .route({ description: - 'Get an end user by ID\nThis endpoint is scoped to the current app token\'s tenant/app to prevent\ncross-tenant/app access when an end-user ID is known.', + 'Retrieve an end user by ID. Useful when other APIs return an end-user ID (e.g., `created_by` from [Upload File](/api-reference/files/upload-file)).', inputStructure: 'detailed', method: 'GET', operationId: 'getEndUsersByEndUserId', path: '/end-users/{end_user_id}', - summary: 'Get end user detail', - tags: ['service_api'], + summary: 'Get End User Info', + tags: ['End Users'], }) .input(z.object({ params: zGetEndUsersByEndUserIdPath })) .output(zGetEndUsersByEndUserIdResponse) @@ -1920,23 +2006,23 @@ export const endUsers = { } /** - * Upload a file for use in conversations + * Upload File * - * Upload a file for use in conversations - * Accepts a single file upload via multipart/form-data. + * Upload a file for use when sending messages, enabling multimodal understanding of images, documents, audio, and video. Uploaded files are for use by the current end-user only. */ export const post33 = oc .route({ description: - 'Upload a file for use in conversations\nAccepts a single file upload via multipart/form-data.', + 'Upload a file for use when sending messages, enabling multimodal understanding of images, documents, audio, and video. Uploaded files are for use by the current end-user only.', inputStructure: 'detailed', method: 'POST', operationId: 'postFilesUpload', path: '/files/upload', successStatus: 201, - summary: 'Upload a file for use in conversations', - tags: ['service_api'], + summary: 'Upload File', + tags: ['Files'], }) + .input(z.object({ body: zPostFilesUploadBody })) .output(zPostFilesUploadResponse) export const upload = { @@ -1944,22 +2030,20 @@ export const upload = { } /** - * Preview/Download a file that was uploaded via Service API + * Download File * - * Preview or download a file uploaded via Service API - * Provides secure file preview/download functionality. - * Files can only be accessed if they belong to messages within the requesting app's context. + * Preview or download uploaded files previously uploaded via the [Upload File](/api-reference/files/upload-file) API. Files can only be accessed if they belong to messages within the requesting application. */ export const get22 = oc .route({ description: - 'Preview or download a file uploaded via Service API\nProvides secure file preview/download functionality.\nFiles can only be accessed if they belong to messages within the requesting app\'s context.', + 'Preview or download uploaded files previously uploaded via the [Upload File](/api-reference/files/upload-file) API. Files can only be accessed if they belong to messages within the requesting application.', inputStructure: 'detailed', method: 'GET', operationId: 'getFilesByFileIdPreview', path: '/files/{file_id}/preview', - summary: 'Preview/Download a file that was uploaded via Service API', - tags: ['service_api'], + summary: 'Download File', + tags: ['Files'], }) .input( z.object({ @@ -1983,31 +2067,39 @@ export const files = { } /** - * Get a paused human input form by token + * Get Human Input Form + * + * Retrieve a paused Human Input form's contents using the `form_token` from a `human_input_required` event. Requires **WebApp** delivery. */ export const get23 = oc .route({ - description: 'Get a paused human input form by token', + description: + 'Retrieve a paused Human Input form\'s contents using the `form_token` from a `human_input_required` event. Requires **WebApp** delivery.', inputStructure: 'detailed', method: 'GET', operationId: 'getFormHumanInputByFormToken', path: '/form/human_input/{form_token}', - tags: ['service_api'], + summary: 'Get Human Input Form', + tags: ['Human Input'], }) .input(z.object({ params: zGetFormHumanInputByFormTokenPath })) .output(zGetFormHumanInputByFormTokenResponse) /** - * Submit a paused human input form by token + * Submit Human Input Form + * + * Submit the recipient's response to a paused Human Input form. The workflow resumes on acceptance; use [Stream Workflow Events](/api-reference/chatflows/stream-workflow-events) to follow subsequent events. Requires **WebApp** delivery. */ export const post34 = oc .route({ - description: 'Submit a paused human input form by token', + description: + 'Submit the recipient\'s response to a paused Human Input form. The workflow resumes on acceptance; use [Stream Workflow Events](/api-reference/chatflows/stream-workflow-events) to follow subsequent events. Requires **WebApp** delivery.', inputStructure: 'detailed', method: 'POST', operationId: 'postFormHumanInputByFormToken', path: '/form/human_input/{form_token}', - tags: ['service_api'], + summary: 'Submit Human Input Form', + tags: ['Human Input'], }) .input( z.object({ @@ -2031,21 +2123,20 @@ export const form = { } /** - * Get app information + * Get App Info * - * Get basic application information - * Returns basic information about the application including name, description, tags, and mode. + * Retrieve basic information about this application, including name, description, tags, and mode. */ export const get24 = oc .route({ description: - 'Get basic application information\nReturns basic information about the application including name, description, tags, and mode.', + 'Retrieve basic information about this application, including name, description, tags, and mode.', inputStructure: 'detailed', method: 'GET', operationId: 'getInfo', path: '/info', - summary: 'Get app information', - tags: ['service_api'], + summary: 'Get App Info', + tags: ['Applications'], }) .output(zGetInfoResponse) @@ -2054,21 +2145,20 @@ export const info = { } /** - * Submit feedback for a message + * Submit Message Feedback * - * Submit feedback for a message - * Allows users to rate messages as like/dislike and provide optional feedback content. + * Submit feedback for a message. End users can rate messages as `like` or `dislike`, and optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted feedback. */ export const post35 = oc .route({ description: - 'Submit feedback for a message\nAllows users to rate messages as like/dislike and provide optional feedback content.', + 'Submit feedback for a message. End users can rate messages as `like` or `dislike`, and optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted feedback.', inputStructure: 'detailed', method: 'POST', operationId: 'postMessagesByMessageIdFeedbacks', path: '/messages/{message_id}/feedbacks', - summary: 'Submit feedback for a message', - tags: ['service_api'], + summary: 'Submit Message Feedback', + tags: ['Feedback'], }) .input( z.object({ @@ -2083,23 +2173,26 @@ export const feedbacks2 = { } /** - * Get suggested follow-up questions for a message + * Get Next Suggested Questions * - * Get suggested follow-up questions for a message - * Returns AI-generated follow-up questions based on the message content. + * Get next questions suggestions for the current message. */ export const get25 = oc .route({ - description: - 'Get suggested follow-up questions for a message\nReturns AI-generated follow-up questions based on the message content.', + description: 'Get next questions suggestions for the current message.', inputStructure: 'detailed', method: 'GET', operationId: 'getMessagesByMessageIdSuggested', path: '/messages/{message_id}/suggested', - summary: 'Get suggested follow-up questions for a message', - tags: ['service_api'], + summary: 'Get Next Suggested Questions', + tags: ['Chatflows', 'Chats'], }) - .input(z.object({ params: zGetMessagesByMessageIdSuggestedPath })) + .input( + z.object({ + params: zGetMessagesByMessageIdSuggestedPath, + query: zGetMessagesByMessageIdSuggestedQuery, + }), + ) .output(zGetMessagesByMessageIdSuggestedResponse) export const suggested = { @@ -2112,21 +2205,20 @@ export const byMessageId = { } /** - * List messages in a conversation + * List Conversation Messages * - * List messages in a conversation - * Retrieves messages with pagination support using first_id. + * Returns historical chat records in a scrolling load format, with the first page returning the latest `limit` messages, i.e., in reverse order. */ export const get26 = oc .route({ description: - 'List messages in a conversation\nRetrieves messages with pagination support using first_id.', + 'Returns historical chat records in a scrolling load format, with the first page returning the latest `limit` messages, i.e., in reverse order.', inputStructure: 'detailed', method: 'GET', operationId: 'getMessages', path: '/messages', - summary: 'List messages in a conversation', - tags: ['service_api'], + summary: 'List Conversation Messages', + tags: ['Conversations'], }) .input(z.object({ query: zGetMessagesQuery })) .output(zGetMessagesResponse) @@ -2137,21 +2229,20 @@ export const messages = { } /** - * Get app metadata + * Get App Meta * - * Get application metadata - * Returns metadata about the application including configuration and settings. + * Retrieve metadata about this application, including tool icons and other configuration details. */ export const get27 = oc .route({ description: - 'Get application metadata\nReturns metadata about the application including configuration and settings.', + 'Retrieve metadata about this application, including tool icons and other configuration details.', inputStructure: 'detailed', method: 'GET', operationId: 'getMeta', path: '/meta', - summary: 'Get app metadata', - tags: ['service_api'], + summary: 'Get App Meta', + tags: ['Applications'], }) .output(zGetMetaResponse) @@ -2160,21 +2251,20 @@ export const meta = { } /** - * Retrieve app parameters + * Get App Parameters * - * Retrieve application input parameters and configuration - * Returns the input form parameters and configuration for the application. + * Retrieve the application's input form configuration, including feature switches, input parameter names, types, and default values. */ export const get28 = oc .route({ description: - 'Retrieve application input parameters and configuration\nReturns the input form parameters and configuration for the application.', + 'Retrieve the application\'s input form configuration, including feature switches, input parameter names, types, and default values.', inputStructure: 'detailed', method: 'GET', operationId: 'getParameters', path: '/parameters', - summary: 'Retrieve app parameters', - tags: ['service_api'], + summary: 'Get App Parameters', + tags: ['Applications'], }) .output(zGetParametersResponse) @@ -2183,21 +2273,20 @@ export const parameters = { } /** - * Retrieve app site info + * Get App WebApp Settings * - * Get application site configuration - * Returns the site configuration for the application including theme, icons, and text. + * Retrieve the WebApp settings of this application, including site configuration, theme, and customization options. */ export const get29 = oc .route({ description: - 'Get application site configuration\nReturns the site configuration for the application including theme, icons, and text.', + 'Retrieve the WebApp settings of this application, including site configuration, theme, and customization options.', inputStructure: 'detailed', method: 'GET', operationId: 'getSite', path: '/site', - summary: 'Retrieve app site info', - tags: ['service_api'], + summary: 'Get App WebApp Settings', + tags: ['Applications'], }) .output(zGetSiteResponse) @@ -2206,21 +2295,19 @@ export const site = { } /** - * Convert text to audio using text-to-speech + * Convert Text to Audio * - * Convert text to audio using text-to-speech - * Converts the provided text to audio using the specified voice. + * Convert text to speech. */ export const post36 = oc .route({ - description: - 'Convert text to audio using text-to-speech\nConverts the provided text to audio using the specified voice.', + description: 'Convert text to speech.', inputStructure: 'detailed', method: 'POST', operationId: 'postTextToAudio', path: '/text-to-audio', - summary: 'Convert text to audio using text-to-speech', - tags: ['service_api'], + summary: 'Convert Text to Audio', + tags: ['TTS'], }) .input(z.object({ body: zPostTextToAudioBody })) .output(zPostTextToAudioResponse) @@ -2230,16 +2317,20 @@ export const textToAudio = { } /** - * Get workflow execution events stream after resume + * Stream Workflow Events + * + * Resume the Server-Sent Events stream for a workflow run after a pause or a dropped SSE connection. For runs that have already finished, the stream emits a single `workflow_finished` event and closes. */ export const get30 = oc .route({ - description: 'Get workflow execution events stream after resume', + description: + 'Resume the Server-Sent Events stream for a workflow run after a pause or a dropped SSE connection. For runs that have already finished, the stream emits a single `workflow_finished` event and closes.', inputStructure: 'detailed', method: 'GET', operationId: 'getWorkflowByTaskIdEvents', path: '/workflow/{task_id}/events', - tags: ['service_api'], + summary: 'Stream Workflow Events', + tags: ['Chatflows', 'Workflows'], }) .input( z.object({ params: zGetWorkflowByTaskIdEventsPath, query: zGetWorkflowByTaskIdEventsQuery }), @@ -2259,21 +2350,19 @@ export const workflow = { } /** - * Get workflow app logs + * List Workflow Logs * - * Get workflow execution logs - * Returns paginated workflow execution logs with filtering options. + * Retrieve paginated workflow execution logs with filtering options. */ export const get31 = oc .route({ - description: - 'Get workflow execution logs\nReturns paginated workflow execution logs with filtering options.', + description: 'Retrieve paginated workflow execution logs with filtering options.', inputStructure: 'detailed', method: 'GET', operationId: 'getWorkflowsLogs', path: '/workflows/logs', - summary: 'Get workflow app logs', - tags: ['service_api'], + summary: 'List Workflow Logs', + tags: ['Chatflows', 'Workflows'], }) .input(z.object({ query: zGetWorkflowsLogsQuery.optional() })) .output(zGetWorkflowsLogsResponse) @@ -2283,21 +2372,20 @@ export const logs = { } /** - * Get a workflow task running detail + * Get Workflow Run Detail * - * Get workflow run details - * Returns detailed information about a specific workflow run. + * Retrieve the current execution results of a workflow task based on the workflow execution ID. */ export const get32 = oc .route({ description: - 'Get workflow run details\nReturns detailed information about a specific workflow run.', + 'Retrieve the current execution results of a workflow task based on the workflow execution ID.', inputStructure: 'detailed', method: 'GET', operationId: 'getWorkflowsRunByWorkflowRunId', path: '/workflows/run/{workflow_run_id}', - summary: 'Get a workflow task running detail', - tags: ['service_api'], + summary: 'Get Workflow Run Detail', + tags: ['Chatflows', 'Workflows'], }) .input(z.object({ params: zGetWorkflowsRunByWorkflowRunIdPath })) .output(zGetWorkflowsRunByWorkflowRunIdResponse) @@ -2307,22 +2395,19 @@ export const byWorkflowRunId = { } /** - * Execute a workflow + * Run Workflow * - * Execute a workflow - * Runs a workflow with the provided inputs and returns the results. - * Supports both blocking and streaming response modes. + * Execute a workflow. Cannot be executed without a published workflow. */ export const post37 = oc .route({ - description: - 'Execute a workflow\nRuns a workflow with the provided inputs and returns the results.\nSupports both blocking and streaming response modes.', + description: 'Execute a workflow. Cannot be executed without a published workflow.', inputStructure: 'detailed', method: 'POST', operationId: 'postWorkflowsRun', path: '/workflows/run', - summary: 'Execute a workflow', - tags: ['service_api'], + summary: 'Run Workflow', + tags: ['Workflows'], }) .input(z.object({ body: zPostWorkflowsRunBody })) .output(zPostWorkflowsRunResponse) @@ -2333,21 +2418,26 @@ export const run3 = { } /** - * Stop a running workflow task + * Stop Workflow Task * - * Stop a running workflow task + * Stop a running workflow task. Only supported in `streaming` mode. */ export const post38 = oc .route({ - description: 'Stop a running workflow task', + description: 'Stop a running workflow task. Only supported in `streaming` mode.', inputStructure: 'detailed', method: 'POST', operationId: 'postWorkflowsTasksByTaskIdStop', path: '/workflows/tasks/{task_id}/stop', - summary: 'Stop a running workflow task', - tags: ['service_api'], + summary: 'Stop Workflow Task', + tags: ['Workflows'], }) - .input(z.object({ params: zPostWorkflowsTasksByTaskIdStopPath })) + .input( + z.object({ + body: zPostWorkflowsTasksByTaskIdStopBody, + params: zPostWorkflowsTasksByTaskIdStopPath, + }), + ) .output(zPostWorkflowsTasksByTaskIdStopResponse) export const stop3 = { @@ -2363,21 +2453,20 @@ export const tasks = { } /** - * Run specific workflow by ID + * Run Workflow by ID * - * Execute a specific workflow by ID - * Executes a specific workflow version identified by its ID. + * Execute a specific workflow version identified by its ID. Useful for running a particular published version of the workflow. */ export const post39 = oc .route({ description: - 'Execute a specific workflow by ID\nExecutes a specific workflow version identified by its ID.', + 'Execute a specific workflow version identified by its ID. Useful for running a particular published version of the workflow.', inputStructure: 'detailed', method: 'POST', operationId: 'postWorkflowsByWorkflowIdRun', path: '/workflows/{workflow_id}/run', - summary: 'Run specific workflow by ID', - tags: ['service_api'], + summary: 'Run Workflow by ID', + tags: ['Workflows'], }) .input( z.object({ @@ -2403,21 +2492,20 @@ export const workflows = { } /** - * Get available models by model type + * Get Available Models * - * Get available models by model type - * Returns a list of available models for the specified model type. + * Retrieve the list of available models by type. Primarily used to query `text-embedding` and `rerank` models for knowledge base configuration. */ export const get33 = oc .route({ description: - 'Get available models by model type\nReturns a list of available models for the specified model type.', + 'Retrieve the list of available models by type. Primarily used to query `text-embedding` and `rerank` models for knowledge base configuration.', inputStructure: 'detailed', method: 'GET', operationId: 'getWorkspacesCurrentModelsModelTypesByModelType', path: '/workspaces/current/models/model-types/{model_type}', - summary: 'Get available models by model type', - tags: ['service_api'], + summary: 'Get Available Models', + tags: ['Models'], }) .input(z.object({ params: zGetWorkspacesCurrentModelsModelTypesByModelTypePath })) .output(zGetWorkspacesCurrentModelsModelTypesByModelTypeResponse) diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index a3cea1127da..75d6a4b5a97 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -115,6 +115,23 @@ export type ChatRequestPayload = { workflow_id?: string | null } +export type ChatRequestPayloadWithUser = { + auto_generate_name?: boolean + conversation_id?: string | null + files?: Array<{ + [key: string]: unknown + }> | null + inputs: { + [key: string]: unknown + } + query: string + response_mode?: 'blocking' | 'streaming' | null + retriever_from?: string + trace_session_id?: string | null + user: string + workflow_id?: string | null +} + export type ChildChunkCreatePayload = { content: string } @@ -165,6 +182,20 @@ export type CompletionRequestPayload = { trace_session_id?: string | null } +export type CompletionRequestPayloadWithUser = { + files?: Array<{ + [key: string]: unknown + }> | null + inputs: { + [key: string]: unknown + } + query?: string + response_mode?: 'blocking' | 'streaming' | null + retriever_from?: string + trace_session_id?: string | null + user: string +} + export type Condition = { comparison_operator: | '<' @@ -201,11 +232,37 @@ export type ConversationListQuery = { sort_by?: '-created_at' | '-updated_at' | 'created_at' | 'updated_at' } -export type ConversationRenamePayload = { +export type ConversationRenamePayload = ( + | { + auto_generate: true + name?: string | null + } + | { + auto_generate?: false + name: string + } +) & { auto_generate?: boolean name?: string | null } +export type ConversationRenamePayloadWithUser = ( + | { + auto_generate: true + name?: string | null + user?: string + } + | { + auto_generate?: false + name: string + user?: string + } +) & { + auto_generate?: boolean + name?: string | null + user?: string +} + export type ConversationVariableInfiniteScrollPaginationResponse = { data: Array has_more: boolean @@ -226,6 +283,11 @@ export type ConversationVariableUpdatePayload = { value: unknown } +export type ConversationVariableUpdatePayloadWithUser = { + user?: string + value: unknown +} + export type ConversationVariablesQuery = { last_id?: string | null limit?: number @@ -651,7 +713,24 @@ export type DocumentTextCreatePayload = { text: string } -export type DocumentTextUpdate = { +export type DocumentTextUpdate = ( + | { + doc_form?: string + doc_language?: string + name: string + process_rule?: ProcessRule | null + retrieval_model?: RetrievalModel | null + text: string + } + | { + doc_form?: string + doc_language?: string + name?: string | null + process_rule?: ProcessRule | null + retrieval_model?: RetrievalModel | null + text?: null + } +) & { doc_form?: string doc_language?: string name?: string | null @@ -875,6 +954,14 @@ export type HumanInputFormSubmitPayload = { } } +export type HumanInputFormSubmitPayloadWithUser = { + action: string + inputs: { + [key: string]: JsonValue2 + } + user: string +} + export type HumanInputFormSubmitResponse = { [key: string]: never } @@ -923,6 +1010,12 @@ export type MessageFeedbackPayload = { rating?: 'dislike' | 'like' | null } +export type MessageFeedbackPayloadWithUser = { + content?: string | null + rating?: 'dislike' | 'like' | null + user: string +} + export type MessageFile = { belongs_to?: string | null filename: string @@ -1025,6 +1118,10 @@ export type ModelStatus export type ModelType = 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' +export type OptionalServiceApiUserPayload = { + user?: string +} + export type ParagraphInputConfig = { default?: StringSource | null output_variable_name: string @@ -1112,6 +1209,10 @@ export type ProviderWithModelsResponse = { tenant_id: string } +export type RequiredServiceApiUserPayload = { + user: string +} + export type RerankingModel = { reranking_model_name?: string | null reranking_provider_name?: string | null @@ -1356,11 +1457,17 @@ export type TagDeletePayload = { tag_id: string } -export type TagUnbindingPayload = { - tag_id?: string | null - tag_ids?: Array - target_id: string -} +export type TagUnbindingPayload + = | { + tag_id: string + tag_ids?: Array + target_id: string + } + | { + tag_id?: string + tag_ids: Array + target_id: string + } export type TagUpdatePayload = { name: string @@ -1374,6 +1481,14 @@ export type TextToAudioPayload = { voice?: string | null } +export type TextToAudioPayloadWithUser = { + message_id?: string | null + streaming?: boolean | null + text?: string | null + user?: string + voice?: string | null +} + export type UrlResponse = { url: string } @@ -1472,6 +1587,18 @@ export type WorkflowRunPayload = { trace_session_id?: string | null } +export type WorkflowRunPayloadWithUser = { + files?: Array<{ + [key: string]: unknown + }> | null + inputs: { + [key: string]: unknown + } + response_mode?: 'blocking' | 'streaming' | null + trace_session_id?: string | null + user: string +} + export type WorkflowRunResponse = { created_at?: number | null elapsed_time?: number | number | null @@ -1544,6 +1671,7 @@ export type GetAppFeedbacksData = { export type GetAppFeedbacksErrors = { 401: unknown + 403: unknown } export type GetAppFeedbacksResponses = { @@ -1555,7 +1683,7 @@ export type GetAppFeedbacksResponse = GetAppFeedbacksResponses[keyof GetAppFeedb export type PostAppsAnnotationReplyByActionData = { body: AnnotationReplyActionPayload path: { - action: string + action: 'disable' | 'enable' } query?: never url: '/apps/annotation-reply/{action}' @@ -1563,6 +1691,7 @@ export type PostAppsAnnotationReplyByActionData = { export type PostAppsAnnotationReplyByActionErrors = { 401: unknown + 403: unknown } export type PostAppsAnnotationReplyByActionResponses = { @@ -1583,7 +1712,9 @@ export type GetAppsAnnotationReplyByActionStatusByJobIdData = { } export type GetAppsAnnotationReplyByActionStatusByJobIdErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1607,6 +1738,7 @@ export type GetAppsAnnotationsData = { export type GetAppsAnnotationsErrors = { 401: unknown + 403: unknown } export type GetAppsAnnotationsResponses = { @@ -1625,6 +1757,7 @@ export type PostAppsAnnotationsData = { export type PostAppsAnnotationsErrors = { 401: unknown + 403: unknown } export type PostAppsAnnotationsResponses = { @@ -1679,7 +1812,10 @@ export type PutAppsAnnotationsByAnnotationIdResponse = PutAppsAnnotationsByAnnotationIdResponses[keyof PutAppsAnnotationsByAnnotationIdResponses] export type PostAudioToTextData = { - body?: never + body: { + file: Blob | File + user?: string + } path?: never query?: never url: '/audio-to-text' @@ -1688,6 +1824,7 @@ export type PostAudioToTextData = { export type PostAudioToTextErrors = { 400: unknown 401: unknown + 403: unknown 413: unknown 415: unknown 500: unknown @@ -1700,7 +1837,7 @@ export type PostAudioToTextResponses = { export type PostAudioToTextResponse = PostAudioToTextResponses[keyof PostAudioToTextResponses] export type PostChatMessagesData = { - body: ChatRequestPayload + body: ChatRequestPayloadWithUser path?: never query?: never url: '/chat-messages' @@ -1709,6 +1846,7 @@ export type PostChatMessagesData = { export type PostChatMessagesErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown 429: unknown 500: unknown @@ -1721,7 +1859,7 @@ export type PostChatMessagesResponses = { export type PostChatMessagesResponse = PostChatMessagesResponses[keyof PostChatMessagesResponses] export type PostChatMessagesByTaskIdStopData = { - body?: never + body: RequiredServiceApiUserPayload path: { task_id: string } @@ -1730,7 +1868,9 @@ export type PostChatMessagesByTaskIdStopData = { } export type PostChatMessagesByTaskIdStopErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1742,7 +1882,7 @@ export type PostChatMessagesByTaskIdStopResponse = PostChatMessagesByTaskIdStopResponses[keyof PostChatMessagesByTaskIdStopResponses] export type PostCompletionMessagesData = { - body: CompletionRequestPayload + body: CompletionRequestPayloadWithUser path?: never query?: never url: '/completion-messages' @@ -1751,7 +1891,9 @@ export type PostCompletionMessagesData = { export type PostCompletionMessagesErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown + 429: unknown 500: unknown } @@ -1763,7 +1905,7 @@ export type PostCompletionMessagesResponse = PostCompletionMessagesResponses[keyof PostCompletionMessagesResponses] export type PostCompletionMessagesByTaskIdStopData = { - body?: never + body: RequiredServiceApiUserPayload path: { task_id: string } @@ -1772,7 +1914,9 @@ export type PostCompletionMessagesByTaskIdStopData = { } export type PostCompletionMessagesByTaskIdStopErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1790,12 +1934,15 @@ export type GetConversationsData = { last_id?: string limit?: number sort_by?: '-created_at' | '-updated_at' | 'created_at' | 'updated_at' + user?: string } url: '/conversations' } export type GetConversationsErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1806,7 +1953,7 @@ export type GetConversationsResponses = { export type GetConversationsResponse = GetConversationsResponses[keyof GetConversationsResponses] export type DeleteConversationsByCIdData = { - body?: never + body: OptionalServiceApiUserPayload path: { c_id: string } @@ -1815,7 +1962,9 @@ export type DeleteConversationsByCIdData = { } export type DeleteConversationsByCIdErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1827,7 +1976,7 @@ export type DeleteConversationsByCIdResponse = DeleteConversationsByCIdResponses[keyof DeleteConversationsByCIdResponses] export type PostConversationsByCIdNameData = { - body: ConversationRenamePayload + body: ConversationRenamePayloadWithUser path: { c_id: string } @@ -1836,7 +1985,9 @@ export type PostConversationsByCIdNameData = { } export type PostConversationsByCIdNameErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1855,13 +2006,16 @@ export type GetConversationsByCIdVariablesData = { query?: { last_id?: string limit?: number + user?: string variable_name?: string } url: '/conversations/{c_id}/variables' } export type GetConversationsByCIdVariablesErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1873,7 +2027,7 @@ export type GetConversationsByCIdVariablesResponse = GetConversationsByCIdVariablesResponses[keyof GetConversationsByCIdVariablesResponses] export type PutConversationsByCIdVariablesByVariableIdData = { - body: ConversationVariableUpdatePayload + body: ConversationVariableUpdatePayloadWithUser path: { c_id: string variable_id: string @@ -1885,6 +2039,7 @@ export type PutConversationsByCIdVariablesByVariableIdData = { export type PutConversationsByCIdVariablesByVariableIdErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -1910,6 +2065,7 @@ export type GetDatasetsData = { export type GetDatasetsErrors = { 401: unknown + 403: unknown } export type GetDatasetsResponses = { @@ -1928,6 +2084,8 @@ export type PostDatasetsData = { export type PostDatasetsErrors = { 400: unknown 401: unknown + 403: unknown + 409: unknown } export type PostDatasetsResponses = { @@ -1937,7 +2095,9 @@ export type PostDatasetsResponses = { export type PostDatasetsResponse = PostDatasetsResponses[keyof PostDatasetsResponses] export type PostDatasetsPipelineFileUploadData = { - body?: never + body: { + file: Blob | File + } path?: never query?: never url: '/datasets/pipeline/file-upload' @@ -1946,6 +2106,7 @@ export type PostDatasetsPipelineFileUploadData = { export type PostDatasetsPipelineFileUploadErrors = { 400: unknown 401: unknown + 403: unknown 413: unknown 415: unknown } @@ -1985,6 +2146,7 @@ export type GetDatasetsTagsData = { export type GetDatasetsTagsErrors = { 401: unknown + 403: unknown } export type GetDatasetsTagsResponses = { @@ -2078,6 +2240,7 @@ export type DeleteDatasetsByDatasetIdData = { export type DeleteDatasetsByDatasetIdErrors = { 401: unknown + 403: unknown 404: unknown 409: unknown } @@ -2148,6 +2311,7 @@ export type PostDatasetsByDatasetIdDocumentCreateByFileData = { export type PostDatasetsByDatasetIdDocumentCreateByFileErrors = { 400: unknown 401: unknown + 403: unknown } export type PostDatasetsByDatasetIdDocumentCreateByFileResponses = { @@ -2169,6 +2333,7 @@ export type PostDatasetsByDatasetIdDocumentCreateByTextData = { export type PostDatasetsByDatasetIdDocumentCreateByTextErrors = { 400: unknown 401: unknown + 403: unknown } export type PostDatasetsByDatasetIdDocumentCreateByTextResponses = { @@ -2193,6 +2358,7 @@ export type PostDatasetsByDatasetIdDocumentCreateByFile2Data = { export type PostDatasetsByDatasetIdDocumentCreateByFile2Errors = { 400: unknown 401: unknown + 403: unknown } export type PostDatasetsByDatasetIdDocumentCreateByFile2Responses = { @@ -2214,6 +2380,7 @@ export type PostDatasetsByDatasetIdDocumentCreateByText2Data = { export type PostDatasetsByDatasetIdDocumentCreateByText2Errors = { 400: unknown 401: unknown + 403: unknown } export type PostDatasetsByDatasetIdDocumentCreateByText2Responses = { @@ -2239,6 +2406,7 @@ export type GetDatasetsByDatasetIdDocumentsData = { export type GetDatasetsByDatasetIdDocumentsErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2259,13 +2427,16 @@ export type PostDatasetsByDatasetIdDocumentsDownloadZipData = { } export type PostDatasetsByDatasetIdDocumentsDownloadZipErrors = { - 401: unknown - 403: unknown - 404: unknown + 401: Blob | File + 403: Blob | File + 404: Blob | File } +export type PostDatasetsByDatasetIdDocumentsDownloadZipError + = PostDatasetsByDatasetIdDocumentsDownloadZipErrors[keyof PostDatasetsByDatasetIdDocumentsDownloadZipErrors] + export type PostDatasetsByDatasetIdDocumentsDownloadZipResponses = { - 200: BinaryFileResponse + 200: Blob | File } export type PostDatasetsByDatasetIdDocumentsDownloadZipResponse @@ -2282,6 +2453,7 @@ export type PostDatasetsByDatasetIdDocumentsMetadataData = { export type PostDatasetsByDatasetIdDocumentsMetadataErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2295,7 +2467,7 @@ export type PostDatasetsByDatasetIdDocumentsMetadataResponse export type PatchDatasetsByDatasetIdDocumentsStatusByActionData = { body: DocumentStatusPayload path: { - action: string + action: 'archive' | 'disable' | 'enable' | 'un_archive' dataset_id: string } query?: never @@ -2328,6 +2500,7 @@ export type GetDatasetsByDatasetIdDocumentsByBatchIndexingStatusData = { export type GetDatasetsByDatasetIdDocumentsByBatchIndexingStatusErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2349,6 +2522,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdData = { } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdErrors = { + 400: unknown 401: unknown 403: unknown 404: unknown @@ -2374,6 +2548,7 @@ export type GetDatasetsByDatasetIdDocumentsByDocumentIdData = { } export type GetDatasetsByDatasetIdDocumentsByDocumentIdErrors = { + 400: unknown 401: unknown 403: unknown 404: unknown @@ -2401,6 +2576,7 @@ export type PatchDatasetsByDatasetIdDocumentsByDocumentIdData = { export type PatchDatasetsByDatasetIdDocumentsByDocumentIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2451,6 +2627,7 @@ export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsData = { export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2474,6 +2651,7 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsData = { export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2497,6 +2675,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdDat export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2520,6 +2699,7 @@ export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdData = export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2543,6 +2723,7 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdData export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2570,6 +2751,7 @@ export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildC export type GetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2592,7 +2774,9 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChild } export type PostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2618,7 +2802,9 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2645,7 +2831,9 @@ export type PatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChil export type PatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2671,7 +2859,9 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileData = { } export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2693,7 +2883,9 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextData = { } export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2718,7 +2910,9 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Data = { } export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Errors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -2741,6 +2935,7 @@ export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText2Data = { export type PostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText2Errors = { 401: unknown + 403: unknown 404: unknown } @@ -2761,8 +2956,11 @@ export type PostDatasetsByDatasetIdHitTestingData = { } export type PostDatasetsByDatasetIdHitTestingErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown + 500: unknown } export type PostDatasetsByDatasetIdHitTestingResponses = { @@ -2783,6 +2981,7 @@ export type GetDatasetsByDatasetIdMetadataData = { export type GetDatasetsByDatasetIdMetadataErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2804,6 +3003,7 @@ export type PostDatasetsByDatasetIdMetadataData = { export type PostDatasetsByDatasetIdMetadataErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2825,6 +3025,7 @@ export type GetDatasetsByDatasetIdMetadataBuiltInData = { export type GetDatasetsByDatasetIdMetadataBuiltInErrors = { 401: unknown + 403: unknown } export type GetDatasetsByDatasetIdMetadataBuiltInResponses = { @@ -2837,7 +3038,7 @@ export type GetDatasetsByDatasetIdMetadataBuiltInResponse export type PostDatasetsByDatasetIdMetadataBuiltInByActionData = { body?: never path: { - action: string + action: 'disable' | 'enable' dataset_id: string } query?: never @@ -2846,6 +3047,7 @@ export type PostDatasetsByDatasetIdMetadataBuiltInByActionData = { export type PostDatasetsByDatasetIdMetadataBuiltInByActionErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2868,6 +3070,7 @@ export type DeleteDatasetsByDatasetIdMetadataByMetadataIdData = { export type DeleteDatasetsByDatasetIdMetadataByMetadataIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2890,6 +3093,7 @@ export type PatchDatasetsByDatasetIdMetadataByMetadataIdData = { export type PatchDatasetsByDatasetIdMetadataByMetadataIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -2913,6 +3117,8 @@ export type GetDatasetsByDatasetIdPipelineDatasourcePluginsData = { export type GetDatasetsByDatasetIdPipelineDatasourcePluginsErrors = { 401: unknown + 403: unknown + 404: unknown } export type GetDatasetsByDatasetIdPipelineDatasourcePluginsResponses = { @@ -2934,6 +3140,8 @@ export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunData = { export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunErrors = { 401: unknown + 403: unknown + 404: unknown } export type PostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponses = { @@ -2954,6 +3162,9 @@ export type PostDatasetsByDatasetIdPipelineRunData = { export type PostDatasetsByDatasetIdPipelineRunErrors = { 401: unknown + 403: unknown + 404: unknown + 500: unknown } export type PostDatasetsByDatasetIdPipelineRunResponses = { @@ -2973,8 +3184,11 @@ export type PostDatasetsByDatasetIdRetrieveData = { } export type PostDatasetsByDatasetIdRetrieveErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown + 500: unknown } export type PostDatasetsByDatasetIdRetrieveResponses = { @@ -2995,6 +3209,7 @@ export type GetDatasetsByDatasetIdTagsData = { export type GetDatasetsByDatasetIdTagsErrors = { 401: unknown + 403: unknown } export type GetDatasetsByDatasetIdTagsResponses = { @@ -3015,6 +3230,7 @@ export type GetEndUsersByEndUserIdData = { export type GetEndUsersByEndUserIdErrors = { 401: unknown + 403: unknown 404: unknown } @@ -3026,7 +3242,10 @@ export type GetEndUsersByEndUserIdResponse = GetEndUsersByEndUserIdResponses[keyof GetEndUsersByEndUserIdResponses] export type PostFilesUploadData = { - body?: never + body: { + file: Blob | File + user?: string + } path?: never query?: never url: '/files/upload' @@ -3035,6 +3254,7 @@ export type PostFilesUploadData = { export type PostFilesUploadErrors = { 400: unknown 401: unknown + 403: unknown 413: unknown 415: unknown } @@ -3052,18 +3272,22 @@ export type GetFilesByFileIdPreviewData = { } query?: { as_attachment?: boolean + user?: string } url: '/files/{file_id}/preview' } export type GetFilesByFileIdPreviewErrors = { - 401: unknown - 403: unknown - 404: unknown + 401: Blob | File + 403: Blob | File + 404: Blob | File } +export type GetFilesByFileIdPreviewError + = GetFilesByFileIdPreviewErrors[keyof GetFilesByFileIdPreviewErrors] + export type GetFilesByFileIdPreviewResponses = { - 200: BinaryFileResponse + 200: Blob | File } export type GetFilesByFileIdPreviewResponse @@ -3080,6 +3304,7 @@ export type GetFormHumanInputByFormTokenData = { export type GetFormHumanInputByFormTokenErrors = { 401: unknown + 403: unknown 404: unknown 412: unknown } @@ -3092,7 +3317,7 @@ export type GetFormHumanInputByFormTokenResponse = GetFormHumanInputByFormTokenResponses[keyof GetFormHumanInputByFormTokenResponses] export type PostFormHumanInputByFormTokenData = { - body: HumanInputFormSubmitPayload + body: HumanInputFormSubmitPayloadWithUser path: { form_token: string } @@ -3103,6 +3328,7 @@ export type PostFormHumanInputByFormTokenData = { export type PostFormHumanInputByFormTokenErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown 412: unknown } @@ -3123,6 +3349,7 @@ export type GetInfoData = { export type GetInfoErrors = { 401: unknown + 403: unknown 404: unknown } @@ -3139,12 +3366,15 @@ export type GetMessagesData = { conversation_id: string first_id?: string limit?: number + user?: string } url: '/messages' } export type GetMessagesErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -3155,7 +3385,7 @@ export type GetMessagesResponses = { export type GetMessagesResponse = GetMessagesResponses[keyof GetMessagesResponses] export type PostMessagesByMessageIdFeedbacksData = { - body: MessageFeedbackPayload + body: MessageFeedbackPayloadWithUser path: { message_id: string } @@ -3165,6 +3395,7 @@ export type PostMessagesByMessageIdFeedbacksData = { export type PostMessagesByMessageIdFeedbacksErrors = { 401: unknown + 403: unknown 404: unknown } @@ -3180,13 +3411,16 @@ export type GetMessagesByMessageIdSuggestedData = { path: { message_id: string } - query?: never + query: { + user: string + } url: '/messages/{message_id}/suggested' } export type GetMessagesByMessageIdSuggestedErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown 500: unknown } @@ -3207,6 +3441,7 @@ export type GetMetaData = { export type GetMetaErrors = { 401: unknown + 403: unknown 404: unknown } @@ -3224,7 +3459,9 @@ export type GetParametersData = { } export type GetParametersErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -3253,20 +3490,23 @@ export type GetSiteResponses = { export type GetSiteResponse = GetSiteResponses[keyof GetSiteResponses] export type PostTextToAudioData = { - body: TextToAudioPayload + body: TextToAudioPayloadWithUser path?: never query?: never url: '/text-to-audio' } export type PostTextToAudioErrors = { - 400: unknown - 401: unknown - 500: unknown + 400: Blob | File + 401: Blob | File + 403: Blob | File + 500: Blob | File } +export type PostTextToAudioError = PostTextToAudioErrors[keyof PostTextToAudioErrors] + export type PostTextToAudioResponses = { - 200: AudioBinaryResponse + 200: Blob | File } export type PostTextToAudioResponse = PostTextToAudioResponses[keyof PostTextToAudioResponses] @@ -3285,7 +3525,9 @@ export type GetWorkflowByTaskIdEventsData = { } export type GetWorkflowByTaskIdEventsErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -3314,6 +3556,7 @@ export type GetWorkflowsLogsData = { export type GetWorkflowsLogsErrors = { 401: unknown + 403: unknown } export type GetWorkflowsLogsResponses = { @@ -3323,7 +3566,7 @@ export type GetWorkflowsLogsResponses = { export type GetWorkflowsLogsResponse = GetWorkflowsLogsResponses[keyof GetWorkflowsLogsResponses] export type PostWorkflowsRunData = { - body: WorkflowRunPayload + body: WorkflowRunPayloadWithUser path?: never query?: never url: '/workflows/run' @@ -3332,6 +3575,7 @@ export type PostWorkflowsRunData = { export type PostWorkflowsRunErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown 429: unknown 500: unknown @@ -3353,7 +3597,9 @@ export type GetWorkflowsRunByWorkflowRunIdData = { } export type GetWorkflowsRunByWorkflowRunIdErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -3365,7 +3611,7 @@ export type GetWorkflowsRunByWorkflowRunIdResponse = GetWorkflowsRunByWorkflowRunIdResponses[keyof GetWorkflowsRunByWorkflowRunIdResponses] export type PostWorkflowsTasksByTaskIdStopData = { - body?: never + body: RequiredServiceApiUserPayload path: { task_id: string } @@ -3374,7 +3620,9 @@ export type PostWorkflowsTasksByTaskIdStopData = { } export type PostWorkflowsTasksByTaskIdStopErrors = { + 400: unknown 401: unknown + 403: unknown 404: unknown } @@ -3386,7 +3634,7 @@ export type PostWorkflowsTasksByTaskIdStopResponse = PostWorkflowsTasksByTaskIdStopResponses[keyof PostWorkflowsTasksByTaskIdStopResponses] export type PostWorkflowsByWorkflowIdRunData = { - body: WorkflowRunPayload + body: WorkflowRunPayloadWithUser path: { workflow_id: string } @@ -3397,6 +3645,7 @@ export type PostWorkflowsByWorkflowIdRunData = { export type PostWorkflowsByWorkflowIdRunErrors = { 400: unknown 401: unknown + 403: unknown 404: unknown 429: unknown 500: unknown diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index ce31666d074..efc05030887 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -140,6 +140,22 @@ export const zChatRequestPayload = z.object({ workflow_id: z.string().nullish(), }) +/** + * ChatRequestPayload + */ +export const zChatRequestPayloadWithUser = z.object({ + auto_generate_name: z.boolean().optional().default(true), + conversation_id: z.string().nullish(), + files: z.array(z.record(z.string(), z.unknown())).nullish(), + inputs: z.record(z.string(), z.unknown()), + query: z.string(), + response_mode: z.enum(['blocking', 'streaming']).nullish(), + retriever_from: z.string().optional().default('dev'), + trace_session_id: z.string().nullish(), + user: z.string(), + workflow_id: z.string().nullish(), +}) + /** * ChildChunkCreatePayload */ @@ -207,6 +223,19 @@ export const zCompletionRequestPayload = z.object({ trace_session_id: z.string().nullish(), }) +/** + * CompletionRequestPayload + */ +export const zCompletionRequestPayloadWithUser = z.object({ + files: z.array(z.record(z.string(), z.unknown())).nullish(), + inputs: z.record(z.string(), z.unknown()), + query: z.string().optional().default(''), + response_mode: z.enum(['blocking', 'streaming']).nullish(), + retriever_from: z.string().optional().default('dev'), + trace_session_id: z.string().nullish(), + user: z.string(), +}) + /** * Condition * @@ -249,13 +278,42 @@ export const zConversationListQuery = z.object({ .default('-updated_at'), }) -/** - * ConversationRenamePayload - */ -export const zConversationRenamePayload = z.object({ - auto_generate: z.boolean().optional().default(false), - name: z.string().nullish(), -}) +export const zConversationRenamePayload = z.intersection( + z.union([ + z.object({ + auto_generate: z.literal(true), + name: z.string().nullish(), + }), + z.object({ + auto_generate: z.literal(false).optional().default(false), + name: z.string().regex(/.*\S.*/), + }), + ]), + z.object({ + auto_generate: z.boolean().optional().default(false), + name: z.string().nullish(), + }), +) + +export const zConversationRenamePayloadWithUser = z.intersection( + z.union([ + z.object({ + auto_generate: z.literal(true), + name: z.string().nullish(), + user: z.string().optional(), + }), + z.object({ + auto_generate: z.literal(false).optional().default(false), + name: z.string().regex(/.*\S.*/), + user: z.string().optional(), + }), + ]), + z.object({ + auto_generate: z.boolean().optional().default(false), + name: z.string().nullish(), + user: z.string().optional(), + }), +) /** * ConversationVariableResponse @@ -286,6 +344,14 @@ export const zConversationVariableUpdatePayload = z.object({ value: z.unknown(), }) +/** + * ConversationVariableUpdatePayload + */ +export const zConversationVariableUpdatePayloadWithUser = z.object({ + user: z.string().optional(), + value: z.unknown(), +}) + /** * ConversationVariablesQuery */ @@ -1071,6 +1137,15 @@ export const zHumanInputFormSubmitPayload = z.object({ inputs: z.record(z.string(), zJsonValue2), }) +/** + * HumanInputFormSubmitPayload + */ +export const zHumanInputFormSubmitPayloadWithUser = z.object({ + action: z.string(), + inputs: z.record(z.string(), zJsonValue2), + user: z.string(), +}) + /** * KnowledgeTagResponse */ @@ -1094,6 +1169,15 @@ export const zMessageFeedbackPayload = z.object({ rating: z.enum(['dislike', 'like']).nullish(), }) +/** + * MessageFeedbackPayload + */ +export const zMessageFeedbackPayloadWithUser = z.object({ + content: z.string().nullish(), + rating: z.enum(['dislike', 'like']).nullish(), + user: z.string(), +}) + /** * MessageFile */ @@ -1235,6 +1319,13 @@ export const zModelType = z.enum([ 'tts', ]) +/** + * ServiceApiUserPayload + */ +export const zOptionalServiceApiUserPayload = z.object({ + user: z.string().optional(), +}) + /** * PermissionEnum * @@ -1322,6 +1413,13 @@ export const zProviderWithModelsListResponse = z.object({ data: z.array(zProviderWithModelsResponse), }) +/** + * ServiceApiUserPayload + */ +export const zRequiredServiceApiUserPayload = z.object({ + user: z.string(), +}) + /** * RerankingModel */ @@ -1654,13 +1752,20 @@ export const zTagDeletePayload = z.object({ /** * TagUnbindingPayload * - * Accept the legacy single-tag Service API payload while exposing a normalized tag_ids list internally. + * Accepts either the legacy tag_id payload or the normalized tag_ids payload. */ -export const zTagUnbindingPayload = z.object({ - tag_id: z.string().nullish(), - tag_ids: z.array(z.string()).optional(), - target_id: z.string(), -}) +export const zTagUnbindingPayload = z.union([ + z.object({ + tag_id: z.string(), + tag_ids: z.array(z.string()).min(1).optional(), + target_id: z.string(), + }), + z.object({ + tag_id: z.string().optional(), + tag_ids: z.array(z.string()).min(1), + target_id: z.string(), + }), +]) /** * TagUpdatePayload @@ -1680,6 +1785,17 @@ export const zTextToAudioPayload = z.object({ voice: z.string().nullish(), }) +/** + * TextToAudioPayload + */ +export const zTextToAudioPayloadWithUser = z.object({ + message_id: z.string().nullish(), + streaming: z.boolean().nullish(), + text: z.string().nullish(), + user: z.string().optional(), + voice: z.string().nullish(), +}) + /** * UrlResponse */ @@ -1899,17 +2015,34 @@ export const zDocumentTextCreatePayload = z.object({ text: z.string(), }) -/** - * DocumentTextUpdate - */ -export const zDocumentTextUpdate = z.object({ - doc_form: z.string().optional().default('text_model'), - doc_language: z.string().optional().default('English'), - name: z.string().nullish(), - process_rule: zProcessRule.nullish(), - retrieval_model: zRetrievalModel.nullish(), - text: z.string().nullish(), -}) +export const zDocumentTextUpdate = z.intersection( + z.union([ + z.object({ + doc_form: z.string().optional().default('text_model'), + doc_language: z.string().optional().default('English'), + name: z.string(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), + text: z.string(), + }), + z.object({ + doc_form: z.string().optional().default('text_model'), + doc_language: z.string().optional().default('English'), + name: z.string().nullish(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), + text: z.null().optional(), + }), + ]), + z.object({ + doc_form: z.string().optional().default('text_model'), + doc_language: z.string().optional().default('English'), + name: z.string().nullish(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), + text: z.string().nullish(), + }), +) /** * HitTestingPayload @@ -2005,6 +2138,17 @@ export const zWorkflowRunPayload = z.object({ trace_session_id: z.string().nullish(), }) +/** + * WorkflowRunPayload + */ +export const zWorkflowRunPayloadWithUser = z.object({ + files: z.array(z.record(z.string(), z.unknown())).nullish(), + inputs: z.record(z.string(), z.unknown()), + response_mode: z.enum(['blocking', 'streaming']).nullish(), + trace_session_id: z.string().nullish(), + user: z.string(), +}) + /** * WorkflowRunResponse */ @@ -2071,28 +2215,28 @@ export const zGetAppFeedbacksQuery = z.object({ }) /** - * Feedbacks retrieved successfully + * A list of application feedbacks. */ export const zGetAppFeedbacksResponse = zAppFeedbackListResponse export const zPostAppsAnnotationReplyByActionBody = zAnnotationReplyActionPayload export const zPostAppsAnnotationReplyByActionPath = z.object({ - action: z.string(), + action: z.enum(['disable', 'enable']), }) /** - * Action completed successfully + * Annotation reply settings task initiated. */ export const zPostAppsAnnotationReplyByActionResponse = zAnnotationJobStatusResponse export const zGetAppsAnnotationReplyByActionStatusByJobIdPath = z.object({ action: z.string(), - job_id: z.string(), + job_id: z.uuid(), }) /** - * Job status retrieved successfully + * Successfully retrieved task status. */ export const zGetAppsAnnotationReplyByActionStatusByJobIdResponse = zAnnotationJobStatusResponse @@ -2103,49 +2247,59 @@ export const zGetAppsAnnotationsQuery = z.object({ }) /** - * Annotations retrieved successfully + * Successfully retrieved annotation list. */ export const zGetAppsAnnotationsResponse = zAnnotationList export const zPostAppsAnnotationsBody = zAnnotationCreatePayload /** - * Annotation created successfully + * Annotation created successfully. */ export const zPostAppsAnnotationsResponse = zAnnotation export const zDeleteAppsAnnotationsByAnnotationIdPath = z.object({ - annotation_id: z.string(), + annotation_id: z.uuid(), }) /** - * Annotation deleted successfully + * Annotation deleted successfully. */ export const zDeleteAppsAnnotationsByAnnotationIdResponse = z.void() export const zPutAppsAnnotationsByAnnotationIdBody = zAnnotationCreatePayload export const zPutAppsAnnotationsByAnnotationIdPath = z.object({ - annotation_id: z.string(), + annotation_id: z.uuid(), }) /** - * Annotation updated successfully + * Annotation updated successfully. */ export const zPutAppsAnnotationsByAnnotationIdResponse = zAnnotation +export const zPostAudioToTextBody = z.object({ + file: z.custom(), + user: z.string().optional(), +}) + /** - * Audio successfully transcribed + * Successfully converted audio to text. */ export const zPostAudioToTextResponse = zAudioTranscriptResponse -export const zPostChatMessagesBody = zChatRequestPayload +export const zPostChatMessagesBody = zChatRequestPayloadWithUser /** - * Message sent successfully + * Successful response. The content type and structure depend on the `response_mode` parameter in the request. + * + * - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. + * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. */ export const zPostChatMessagesResponse = zGeneratedAppResponse +export const zPostChatMessagesByTaskIdStopBody = zRequiredServiceApiUserPayload + export const zPostChatMessagesByTaskIdStopPath = z.object({ task_id: z.string(), }) @@ -2155,13 +2309,18 @@ export const zPostChatMessagesByTaskIdStopPath = z.object({ */ export const zPostChatMessagesByTaskIdStopResponse = zSimpleResultResponse -export const zPostCompletionMessagesBody = zCompletionRequestPayload +export const zPostCompletionMessagesBody = zCompletionRequestPayloadWithUser /** - * Completion created successfully + * Successful response. The content type and structure depend on the `response_mode` parameter in the request. + * + * - If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` object. + * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkCompletionEvent` objects. */ export const zPostCompletionMessagesResponse = zGeneratedAppResponse +export const zPostCompletionMessagesByTaskIdStopBody = zRequiredServiceApiUserPayload + export const zPostCompletionMessagesByTaskIdStopPath = z.object({ task_id: z.string(), }) @@ -2178,58 +2337,63 @@ export const zGetConversationsQuery = z.object({ .enum(['-created_at', '-updated_at', 'created_at', 'updated_at']) .optional() .default('-updated_at'), + user: z.string().optional(), }) /** - * Conversations retrieved successfully + * Successfully retrieved conversations list. */ export const zGetConversationsResponse = zConversationInfiniteScrollPagination +export const zDeleteConversationsByCIdBody = zOptionalServiceApiUserPayload + export const zDeleteConversationsByCIdPath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) /** - * Conversation deleted successfully + * Conversation deleted successfully. */ export const zDeleteConversationsByCIdResponse = z.void() -export const zPostConversationsByCIdNameBody = zConversationRenamePayload +export const zPostConversationsByCIdNameBody = zConversationRenamePayloadWithUser export const zPostConversationsByCIdNamePath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) /** - * Conversation renamed successfully + * Conversation renamed successfully. */ export const zPostConversationsByCIdNameResponse = zSimpleConversation export const zGetConversationsByCIdVariablesPath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) export const zGetConversationsByCIdVariablesQuery = z.object({ last_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), + user: z.string().optional(), variable_name: z.string().min(1).max(255).optional(), }) /** - * Variables retrieved successfully + * Successfully retrieved conversation variables. */ export const zGetConversationsByCIdVariablesResponse = zConversationVariableInfiniteScrollPaginationResponse -export const zPutConversationsByCIdVariablesByVariableIdBody = zConversationVariableUpdatePayload +export const zPutConversationsByCIdVariablesByVariableIdBody + = zConversationVariableUpdatePayloadWithUser export const zPutConversationsByCIdVariablesByVariableIdPath = z.object({ - c_id: z.string(), - variable_id: z.string(), + c_id: z.uuid(), + variable_id: z.uuid(), }) /** - * Variable updated successfully + * Variable updated successfully. */ export const zPutConversationsByCIdVariablesByVariableIdResponse = zConversationVariableResponse @@ -2242,88 +2406,92 @@ export const zGetDatasetsQuery = z.object({ }) /** - * Datasets retrieved successfully + * List of knowledge bases. */ export const zGetDatasetsResponse = zDatasetListResponse export const zPostDatasetsBody = zDatasetCreatePayload /** - * Dataset created successfully + * Knowledge base created successfully. */ export const zPostDatasetsResponse = zDatasetDetailResponse +export const zPostDatasetsPipelineFileUploadBody = z.object({ + file: z.custom(), +}) + /** - * File uploaded successfully + * File uploaded successfully. */ export const zPostDatasetsPipelineFileUploadResponse = zPipelineUploadFileResponse export const zDeleteDatasetsTagsBody = zTagDeletePayload /** - * Tag deleted successfully + * Success. */ export const zDeleteDatasetsTagsResponse = z.void() /** - * Tags retrieved successfully + * List of tags. */ export const zGetDatasetsTagsResponse = zKnowledgeTagListResponse export const zPatchDatasetsTagsBody = zTagUpdatePayload /** - * Tag updated successfully + * Tag updated successfully. */ export const zPatchDatasetsTagsResponse = zKnowledgeTagResponse export const zPostDatasetsTagsBody = zTagCreatePayload /** - * Tag created successfully + * Tag created successfully. */ export const zPostDatasetsTagsResponse = zKnowledgeTagResponse export const zPostDatasetsTagsBindingBody = zTagBindingPayload /** - * Tags bound successfully + * Success. */ export const zPostDatasetsTagsBindingResponse = z.void() export const zPostDatasetsTagsUnbindingBody = zTagUnbindingPayload /** - * Tags unbound successfully + * Success. */ export const zPostDatasetsTagsUnbindingResponse = z.void() export const zDeleteDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Dataset deleted successfully + * Success. */ export const zDeleteDatasetsByDatasetIdResponse = z.void() export const zGetDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Dataset retrieved successfully + * Knowledge base details. */ export const zGetDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMembersResponse export const zPatchDatasetsByDatasetIdBody = zDatasetUpdatePayload export const zPatchDatasetsByDatasetIdPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Dataset updated successfully + * Knowledge base updated successfully. */ export const zPatchDatasetsByDatasetIdResponse = zDatasetDetailWithPartialMembersResponse @@ -2333,22 +2501,22 @@ export const zPostDatasetsByDatasetIdDocumentCreateByFileBody = z.object({ }) export const zPostDatasetsByDatasetIdDocumentCreateByFilePath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Document created successfully + * Document created successfully. */ export const zPostDatasetsByDatasetIdDocumentCreateByFileResponse = zDocumentAndBatchResponse export const zPostDatasetsByDatasetIdDocumentCreateByTextBody = zDocumentTextCreatePayload export const zPostDatasetsByDatasetIdDocumentCreateByTextPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Document created successfully + * Document created successfully. */ export const zPostDatasetsByDatasetIdDocumentCreateByTextResponse = zDocumentAndBatchResponse @@ -2358,18 +2526,18 @@ export const zPostDatasetsByDatasetIdDocumentCreateByFile2Body = z.object({ }) export const zPostDatasetsByDatasetIdDocumentCreateByFile2Path = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Document created successfully + * Document created successfully. */ export const zPostDatasetsByDatasetIdDocumentCreateByFile2Response = zDocumentAndBatchResponse export const zPostDatasetsByDatasetIdDocumentCreateByText2Body = zDocumentTextCreatePayload export const zPostDatasetsByDatasetIdDocumentCreateByText2Path = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** @@ -2378,7 +2546,7 @@ export const zPostDatasetsByDatasetIdDocumentCreateByText2Path = z.object({ export const zPostDatasetsByDatasetIdDocumentCreateByText2Response = zDocumentAndBatchResponse export const zGetDatasetsByDatasetIdDocumentsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsQuery = z.object({ @@ -2389,37 +2557,37 @@ export const zGetDatasetsByDatasetIdDocumentsQuery = z.object({ }) /** - * Documents retrieved successfully + * List of documents. */ export const zGetDatasetsByDatasetIdDocumentsResponse = zDocumentListResponse export const zPostDatasetsByDatasetIdDocumentsDownloadZipBody = zDocumentBatchDownloadZipPayload export const zPostDatasetsByDatasetIdDocumentsDownloadZipPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * ZIP archive generated successfully + * ZIP archive containing the requested documents. */ -export const zPostDatasetsByDatasetIdDocumentsDownloadZipResponse = zBinaryFileResponse +export const zPostDatasetsByDatasetIdDocumentsDownloadZipResponse = z.custom() export const zPostDatasetsByDatasetIdDocumentsMetadataBody = zMetadataOperationData export const zPostDatasetsByDatasetIdDocumentsMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Documents metadata updated successfully + * Document metadata updated successfully. */ export const zPostDatasetsByDatasetIdDocumentsMetadataResponse = zDatasetMetadataActionResponse export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBody = zDocumentStatusPayload export const zPatchDatasetsByDatasetIdDocumentsStatusByActionPath = z.object({ - action: z.string(), - dataset_id: z.string(), + action: z.enum(['archive', 'disable', 'enable', 'un_archive']), + dataset_id: z.uuid(), }) /** @@ -2429,28 +2597,28 @@ export const zPatchDatasetsByDatasetIdDocumentsStatusByActionResponse = zSimpleR export const zGetDatasetsByDatasetIdDocumentsByBatchIndexingStatusPath = z.object({ batch: z.string(), - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Indexing status retrieved successfully + * Indexing status for documents in the batch. */ export const zGetDatasetsByDatasetIdDocumentsByBatchIndexingStatusResponse = zDocumentStatusListResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Document deleted successfully + * Success. */ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdQuery = z.object({ @@ -2458,7 +2626,7 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdQuery = z.object({ }) /** - * Document retrieved successfully + * Document details. The response shape varies based on the `metadata` query parameter. When `metadata` is `only`, only `id`, `doc_type`, and `doc_metadata` are returned. When `metadata` is `without`, `doc_type` and `doc_metadata` are omitted. */ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdResponse = zDocumentDetailResponse @@ -2468,8 +2636,8 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdBody = z.object({ }) export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2478,18 +2646,18 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdResponse = zDocumentAndBatchResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Download URL generated successfully + * Download URL generated successfully. */ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdDownloadResponse = zUrlResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.object({ @@ -2500,42 +2668,42 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.objec }) /** - * Segments retrieved successfully + * List of chunks. */ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = zSegmentListResponse export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBody = zSegmentCreatePayload export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Segments created successfully + * Chunks created successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = zSegmentCreateListResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Segment deleted successfully + * Success. */ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Segment retrieved successfully + * Chunk details. */ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = zSegmentDetailResponse @@ -2544,22 +2712,22 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdBod = zSegmentUpdatePayload export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Segment updated successfully + * Chunk updated successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = zSegmentDetailResponse export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksQuery @@ -2570,7 +2738,7 @@ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChil }) /** - * Child chunks retrieved successfully + * List of child chunks. */ export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksResponse = zChildChunkListResponse @@ -2580,27 +2748,27 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Child chunk created successfully + * Child chunk created successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksResponse = zChildChunkDetailResponse export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdPath = z.object({ - child_chunk_id: z.string(), - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + child_chunk_id: z.uuid(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Child chunk deleted successfully + * Success. */ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse = z.void() @@ -2610,14 +2778,14 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdCh export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdPath = z.object({ - child_chunk_id: z.string(), - dataset_id: z.string(), - document_id: z.string(), - segment_id: z.string(), + child_chunk_id: z.uuid(), + dataset_id: z.uuid(), + document_id: z.uuid(), + segment_id: z.uuid(), }) /** - * Child chunk updated successfully + * Child chunk updated successfully. */ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse = zChildChunkDetailResponse @@ -2628,12 +2796,12 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileBody = z.o }) export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFilePath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Document updated successfully + * Document updated successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileResponse = zDocumentAndBatchResponse @@ -2641,12 +2809,12 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFileResponse export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextBody = zDocumentTextUpdate export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextPath = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Document updated successfully + * Document updated successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByTextResponse = zDocumentAndBatchResponse @@ -2657,12 +2825,12 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Body = z. }) export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Path = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** - * Document updated successfully + * Document updated successfully. */ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Response = zDocumentAndBatchResponse @@ -2670,8 +2838,8 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByFile2Response export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText2Body = zDocumentTextUpdate export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText2Path = z.object({ - dataset_id: z.string(), - document_id: z.string(), + dataset_id: z.uuid(), + document_id: z.uuid(), }) /** @@ -2683,78 +2851,78 @@ export const zPostDatasetsByDatasetIdDocumentsByDocumentIdUpdateByText2Response export const zPostDatasetsByDatasetIdHitTestingBody = zHitTestingPayload export const zPostDatasetsByDatasetIdHitTestingPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Hit testing results + * Retrieval results. */ export const zPostDatasetsByDatasetIdHitTestingResponse = zHitTestingResponse export const zGetDatasetsByDatasetIdMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Metadata retrieved successfully + * Metadata fields for the knowledge base. */ export const zGetDatasetsByDatasetIdMetadataResponse = zDatasetMetadataListResponse export const zPostDatasetsByDatasetIdMetadataBody = zMetadataArgs export const zPostDatasetsByDatasetIdMetadataPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Metadata created successfully + * Metadata field created successfully. */ export const zPostDatasetsByDatasetIdMetadataResponse = zDatasetMetadataResponse export const zGetDatasetsByDatasetIdMetadataBuiltInPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Built-in fields retrieved successfully + * Built-in metadata fields. */ export const zGetDatasetsByDatasetIdMetadataBuiltInResponse = zDatasetMetadataBuiltInFieldsResponse export const zPostDatasetsByDatasetIdMetadataBuiltInByActionPath = z.object({ - action: z.string(), - dataset_id: z.string(), + action: z.enum(['disable', 'enable']), + dataset_id: z.uuid(), }) /** - * Action completed successfully + * Built-in metadata field toggled successfully. */ export const zPostDatasetsByDatasetIdMetadataBuiltInByActionResponse = zDatasetMetadataActionResponse export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ - dataset_id: z.string(), - metadata_id: z.string(), + dataset_id: z.uuid(), + metadata_id: z.uuid(), }) /** - * Metadata deleted successfully + * Success. */ export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.void() export const zPatchDatasetsByDatasetIdMetadataByMetadataIdBody = zMetadataUpdatePayload export const zPatchDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ - dataset_id: z.string(), - metadata_id: z.string(), + dataset_id: z.uuid(), + metadata_id: z.uuid(), }) /** - * Metadata updated successfully + * Metadata field updated successfully. */ export const zPatchDatasetsByDatasetIdMetadataByMetadataIdResponse = zDatasetMetadataResponse export const zGetDatasetsByDatasetIdPipelineDatasourcePluginsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) export const zGetDatasetsByDatasetIdPipelineDatasourcePluginsQuery = z.object({ @@ -2762,7 +2930,7 @@ export const zGetDatasetsByDatasetIdPipelineDatasourcePluginsQuery = z.object({ }) /** - * Datasource plugins retrieved successfully + * List of datasource nodes configured in the pipeline. */ export const zGetDatasetsByDatasetIdPipelineDatasourcePluginsResponse = zDatasourcePluginListResponse @@ -2771,12 +2939,12 @@ export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunBody = zDatasourceNodeRunPayload export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), node_id: z.string(), }) /** - * Datasource node run successfully + * Streaming response with node execution events. */ export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponse = zGeneratedAppResponse @@ -2784,83 +2952,89 @@ export const zPostDatasetsByDatasetIdPipelineDatasourceNodesByNodeIdRunResponse export const zPostDatasetsByDatasetIdPipelineRunBody = zPipelineRunApiEntity export const zPostDatasetsByDatasetIdPipelineRunPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Pipeline run successfully + * Pipeline execution result. Format depends on `response_mode`: streaming returns a `text/event-stream`, blocking returns a JSON object. */ export const zPostDatasetsByDatasetIdPipelineRunResponse = zGeneratedAppResponse export const zPostDatasetsByDatasetIdRetrieveBody = zHitTestingPayload export const zPostDatasetsByDatasetIdRetrievePath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Hit testing results + * Retrieval results. */ export const zPostDatasetsByDatasetIdRetrieveResponse = zHitTestingResponse export const zGetDatasetsByDatasetIdTagsPath = z.object({ - dataset_id: z.string(), + dataset_id: z.uuid(), }) /** - * Tags retrieved successfully + * Tags bound to the knowledge base. */ export const zGetDatasetsByDatasetIdTagsResponse = zDatasetBoundTagListResponse export const zGetEndUsersByEndUserIdPath = z.object({ - end_user_id: z.string(), + end_user_id: z.uuid(), }) /** - * End user retrieved successfully + * End user retrieved successfully. */ export const zGetEndUsersByEndUserIdResponse = zEndUserDetail +export const zPostFilesUploadBody = z.object({ + file: z.custom(), + user: z.string().optional(), +}) + /** - * File uploaded successfully + * File uploaded successfully. */ export const zPostFilesUploadResponse = zFileResponse export const zGetFilesByFileIdPreviewPath = z.object({ - file_id: z.string(), + file_id: z.uuid(), }) export const zGetFilesByFileIdPreviewQuery = z.object({ as_attachment: z.boolean().optional().default(false), + user: z.string().optional(), }) /** - * File retrieved successfully + * Returns the raw file content. The `Content-Type` header is set to the file's MIME type. If `as_attachment` is `true`, the file is returned as a download with `Content-Disposition: attachment`. */ -export const zGetFilesByFileIdPreviewResponse = zBinaryFileResponse +export const zGetFilesByFileIdPreviewResponse = z.custom() export const zGetFormHumanInputByFormTokenPath = z.object({ form_token: z.string(), }) /** - * Form retrieved successfully + * Form contents retrieved successfully. */ export const zGetFormHumanInputByFormTokenResponse = zHumanInputFormDefinitionResponse -export const zPostFormHumanInputByFormTokenBody = zHumanInputFormSubmitPayload +export const zPostFormHumanInputByFormTokenBody = zHumanInputFormSubmitPayloadWithUser export const zPostFormHumanInputByFormTokenPath = z.object({ form_token: z.string(), }) /** - * Form submitted successfully + * Form submitted successfully. The response body is an empty object. */ export const zPostFormHumanInputByFormTokenResponse = zHumanInputFormSubmitResponse /** - * Application info retrieved successfully + * Basic information of the application. */ export const zGetInfoResponse = zAppInfoResponse @@ -2868,17 +3042,18 @@ export const zGetMessagesQuery = z.object({ conversation_id: z.string(), first_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), + user: z.string().optional(), }) /** - * Messages retrieved successfully + * Successfully retrieved conversation history. */ export const zGetMessagesResponse = zMessageInfiniteScrollPagination -export const zPostMessagesByMessageIdFeedbacksBody = zMessageFeedbackPayload +export const zPostMessagesByMessageIdFeedbacksBody = zMessageFeedbackPayloadWithUser export const zPostMessagesByMessageIdFeedbacksPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), }) /** @@ -2887,7 +3062,11 @@ export const zPostMessagesByMessageIdFeedbacksPath = z.object({ export const zPostMessagesByMessageIdFeedbacksResponse = zResultResponse export const zGetMessagesByMessageIdSuggestedPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), +}) + +export const zGetMessagesByMessageIdSuggestedQuery = z.object({ + user: z.string(), }) /** @@ -2896,26 +3075,26 @@ export const zGetMessagesByMessageIdSuggestedPath = z.object({ export const zGetMessagesByMessageIdSuggestedResponse = zSimpleResultStringListResponse /** - * Metadata retrieved successfully + * Successfully retrieved application meta information. */ export const zGetMetaResponse = zAppMetaResponse /** - * Parameters retrieved successfully + * Application parameters information. */ export const zGetParametersResponse = zParameters /** - * Site configuration retrieved successfully + * WebApp settings of the application. */ export const zGetSiteResponse = zSite -export const zPostTextToAudioBody = zTextToAudioPayload +export const zPostTextToAudioBody = zTextToAudioPayloadWithUser /** - * Text successfully converted to audio + * Returns the generated audio. Generator responses are streamed by the service as `audio/mpeg`; otherwise the provider output is returned directly. */ -export const zPostTextToAudioResponse = zAudioBinaryResponse +export const zPostTextToAudioResponse = z.custom() export const zGetWorkflowByTaskIdEventsPath = z.object({ task_id: z.string(), @@ -2928,7 +3107,7 @@ export const zGetWorkflowByTaskIdEventsQuery = z.object({ }) /** - * SSE event stream + * Server-Sent Events stream. Each event is delivered as `data: {JSON}\n\n`. Event payloads follow the same schemas as the original streaming response. */ export const zGetWorkflowByTaskIdEventsResponse = zEventStreamResponse @@ -2944,14 +3123,17 @@ export const zGetWorkflowsLogsQuery = z.object({ }) /** - * Logs retrieved successfully + * Successfully retrieved workflow logs. */ export const zGetWorkflowsLogsResponse = zWorkflowAppLogPaginationResponse -export const zPostWorkflowsRunBody = zWorkflowRunPayload +export const zPostWorkflowsRunBody = zWorkflowRunPayloadWithUser /** - * Workflow executed successfully + * Successful response. The content type and structure depend on the `response_mode` parameter in the request. + * + * - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. + * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. */ export const zPostWorkflowsRunResponse = zGeneratedAppResponse @@ -2960,10 +3142,12 @@ export const zGetWorkflowsRunByWorkflowRunIdPath = z.object({ }) /** - * Workflow run details retrieved successfully + * Successfully retrieved workflow run details. */ export const zGetWorkflowsRunByWorkflowRunIdResponse = zWorkflowRunResponse +export const zPostWorkflowsTasksByTaskIdStopBody = zRequiredServiceApiUserPayload + export const zPostWorkflowsTasksByTaskIdStopPath = z.object({ task_id: z.string(), }) @@ -2973,14 +3157,17 @@ export const zPostWorkflowsTasksByTaskIdStopPath = z.object({ */ export const zPostWorkflowsTasksByTaskIdStopResponse = zSimpleResultResponse -export const zPostWorkflowsByWorkflowIdRunBody = zWorkflowRunPayload +export const zPostWorkflowsByWorkflowIdRunBody = zWorkflowRunPayloadWithUser export const zPostWorkflowsByWorkflowIdRunPath = z.object({ workflow_id: z.string(), }) /** - * Workflow executed successfully + * Successful response. The content type and structure depend on the `response_mode` parameter in the request. + * + * - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. + * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. */ export const zPostWorkflowsByWorkflowIdRunResponse = zGeneratedAppResponse @@ -2989,7 +3176,7 @@ export const zGetWorkspacesCurrentModelsModelTypesByModelTypePath = z.object({ }) /** - * Models retrieved successfully + * Available models for the specified type. */ export const zGetWorkspacesCurrentModelsModelTypesByModelTypeResponse = zProviderWithModelsListResponse diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index 47eaed612cf..524942838c3 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -146,7 +146,16 @@ export type ConversationListQuery = { sort_by?: '-created_at' | '-updated_at' | 'created_at' | 'updated_at' } -export type ConversationRenamePayload = { +export type ConversationRenamePayload = ( + | { + auto_generate: true + name?: string | null + } + | { + auto_generate?: false + name: string + } +) & { auto_generate?: boolean name?: string | null } diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index aa96e4b3231..011a9f83054 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -168,13 +168,22 @@ export const zConversationListQuery = z.object({ .default('-updated_at'), }) -/** - * ConversationRenamePayload - */ -export const zConversationRenamePayload = z.object({ - auto_generate: z.boolean().optional().default(false), - name: z.string().nullish(), -}) +export const zConversationRenamePayload = z.intersection( + z.union([ + z.object({ + auto_generate: z.literal(true), + name: z.string().nullish(), + }), + z.object({ + auto_generate: z.literal(false).optional().default(false), + name: z.string().regex(/.*\S.*/), + }), + ]), + z.object({ + auto_generate: z.boolean().optional().default(false), + name: z.string().nullish(), + }), +) /** * EmailCodeLoginSendPayload @@ -954,7 +963,7 @@ export const zGetConversationsQuery = z.object({ export const zGetConversationsResponse = zConversationInfiniteScrollPagination export const zDeleteConversationsByCIdPath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) /** @@ -965,7 +974,7 @@ export const zDeleteConversationsByCIdResponse = z.void() export const zPostConversationsByCIdNameBody = zConversationRenamePayload export const zPostConversationsByCIdNamePath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) export const zPostConversationsByCIdNameQuery = z.object({ @@ -979,7 +988,7 @@ export const zPostConversationsByCIdNameQuery = z.object({ export const zPostConversationsByCIdNameResponse = zSimpleConversation export const zPatchConversationsByCIdPinPath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) /** @@ -988,7 +997,7 @@ export const zPatchConversationsByCIdPinPath = z.object({ export const zPatchConversationsByCIdPinResponse = zResultResponse export const zPatchConversationsByCIdUnpinPath = z.object({ - c_id: z.string(), + c_id: z.uuid(), }) /** @@ -1106,7 +1115,7 @@ export const zGetMessagesResponse = zWebMessageInfiniteScrollPagination export const zPostMessagesByMessageIdFeedbacksBody = zMessageFeedbackPayload export const zPostMessagesByMessageIdFeedbacksPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), }) export const zPostMessagesByMessageIdFeedbacksQuery = z.object({ @@ -1120,7 +1129,7 @@ export const zPostMessagesByMessageIdFeedbacksQuery = z.object({ export const zPostMessagesByMessageIdFeedbacksResponse = zResultResponse export const zGetMessagesByMessageIdMoreLikeThisPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), }) export const zGetMessagesByMessageIdMoreLikeThisQuery = z.object({ @@ -1133,7 +1142,7 @@ export const zGetMessagesByMessageIdMoreLikeThisQuery = z.object({ export const zGetMessagesByMessageIdMoreLikeThisResponse = zGeneratedAppResponse export const zGetMessagesByMessageIdSuggestedQuestionsPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), }) /** @@ -1198,7 +1207,7 @@ export const zPostSavedMessagesQuery = z.object({ export const zPostSavedMessagesResponse = zResultResponse export const zDeleteSavedMessagesByMessageIdPath = z.object({ - message_id: z.string(), + message_id: z.uuid(), }) /** diff --git a/packages/iconify-collections/assets/public/agent/building-blocks.svg b/packages/iconify-collections/assets/public/agent/building-blocks.svg new file mode 100644 index 00000000000..8037f6f0865 --- /dev/null +++ b/packages/iconify-collections/assets/public/agent/building-blocks.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/access-point.svg b/packages/iconify-collections/assets/vender/agent-v2/access-point.svg new file mode 100644 index 00000000000..6f2c2f1926f --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/access-point.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg b/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg new file mode 100644 index 00000000000..1632376e944 --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/plan.svg b/packages/iconify-collections/assets/vender/agent-v2/plan.svg new file mode 100644 index 00000000000..a9456e4806b --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/plan.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg b/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg new file mode 100644 index 00000000000..f0be50ee5fe --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg b/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg new file mode 100644 index 00000000000..0f8617ef3a0 --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/roster-active.svg b/packages/iconify-collections/assets/vender/main-nav/roster-active.svg new file mode 100644 index 00000000000..0d0fbb95fe9 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/roster-active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/roster.svg b/packages/iconify-collections/assets/vender/main-nav/roster.svg new file mode 100644 index 00000000000..5169111425e --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/roster.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/workflow/agent.svg b/packages/iconify-collections/assets/vender/workflow/agent.svg index f30c0b455fc..09bf8c0683c 100644 --- a/packages/iconify-collections/assets/vender/workflow/agent.svg +++ b/packages/iconify-collections/assets/vender/workflow/agent.svg @@ -1,8 +1,6 @@ - - - - - - + + + + diff --git a/packages/iconify-collections/custom-public/icons.json b/packages/iconify-collections/custom-public/icons.json index 7a4ea8bdaf1..0f83cf3b3e8 100644 --- a/packages/iconify-collections/custom-public/icons.json +++ b/packages/iconify-collections/custom-public/icons.json @@ -1,7 +1,10 @@ { "prefix": "custom-public", - "lastModified": 1781246368, + "lastModified": 1781515983, "icons": { + "agent-building-blocks": { + "body": "" + }, "avatar-user": { "body": "", "width": 512, diff --git a/packages/iconify-collections/custom-public/info.json b/packages/iconify-collections/custom-public/info.json index 115e9e25f90..1b439b73724 100644 --- a/packages/iconify-collections/custom-public/info.json +++ b/packages/iconify-collections/custom-public/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-public", "name": "Dify Custom Public", - "total": 144, + "total": 145, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", @@ -13,12 +13,12 @@ "url": "https://github.com/langgenius/dify/blob/main/LICENSE" }, "samples": [ + "agent-building-blocks", "avatar-user", "billing-ar-cube-1", "billing-asterisk", "billing-aws-marketplace-dark", - "billing-aws-marketplace-light", - "billing-azure" + "billing-aws-marketplace-light" ], "palette": false } diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index bf08a795057..048c5af7553 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1,7 +1,29 @@ { "prefix": "custom-vender", - "lastModified": 1781246368, + "lastModified": 1781515983, "icons": { + "agent-v2-access-point": { + "body": "", + "width": 15, + "height": 15 + }, + "agent-v2-end-user-auth": { + "body": "" + }, + "agent-v2-plan": { + "body": "", + "width": 18, + "height": 18 + }, + "agent-v2-prompt-insert": { + "body": "", + "width": 14, + "height": 14 + }, + "agent-v2-robot-3": { + "body": "", + "width": 17 + }, "features-citations": { "body": "", "width": 24, @@ -811,6 +833,16 @@ "width": 24, "height": 24 }, + "main-nav-roster": { + "body": "", + "width": 18, + "height": 18 + }, + "main-nav-roster-active": { + "body": "", + "width": 18, + "height": 18 + }, "main-nav-studio": { "body": "", "width": 20, @@ -1295,9 +1327,9 @@ "height": 24 }, "workflow-agent": { - "body": "", - "width": 16, - "height": 16 + "body": "", + "width": 24, + "height": 24 }, "workflow-answer": { "body": "", diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index cef352ac549..608b5020f04 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 319, + "total": 326, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", @@ -13,12 +13,12 @@ "url": "https://github.com/langgenius/dify/blob/main/LICENSE" }, "samples": [ - "features-citations", - "features-content-moderation", - "features-document", - "features-folder-upload", - "features-love-message", - "features-message-fast" + "agent-v2-access-point", + "agent-v2-end-user-auth", + "agent-v2-plan", + "agent-v2-prompt-insert", + "agent-v2-robot-3", + "features-citations" ], "palette": false } diff --git a/web/.env.example b/web/.env.example index 762db54dafa..112232e529c 100644 --- a/web/.env.example +++ b/web/.env.example @@ -90,6 +90,9 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false # /refine slash commands in the "Go to Anything" command palette) NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +# Enable Agent v2 frontend entry points. +NEXT_PUBLIC_ENABLE_AGENT_V2=false + # The maximum number of tree node depth for workflow NEXT_PUBLIC_MAX_TREE_DEPTH=50 diff --git a/web/AGENTS.md b/web/AGENTS.md index 21def787b9e..6221b2376ae 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -38,6 +38,15 @@ - Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats. - Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated. +## Agent V2 Frontend + +- Keep Agent V2 separate from legacy workflow Agent. Use `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, the `agent_node_kind: 'dify_agent'` and `version: '2'` payload discriminator, and `BlockEnum.AgentV2` where the graph type is already migrated. Do not bridge Agent V2 to legacy `agent_strategy_*` behavior or data shapes. +- Use generated contracts and `consoleQuery` / `consoleClient` from `@/service/client` for Agent V2 backend calls. Do not add handwritten REST helpers, handwritten API types, mock-backed app state, or direct edits to generated contract files. +- Treat TanStack Query as the server source of truth. Scope editable drafts with an instance-level `AgentComposerProvider`, hydrate Jotai `originalDraft`, `publishedDraft`, and `draft` from contract data, and compute dirty or unpublished state from those draft snapshots. +- Keep transitional defaults and mock data at the owning surface, such as the configure page or workflow node, not in shared composer defaults. +- Use `@langgenius/dify-ui/*` primitives and primitive data/CSS selectors first. Add call-site Tailwind only for real design deltas, avoid arbitrary values when token utilities exist, and keep focus rings visible without making inert layout regions focusable. +- Keep Agent V2 copy in the `agentV2` i18n namespace, currently backed by `agent-v-2.json` in the maintained locale set. + ## Automated Test Generation - Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests. diff --git a/web/__tests__/env.spec.ts b/web/__tests__/env.spec.ts new file mode 100644 index 00000000000..89781d32685 --- /dev/null +++ b/web/__tests__/env.spec.ts @@ -0,0 +1,42 @@ +describe('env runtime transport', () => { + const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + vi.doUnmock('../utils/client') + document.body.removeAttribute('data-enable-agent-v2') + document.body.removeAttribute('data-enable-agent-v-2') + delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + }) + + afterAll(() => { + if (originalAgentV2Env === undefined) + delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + else + process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env + }) + + it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => { + document.body.setAttribute('data-enable-agent-v2', 'true') + + const { env } = await import('../env') + + expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true) + }) + + it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => { + process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true' + + vi.doMock('../utils/client', () => ({ + isClient: false, + isServer: true, + })) + + const { getDatasetMap } = await import('../env') + const datasetMap = getDatasetMap() + + expect(datasetMap['data-enable-agent-v2']).toBe(true) + expect(datasetMap['data-enable-agent-v-2']).toBeUndefined() + }) +}) diff --git a/web/app/(commonLayout)/__tests__/role-route-guard.spec.tsx b/web/app/(commonLayout)/__tests__/role-route-guard.spec.tsx index f26723d0199..761d8c1c8bf 100644 --- a/web/app/(commonLayout)/__tests__/role-route-guard.spec.tsx +++ b/web/app/(commonLayout)/__tests__/role-route-guard.spec.tsx @@ -47,14 +47,14 @@ vi.mock('@/service/client', () => ({ const mockUseQuery = vi.mocked(useQuery) -function renderGuard(children: ReactNode) { +function renderGuard(children: ReactNode, systemFeatures: { enable_app_deploy?: boolean } = {}) { return renderWithSystemFeatures( {children} , { systemFeatures: { - enable_app_deploy: true, + enable_app_deploy: systemFeatures.enable_app_deploy ?? true, }, }, ) @@ -95,6 +95,16 @@ describe('RoleRouteGuard', () => { expect(mocks.redirect).toHaveBeenCalledWith('/datasets') }) + it('should allow dataset operator on routes outside the guarded list', () => { + mockPathname = '/new-route' + setCurrentWorkspaceQuery({ role: 'dataset_operator' }) + + renderGuard(
content
) + + expect(screen.getByText('content')).toBeInTheDocument() + expect(mocks.redirect).not.toHaveBeenCalled() + }) + it('should redirect dataset operator on deployments routes', () => { mockPathname = '/deployments/create' setCurrentWorkspaceQuery({ role: 'dataset_operator' }) @@ -104,6 +114,24 @@ describe('RoleRouteGuard', () => { expect(mocks.redirect).toHaveBeenCalledWith('/datasets') }) + it('should prefer app deploy redirect when app deploy is disabled', () => { + mockPathname = '/deployments/create' + setCurrentWorkspaceQuery({ role: 'dataset_operator' }) + + expect(() => renderGuard(
content
, { enable_app_deploy: false })).toThrow('NEXT_REDIRECT:/') + + expect(mocks.redirect).toHaveBeenCalledWith('/') + }) + + it('should redirect app deploy routes when app deploy is disabled', () => { + mockPathname = '/deployments/create' + setCurrentWorkspaceQuery({ role: 'editor' }) + + expect(() => renderGuard(
content
, { enable_app_deploy: false })).toThrow('NEXT_REDIRECT:/') + + expect(mocks.redirect).toHaveBeenCalledWith('/') + }) + it('should allow dataset operator on non-guarded routes', () => { mockPathname = '/plugins' setCurrentWorkspaceQuery({ role: 'dataset_operator' }) diff --git a/web/app/(commonLayout)/role-route-guard.tsx b/web/app/(commonLayout)/role-route-guard.tsx index 05cf59aee42..fc81c091716 100644 --- a/web/app/(commonLayout)/role-route-guard.tsx +++ b/web/app/(commonLayout)/role-route-guard.tsx @@ -7,7 +7,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { redirect, usePathname } from '@/next/navigation' import { consoleQuery } from '@/service/client' -const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/explore', '/tools', '/integrations'] as const +const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const function isPathUnderRoute(pathname: string, route: string) { return pathname === route || pathname.startsWith(`${route}/`) @@ -30,7 +30,7 @@ export function RoleRouteGuard({ children }: { children: ReactNode }) { return if (shouldRedirectAppDeploy) - redirect('/apps') + redirect('/') if (shouldRedirectDatasetOperator) redirect('/datasets') diff --git a/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts b/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts new file mode 100644 index 00000000000..343e0797776 --- /dev/null +++ b/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts @@ -0,0 +1,41 @@ +const mocks = vi.hoisted(() => ({ + agentV2Enabled: true, + notFound: vi.fn(() => { + throw new Error('NEXT_NOT_FOUND') + }), +})) + +vi.mock('@/env', () => ({ + env: { + get NEXT_PUBLIC_ENABLE_AGENT_V2() { + return mocks.agentV2Enabled + }, + }, +})) + +vi.mock('@/next/navigation', () => ({ + notFound: () => mocks.notFound(), +})) + +describe('guardAgentV2Route', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.agentV2Enabled = true + }) + + it('should allow roster routes when Agent v2 is enabled', async () => { + const { guardAgentV2Route } = await import('../feature-guard') + + expect(() => guardAgentV2Route()).not.toThrow() + expect(mocks.notFound).not.toHaveBeenCalled() + }) + + it('should throw notFound when Agent v2 is disabled', async () => { + mocks.agentV2Enabled = false + + const { guardAgentV2Route } = await import('../feature-guard') + + expect(() => guardAgentV2Route()).toThrow('NEXT_NOT_FOUND') + expect(mocks.notFound).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx b/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx new file mode 100644 index 00000000000..0d4dbc66dc2 --- /dev/null +++ b/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react' + +const mocks = vi.hoisted(() => ({ + guardAgentV2Route: vi.fn(), +})) + +vi.mock('../feature-guard', () => ({ + guardAgentV2Route: () => mocks.guardAgentV2Route(), +})) + +describe('RosterLayout', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render children when Agent v2 is enabled', async () => { + const { default: RosterLayout } = await import('../layout') + + render( + +
Roster content
+
, + ) + + expect(mocks.guardAgentV2Route).toHaveBeenCalledTimes(1) + expect(screen.getByText('Roster content')).toBeInTheDocument() + }) + + it('should block rendering when the roster guard throws notFound', async () => { + mocks.guardAgentV2Route.mockImplementation(() => { + throw new Error('NEXT_NOT_FOUND') + }) + + const { default: RosterLayout } = await import('../layout') + + expect(() => render( + +
Roster content
+
, + )).toThrow('NEXT_NOT_FOUND') + expect(mocks.guardAgentV2Route).toHaveBeenCalled() + }) +}) diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx new file mode 100644 index 00000000000..31d2fac8652 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx new file mode 100644 index 00000000000..a8a25ba6c3f --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx new file mode 100644 index 00000000000..384443d58bc --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react' +import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout' + +type LayoutProps = { + children: ReactNode + params: Promise<{ agentId: string }> +} + +export default async function Layout({ + children, + params, +}: LayoutProps) { + const { agentId } = await params + + return ( + + {children} + + ) +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx new file mode 100644 index 00000000000..1cbcd7bbd90 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx new file mode 100644 index 00000000000..ad61db5f8b8 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx new file mode 100644 index 00000000000..470fa13f12c --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from '@/next/navigation' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + redirect(`/roster/agent/${agentId}/configure`) +} diff --git a/web/app/(commonLayout)/roster/feature-guard.ts b/web/app/(commonLayout)/roster/feature-guard.ts new file mode 100644 index 00000000000..41f74b6696e --- /dev/null +++ b/web/app/(commonLayout)/roster/feature-guard.ts @@ -0,0 +1,7 @@ +import { env } from '@/env' +import { notFound } from '@/next/navigation' + +export const guardAgentV2Route = () => { + if (!env.NEXT_PUBLIC_ENABLE_AGENT_V2) + notFound() +} diff --git a/web/app/(commonLayout)/roster/layout.tsx b/web/app/(commonLayout)/roster/layout.tsx new file mode 100644 index 00000000000..be88ab89e2c --- /dev/null +++ b/web/app/(commonLayout)/roster/layout.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from 'react' +import { guardAgentV2Route } from './feature-guard' + +export default function Layout({ + children, +}: { + children: ReactNode +}) { + guardAgentV2Route() + + return children +} diff --git a/web/app/(commonLayout)/roster/page.tsx b/web/app/(commonLayout)/roster/page.tsx new file mode 100644 index 00000000000..fff610b6448 --- /dev/null +++ b/web/app/(commonLayout)/roster/page.tsx @@ -0,0 +1,5 @@ +import RosterPage from '@/features/agent-v2/roster/page' + +export default function Page() { + return +} diff --git a/web/app/(shareLayout)/agent/[token]/page.tsx b/web/app/(shareLayout)/agent/[token]/page.tsx new file mode 100644 index 00000000000..5dc2777c9c4 --- /dev/null +++ b/web/app/(shareLayout)/agent/[token]/page.tsx @@ -0,0 +1,15 @@ +'use client' + +import * as React from 'react' +import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' + +function Agent() { + return ( + + + + ) +} + +export default React.memo(Agent) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx index 083e93b086d..cd72279fe38 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx @@ -186,6 +186,20 @@ describe('SettingBuiltInTool', () => { expect(screen.getByTestId('mock-form')).toBeInTheDocument() }) + it('should render a masked drawer with balanced vertical offsets', async () => { + const { baseElement } = renderComponent() + await waitFor(() => { + expect(screen.getByTestId('mock-form')).toBeInTheDocument() + }) + + expect(baseElement.querySelector('.bg-background-overlay')).toBeInTheDocument() + const drawerPopup = baseElement.querySelector('[role="dialog"]') + expect(drawerPopup).toHaveClass( + 'data-[swipe-direction=right]:top-6', + 'data-[swipe-direction=right]:bottom-6', + ) + }) + it('should call onSave with updated values when save button clicked', async () => { const { onSave } = renderComponent() await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument()) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index ccfbd3be14b..113ef2a1c40 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -181,9 +181,9 @@ const SettingBuiltInTool: FC = ({ }} > - + - + {isLoading && } {!isLoading && ( diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index 45647160a01..8b1d7b106f8 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -19,6 +19,7 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' type ISelectDataSetProps = { isShow: boolean + modal?: boolean onClose: () => void selectedIds: string[] onSelect: (dataSet: DataSet[]) => void @@ -26,6 +27,7 @@ type ISelectDataSetProps = { const SelectDataSet: FC = ({ isShow, + modal, onClose, selectedIds, onSelect, @@ -90,8 +92,8 @@ const SelectDataSet: FC = ({ }, [handleClose]) return ( - - + + {t('feature.dataSet.selectTitle', { ns: 'appDebug' })} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index ba11de128db..5350bb04474 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -30,6 +30,7 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section' type SettingsModalProps = { currentDataset: DataSet + height?: string onCancel: () => void onSave: (newDataset: DataSet) => void } @@ -44,6 +45,7 @@ const labelClass = ` const SettingsModal: FC = ({ currentDataset, + height = 'calc(100vh - 72px)', onCancel, onSave, }) => { @@ -186,9 +188,9 @@ const SettingsModal: FC = ({ return (
diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 1010720e838..fa83560c0ce 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -8,10 +8,18 @@ import { useHover } from 'ahooks' import { cva } from 'class-variance-authority' import { init } from 'emoji-mart' import * as React from 'react' -import { useRef } from 'react' +import { useRef, useSyncExternalStore } from 'react' init({ data }) +const subscribeHydrationState = () => () => {} + +const useIsHydrated = () => useSyncExternalStore( + subscribeHydrationState, + () => true, + () => false, +) + type AppIconProps = { size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' rounded?: boolean @@ -105,9 +113,20 @@ const AppIcon: FC = ({ }) => { const isValidImageIcon = iconType === 'image' && imageUrl const emojiIcon = (icon && icon !== '') ? icon : '🤖' - const Icon = + const isHydrated = useIsHydrated() + const Icon = isHydrated ? : emojiIcon const wrapperRef = useRef(null) const isHovering = useHover(wrapperRef) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!onClick) + return + + if (event.key !== 'Enter' && event.key !== ' ') + return + + event.preventDefault() + onClick() + } return ( = ({ className={cn(appIconVariants({ size, rounded }), className)} style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }} onClick={onClick} + onKeyDown={onClick ? handleKeyDown : undefined} + role={onClick ? 'button' : undefined} + tabIndex={onClick ? 0 : undefined} > { isValidImageIcon diff --git a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx index 4947d2c7c01..7946523c7ba 100644 --- a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx @@ -57,6 +57,7 @@ const renderPanel = (props: Partial<{ onClose: () => void inWorkflow: boolean showFileUpload: boolean + showAnnotationReply: boolean }> = {}) => { return render( @@ -68,6 +69,7 @@ const renderPanel = (props: Partial<{ onClose={props.onClose ?? vi.fn()} inWorkflow={props.inWorkflow} showFileUpload={props.showFileUpload} + showAnnotationReply={props.showAnnotationReply} /> , ) @@ -191,5 +193,11 @@ describe('NewFeaturePanel', () => { expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument() }) + + it('should not render AnnotationReply when showAnnotationReply is false', () => { + renderPanel({ isChatMode: true, inWorkflow: false, showAnnotationReply: false }) + + expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument() + }) }) }) diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index 3c8f441521a..2cb21d9382c 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import type { OnFeaturesChange } from '@/app/components/base/features/types' import type { InputVar } from '@/app/components/workflow/types' import type { PromptVariable } from '@/models/debug' @@ -26,9 +27,14 @@ type Props = Readonly<{ onClose: () => void inWorkflow?: boolean showFileUpload?: boolean + showModeration?: boolean + showAnnotationReply?: boolean promptVariables?: PromptVariable[] workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void + title?: ReactNode + description?: ReactNode + drawerClassName?: string }> const NewFeaturePanel = ({ @@ -39,9 +45,14 @@ const NewFeaturePanel = ({ onClose, inWorkflow = true, showFileUpload = true, + showModeration = true, + showAnnotationReply = true, promptVariables, workflowVariables, onAutoAddPromptVariable, + title, + description, + drawerClassName, }: Props) => { const { t } = useTranslation() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) @@ -52,13 +63,14 @@ const NewFeaturePanel = ({ show={show} onClose={onClose} inWorkflow={inWorkflow} + className={drawerClassName} >
{/* header */}
-
{t('common.features', { ns: 'workflow' })}
-
{t('common.featuresDescription', { ns: 'workflow' })}
+
{title ?? t('common.features', { ns: 'workflow' })}
+
{description ?? t('common.featuresDescription', { ns: 'workflow' })}
)} - {(isChatMode || !inWorkflow) && } - {!inWorkflow && isChatMode && ( + {showModeration && (isChatMode || !inWorkflow) && } + {showAnnotationReply && !inWorkflow && isChatMode && ( )}
diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx index 4f55e6eb7b2..c6359f3713b 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx @@ -302,6 +302,37 @@ describe('ModerationSettingModal', () => { })) }) + it('should save the latest preset response when content textarea changes', async () => { + const data: ModerationConfig = { + ...defaultData, + config: { + keywords: 'bad', + inputs_config: { enabled: true, preset_response: 'blocked' }, + outputs_config: { enabled: false, preset_response: '' }, + }, + } + await renderModal( + , + ) + + fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), { + target: { value: 'updated blocked response' }, + }) + fireEvent.click(screen.getByText(/operation\.save/)) + + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ + config: expect.objectContaining({ + inputs_config: expect.objectContaining({ + preset_response: 'updated blocked response', + }), + }), + })) + }) + it('should show api selector when api type is selected', async () => { await renderModal( { expect(mockSetShowAccountSettingModal).toHaveBeenCalled() - expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' }) + expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ + payload: 'provider', + onCancelCallback: expect.any(Function), + }) }) it('should not save when OpenAI type is selected but not configured', async () => { diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx index 9df107bf848..b008490ae6f 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { ModerationContentConfig } from '@/models/debug' import { Switch } from '@langgenius/dify-ui/switch' import { Textarea } from '@langgenius/dify-ui/textarea' +import { useState } from 'react' import { useTranslation } from 'react-i18next' type ModerationContentProps = { @@ -19,57 +20,71 @@ const ModerationContent: FC = ({ onConfigChange, }) => { const { t } = useTranslation() + const [presetResponse, setPresetResponse] = useState(config.preset_response || '') const handleConfigChange = (field: string, value: boolean | string) => { if (field === 'preset_response' && typeof value === 'string') value = value.slice(0, 100) - onConfigChange({ ...config, [field]: value }) + + onConfigChange({ + ...config, + preset_response: field === 'preset_response' ? value as string : presetResponse, + [field]: value, + }) + } + + const handlePresetResponseChange = (value: string) => { + const nextValue = value.slice(0, 100) + setPresetResponse(nextValue) + handleConfigChange('preset_response', nextValue) } return ( -
-
-
-
{title}
-
- { - info && ( -
{info}
- ) - } - handleConfigChange('enabled', v)} - /> -
+
+
+
{title}
+
+ { + info && ( +
{info}
+ ) + } + handleConfigChange('enabled', v)} + />
- { - config.enabled && showPreset && ( -
-
+
+ { + config.enabled && showPreset && ( +
+
+ {t('feature.moderation.modal.content.preset', { ns: 'appDebug' })} - {t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })} -
- {/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */} -
-