fix(agent): support agent-id chat and inline draft bindings (#37483)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123 2026-06-16 14:04:30 +08:00 committed by GitHub
parent 30506f7221
commit b4e3a9095b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1853 additions and 241 deletions

View File

@ -1,5 +1,6 @@
import logging
from typing import Any, Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
@ -10,6 +11,7 @@ import services
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.schema import 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.error import (
AppUnavailableError,
CompletionRequestError,
@ -23,6 +25,7 @@ from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
setup_required,
with_current_tenant_id,
with_current_user,
with_current_user_id,
)
@ -186,51 +189,27 @@ class ChatMessageApi(Resource):
@edit_permission_required
@with_current_user
def post(self, current_user: Account, app_model: App):
raw_payload = console_ns.payload or {}
args_model = ChatMessagePayload.model_validate(raw_payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
return _create_chat_message(current_user=current_user, app_model=app_model)
streaming = _resolve_debugger_chat_streaming(
app_mode=AppMode.value_of(app_model.mode),
response_mode=args_model.response_mode,
response_mode_provided=isinstance(raw_payload, dict) and "response_mode" in raw_payload,
)
if AppMode.value_of(app_model.mode) == AppMode.AGENT:
args["response_mode"] = "streaming"
args["auto_generate_name"] = False
external_trace_id = get_external_trace_id(request)
if external_trace_id:
args["external_trace_id"] = external_trace_id
try:
response = AppGenerateService.generate(
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
)
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeRateLimitError as ex:
raise InvokeRateLimitHttpError(ex.description)
except InvokeError as e:
raise CompletionRequestError(e.description)
except ValueError as e:
raise e
except Exception as e:
logger.exception("internal server error.")
raise InternalServerError()
@console_ns.route("/agent/<uuid:agent_id>/chat-messages")
class AgentChatMessageApi(Resource):
@console_ns.doc("create_agent_chat_message")
@console_ns.doc(description="Generate an Agent App chat message for debugging")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.expect(console_ns.models[ChatMessagePayload.__name__])
@console_ns.response(200, "Chat message generated successfully", console_ns.models[GeneratedAppResponse.__name__])
@console_ns.response(400, "Invalid request parameters")
@console_ns.response(404, "Agent or conversation not found")
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _create_chat_message(current_user=current_user, app_model=app_model)
@console_ns.route("/apps/<uuid:app_id>/chat-messages/<string:task_id>/stop")
@ -245,12 +224,79 @@ class ChatMessageStopApi(Resource):
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@with_current_user_id
def post(self, current_user_id: str, app_model: App, task_id: str):
return _stop_chat_message(current_user_id=current_user_id, app_model=app_model, task_id=task_id)
AppTaskService.stop_task(
task_id=task_id,
invoke_from=InvokeFrom.DEBUGGER,
user_id=current_user_id,
app_mode=AppMode.value_of(app_model.mode),
@console_ns.route("/agent/<uuid:agent_id>/chat-messages/<string:task_id>/stop")
class AgentChatMessageStopApi(Resource):
@console_ns.doc("stop_agent_chat_message")
@console_ns.doc(description="Stop a running Agent App chat message generation")
@console_ns.doc(params={"agent_id": "Agent ID", "task_id": "Task ID to stop"})
@console_ns.response(200, "Task stopped successfully", console_ns.models[SimpleResultResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user_id: str, agent_id: UUID, task_id: str):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _stop_chat_message(current_user_id=current_user_id, app_model=app_model, task_id=task_id)
def _create_chat_message(*, current_user: Account, app_model: App):
raw_payload = console_ns.payload or {}
args_model = ChatMessagePayload.model_validate(raw_payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
streaming = _resolve_debugger_chat_streaming(
app_mode=AppMode.value_of(app_model.mode),
response_mode=args_model.response_mode,
response_mode_provided=isinstance(raw_payload, dict) and "response_mode" in raw_payload,
)
if AppMode.value_of(app_model.mode) == AppMode.AGENT:
args["response_mode"] = "streaming"
args["auto_generate_name"] = False
external_trace_id = get_external_trace_id(request)
if external_trace_id:
args["external_trace_id"] = external_trace_id
try:
response = AppGenerateService.generate(
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
)
return {"result": "success"}, 200
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
except services.errors.conversation.ConversationCompletedError:
raise ConversationCompletedError()
except services.errors.app_model_config.AppModelConfigBrokenError:
logger.exception("App model config broken.")
raise AppUnavailableError()
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeRateLimitError as ex:
raise InvokeRateLimitHttpError(ex.description)
except InvokeError as e:
raise CompletionRequestError(e.description)
except ValueError as e:
raise e
except Exception as e:
logger.exception("internal server error.")
raise InternalServerError()
def _stop_chat_message(*, current_user_id: str, app_model: App, task_id: str):
AppTaskService.stop_task(
task_id=task_id,
invoke_from=InvokeFrom.DEBUGGER,
user_id=current_user_id,
app_mode=AppMode.value_of(app_model.mode),
)
return {"result": "success"}, 200

View File

@ -13,6 +13,7 @@ from controllers.common.controller_schemas import MessageFeedbackPayload as _Mes
from controllers.common.fields import SimpleResultResponse, TextFileResponse
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.error import (
CompletionRequestError,
ProviderModelCurrentlyNotSupportError,
@ -25,6 +26,7 @@ from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
setup_required,
with_current_tenant_id,
with_current_user,
)
from core.app.entities.app_invoke_entities import InvokeFrom
@ -183,67 +185,25 @@ class ChatMessageListApi(Resource):
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@edit_permission_required
def get(self, app_model: App):
args = ChatMessagesQuery.model_validate(request.args.to_dict())
return _list_chat_messages(app_model=app_model)
conversation = db.session.scalar(
select(Conversation)
.where(Conversation.id == args.conversation_id, Conversation.app_id == app_model.id)
.limit(1)
)
if not conversation:
raise NotFound("Conversation Not Exists.")
if args.first_id:
first_message = db.session.scalar(
select(Message).where(Message.conversation_id == conversation.id, Message.id == args.first_id).limit(1)
)
if not first_message:
raise NotFound("First message not found")
history_messages = db.session.scalars(
select(Message)
.where(
Message.conversation_id == conversation.id,
Message.created_at < first_message.created_at,
Message.id != first_message.id,
)
.order_by(Message.created_at.desc())
.limit(args.limit)
).all()
else:
history_messages = db.session.scalars(
select(Message)
.where(Message.conversation_id == conversation.id)
.order_by(Message.created_at.desc())
.limit(args.limit)
).all()
# Initialize has_more based on whether we have a full page
if len(history_messages) == args.limit:
current_page_first_message = history_messages[-1]
# Check if there are more messages before the current page
has_more = db.session.scalar(
select(
exists().where(
Message.conversation_id == conversation.id,
Message.created_at < current_page_first_message.created_at,
Message.id != current_page_first_message.id,
)
)
)
else:
# If we don't have a full page, there are no more messages
has_more = False
history_messages = list(reversed(history_messages))
attach_message_extra_contents(history_messages)
return MessageInfiniteScrollPaginationResponse.model_validate(
InfiniteScrollPagination(data=history_messages, limit=args.limit, has_more=has_more),
from_attributes=True,
).model_dump(mode="json")
@console_ns.route("/agent/<uuid:agent_id>/chat-messages")
class AgentChatMessageListApi(Resource):
@console_ns.doc("list_agent_chat_messages")
@console_ns.doc(description="Get Agent App chat messages for a conversation with pagination")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.doc(params=query_params_from_model(ChatMessagesQuery))
@console_ns.response(200, "Success", console_ns.models[MessageInfiniteScrollPaginationResponse.__name__])
@console_ns.response(404, "Agent or conversation not found")
@login_required
@account_initialization_required
@setup_required
@edit_permission_required
@with_current_tenant_id
def get(self, current_tenant_id: str, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _list_chat_messages(app_model=app_model)
@console_ns.route("/apps/<uuid:app_id>/feedbacks")
@ -261,44 +221,25 @@ class MessageFeedbackApi(Resource):
@account_initialization_required
@with_current_user
def post(self, current_user: Account, app_model: App):
args = MessageFeedbackPayload.model_validate(console_ns.payload)
return _update_message_feedback(current_user=current_user, app_model=app_model)
message_id = str(args.message_id)
message = db.session.scalar(
select(Message).where(Message.id == message_id, Message.app_id == app_model.id).limit(1)
)
if not message:
raise NotFound("Message Not Exists.")
feedback = message.admin_feedback
if not args.rating and feedback:
db.session.delete(feedback)
elif args.rating and feedback:
feedback.rating = FeedbackRating(args.rating)
feedback.content = args.content
elif not args.rating and not feedback:
raise ValueError("rating cannot be None when feedback not exists")
else:
rating_value = args.rating
if rating_value is None:
raise ValueError("rating is required to create feedback")
feedback = MessageFeedback(
app_id=app_model.id,
conversation_id=message.conversation_id,
message_id=message.id,
rating=FeedbackRating(rating_value),
content=args.content,
from_source=FeedbackFromSource.ADMIN,
from_account_id=current_user.id,
)
db.session.add(feedback)
db.session.commit()
return {"result": "success"}
@console_ns.route("/agent/<uuid:agent_id>/feedbacks")
class AgentMessageFeedbackApi(Resource):
@console_ns.doc("create_agent_message_feedback")
@console_ns.doc(description="Create or update Agent App message feedback")
@console_ns.doc(params={"agent_id": "Agent ID"})
@console_ns.expect(console_ns.models[MessageFeedbackPayload.__name__])
@console_ns.response(200, "Feedback updated successfully", console_ns.models[SimpleResultResponse.__name__])
@console_ns.response(404, "Agent or message not found")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, agent_id: UUID):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _update_message_feedback(current_user=current_user, app_model=app_model)
@console_ns.route("/apps/<uuid:app_id>/annotations/count")
@ -340,31 +281,28 @@ class MessageSuggestedQuestionApi(Resource):
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
@with_current_user
def get(self, current_user: Account, app_model: App, message_id: UUID):
message_id_str = str(message_id)
return _get_message_suggested_questions(current_user=current_user, app_model=app_model, message_id=message_id)
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model, message_id=message_id_str, user=current_user, invoke_from=InvokeFrom.DEBUGGER
)
except MessageNotExistsError:
raise NotFound("Message not found")
except ConversationNotExistsError:
raise NotFound("Conversation not found")
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e:
raise CompletionRequestError(e.description)
except SuggestedQuestionsAfterAnswerDisabledError:
raise AppSuggestedQuestionsAfterAnswerDisabledError()
except Exception:
logger.exception("internal server error.")
raise InternalServerError()
return {"data": questions}
@console_ns.route("/agent/<uuid:agent_id>/chat-messages/<uuid:message_id>/suggested-questions")
class AgentMessageSuggestedQuestionApi(Resource):
@console_ns.doc("get_agent_message_suggested_questions")
@console_ns.doc(description="Get suggested questions for an Agent App message")
@console_ns.doc(params={"agent_id": "Agent ID", "message_id": "Message ID"})
@console_ns.response(
200,
"Suggested questions retrieved successfully",
console_ns.models[SuggestedQuestionsResponse.__name__],
)
@console_ns.response(404, "Agent, message, or conversation not found")
@setup_required
@login_required
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account, agent_id: UUID, message_id: UUID):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _get_message_suggested_questions(current_user=current_user, app_model=app_model, message_id=message_id)
@console_ns.route("/apps/<uuid:app_id>/feedbacks/export")
@ -423,14 +361,167 @@ class MessageApi(Resource):
@login_required
@account_initialization_required
def get(self, app_model: App, message_id: UUID):
message_id_str = str(message_id)
return _get_message_detail(app_model=app_model, message_id=message_id)
message = db.session.scalar(
select(Message).where(Message.id == message_id_str, Message.app_id == app_model.id).limit(1)
@console_ns.route("/agent/<uuid:agent_id>/messages/<uuid:message_id>")
class AgentMessageApi(Resource):
@console_ns.doc("get_agent_message")
@console_ns.doc(description="Get Agent App message details by ID")
@console_ns.doc(params={"agent_id": "Agent ID", "message_id": "Message ID"})
@console_ns.response(200, "Message retrieved successfully", console_ns.models[MessageDetailResponse.__name__])
@console_ns.response(404, "Agent or message not found")
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, current_tenant_id: str, agent_id: UUID, message_id: UUID):
app_model = resolve_agent_app_model(tenant_id=current_tenant_id, agent_id=agent_id)
return _get_message_detail(app_model=app_model, message_id=message_id)
def _list_chat_messages(*, app_model: App):
args = ChatMessagesQuery.model_validate(request.args.to_dict())
conversation = db.session.scalar(
select(Conversation)
.where(Conversation.id == args.conversation_id, Conversation.app_id == app_model.id)
.limit(1)
)
if not conversation:
raise NotFound("Conversation Not Exists.")
if args.first_id:
first_message = db.session.scalar(
select(Message).where(Message.conversation_id == conversation.id, Message.id == args.first_id).limit(1)
)
if not message:
raise NotFound("Message Not Exists.")
if not first_message:
raise NotFound("First message not found")
attach_message_extra_contents([message])
return MessageDetailResponse.model_validate(message, from_attributes=True).model_dump(mode="json")
history_messages = db.session.scalars(
select(Message)
.where(
Message.conversation_id == conversation.id,
Message.created_at < first_message.created_at,
Message.id != first_message.id,
)
.order_by(Message.created_at.desc())
.limit(args.limit)
).all()
else:
history_messages = db.session.scalars(
select(Message)
.where(Message.conversation_id == conversation.id)
.order_by(Message.created_at.desc())
.limit(args.limit)
).all()
# Initialize has_more based on whether we have a full page
if len(history_messages) == args.limit:
current_page_first_message = history_messages[-1]
# Check if there are more messages before the current page
has_more = db.session.scalar(
select(
exists().where(
Message.conversation_id == conversation.id,
Message.created_at < current_page_first_message.created_at,
Message.id != current_page_first_message.id,
)
)
)
else:
# If we don't have a full page, there are no more messages
has_more = False
history_messages = list(reversed(history_messages))
attach_message_extra_contents(history_messages)
return MessageInfiniteScrollPaginationResponse.model_validate(
InfiniteScrollPagination(data=history_messages, limit=args.limit, has_more=has_more),
from_attributes=True,
).model_dump(mode="json")
def _update_message_feedback(*, current_user: Account, app_model: App):
args = MessageFeedbackPayload.model_validate(console_ns.payload)
message_id = str(args.message_id)
message = db.session.scalar(
select(Message).where(Message.id == message_id, Message.app_id == app_model.id).limit(1)
)
if not message:
raise NotFound("Message Not Exists.")
feedback = message.admin_feedback
if not args.rating and feedback:
db.session.delete(feedback)
elif args.rating and feedback:
feedback.rating = FeedbackRating(args.rating)
feedback.content = args.content
elif not args.rating and not feedback:
raise ValueError("rating cannot be None when feedback not exists")
else:
rating_value = args.rating
if rating_value is None:
raise ValueError("rating is required to create feedback")
feedback = MessageFeedback(
app_id=app_model.id,
conversation_id=message.conversation_id,
message_id=message.id,
rating=FeedbackRating(rating_value),
content=args.content,
from_source=FeedbackFromSource.ADMIN,
from_account_id=current_user.id,
)
db.session.add(feedback)
db.session.commit()
return {"result": "success"}
def _get_message_suggested_questions(*, current_user: Account, app_model: App, message_id: UUID):
message_id_str = str(message_id)
try:
questions = MessageService.get_suggested_questions_after_answer(
app_model=app_model, message_id=message_id_str, user=current_user, invoke_from=InvokeFrom.DEBUGGER
)
except MessageNotExistsError:
raise NotFound("Message not found")
except ConversationNotExistsError:
raise NotFound("Conversation not found")
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e:
raise CompletionRequestError(e.description)
except SuggestedQuestionsAfterAnswerDisabledError:
raise AppSuggestedQuestionsAfterAnswerDisabledError()
except Exception:
logger.exception("internal server error.")
raise InternalServerError()
return {"data": questions}
def _get_message_detail(*, app_model: App, message_id: UUID):
message_id_str = str(message_id)
message = db.session.scalar(
select(Message).where(Message.id == message_id_str, Message.app_id == app_model.id).limit(1)
)
if not message:
raise NotFound("Message Not Exists.")
attach_message_extra_contents([message])
return MessageDetailResponse.model_validate(message, from_attributes=True).model_dump(mode="json")

View File

@ -392,6 +392,58 @@ Check if activation token is valid
| 400 | Invalid request parameters | |
| 403 | Insufficient permissions | |
### [GET] /agent/{agent_id}/chat-messages
Get Agent App chat messages for a conversation with pagination
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| 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, <br>**Default:** 20 |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [MessageInfiniteScrollPaginationResponse](#messageinfinitescrollpaginationresponse)<br> |
| 404 | Agent or conversation not found | |
### [GET] /agent/{agent_id}/chat-messages/{message_id}/suggested-questions
Get suggested questions for an Agent App message
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| message_id | path | Message ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Suggested questions retrieved successfully | **application/json**: [SuggestedQuestionsResponse](#suggestedquestionsresponse)<br> |
| 404 | Agent, message, or conversation not found | |
### [POST] /agent/{agent_id}/chat-messages/{task_id}/stop
Stop a running Agent App chat message generation
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| task_id | path | Task ID to stop | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
### [GET] /agent/{agent_id}/composer
#### Parameters
@ -527,6 +579,28 @@ Update an Agent App's presentation features (opener, follow-up, citations, ...)
| 400 | Invalid configuration | |
| 404 | Agent not found | |
### [POST] /agent/{agent_id}/feedbacks
Create or update Agent App message feedback
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
#### Request Body
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)<br> |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Feedback updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)<br> |
| 404 | Agent or message not found | |
### [DELETE] /agent/{agent_id}/files
Delete one Agent App drive file by key
@ -564,6 +638,23 @@ Commit an uploaded file into the Agent App drive under files/<name>
| ---- | ----------- | ------ |
| 201 | File committed into the agent drive | **application/json**: [AgentDriveFileCommitResponse](#agentdrivefilecommitresponse)<br> |
### [GET] /agent/{agent_id}/messages/{message_id}
Get Agent App message details by ID
#### Parameters
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| agent_id | path | Agent ID | Yes | string |
| message_id | path | Message ID | Yes | string |
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Message retrieved successfully | **application/json**: [MessageDetailResponse](#messagedetailresponse)<br> |
| 404 | Agent or message not found | |
### [GET] /agent/{agent_id}/referencing-workflows
List workflow apps that reference this Agent App's bound Agent (read-only)

View File

@ -9,7 +9,14 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from core.workflow.nodes.agent_v2.validators import WorkflowAgentNodeValidator
from models.agent import Agent, AgentScope, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding
from models.agent import (
Agent,
AgentConfigSnapshot,
AgentScope,
AgentStatus,
WorkflowAgentBindingType,
WorkflowAgentNodeBinding,
)
from models.agent_config_entities import DeclaredOutputConfig, WorkflowNodeJobConfig
from models.workflow import Workflow
@ -66,7 +73,7 @@ class WorkflowAgentPublishService:
WorkflowAgentNodeValidator.validate_draft_workflow(session=session, workflow=draft_workflow)
@classmethod
def sync_roster_agent_bindings_for_draft(
def sync_agent_bindings_for_draft(
cls,
*,
session: Session,
@ -96,7 +103,7 @@ class WorkflowAgentPublishService:
continue
if not isinstance(binding_payload, Mapping):
raise ValueError(f"Workflow Agent node {node_id} has invalid agent_binding.")
cls._sync_roster_agent_binding_for_node(
cls._sync_agent_binding_for_node(
session=session,
draft_workflow=draft_workflow,
node_id=node_id,
@ -108,7 +115,21 @@ class WorkflowAgentPublishService:
session.flush()
@classmethod
def _sync_roster_agent_binding_for_node(
def sync_roster_agent_bindings_for_draft(
cls,
*,
session: Session,
draft_workflow: Workflow,
account_id: str,
) -> None:
cls.sync_agent_bindings_for_draft(
session=session,
draft_workflow=draft_workflow,
account_id=account_id,
)
@classmethod
def _sync_agent_binding_for_node(
cls,
*,
session: Session,
@ -120,26 +141,33 @@ class WorkflowAgentPublishService:
account_id: str,
) -> None:
binding_type = node_binding.get("binding_type")
if binding_type != WorkflowAgentBindingType.ROSTER_AGENT.value:
raise ValueError(f"Workflow Agent node {node_id} only supports roster_agent graph binding.")
agent_id = node_binding.get("agent_id")
if not isinstance(agent_id, str) or not agent_id:
raise ValueError(f"Workflow Agent node {node_id} roster_agent binding requires agent_id.")
raise ValueError(f"Workflow Agent node {node_id} agent binding requires agent_id.")
agent = session.scalar(
select(Agent)
.where(
Agent.tenant_id == draft_workflow.tenant_id,
Agent.id == agent_id,
Agent.scope == AgentScope.ROSTER,
Agent.status == AgentStatus.ACTIVE,
if binding_type == WorkflowAgentBindingType.ROSTER_AGENT.value:
agent, current_snapshot_id = cls._resolve_roster_agent_graph_binding(
session=session,
draft_workflow=draft_workflow,
node_id=node_id,
agent_id=agent_id,
)
.limit(1)
)
if agent is None:
raise ValueError(f"Workflow Agent node {node_id} references an unavailable roster agent.")
if not agent.active_config_snapshot_id:
raise ValueError(f"Workflow Agent node {node_id} roster agent has no active config snapshot.")
resolved_binding_type = WorkflowAgentBindingType.ROSTER_AGENT
elif binding_type == WorkflowAgentBindingType.INLINE_AGENT.value:
raw_current_snapshot_id = node_binding.get("current_snapshot_id")
if not isinstance(raw_current_snapshot_id, str) or not raw_current_snapshot_id:
raise ValueError(f"Workflow Agent node {node_id} inline_agent binding requires current_snapshot_id.")
current_snapshot_id = raw_current_snapshot_id
agent = cls._resolve_inline_agent_graph_binding(
session=session,
draft_workflow=draft_workflow,
node_id=node_id,
agent_id=agent_id,
current_snapshot_id=current_snapshot_id,
)
resolved_binding_type = WorkflowAgentBindingType.INLINE_AGENT
else:
raise ValueError(f"Workflow Agent node {node_id} has unsupported agent_binding type.")
binding = existing_binding
node_job_config = cls._node_job_config_from_node_data(
@ -160,11 +188,84 @@ class WorkflowAgentPublishService:
else:
binding.node_job_config = node_job_config
binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT
binding.binding_type = resolved_binding_type
binding.agent_id = agent.id
binding.current_snapshot_id = agent.active_config_snapshot_id
binding.current_snapshot_id = current_snapshot_id
binding.updated_by = account_id
@classmethod
def _resolve_roster_agent_graph_binding(
cls,
*,
session: Session,
draft_workflow: Workflow,
node_id: str,
agent_id: str,
) -> tuple[Agent, str]:
agent = session.scalar(
select(Agent)
.where(
Agent.tenant_id == draft_workflow.tenant_id,
Agent.id == agent_id,
Agent.scope == AgentScope.ROSTER,
Agent.status == AgentStatus.ACTIVE,
)
.limit(1)
)
if agent is None:
raise ValueError(f"Workflow Agent node {node_id} references an unavailable roster agent.")
if agent.scope != AgentScope.ROSTER:
raise ValueError(f"Workflow Agent node {node_id} roster_agent binding must reference a roster agent.")
if not agent.active_config_snapshot_id:
raise ValueError(f"Workflow Agent node {node_id} roster agent has no active config snapshot.")
return agent, agent.active_config_snapshot_id
@classmethod
def _resolve_inline_agent_graph_binding(
cls,
*,
session: Session,
draft_workflow: Workflow,
node_id: str,
agent_id: str,
current_snapshot_id: str,
) -> Agent:
agent = session.scalar(
select(Agent)
.where(
Agent.tenant_id == draft_workflow.tenant_id,
Agent.id == agent_id,
Agent.scope == AgentScope.WORKFLOW_ONLY,
Agent.app_id == draft_workflow.app_id,
Agent.workflow_id == draft_workflow.id,
Agent.workflow_node_id == node_id,
Agent.status == AgentStatus.ACTIVE,
)
.limit(1)
)
if agent is None:
raise ValueError(f"Workflow Agent node {node_id} references an unavailable inline agent.")
if (
agent.scope != AgentScope.WORKFLOW_ONLY
or agent.app_id != draft_workflow.app_id
or agent.workflow_id != draft_workflow.id
or agent.workflow_node_id != node_id
):
raise ValueError(f"Workflow Agent node {node_id} inline_agent binding does not belong to this node.")
snapshot = session.scalar(
select(AgentConfigSnapshot)
.where(
AgentConfigSnapshot.tenant_id == draft_workflow.tenant_id,
AgentConfigSnapshot.agent_id == agent.id,
AgentConfigSnapshot.id == current_snapshot_id,
)
.limit(1)
)
if snapshot is None or snapshot.agent_id != agent.id:
raise ValueError(f"Workflow Agent node {node_id} references a missing inline agent config snapshot.")
return agent
@classmethod
def _node_job_config_from_node_data(
cls,

View File

@ -323,7 +323,7 @@ class WorkflowService:
from services.agent.workflow_publish_service import WorkflowAgentPublishService
db.session.flush()
WorkflowAgentPublishService.sync_roster_agent_bindings_for_draft(
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=cast(Session, db.session),
draft_workflow=workflow,
account_id=account.id,

View File

@ -4,6 +4,7 @@ from typing import Any, cast
import pytest
from flask import Flask
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.console import console_ns
from controllers.console.agent import composer as composer_controller
@ -25,6 +26,15 @@ from controllers.console.agent.roster import (
AgentRosterVersionDetailApi,
AgentRosterVersionsApi,
)
from controllers.console.app import completion as completion_controller
from controllers.console.app import message as message_controller
from controllers.console.app.completion import AgentChatMessageApi, AgentChatMessageStopApi
from controllers.console.app.message import (
AgentChatMessageListApi,
AgentMessageApi,
AgentMessageFeedbackApi,
AgentMessageSuggestedQuestionApi,
)
from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant
@ -132,6 +142,11 @@ def test_agent_v2_console_routes_are_agent_id_first() -> None:
"/agent/<uuid:agent_id>/sandbox/files",
"/agent/<uuid:agent_id>/skills/upload",
"/agent/<uuid:agent_id>/files",
"/agent/<uuid:agent_id>/chat-messages",
"/agent/<uuid:agent_id>/chat-messages/<string:task_id>/stop",
"/agent/<uuid:agent_id>/feedbacks",
"/agent/<uuid:agent_id>/chat-messages/<uuid:message_id>/suggested-questions",
"/agent/<uuid:agent_id>/messages/<uuid:message_id>",
"/agent/invite-options",
):
assert route in paths
@ -471,6 +486,272 @@ def test_agent_composer_routes_resolve_app_from_agent_id(
assert cast(dict[str, object], captured["candidates"])["app_id"] == "app-1"
def test_agent_chat_generate_and_stop_routes_resolve_app_from_agent_id(
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
) -> None:
agent_id = "00000000-0000-0000-0000-000000000001"
app_model = SimpleNamespace(id="app-1", mode="agent")
captured: dict[str, object] = {}
def resolve_agent_app_model(**kwargs: object) -> object:
captured["resolve"] = kwargs
return app_model
def create_chat_message(**kwargs: object) -> dict[str, object]:
captured["create"] = kwargs
return {"result": "generated"}
def stop_chat_message(**kwargs: object) -> tuple[dict[str, object], int]:
captured["stop"] = kwargs
return {"result": "success"}, 200
monkeypatch.setattr(completion_controller, "resolve_agent_app_model", resolve_agent_app_model)
monkeypatch.setattr(completion_controller, "_create_chat_message", create_chat_message)
monkeypatch.setattr(completion_controller, "_stop_chat_message", stop_chat_message)
with app.test_request_context(json={"inputs": {}, "query": "hello"}):
assert unwrap(AgentChatMessageApi.post)(
AgentChatMessageApi(), "tenant-1", SimpleNamespace(id=account_id), agent_id
) == {"result": "generated"}
assert cast(dict[str, object], captured["resolve"]) == {"tenant_id": "tenant-1", "agent_id": agent_id}
create_call = cast(dict[str, object], captured["create"])
assert create_call["app_model"] is app_model
assert cast(SimpleNamespace, create_call["current_user"]).id == account_id
assert unwrap(AgentChatMessageStopApi.post)(
AgentChatMessageStopApi(), "tenant-1", account_id, agent_id, "task-1"
) == ({"result": "success"}, 200)
stop_call = cast(dict[str, object], captured["stop"])
assert stop_call == {"current_user_id": account_id, "app_model": app_model, "task_id": "task-1"}
def test_agent_chat_helper_forces_agent_streaming_and_external_trace(
app: Flask, monkeypatch: pytest.MonkeyPatch, account_id: str
) -> None:
app_model = SimpleNamespace(id="app-1", mode="agent")
current_user = SimpleNamespace(id=account_id)
captured: dict[str, object] = {}
def generate(**kwargs: object) -> dict[str, object]:
captured.update(kwargs)
return {"answer": "ok"}
monkeypatch.setattr(completion_controller.AppGenerateService, "generate", generate)
monkeypatch.setattr(
completion_controller.helper,
"compact_generate_response",
lambda response: {"response": response},
)
with app.test_request_context(
json={"inputs": {}, "query": "hello", "response_mode": "streaming"},
headers={"X-Trace-Id": "trace-1"},
):
result = completion_controller._create_chat_message(current_user=current_user, app_model=app_model)
assert result == {"response": {"answer": "ok"}}
assert captured["app_model"] is app_model
assert captured["user"] is current_user
assert captured["streaming"] is True
args = cast(dict[str, object], captured["args"])
assert args["response_mode"] == "streaming"
assert args["auto_generate_name"] is False
assert args["external_trace_id"] == "trace-1"
@pytest.mark.parametrize(
("error", "expected"),
[
(completion_controller.services.errors.conversation.ConversationNotExistsError(), NotFound),
(
completion_controller.services.errors.conversation.ConversationCompletedError(),
completion_controller.ConversationCompletedError,
),
(
completion_controller.services.errors.app_model_config.AppModelConfigBrokenError(),
completion_controller.AppUnavailableError,
),
(
completion_controller.ProviderTokenNotInitError("not initialized"),
completion_controller.ProviderNotInitializeError,
),
(completion_controller.QuotaExceededError(), completion_controller.ProviderQuotaExceededError),
(
completion_controller.ModelCurrentlyNotSupportError(),
completion_controller.ProviderModelCurrentlyNotSupportError,
),
(completion_controller.InvokeRateLimitError("rate limited"), completion_controller.InvokeRateLimitHttpError),
(completion_controller.InvokeError("invoke failed"), completion_controller.CompletionRequestError),
(RuntimeError("unexpected"), InternalServerError),
],
)
def test_agent_chat_helper_maps_generation_errors(
app: Flask,
monkeypatch: pytest.MonkeyPatch,
error: Exception,
expected: type[Exception],
) -> None:
app_model = SimpleNamespace(id="app-1", mode="chat")
monkeypatch.setattr(completion_controller.AppGenerateService, "generate", lambda **_: (_ for _ in ()).throw(error))
with app.test_request_context(json={"inputs": {}, "query": "hello"}):
with pytest.raises(expected):
completion_controller._create_chat_message(
current_user=SimpleNamespace(id="account-1"),
app_model=app_model,
)
def test_agent_chat_message_routes_resolve_app_from_agent_id(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
agent_id = "00000000-0000-0000-0000-000000000001"
message_id = "00000000-0000-0000-0000-000000000002"
app_model = SimpleNamespace(id="app-1")
current_user = SimpleNamespace(id="account-1")
captured: dict[str, object] = {}
def resolve_agent_app_model(**kwargs: object) -> object:
captured["resolve"] = kwargs
return app_model
def list_chat_messages(**kwargs: object) -> dict[str, object]:
captured["list"] = kwargs
return {"data": []}
def update_message_feedback(**kwargs: object) -> dict[str, object]:
captured["feedback"] = kwargs
return {"result": "success"}
def get_message_suggested_questions(**kwargs: object) -> dict[str, object]:
captured["suggested"] = kwargs
return {"data": ["next"]}
def get_message_detail(**kwargs: object) -> dict[str, object]:
captured["detail"] = kwargs
return {"id": message_id}
monkeypatch.setattr(message_controller, "resolve_agent_app_model", resolve_agent_app_model)
monkeypatch.setattr(message_controller, "_list_chat_messages", list_chat_messages)
monkeypatch.setattr(message_controller, "_update_message_feedback", update_message_feedback)
monkeypatch.setattr(message_controller, "_get_message_suggested_questions", get_message_suggested_questions)
monkeypatch.setattr(message_controller, "_get_message_detail", get_message_detail)
assert unwrap(AgentChatMessageListApi.get)(AgentChatMessageListApi(), "tenant-1", agent_id) == {"data": []}
assert cast(dict[str, object], captured["list"])["app_model"] is app_model
with app.test_request_context(json={"message_id": message_id, "rating": "like"}):
assert unwrap(AgentMessageFeedbackApi.post)(AgentMessageFeedbackApi(), "tenant-1", current_user, agent_id) == {
"result": "success"
}
feedback_call = cast(dict[str, object], captured["feedback"])
assert feedback_call["app_model"] is app_model
assert feedback_call["current_user"] is current_user
assert unwrap(AgentMessageSuggestedQuestionApi.get)(
AgentMessageSuggestedQuestionApi(), "tenant-1", current_user, agent_id, message_id
) == {"data": ["next"]}
suggested_call = cast(dict[str, object], captured["suggested"])
assert suggested_call["app_model"] is app_model
assert suggested_call["current_user"] is current_user
assert suggested_call["message_id"] == message_id
assert unwrap(AgentMessageApi.get)(AgentMessageApi(), "tenant-1", agent_id, message_id) == {"id": message_id}
detail_call = cast(dict[str, object], captured["detail"])
assert detail_call == {"app_model": app_model, "message_id": message_id}
def test_list_chat_messages_supports_first_id_pagination(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
conversation_id = "00000000-0000-0000-0000-000000000010"
first_message_id = "00000000-0000-0000-0000-000000000011"
older_message_id = "00000000-0000-0000-0000-000000000012"
conversation = SimpleNamespace(id=conversation_id)
first_message = SimpleNamespace(id=first_message_id, created_at=2)
older_message = SimpleNamespace(id=older_message_id, created_at=1)
scalar_values = iter([conversation, first_message, True])
scalars_result = SimpleNamespace(all=lambda: [older_message])
session = SimpleNamespace(
scalar=lambda _stmt: next(scalar_values),
scalars=lambda _stmt: scalars_result,
)
class FakeMessagePaginationResponse:
@classmethod
def model_validate(cls, pagination: object, from_attributes: bool = False) -> object:
return SimpleNamespace(
model_dump=lambda mode: {
"data": [item.id for item in pagination.data],
"limit": pagination.limit,
"has_more": pagination.has_more,
}
)
monkeypatch.setattr(message_controller, "db", SimpleNamespace(session=session))
monkeypatch.setattr(message_controller, "attach_message_extra_contents", lambda messages: None)
monkeypatch.setattr(message_controller, "MessageInfiniteScrollPaginationResponse", FakeMessagePaginationResponse)
with app.test_request_context(
"/console/api/agent/agent-1/chat-messages"
f"?conversation_id={conversation_id}&first_id={first_message_id}&limit=1"
):
result = message_controller._list_chat_messages(app_model=SimpleNamespace(id="app-1"))
assert result == {"data": [older_message_id], "limit": 1, "has_more": True}
def test_update_message_feedback_rejects_empty_rating_without_existing_feedback(
app: Flask, monkeypatch: pytest.MonkeyPatch
) -> None:
message_id = "00000000-0000-0000-0000-000000000002"
message = SimpleNamespace(id=message_id, app_id="app-1", admin_feedback=None)
session = SimpleNamespace(scalar=lambda _stmt: message)
monkeypatch.setattr(message_controller, "db", SimpleNamespace(session=session))
with app.test_request_context(json={"message_id": message_id, "rating": None}):
with pytest.raises(ValueError, match="rating cannot be None"):
message_controller._update_message_feedback(
current_user=SimpleNamespace(id="account-1"),
app_model=SimpleNamespace(id="app-1"),
)
@pytest.mark.parametrize(
("error", "expected"),
[
(message_controller.MessageNotExistsError(), NotFound),
(message_controller.ConversationNotExistsError(), NotFound),
(
message_controller.ProviderTokenNotInitError("not initialized"),
message_controller.ProviderNotInitializeError,
),
(message_controller.QuotaExceededError(), message_controller.ProviderQuotaExceededError),
(message_controller.ModelCurrentlyNotSupportError(), message_controller.ProviderModelCurrentlyNotSupportError),
(message_controller.InvokeError("invoke failed"), message_controller.CompletionRequestError),
(
message_controller.SuggestedQuestionsAfterAnswerDisabledError(),
message_controller.AppSuggestedQuestionsAfterAnswerDisabledError,
),
(RuntimeError("unexpected"), InternalServerError),
],
)
def test_get_message_suggested_questions_maps_service_errors(
monkeypatch: pytest.MonkeyPatch,
error: Exception,
expected: type[Exception],
) -> None:
monkeypatch.setattr(
message_controller.MessageService,
"get_suggested_questions_after_answer",
lambda **_: (_ for _ in ()).throw(error),
)
with pytest.raises(expected):
message_controller._get_message_suggested_questions(
current_user=SimpleNamespace(id="account-1"),
app_model=SimpleNamespace(id="app-1"),
message_id="00000000-0000-0000-0000-000000000002",
)
def test_dify_tool_candidate_response_keeps_granularity_fields():
"""Both selection granularities must survive the fields-layer model —
the frontend needs granularity/tools_count to render the Tools menu."""

View File

@ -1261,6 +1261,225 @@ class TestWorkflowAgentDraftBindingSync:
],
).model_dump(mode="json")
def test_creates_inline_binding_from_agent_node_graph(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_task": "Use the current node context.",
"agent_binding": {
"binding_type": "inline_agent",
"agent_id": "inline-agent-1",
"current_snapshot_id": "inline-snapshot-1",
},
},
}
]
}
),
)
agent = Agent(
id="inline-agent-1",
tenant_id="tenant-1",
name="Workflow Agent agent-node",
agent_kind=AgentKind.DIFY_AGENT,
scope=AgentScope.WORKFLOW_ONLY,
source=AgentSource.WORKFLOW,
app_id="app-1",
workflow_id="workflow-1",
workflow_node_id="agent-node",
status=AgentStatus.ACTIVE,
active_config_snapshot_id="inline-snapshot-1",
)
snapshot = AgentConfigSnapshot(
id="inline-snapshot-1",
tenant_id="tenant-1",
agent_id="inline-agent-1",
version=1,
config_snapshot=AgentSoulConfig(),
)
session = FakeSession(scalar=[agent, snapshot], scalars=[[]])
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=session,
draft_workflow=workflow,
account_id="account-1",
)
binding = next(item for item in session.added if isinstance(item, WorkflowAgentNodeBinding))
assert binding.binding_type == WorkflowAgentBindingType.INLINE_AGENT
assert binding.agent_id == "inline-agent-1"
assert binding.current_snapshot_id == "inline-snapshot-1"
assert binding.node_job_config_dict == WorkflowNodeJobConfig(
workflow_prompt="Use the current node context.",
).model_dump(mode="json")
def test_rejects_inline_binding_for_agent_owned_by_another_node(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_binding": {
"binding_type": "inline_agent",
"agent_id": "inline-agent-1",
"current_snapshot_id": "inline-snapshot-1",
},
},
}
]
}
),
)
agent = Agent(
id="inline-agent-1",
tenant_id="tenant-1",
name="Workflow Agent other-node",
agent_kind=AgentKind.DIFY_AGENT,
scope=AgentScope.WORKFLOW_ONLY,
source=AgentSource.WORKFLOW,
app_id="app-1",
workflow_id="workflow-1",
workflow_node_id="other-node",
status=AgentStatus.ACTIVE,
active_config_snapshot_id="inline-snapshot-1",
)
session = FakeSession(scalar=[agent], scalars=[[]])
with pytest.raises(ValueError, match="inline_agent binding does not belong to this node"):
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=session,
draft_workflow=workflow,
account_id="account-1",
)
def test_rejects_agent_node_graph_binding_with_unsupported_type(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_binding": {
"binding_type": "unknown",
"agent_id": "agent-1",
},
},
}
]
}
),
)
with pytest.raises(ValueError, match="unsupported agent_binding type"):
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=FakeSession(scalars=[[]]),
draft_workflow=workflow,
account_id="account-1",
)
def test_rejects_inline_binding_without_current_snapshot_id(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_binding": {
"binding_type": "inline_agent",
"agent_id": "inline-agent-1",
},
},
}
]
}
),
)
with pytest.raises(ValueError, match="inline_agent binding requires current_snapshot_id"):
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=FakeSession(scalars=[[]]),
draft_workflow=workflow,
account_id="account-1",
)
def test_rejects_inline_binding_with_missing_snapshot(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_binding": {
"binding_type": "inline_agent",
"agent_id": "inline-agent-1",
"current_snapshot_id": "missing-snapshot",
},
},
}
]
}
),
)
agent = Agent(
id="inline-agent-1",
tenant_id="tenant-1",
name="Workflow Agent agent-node",
agent_kind=AgentKind.DIFY_AGENT,
scope=AgentScope.WORKFLOW_ONLY,
source=AgentSource.WORKFLOW,
app_id="app-1",
workflow_id="workflow-1",
workflow_node_id="agent-node",
status=AgentStatus.ACTIVE,
active_config_snapshot_id="inline-snapshot-1",
)
with pytest.raises(ValueError, match="missing inline agent config snapshot"):
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
session=FakeSession(scalar=[agent, None], scalars=[[]]),
draft_workflow=workflow,
account_id="account-1",
)
def test_updates_existing_roster_binding_prompt_from_agent_node_graph(self):
workflow = Workflow(
id="workflow-1",

View File

@ -11,6 +11,11 @@ import {
zDeleteAgentByAgentIdResponse,
zDeleteAgentByAgentIdSkillsBySlugPath,
zDeleteAgentByAgentIdSkillsBySlugResponse,
zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsPath,
zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponse,
zGetAgentByAgentIdChatMessagesPath,
zGetAgentByAgentIdChatMessagesQuery,
zGetAgentByAgentIdChatMessagesResponse,
zGetAgentByAgentIdComposerCandidatesPath,
zGetAgentByAgentIdComposerCandidatesResponse,
zGetAgentByAgentIdComposerPath,
@ -24,6 +29,8 @@ import {
zGetAgentByAgentIdDriveFilesPreviewResponse,
zGetAgentByAgentIdDriveFilesQuery,
zGetAgentByAgentIdDriveFilesResponse,
zGetAgentByAgentIdMessagesByMessageIdPath,
zGetAgentByAgentIdMessagesByMessageIdResponse,
zGetAgentByAgentIdPath,
zGetAgentByAgentIdReferencingWorkflowsPath,
zGetAgentByAgentIdReferencingWorkflowsResponse,
@ -43,12 +50,17 @@ import {
zGetAgentQuery,
zGetAgentResponse,
zPostAgentBody,
zPostAgentByAgentIdChatMessagesByTaskIdStopPath,
zPostAgentByAgentIdChatMessagesByTaskIdStopResponse,
zPostAgentByAgentIdComposerValidateBody,
zPostAgentByAgentIdComposerValidatePath,
zPostAgentByAgentIdComposerValidateResponse,
zPostAgentByAgentIdFeaturesBody,
zPostAgentByAgentIdFeaturesPath,
zPostAgentByAgentIdFeaturesResponse,
zPostAgentByAgentIdFeedbacksBody,
zPostAgentByAgentIdFeedbacksPath,
zPostAgentByAgentIdFeedbacksResponse,
zPostAgentByAgentIdFilesBody,
zPostAgentByAgentIdFilesPath,
zPostAgentByAgentIdFilesResponse,
@ -85,7 +97,79 @@ export const inviteOptions = {
get,
}
/**
* Get suggested questions for an Agent App message
*/
export const get2 = oc
.route({
description: 'Get suggested questions for an Agent App message',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdChatMessagesByMessageIdSuggestedQuestions',
path: '/agent/{agent_id}/chat-messages/{message_id}/suggested-questions',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsPath }))
.output(zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponse)
export const suggestedQuestions = {
get: get2,
}
export const byMessageId = {
suggestedQuestions,
}
/**
* Stop a running Agent App chat message generation
*/
export const post = oc
.route({
description: 'Stop a running Agent App chat message generation',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdChatMessagesByTaskIdStop',
path: '/agent/{agent_id}/chat-messages/{task_id}/stop',
tags: ['console'],
})
.input(z.object({ params: zPostAgentByAgentIdChatMessagesByTaskIdStopPath }))
.output(zPostAgentByAgentIdChatMessagesByTaskIdStopResponse)
export const stop = {
post,
}
export const byTaskId = {
stop,
}
/**
* Get Agent App chat messages for a conversation with pagination
*/
export const get3 = oc
.route({
description: 'Get Agent App chat messages for a conversation with pagination',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdChatMessages',
path: '/agent/{agent_id}/chat-messages',
tags: ['console'],
})
.input(
z.object({
params: zGetAgentByAgentIdChatMessagesPath,
query: zGetAgentByAgentIdChatMessagesQuery,
}),
)
.output(zGetAgentByAgentIdChatMessagesResponse)
export const chatMessages = {
get: get3,
byMessageId,
byTaskId,
}
export const get4 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -97,10 +181,10 @@ export const get2 = oc
.output(zGetAgentByAgentIdComposerCandidatesResponse)
export const candidates = {
get: get2,
get: get4,
}
export const post = oc
export const post2 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -117,10 +201,10 @@ export const post = oc
.output(zPostAgentByAgentIdComposerValidateResponse)
export const validate = {
post,
post: post2,
}
export const get3 = oc
export const get5 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -143,7 +227,7 @@ export const put = oc
.output(zPutAgentByAgentIdComposerResponse)
export const composer = {
get: get3,
get: get5,
put,
candidates,
validate,
@ -152,7 +236,7 @@ export const composer = {
/**
* Time-limited external signed URL for one Agent App drive value
*/
export const get4 = oc
export const get6 = oc
.route({
description: 'Time-limited external signed URL for one Agent App drive value',
inputStructure: 'detailed',
@ -170,13 +254,13 @@ export const get4 = oc
.output(zGetAgentByAgentIdDriveFilesDownloadResponse)
export const download = {
get: get4,
get: get6,
}
/**
* Truncated text preview of one Agent App drive value
*/
export const get5 = oc
export const get7 = oc
.route({
description: 'Truncated text preview of one Agent App drive value',
inputStructure: 'detailed',
@ -194,13 +278,13 @@ export const get5 = oc
.output(zGetAgentByAgentIdDriveFilesPreviewResponse)
export const preview = {
get: get5,
get: get7,
}
/**
* List agent drive entries for an Agent App
*/
export const get6 = oc
export const get8 = oc
.route({
description: 'List agent drive entries for an Agent App',
inputStructure: 'detailed',
@ -218,7 +302,7 @@ export const get6 = oc
.output(zGetAgentByAgentIdDriveFilesResponse)
export const files = {
get: get6,
get: get8,
download,
preview,
}
@ -230,7 +314,7 @@ export const drive = {
/**
* Update an Agent App's presentation features (opener, follow-up, citations, ...)
*/
export const post2 = oc
export const post3 = oc
.route({
description: 'Update an Agent App\'s presentation features (opener, follow-up, citations, ...)',
inputStructure: 'detailed',
@ -245,7 +329,28 @@ export const post2 = oc
.output(zPostAgentByAgentIdFeaturesResponse)
export const features = {
post: post2,
post: post3,
}
/**
* Create or update Agent App message feedback
*/
export const post4 = oc
.route({
description: 'Create or update Agent App message feedback',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postAgentByAgentIdFeedbacks',
path: '/agent/{agent_id}/feedbacks',
tags: ['console'],
})
.input(
z.object({ body: zPostAgentByAgentIdFeedbacksBody, params: zPostAgentByAgentIdFeedbacksPath }),
)
.output(zPostAgentByAgentIdFeedbacksResponse)
export const feedbacks = {
post: post4,
}
/**
@ -268,7 +373,7 @@ export const delete_ = oc
/**
* Commit an uploaded file into the Agent App drive under files/<name>
*/
export const post3 = oc
export const post5 = oc
.route({
description: 'Commit an uploaded file into the Agent App drive under files/<name>',
inputStructure: 'detailed',
@ -283,13 +388,36 @@ export const post3 = oc
export const files2 = {
delete: delete_,
post: post3,
post: post5,
}
/**
* Get Agent App message details by ID
*/
export const get9 = oc
.route({
description: 'Get Agent App message details by ID',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getAgentByAgentIdMessagesByMessageId',
path: '/agent/{agent_id}/messages/{message_id}',
tags: ['console'],
})
.input(z.object({ params: zGetAgentByAgentIdMessagesByMessageIdPath }))
.output(zGetAgentByAgentIdMessagesByMessageIdResponse)
export const byMessageId2 = {
get: get9,
}
export const messages = {
byMessageId: byMessageId2,
}
/**
* List workflow apps that reference this Agent App's bound Agent (read-only)
*/
export const get7 = oc
export const get10 = oc
.route({
description: 'List workflow apps that reference this Agent App\'s bound Agent (read-only)',
inputStructure: 'detailed',
@ -302,13 +430,13 @@ export const get7 = oc
.output(zGetAgentByAgentIdReferencingWorkflowsResponse)
export const referencingWorkflows = {
get: get7,
get: get10,
}
/**
* Read a text/binary preview file in an Agent App conversation sandbox
*/
export const get8 = oc
export const get11 = oc
.route({
description: 'Read a text/binary preview file in an Agent App conversation sandbox',
inputStructure: 'detailed',
@ -326,13 +454,13 @@ export const get8 = oc
.output(zGetAgentByAgentIdSandboxFilesReadResponse)
export const read = {
get: get8,
get: get11,
}
/**
* Upload one Agent App sandbox file as a Dify ToolFile mapping
*/
export const post4 = oc
export const post6 = oc
.route({
description: 'Upload one Agent App sandbox file as a Dify ToolFile mapping',
inputStructure: 'detailed',
@ -350,13 +478,13 @@ export const post4 = oc
.output(zPostAgentByAgentIdSandboxFilesUploadResponse)
export const upload = {
post: post4,
post: post6,
}
/**
* List a directory in an Agent App conversation sandbox
*/
export const get9 = oc
export const get12 = oc
.route({
description: 'List a directory in an Agent App conversation sandbox',
inputStructure: 'detailed',
@ -374,7 +502,7 @@ export const get9 = oc
.output(zGetAgentByAgentIdSandboxFilesResponse)
export const files3 = {
get: get9,
get: get12,
read,
upload,
}
@ -386,7 +514,7 @@ export const sandbox = {
/**
* Validate + standardize a Skill into an Agent App drive
*/
export const post5 = oc
export const post7 = oc
.route({
description: 'Validate + standardize a Skill into an Agent App drive',
inputStructure: 'detailed',
@ -400,13 +528,13 @@ export const post5 = oc
.output(zPostAgentByAgentIdSkillsStandardizeResponse)
export const standardize = {
post: post5,
post: post7,
}
/**
* Upload + validate a Skill package for an Agent App
*/
export const post6 = oc
export const post8 = oc
.route({
description: 'Upload + validate a Skill package for an Agent App',
inputStructure: 'detailed',
@ -420,13 +548,13 @@ export const post6 = oc
.output(zPostAgentByAgentIdSkillsUploadResponse)
export const upload2 = {
post: post6,
post: post8,
}
/**
* Infer CLI tool + ENV suggestions from a standardized Agent App skill
*/
export const post7 = oc
export const post9 = oc
.route({
description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill',
inputStructure: 'detailed',
@ -439,7 +567,7 @@ export const post7 = oc
.output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse)
export const inferTools = {
post: post7,
post: post9,
}
/**
@ -468,7 +596,7 @@ export const skills = {
bySlug,
}
export const get10 = oc
export const get13 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -480,10 +608,10 @@ export const get10 = oc
.output(zGetAgentByAgentIdVersionsByVersionIdResponse)
export const byVersionId = {
get: get10,
get: get13,
}
export const get11 = oc
export const get14 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -495,7 +623,7 @@ export const get11 = oc
.output(zGetAgentByAgentIdVersionsResponse)
export const versions = {
get: get11,
get: get14,
byVersionId,
}
@ -511,7 +639,7 @@ export const delete3 = oc
.input(z.object({ params: zDeleteAgentByAgentIdPath }))
.output(zDeleteAgentByAgentIdResponse)
export const get12 = oc
export const get15 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -535,19 +663,22 @@ export const put2 = oc
export const byAgentId = {
delete: delete3,
get: get12,
get: get15,
put: put2,
chatMessages,
composer,
drive,
features,
feedbacks,
files: files2,
messages,
referencingWorkflows,
sandbox,
skills,
versions,
}
export const get13 = oc
export const get16 = oc
.route({
inputStructure: 'detailed',
method: 'GET',
@ -558,7 +689,7 @@ export const get13 = oc
.input(z.object({ query: zGetAgentQuery.optional() }))
.output(zGetAgentResponse)
export const post8 = oc
export const post10 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -571,8 +702,8 @@ export const post8 = oc
.output(zPostAgentResponse)
export const agent = {
get: get13,
post: post8,
get: get16,
post: post10,
inviteOptions,
byAgentId,
}

View File

@ -66,6 +66,20 @@ export type UpdateAppPayload = {
use_icon_as_answer_icon?: boolean | null
}
export type MessageInfiniteScrollPaginationResponse = {
data: Array<MessageDetailResponse>
has_more: boolean
limit: number
}
export type SuggestedQuestionsResponse = {
data: Array<string>
}
export type SimpleResultResponse = {
result: string
}
export type AgentAppComposerResponse = {
active_config_snapshot: AgentConfigSnapshotSummaryResponse
agent: AgentComposerAgentResponse
@ -129,8 +143,10 @@ export type AgentAppFeaturesPayload = {
text_to_speech?: AgentTextToSpeechFeatureConfig | null
}
export type SimpleResultResponse = {
result: string
export type MessageFeedbackPayload = {
content?: string | null
message_id: string
rating?: 'dislike' | 'like' | null
}
export type AgentDriveDeleteResponse = {
@ -148,6 +164,35 @@ export type AgentDriveFileCommitResponse = {
file: AgentDriveFileResponse
}
export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
error?: string | null
extra_contents?: Array<HumanInputContent>
feedbacks?: Array<Feedback>
from_account_id?: string | null
from_end_user_id?: string | null
from_source: string
id: string
inputs: {
[key: string]: JsonValue
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
export type AgentReferencingWorkflowsResponse = {
data?: Array<AgentReferencingWorkflowResponse>
}
@ -476,6 +521,63 @@ export type AgentDriveFileResponse = {
size?: number | null
}
export type AgentThought = {
chain_id?: string | null
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
thought?: string | null
tool?: string | null
tool_input?: string | null
tool_labels: JsonValue
}
export type ConversationAnnotation = {
account?: SimpleAccount | null
content: string
created_at?: number | null
id: string
question?: string | null
}
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
created_at?: number | null
id: string
}
export type HumanInputContent = {
form_definition?: HumanInputFormDefinition | null
form_submission_data?: HumanInputFormSubmissionData | null
submitted: boolean
type?: ExecutionContentType
workflow_run_id: string
}
export type Feedback = {
content?: string | null
from_account?: SimpleAccount | null
from_end_user_id?: string | null
from_source: string
rating: string
}
export type MessageFile = {
belongs_to?: string | null
filename: string
id: string
mime_type?: string | null
size?: number | null
transfer_method: string
type: string
upload_file_id?: string | null
url?: string | null
}
export type AgentReferencingWorkflowResponse = {
app_id: string
app_mode: string
@ -773,6 +875,40 @@ export type AgentModerationProviderConfig = {
[key: string]: unknown
}
export type SimpleAccount = {
email: string
id: string
name: string
}
export type HumanInputFormDefinition = {
actions?: Array<UserActionConfig>
display_in_ui?: boolean
expiration_time: number
form_content: string
form_id: string
form_token?: string | null
inputs?: Array<FormInputConfig>
node_id: string
node_title: string
resolved_default_values?: {
[key: string]: unknown
}
}
export type HumanInputFormSubmissionData = {
action_id: string
action_text: string
node_id: string
node_title: string
rendered_content: string
submitted_data?: {
[key: string]: JsonValue2
} | null
}
export type ExecutionContentType = 'human_input'
export type EnvSuggestion = {
key: string
reason?: string
@ -973,6 +1109,28 @@ export type AgentModerationIoConfig = {
[key: string]: unknown
}
export type UserActionConfig = {
button_style?: ButtonStyle
id: string
title: string
}
export type FormInputConfig
= | ({
type: 'paragraph'
} & ParagraphInputConfig)
| ({
type: 'select'
} & SelectInputConfig)
| ({
type: 'file'
} & FileInputConfig)
| ({
type: 'file-list'
} & FileListInputConfig)
export type JsonValue2 = unknown
export type AgentModelResponseFormatConfig = {
type?: string | null
[key: string]: unknown
@ -992,6 +1150,55 @@ export type DeclaredOutputRetryConfig = {
retry_interval_ms?: number
}
export type ButtonStyle = 'accent' | 'default' | 'ghost' | 'primary'
export type ParagraphInputConfig = {
default?: StringSource | null
output_variable_name: string
type?: 'paragraph'
}
export type SelectInputConfig = {
option_source: StringListSource
output_variable_name: string
type?: 'select'
}
export type FileInputConfig = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
output_variable_name: string
type?: 'file'
}
export type FileListInputConfig = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
number_limits?: number
output_variable_name: string
type?: 'file-list'
}
export type StringSource = {
selector?: Array<string>
type: ValueSourceType
value?: string
}
export type StringListSource = {
selector?: Array<string>
type: ValueSourceType
value?: Array<string>
}
export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video'
export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' | 'tool_file'
export type ValueSourceType = 'constant' | 'variable'
export type AppPaginationWritable = {
data: Array<AppPartialWritable>
has_more: boolean
@ -1190,6 +1397,68 @@ export type PutAgentByAgentIdResponses = {
export type PutAgentByAgentIdResponse = PutAgentByAgentIdResponses[keyof PutAgentByAgentIdResponses]
export type GetAgentByAgentIdChatMessagesData = {
body?: never
path: {
agent_id: string
}
query: {
conversation_id: string
first_id?: string
limit?: number
}
url: '/agent/{agent_id}/chat-messages'
}
export type GetAgentByAgentIdChatMessagesErrors = {
404: unknown
}
export type GetAgentByAgentIdChatMessagesResponses = {
200: MessageInfiniteScrollPaginationResponse
}
export type GetAgentByAgentIdChatMessagesResponse
= GetAgentByAgentIdChatMessagesResponses[keyof GetAgentByAgentIdChatMessagesResponses]
export type GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsData = {
body?: never
path: {
agent_id: string
message_id: string
}
query?: never
url: '/agent/{agent_id}/chat-messages/{message_id}/suggested-questions'
}
export type GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsErrors = {
404: unknown
}
export type GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponses = {
200: SuggestedQuestionsResponse
}
export type GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponse
= GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponses[keyof GetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponses]
export type PostAgentByAgentIdChatMessagesByTaskIdStopData = {
body?: never
path: {
agent_id: string
task_id: string
}
query?: never
url: '/agent/{agent_id}/chat-messages/{task_id}/stop'
}
export type PostAgentByAgentIdChatMessagesByTaskIdStopResponses = {
200: SimpleResultResponse
}
export type PostAgentByAgentIdChatMessagesByTaskIdStopResponse
= PostAgentByAgentIdChatMessagesByTaskIdStopResponses[keyof PostAgentByAgentIdChatMessagesByTaskIdStopResponses]
export type GetAgentByAgentIdComposerData = {
body?: never
path: {
@ -1329,6 +1598,26 @@ export type PostAgentByAgentIdFeaturesResponses = {
export type PostAgentByAgentIdFeaturesResponse
= PostAgentByAgentIdFeaturesResponses[keyof PostAgentByAgentIdFeaturesResponses]
export type PostAgentByAgentIdFeedbacksData = {
body: MessageFeedbackPayload
path: {
agent_id: string
}
query?: never
url: '/agent/{agent_id}/feedbacks'
}
export type PostAgentByAgentIdFeedbacksErrors = {
404: unknown
}
export type PostAgentByAgentIdFeedbacksResponses = {
200: SimpleResultResponse
}
export type PostAgentByAgentIdFeedbacksResponse
= PostAgentByAgentIdFeedbacksResponses[keyof PostAgentByAgentIdFeedbacksResponses]
export type DeleteAgentByAgentIdFilesData = {
body?: never
path: {
@ -1363,6 +1652,27 @@ export type PostAgentByAgentIdFilesResponses = {
export type PostAgentByAgentIdFilesResponse
= PostAgentByAgentIdFilesResponses[keyof PostAgentByAgentIdFilesResponses]
export type GetAgentByAgentIdMessagesByMessageIdData = {
body?: never
path: {
agent_id: string
message_id: string
}
query?: never
url: '/agent/{agent_id}/messages/{message_id}'
}
export type GetAgentByAgentIdMessagesByMessageIdErrors = {
404: unknown
}
export type GetAgentByAgentIdMessagesByMessageIdResponses = {
200: MessageDetailResponse
}
export type GetAgentByAgentIdMessagesByMessageIdResponse
= GetAgentByAgentIdMessagesByMessageIdResponses[keyof GetAgentByAgentIdMessagesByMessageIdResponses]
export type GetAgentByAgentIdReferencingWorkflowsData = {
body?: never
path: {

View File

@ -2,6 +2,20 @@
import * as z from 'zod'
/**
* SuggestedQuestionsResponse
*/
export const zSuggestedQuestionsResponse = z.object({
data: z.array(z.string()),
})
/**
* SimpleResultResponse
*/
export const zSimpleResultResponse = z.object({
result: z.string(),
})
/**
* AgentDriveDownloadResponse
*/
@ -21,10 +35,12 @@ export const zAgentDrivePreviewResponse = z.object({
})
/**
* SimpleResultResponse
* MessageFeedbackPayload
*/
export const zSimpleResultResponse = z.object({
result: z.string(),
export const zMessageFeedbackPayload = z.object({
content: z.string().nullish(),
message_id: z.string(),
rating: z.enum(['dislike', 'like']).nullish(),
})
/**
@ -303,6 +319,39 @@ export const zAgentDriveFileCommitResponse = z.object({
file: zAgentDriveFileResponse,
})
/**
* AgentThought
*/
export const zAgentThought = z.object({
chain_id: z.string().nullish(),
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
thought: z.string().nullish(),
tool: z.string().nullish(),
tool_input: z.string().nullish(),
tool_labels: zJsonValue,
})
/**
* MessageFile
*/
export const zMessageFile = z.object({
belongs_to: z.string().nullish(),
filename: z.string(),
id: z.string(),
mime_type: z.string().nullish(),
size: z.int().nullish(),
transfer_method: z.string(),
type: z.string(),
upload_file_id: z.string().nullish(),
url: z.string().nullish(),
})
/**
* AgentReferencingWorkflowResponse
*/
@ -743,6 +792,51 @@ export const zAgentComposerFileCandidateResponse = z.object({
url: z.string().nullish(),
})
/**
* SimpleAccount
*/
export const zSimpleAccount = z.object({
email: z.string(),
id: z.string(),
name: z.string(),
})
/**
* ConversationAnnotation
*/
export const zConversationAnnotation = z.object({
account: zSimpleAccount.nullish(),
content: z.string(),
created_at: z.int().nullish(),
id: z.string(),
question: z.string().nullish(),
})
/**
* ConversationAnnotationHitHistory
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
* Feedback
*/
export const zFeedback = z.object({
content: z.string().nullish(),
from_account: zSimpleAccount.nullish(),
from_end_user_id: z.string().nullish(),
from_source: z.string(),
rating: z.string(),
})
/**
* ExecutionContentType
*/
export const zExecutionContentType = z.enum(['human_input'])
/**
* EnvSuggestion
*/
@ -1148,6 +1242,20 @@ export const zAgentSensitiveWordAvoidanceFeatureConfig = z.object({
type: z.string().nullish(),
})
export const zJsonValue2 = z.unknown()
/**
* HumanInputFormSubmissionData
*/
export const zHumanInputFormSubmissionData = z.object({
action_id: z.string(),
action_text: z.string(),
node_id: z.string(),
node_title: z.string(),
rendered_content: z.string(),
submitted_data: z.record(z.string(), zJsonValue2).nullish(),
})
/**
* AgentModelResponseFormatConfig
*/
@ -1430,6 +1538,183 @@ export const zComposerSavePayload = z.object({
version_note: z.string().nullish(),
})
/**
* ButtonStyle
*
* Button styles for user actions.
*/
export const zButtonStyle = z.enum(['accent', 'default', 'ghost', 'primary'])
/**
* UserActionConfig
*
* User action configuration.
*/
export const zUserActionConfig = z.object({
button_style: zButtonStyle.optional().default('default'),
id: z.string().max(20),
title: z.string().max(100),
})
/**
* FileType
*/
export const zFileType = z.enum(['audio', 'custom', 'document', 'image', 'video'])
/**
* FileTransferMethod
*/
export const zFileTransferMethod = z.enum([
'datasource_file',
'local_file',
'remote_url',
'tool_file',
])
/**
* FileInputConfig
*/
export const zFileInputConfig = z.object({
allowed_file_extensions: z.array(z.string()).optional(),
allowed_file_types: z.array(zFileType).optional(),
allowed_file_upload_methods: z.array(zFileTransferMethod).optional(),
output_variable_name: z.string(),
type: z.literal('file').optional().default('file'),
})
/**
* FileListInputConfig
*/
export const zFileListInputConfig = z.object({
allowed_file_extensions: z.array(z.string()).optional(),
allowed_file_types: z.array(zFileType).optional(),
allowed_file_upload_methods: z.array(zFileTransferMethod).optional(),
number_limits: z.int().gte(0).optional().default(0),
output_variable_name: z.string(),
type: z.literal('file-list').optional().default('file-list'),
})
/**
* ValueSourceType
*
* ValueSourceType records whether the value comes from a static setting
* in form definiton, or a variable while the workflow is running.
*/
export const zValueSourceType = z.enum(['constant', 'variable'])
/**
* StringSource
*
* Default configuration for form inputs.
*/
export const zStringSource = z.object({
selector: z.array(z.string()).optional(),
type: zValueSourceType,
value: z.string().optional().default(''),
})
/**
* ParagraphInputConfig
*
* Form input definition.
*/
export const zParagraphInputConfig = z.object({
default: zStringSource.nullish(),
output_variable_name: z.string(),
type: z.literal('paragraph').optional().default('paragraph'),
})
/**
* StringListSource
*/
export const zStringListSource = z.object({
selector: z.array(z.string()).optional(),
type: zValueSourceType,
value: z.array(z.string()).optional(),
})
/**
* SelectInputConfig
*/
export const zSelectInputConfig = z.object({
option_source: zStringListSource,
output_variable_name: z.string(),
type: z.literal('select').optional().default('select'),
})
export const zFormInputConfig = z.discriminatedUnion('type', [
zParagraphInputConfig.extend({ type: z.literal('paragraph') }),
zSelectInputConfig.extend({ type: z.literal('select') }),
zFileInputConfig.extend({ type: z.literal('file') }),
zFileListInputConfig.extend({ type: z.literal('file-list') }),
])
/**
* HumanInputFormDefinition
*/
export const zHumanInputFormDefinition = z.object({
actions: z.array(zUserActionConfig).optional(),
display_in_ui: z.boolean().optional().default(false),
expiration_time: z.int(),
form_content: z.string(),
form_id: z.string(),
form_token: z.string().nullish(),
inputs: z.array(zFormInputConfig).optional(),
node_id: z.string(),
node_title: z.string(),
resolved_default_values: z.record(z.string(), z.unknown()).optional(),
})
/**
* HumanInputContent
*/
export const zHumanInputContent = z.object({
form_definition: zHumanInputFormDefinition.nullish(),
form_submission_data: zHumanInputFormSubmissionData.nullish(),
submitted: z.boolean(),
type: zExecutionContentType.optional().default('human_input'),
workflow_run_id: z.string(),
})
/**
* MessageDetailResponse
*/
export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
error: z.string().nullish(),
extra_contents: z.array(zHumanInputContent).optional(),
feedbacks: z.array(zFeedback).optional(),
from_account_id: z.string().nullish(),
from_end_user_id: z.string().nullish(),
from_source: z.string(),
id: z.string(),
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})
/**
* MessageInfiniteScrollPaginationResponse
*/
export const zMessageInfiniteScrollPaginationResponse = z.object({
data: z.array(zMessageDetailResponse),
has_more: z.boolean(),
limit: z.int(),
})
/**
* AppPartial
*/
@ -1597,6 +1882,42 @@ export const zPutAgentByAgentIdPath = z.object({
*/
export const zPutAgentByAgentIdResponse = zAppDetailWithSite
export const zGetAgentByAgentIdChatMessagesPath = z.object({
agent_id: z.string(),
})
export const zGetAgentByAgentIdChatMessagesQuery = z.object({
conversation_id: z.string(),
first_id: z.string().optional(),
limit: z.int().gte(1).lte(100).optional().default(20),
})
/**
* Success
*/
export const zGetAgentByAgentIdChatMessagesResponse = zMessageInfiniteScrollPaginationResponse
export const zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsPath = z.object({
agent_id: z.string(),
message_id: z.string(),
})
/**
* Suggested questions retrieved successfully
*/
export const zGetAgentByAgentIdChatMessagesByMessageIdSuggestedQuestionsResponse
= zSuggestedQuestionsResponse
export const zPostAgentByAgentIdChatMessagesByTaskIdStopPath = z.object({
agent_id: z.string(),
task_id: z.string(),
})
/**
* Task stopped successfully
*/
export const zPostAgentByAgentIdChatMessagesByTaskIdStopResponse = zSimpleResultResponse
export const zGetAgentByAgentIdComposerPath = z.object({
agent_id: z.string(),
})
@ -1687,6 +2008,17 @@ export const zPostAgentByAgentIdFeaturesPath = z.object({
*/
export const zPostAgentByAgentIdFeaturesResponse = zSimpleResultResponse
export const zPostAgentByAgentIdFeedbacksBody = zMessageFeedbackPayload
export const zPostAgentByAgentIdFeedbacksPath = z.object({
agent_id: z.string(),
})
/**
* Feedback updated successfully
*/
export const zPostAgentByAgentIdFeedbacksResponse = zSimpleResultResponse
export const zDeleteAgentByAgentIdFilesPath = z.object({
agent_id: z.string(),
})
@ -1711,6 +2043,16 @@ export const zPostAgentByAgentIdFilesPath = z.object({
*/
export const zPostAgentByAgentIdFilesResponse = zAgentDriveFileCommitResponse
export const zGetAgentByAgentIdMessagesByMessageIdPath = z.object({
agent_id: z.string(),
message_id: z.string(),
})
/**
* Message retrieved successfully
*/
export const zGetAgentByAgentIdMessagesByMessageIdResponse = zMessageDetailResponse
export const zGetAgentByAgentIdReferencingWorkflowsPath = z.object({
agent_id: z.string(),
})