From b4e3a9095b616f986e1c3c770d0bab70fd8ec08a Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Tue, 16 Jun 2026 14:04:30 +0800 Subject: [PATCH] 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> --- api/controllers/console/app/completion.py | 144 +++++--- api/controllers/console/app/message.py | 341 ++++++++++------- api/openapi/markdown/console-openapi.md | 91 +++++ .../agent/workflow_publish_service.py | 145 ++++++-- api/services/workflow_service.py | 2 +- .../console/agent/test_agent_controllers.py | 281 ++++++++++++++ .../services/agent/test_agent_services.py | 219 +++++++++++ .../generated/api/console/agent/orpc.gen.ts | 209 +++++++++-- .../generated/api/console/agent/types.gen.ts | 314 +++++++++++++++- .../generated/api/console/agent/zod.gen.ts | 348 +++++++++++++++++- 10 files changed, 1853 insertions(+), 241 deletions(-) diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index 452d80bd5f..853f7b023f 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -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//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//chat-messages//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//chat-messages//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 diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index cbab951bf6..ef112b1b1e 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -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//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//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//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//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//chat-messages//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//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//messages/") +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") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 2623d45f6f..544b81fec4 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -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,
**Default:** 20 | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [MessageInfiniteScrollPaginationResponse](#messageinfinitescrollpaginationresponse)
| +| 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)
| +| 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)
| + ### [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)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Feedback updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +| 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/ | ---- | ----------- | ------ | | 201 | File committed into the agent drive | **application/json**: [AgentDriveFileCommitResponse](#agentdrivefilecommitresponse)
| +### [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)
| +| 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) diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index dc2b6cf85c..3d39419d79 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -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, diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 4be120ac78..9f8e4b8309 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -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, 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 3d6b23d95b..acb80075d9 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 @@ -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//sandbox/files", "/agent//skills/upload", "/agent//files", + "/agent//chat-messages", + "/agent//chat-messages//stop", + "/agent//feedbacks", + "/agent//chat-messages//suggested-questions", + "/agent//messages/", "/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.""" 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 8b28632a28..b14782c498 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -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", diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index 1d56ab6f5f..b749f64453 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -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/ */ -export const post3 = oc +export const post5 = oc .route({ description: 'Commit an uploaded file into the Agent App drive under files/', 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, } diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 967e6bf3f1..750834f123 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -66,6 +66,20 @@ export type UpdateAppPayload = { use_icon_as_answer_icon?: boolean | null } +export type MessageInfiniteScrollPaginationResponse = { + data: Array + has_more: boolean + limit: number +} + +export type SuggestedQuestionsResponse = { + data: Array +} + +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 + 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 + feedbacks?: Array + 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 + 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 } @@ -476,6 +521,63 @@ export type AgentDriveFileResponse = { size?: number | null } +export type AgentThought = { + chain_id?: string | null + created_at?: number | null + files: Array + 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 + display_in_ui?: boolean + expiration_time: number + form_content: string + form_id: string + form_token?: string | null + inputs?: Array + 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 + allowed_file_types?: Array + allowed_file_upload_methods?: Array + output_variable_name: string + type?: 'file' +} + +export type FileListInputConfig = { + allowed_file_extensions?: Array + allowed_file_types?: Array + allowed_file_upload_methods?: Array + number_limits?: number + output_variable_name: string + type?: 'file-list' +} + +export type StringSource = { + selector?: Array + type: ValueSourceType + value?: string +} + +export type StringListSource = { + selector?: Array + type: ValueSourceType + value?: Array +} + +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 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: { diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 50f050ff22..b79f2ba9f2 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -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(), })