mirror of https://github.com/langgenius/dify.git
Merge e7533a2db1 into 8b634a9bee
This commit is contained in:
commit
4f5f6447b2
|
|
@ -23,9 +23,11 @@ from .app import (
|
|||
conversation,
|
||||
file,
|
||||
file_preview,
|
||||
human_input_form,
|
||||
message,
|
||||
site,
|
||||
workflow,
|
||||
workflow_events,
|
||||
)
|
||||
from .dataset import (
|
||||
dataset,
|
||||
|
|
@ -50,6 +52,7 @@ __all__ = [
|
|||
"file",
|
||||
"file_preview",
|
||||
"hit_testing",
|
||||
"human_input_form",
|
||||
"index",
|
||||
"message",
|
||||
"metadata",
|
||||
|
|
@ -58,6 +61,7 @@ __all__ = [
|
|||
"segment",
|
||||
"site",
|
||||
"workflow",
|
||||
"workflow_events",
|
||||
]
|
||||
|
||||
api.add_namespace(service_api_ns)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
"""
|
||||
Service API human input form endpoints.
|
||||
|
||||
This module exposes app-token authenticated APIs for fetching and submitting
|
||||
paused human input forms in workflow/chatflow runs.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask import Response
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel
|
||||
from werkzeug.exceptions import InternalServerError, NotFound
|
||||
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||
from extensions.ext_database import db
|
||||
from models.model import App, EndUser
|
||||
from services.human_input_service import Form, FormNotFoundError, HumanInputService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HumanInputFormSubmitPayload(BaseModel):
|
||||
inputs: dict[str, Any]
|
||||
action: str
|
||||
|
||||
|
||||
register_schema_models(service_api_ns, HumanInputFormSubmitPayload)
|
||||
|
||||
|
||||
def _stringify_default_values(values: dict[str, object]) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for key, value in values.items():
|
||||
if value is None:
|
||||
result[key] = ""
|
||||
elif isinstance(value, (dict, list)):
|
||||
result[key] = json.dumps(value, ensure_ascii=False)
|
||||
else:
|
||||
result[key] = str(value)
|
||||
return result
|
||||
|
||||
|
||||
def _to_timestamp(value: datetime) -> int:
|
||||
return int(value.timestamp())
|
||||
|
||||
|
||||
def _jsonify_form_definition(form: Form) -> Response:
|
||||
definition_payload = form.get_definition().model_dump()
|
||||
payload = {
|
||||
"form_content": definition_payload["rendered_content"],
|
||||
"inputs": definition_payload["inputs"],
|
||||
"resolved_default_values": _stringify_default_values(definition_payload["default_values"]),
|
||||
"user_actions": definition_payload["user_actions"],
|
||||
"expiration_time": _to_timestamp(form.expiration_time),
|
||||
}
|
||||
return Response(json.dumps(payload, ensure_ascii=False), mimetype="application/json")
|
||||
|
||||
|
||||
def _ensure_form_belongs_to_app(form: Form, app_model: App) -> None:
|
||||
if form.app_id != app_model.id or form.tenant_id != app_model.tenant_id:
|
||||
raise NotFound("Form not found")
|
||||
|
||||
|
||||
@service_api_ns.route("/form/human_input/<string:form_token>")
|
||||
class WorkflowHumanInputFormApi(Resource):
|
||||
@service_api_ns.doc("get_human_input_form")
|
||||
@service_api_ns.doc(description="Get a paused human input form by token")
|
||||
@service_api_ns.doc(params={"form_token": "Human input form token"})
|
||||
@service_api_ns.doc(
|
||||
responses={
|
||||
200: "Form retrieved successfully",
|
||||
401: "Unauthorized - invalid API token",
|
||||
404: "Form not found",
|
||||
412: "Form already submitted or expired",
|
||||
}
|
||||
)
|
||||
@validate_app_token
|
||||
def get(self, app_model: App, form_token: str):
|
||||
service = HumanInputService(db.engine)
|
||||
form = service.get_form_by_token(form_token)
|
||||
if form is None:
|
||||
raise NotFound("Form not found")
|
||||
|
||||
_ensure_form_belongs_to_app(form, app_model)
|
||||
service.ensure_form_active(form)
|
||||
return _jsonify_form_definition(form)
|
||||
|
||||
@service_api_ns.expect(service_api_ns.models[HumanInputFormSubmitPayload.__name__])
|
||||
@service_api_ns.doc("submit_human_input_form")
|
||||
@service_api_ns.doc(description="Submit a paused human input form by token")
|
||||
@service_api_ns.doc(params={"form_token": "Human input form token"})
|
||||
@service_api_ns.doc(
|
||||
responses={
|
||||
200: "Form submitted successfully",
|
||||
400: "Bad request - invalid submission data",
|
||||
401: "Unauthorized - invalid API token",
|
||||
404: "Form not found",
|
||||
412: "Form already submitted or expired",
|
||||
}
|
||||
)
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True))
|
||||
def post(self, app_model: App, end_user: EndUser, form_token: str):
|
||||
payload = HumanInputFormSubmitPayload.model_validate(service_api_ns.payload or {})
|
||||
|
||||
service = HumanInputService(db.engine)
|
||||
form = service.get_form_by_token(form_token)
|
||||
if form is None:
|
||||
raise NotFound("Form not found")
|
||||
|
||||
_ensure_form_belongs_to_app(form, app_model)
|
||||
|
||||
recipient_type = form.recipient_type
|
||||
if recipient_type is None:
|
||||
logger.warning("Recipient type is None for form, form_id=%s", form.id)
|
||||
raise InternalServerError("Form recipient type is invalid")
|
||||
|
||||
try:
|
||||
service.submit_form_by_token(
|
||||
recipient_type=recipient_type,
|
||||
form_token=form_token,
|
||||
selected_action_id=payload.action,
|
||||
form_data=payload.inputs,
|
||||
submission_end_user_id=end_user.id,
|
||||
)
|
||||
except FormNotFoundError:
|
||||
raise NotFound("Form not found")
|
||||
|
||||
return {}, 200
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Service API workflow resume event stream endpoints.
|
||||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Generator
|
||||
|
||||
from flask import Response, request
|
||||
from flask_restx import Resource
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.service_api import service_api_ns
|
||||
from controllers.service_api.app.error import NotWorkflowAppError
|
||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||
from core.app.apps.advanced_chat.app_generator import AdvancedChatAppGenerator
|
||||
from core.app.apps.base_app_generator import BaseAppGenerator
|
||||
from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter
|
||||
from core.app.apps.message_generator import MessageGenerator
|
||||
from core.app.apps.workflow.app_generator import WorkflowAppGenerator
|
||||
from extensions.ext_database import db
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import App, AppMode, EndUser
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
from services.workflow_event_snapshot_service import build_workflow_event_stream
|
||||
|
||||
|
||||
@service_api_ns.route("/workflow/<string:task_id>/events")
|
||||
class WorkflowEventsApi(Resource):
|
||||
"""Service API for getting workflow execution events after resume."""
|
||||
|
||||
@service_api_ns.doc("get_workflow_events")
|
||||
@service_api_ns.doc(description="Get workflow execution events stream after resume")
|
||||
@service_api_ns.doc(
|
||||
params={
|
||||
"task_id": "Workflow run ID",
|
||||
"user": "End user identifier (query param)",
|
||||
"include_state_snapshot": "Whether to replay from persisted state snapshot",
|
||||
}
|
||||
)
|
||||
@service_api_ns.doc(
|
||||
responses={
|
||||
200: "SSE event stream",
|
||||
401: "Unauthorized - invalid API token",
|
||||
404: "Workflow run not found",
|
||||
}
|
||||
)
|
||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True))
|
||||
def get(self, app_model: App, end_user: EndUser, task_id: str):
|
||||
app_mode = AppMode.value_of(app_model.mode)
|
||||
if app_mode not in {AppMode.WORKFLOW, AppMode.ADVANCED_CHAT}:
|
||||
raise NotWorkflowAppError()
|
||||
|
||||
session_maker = sessionmaker(db.engine)
|
||||
repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker)
|
||||
workflow_run = repo.get_workflow_run_by_id_and_tenant_id(
|
||||
tenant_id=app_model.tenant_id,
|
||||
run_id=task_id,
|
||||
)
|
||||
|
||||
if workflow_run is None:
|
||||
raise NotFound("Workflow run not found")
|
||||
|
||||
if workflow_run.app_id != app_model.id:
|
||||
raise NotFound("Workflow run not found")
|
||||
|
||||
if workflow_run.created_by_role != CreatorUserRole.END_USER:
|
||||
raise NotFound("Workflow run not found")
|
||||
|
||||
if workflow_run.created_by != end_user.id:
|
||||
raise NotFound("Workflow run not found")
|
||||
|
||||
workflow_run_entity = workflow_run
|
||||
|
||||
if workflow_run_entity.finished_at is not None:
|
||||
response = WorkflowResponseConverter.workflow_run_result_to_finish_response(
|
||||
task_id=workflow_run_entity.id,
|
||||
workflow_run=workflow_run_entity,
|
||||
creator_user=end_user,
|
||||
)
|
||||
|
||||
payload = response.model_dump(mode="json")
|
||||
payload["event"] = response.event.value
|
||||
|
||||
def _generate_finished_events() -> Generator[str, None, None]:
|
||||
yield f"data: {json.dumps(payload)}\n\n"
|
||||
|
||||
event_generator = _generate_finished_events
|
||||
else:
|
||||
msg_generator = MessageGenerator()
|
||||
generator: BaseAppGenerator
|
||||
if app_mode == AppMode.ADVANCED_CHAT:
|
||||
generator = AdvancedChatAppGenerator()
|
||||
elif app_mode == AppMode.WORKFLOW:
|
||||
generator = WorkflowAppGenerator()
|
||||
else:
|
||||
raise NotWorkflowAppError()
|
||||
|
||||
include_state_snapshot = request.args.get("include_state_snapshot", "false").lower() == "true"
|
||||
|
||||
def _generate_stream_events():
|
||||
if include_state_snapshot:
|
||||
return generator.convert_to_event_stream(
|
||||
build_workflow_event_stream(
|
||||
app_mode=app_mode,
|
||||
workflow_run=workflow_run_entity,
|
||||
tenant_id=app_model.tenant_id,
|
||||
app_id=app_model.id,
|
||||
session_maker=session_maker,
|
||||
)
|
||||
)
|
||||
return generator.convert_to_event_stream(
|
||||
msg_generator.retrieve_events(app_mode, workflow_run_entity.id),
|
||||
)
|
||||
|
||||
event_generator = _generate_stream_events
|
||||
|
||||
return Response(
|
||||
event_generator(),
|
||||
mimetype="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
"""Unit tests for Service API human input form endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.service_api.app.human_input_form import WorkflowHumanInputFormApi
|
||||
from models.human_input import RecipientType
|
||||
from tests.unit_tests.controllers.service_api.conftest import _unwrap
|
||||
|
||||
|
||||
class TestWorkflowHumanInputFormApi:
|
||||
def test_get_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
definition = SimpleNamespace(
|
||||
model_dump=lambda: {
|
||||
"rendered_content": "Rendered form content",
|
||||
"inputs": [{"output_variable_name": "name"}],
|
||||
"default_values": {"name": "Alice", "age": 30, "meta": {"k": "v"}},
|
||||
"user_actions": [{"id": "approve", "title": "Approve"}],
|
||||
}
|
||||
)
|
||||
form = SimpleNamespace(
|
||||
app_id="app-1",
|
||||
tenant_id="tenant-1",
|
||||
expiration_time=datetime(2099, 1, 1, tzinfo=UTC),
|
||||
get_definition=lambda: definition,
|
||||
)
|
||||
service_mock = Mock()
|
||||
service_mock.get_form_by_token.return_value = form
|
||||
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
|
||||
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
|
||||
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
|
||||
|
||||
api = WorkflowHumanInputFormApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
|
||||
with app.test_request_context("/form/human_input/token-1", method="GET"):
|
||||
response = handler(api, app_model=app_model, form_token="token-1")
|
||||
|
||||
payload = json.loads(response.get_data(as_text=True))
|
||||
assert payload == {
|
||||
"form_content": "Rendered form content",
|
||||
"inputs": [{"output_variable_name": "name"}],
|
||||
"resolved_default_values": {"name": "Alice", "age": "30", "meta": '{"k": "v"}'},
|
||||
"user_actions": [{"id": "approve", "title": "Approve"}],
|
||||
"expiration_time": int(form.expiration_time.timestamp()),
|
||||
}
|
||||
service_mock.get_form_by_token.assert_called_once_with("token-1")
|
||||
service_mock.ensure_form_active.assert_called_once_with(form)
|
||||
|
||||
def test_get_form_not_in_app(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
form = SimpleNamespace(
|
||||
app_id="another-app",
|
||||
tenant_id="tenant-1",
|
||||
expiration_time=datetime(2099, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
service_mock = Mock()
|
||||
service_mock.get_form_by_token.return_value = form
|
||||
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
|
||||
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
|
||||
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
|
||||
|
||||
api = WorkflowHumanInputFormApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
|
||||
with app.test_request_context("/form/human_input/token-1", method="GET"):
|
||||
with pytest.raises(NotFound):
|
||||
handler(api, app_model=app_model, form_token="token-1")
|
||||
|
||||
def test_post_success(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
form = SimpleNamespace(
|
||||
app_id="app-1",
|
||||
tenant_id="tenant-1",
|
||||
recipient_type=RecipientType.BACKSTAGE,
|
||||
)
|
||||
service_mock = Mock()
|
||||
service_mock.get_form_by_token.return_value = form
|
||||
workflow_module = sys.modules["controllers.service_api.app.human_input_form"]
|
||||
monkeypatch.setattr(workflow_module, "HumanInputService", lambda _engine: service_mock)
|
||||
monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object()))
|
||||
|
||||
api = WorkflowHumanInputFormApi()
|
||||
handler = _unwrap(api.post)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1")
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context(
|
||||
"/form/human_input/token-1",
|
||||
method="POST",
|
||||
json={"inputs": {"name": "Alice"}, "action": "approve", "user": "external-1"},
|
||||
):
|
||||
response, status = handler(api, app_model=app_model, end_user=end_user, form_token="token-1")
|
||||
|
||||
assert response == {}
|
||||
assert status == 200
|
||||
service_mock.submit_form_by_token.assert_called_once_with(
|
||||
recipient_type=RecipientType.BACKSTAGE,
|
||||
form_token="token-1",
|
||||
selected_action_id="approve",
|
||||
form_data={"name": "Alice"},
|
||||
submission_end_user_id="end-user-1",
|
||||
)
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
"""Unit tests for Service API workflow event stream endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.service_api.app.error import NotWorkflowAppError
|
||||
from controllers.service_api.app.workflow_events import WorkflowEventsApi
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import AppMode
|
||||
from tests.unit_tests.controllers.service_api.conftest import _unwrap
|
||||
|
||||
|
||||
def _mock_repo_for_run(monkeypatch: pytest.MonkeyPatch, workflow_run):
|
||||
workflow_events_module = sys.modules["controllers.service_api.app.workflow_events"]
|
||||
repo = SimpleNamespace(get_workflow_run_by_id_and_tenant_id=lambda **_kwargs: workflow_run)
|
||||
monkeypatch.setattr(
|
||||
workflow_events_module.DifyAPIRepositoryFactory,
|
||||
"create_api_workflow_run_repository",
|
||||
lambda *_args, **_kwargs: repo,
|
||||
)
|
||||
monkeypatch.setattr(workflow_events_module, "db", SimpleNamespace(engine=object()))
|
||||
return workflow_events_module
|
||||
|
||||
|
||||
class TestWorkflowEventsApi:
|
||||
def test_wrong_app_mode(self, app) -> None:
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(mode=AppMode.CHAT.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
|
||||
with pytest.raises(NotWorkflowAppError):
|
||||
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
def test_workflow_run_not_found(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_mock_repo_for_run(monkeypatch, workflow_run=None)
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
|
||||
with pytest.raises(NotFound):
|
||||
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
def test_workflow_run_permission_denied(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow_run = SimpleNamespace(
|
||||
id="run-1",
|
||||
app_id="app-1",
|
||||
created_by_role=CreatorUserRole.ACCOUNT,
|
||||
created_by="another-user",
|
||||
finished_at=None,
|
||||
)
|
||||
_mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
|
||||
with pytest.raises(NotFound):
|
||||
handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
def test_finished_run_returns_sse(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow_run = SimpleNamespace(
|
||||
id="run-1",
|
||||
app_id="app-1",
|
||||
created_by_role=CreatorUserRole.END_USER,
|
||||
created_by="end-user-1",
|
||||
finished_at=datetime(2099, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
|
||||
monkeypatch.setattr(
|
||||
workflow_events_module.WorkflowResponseConverter,
|
||||
"workflow_run_result_to_finish_response",
|
||||
lambda **_kwargs: SimpleNamespace(
|
||||
model_dump=lambda mode="json": {"task_id": "run-1", "status": "succeeded"},
|
||||
event=SimpleNamespace(value="workflow_finished"),
|
||||
),
|
||||
)
|
||||
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
|
||||
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
assert response.mimetype == "text/event-stream"
|
||||
body = response.get_data(as_text=True).strip()
|
||||
assert body.startswith("data: ")
|
||||
payload = json.loads(body[len("data: ") :])
|
||||
assert payload["task_id"] == "run-1"
|
||||
assert payload["event"] == "workflow_finished"
|
||||
|
||||
def test_running_run_streams_events(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow_run = SimpleNamespace(
|
||||
id="run-1",
|
||||
app_id="app-1",
|
||||
created_by_role=CreatorUserRole.END_USER,
|
||||
created_by="end-user-1",
|
||||
finished_at=None,
|
||||
)
|
||||
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
|
||||
msg_generator = Mock()
|
||||
msg_generator.retrieve_events.return_value = ["raw-event"]
|
||||
workflow_generator = Mock()
|
||||
workflow_generator.convert_to_event_stream.return_value = iter(["data: streamed\n\n"])
|
||||
monkeypatch.setattr(workflow_events_module, "MessageGenerator", lambda: msg_generator)
|
||||
monkeypatch.setattr(workflow_events_module, "WorkflowAppGenerator", lambda: workflow_generator)
|
||||
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1", method="GET"):
|
||||
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
assert response.get_data(as_text=True) == "data: streamed\n\n"
|
||||
msg_generator.retrieve_events.assert_called_once_with(AppMode.WORKFLOW, "run-1")
|
||||
workflow_generator.convert_to_event_stream.assert_called_once_with(["raw-event"])
|
||||
|
||||
def test_running_run_with_snapshot(self, app, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
workflow_run = SimpleNamespace(
|
||||
id="run-1",
|
||||
app_id="app-1",
|
||||
created_by_role=CreatorUserRole.END_USER,
|
||||
created_by="end-user-1",
|
||||
finished_at=None,
|
||||
)
|
||||
workflow_events_module = _mock_repo_for_run(monkeypatch, workflow_run=workflow_run)
|
||||
msg_generator = Mock()
|
||||
workflow_generator = Mock()
|
||||
workflow_generator.convert_to_event_stream.return_value = iter(["data: snapshot\n\n"])
|
||||
snapshot_builder = Mock(return_value=["snapshot-events"])
|
||||
monkeypatch.setattr(workflow_events_module, "MessageGenerator", lambda: msg_generator)
|
||||
monkeypatch.setattr(workflow_events_module, "WorkflowAppGenerator", lambda: workflow_generator)
|
||||
monkeypatch.setattr(workflow_events_module, "build_workflow_event_stream", snapshot_builder)
|
||||
|
||||
api = WorkflowEventsApi()
|
||||
handler = _unwrap(api.get)
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW.value)
|
||||
end_user = SimpleNamespace(id="end-user-1")
|
||||
|
||||
with app.test_request_context("/workflow/run-1/events?user=u1&include_state_snapshot=true", method="GET"):
|
||||
response = handler(api, app_model=app_model, end_user=end_user, task_id="run-1")
|
||||
|
||||
assert response.get_data(as_text=True) == "data: snapshot\n\n"
|
||||
msg_generator.retrieve_events.assert_not_called()
|
||||
snapshot_builder.assert_called_once()
|
||||
workflow_generator.convert_to_event_stream.assert_called_once_with(["snapshot-events"])
|
||||
|
|
@ -191,6 +191,24 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
- `total_price` (decimal) optional Total cost
|
||||
- `currency` (string) optional e.g. `USD` / `RMB`
|
||||
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
|
||||
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
|
||||
- `task_id` (string) Task ID, used for request tracking
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
- `event` (string) fixed to `human_input_required`
|
||||
- `data` (object) detail
|
||||
- `form_id` (string) Human input form ID
|
||||
- `node_id` (string) Human input node ID
|
||||
- `node_title` (string) Human input node title
|
||||
- `form_content` (string) Rendered form content
|
||||
- `inputs` (array[object]) Input field definitions
|
||||
- `actions` (array[object]) User action buttons
|
||||
- `id` (string) Action ID
|
||||
- `title` (string) Button text
|
||||
- `button_style` (string) Button style
|
||||
- `display_in_ui` (bool) Whether this form should be shown in UI
|
||||
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
|
||||
- `resolved_default_values` (object) Runtime-resolved default values
|
||||
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
|
||||
- `event: workflow_finished` workflow execution ends, success or failure in different states in the same event
|
||||
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
|
|
@ -578,6 +596,166 @@ Chat applications support session persistence, allowing previous chat history to
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='Get Human Input Form'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Retrieve a pending Human-in-the-Loop form by `form_token`.
|
||||
|
||||
Use this endpoint when streaming returns `human_input_required` with a `form_token`.
|
||||
|
||||
### Path
|
||||
- `form_token` (string) Required, token returned by the pause event.
|
||||
|
||||
### Response
|
||||
- `form_content` (string) Rendered form content (markdown/plain text)
|
||||
- `inputs` (array[object]) Form input definitions
|
||||
- `resolved_default_values` (object) Default values resolved to strings
|
||||
- `user_actions` (array[object]) Action buttons
|
||||
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
|
||||
|
||||
### Errors
|
||||
- 404, form not found or does not belong to current app
|
||||
- 412, `human_input_form_submitted`, form already submitted
|
||||
- 412, `human_input_form_expired`, form expired
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"form_content": "Please confirm the final answer: {{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "Answer",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "Initial value"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "Approve", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "Reject", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='Submit Human Input Form'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Submit a pending Human-in-the-Loop form.
|
||||
|
||||
### Path
|
||||
- `form_token` (string) Required, token returned by the pause event.
|
||||
|
||||
### Request Body
|
||||
- `inputs` (object) Required, key/value pairs for form fields.
|
||||
- `action` (string) Required, selected action ID from `user_actions`.
|
||||
- `user` (string) Required, end-user identifier.
|
||||
|
||||
### Response
|
||||
Returns an empty object on success.
|
||||
|
||||
### Errors
|
||||
- 400, `invalid_form_data`, submitted data does not match the form schema
|
||||
- 404, form not found or does not belong to current app
|
||||
- 412, `human_input_form_submitted`, form already submitted
|
||||
- 412, `human_input_form_expired`, form expired
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "Approved answer"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='Get Workflow Resume Events'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Continue receiving workflow events after submitting a human input form.
|
||||
|
||||
This endpoint returns `text/event-stream` and can be used to observe resumed execution until completion.
|
||||
|
||||
### Path
|
||||
- `task_id` (string) Required, workflow run ID (`workflow_run_id`).
|
||||
|
||||
### Query
|
||||
- `user` (string) Required, end-user identifier.
|
||||
- `include_state_snapshot` (bool) Optional, set to `true` to replay from persisted state snapshot before continuing with live events.
|
||||
|
||||
### Response
|
||||
Server-Sent Events stream (`text/event-stream`).
|
||||
Typical events include `node_started`, `node_finished`, `human_input_form_filled`, `human_input_form_timeout`, and `workflow_finished`.
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```streaming {{ title: 'Response' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"Approved answer","action_id":"approve","action_text":"Approve"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/messages/:message_id/feedbacks'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -191,6 +191,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
- `total_price` (decimal) オプションの合計コスト
|
||||
- `currency` (string) オプション、例:`USD` / `RMB`
|
||||
- `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332
|
||||
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
|
||||
- `task_id` (string) タスク ID、リクエスト追跡に使用
|
||||
- `workflow_run_id` (string) ワークフロー実行の一意 ID
|
||||
- `event` (string) `human_input_required`に固定
|
||||
- `data` (object) 詳細
|
||||
- `form_id` (string) ヒューマン入力フォーム ID
|
||||
- `node_id` (string) Human Input ノード ID
|
||||
- `node_title` (string) Human Input ノードタイトル
|
||||
- `form_content` (string) レンダリング済みフォーム内容
|
||||
- `inputs` (array[object]) フォーム入力項目の定義
|
||||
- `actions` (array[object]) ユーザーが選択できるアクションボタン
|
||||
- `id` (string) アクション ID
|
||||
- `title` (string) ボタンラベル
|
||||
- `button_style` (string) ボタンスタイル
|
||||
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
|
||||
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
|
||||
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
|
||||
- `expiration_time` (timestamp) フォームの有効期限(Unix 秒)
|
||||
- `event: workflow_finished` ワークフロー実行が終了、成功または失敗は同じイベント内で異なる状態で示されます
|
||||
- `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用
|
||||
- `workflow_run_id` (string) ワークフロー実行の一意ID
|
||||
|
|
@ -579,6 +597,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='Human Input フォームを取得'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
`form_token` から保留中の Human-in-the-Loop フォームを取得します。
|
||||
|
||||
ストリーミングイベントで `human_input_required`(`form_token` を含む)が返された際に使用します。
|
||||
|
||||
### パス
|
||||
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
|
||||
|
||||
### 応答
|
||||
- `form_content` (string) レンダリング済みフォーム内容(markdown/plain text)
|
||||
- `inputs` (array[object]) 入力項目定義
|
||||
- `resolved_default_values` (object) 解決済みデフォルト値(文字列)
|
||||
- `user_actions` (array[object]) アクションボタン一覧
|
||||
- `expiration_time` (timestamp) フォーム有効期限(Unix 秒)
|
||||
|
||||
### エラー
|
||||
- 404, フォームが存在しない、または現在のアプリに属していない
|
||||
- 412, `human_input_form_submitted`, 既に送信済み
|
||||
- 412, `human_input_form_expired`, 期限切れ
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```json {{ title: '応答' }}
|
||||
{
|
||||
"form_content": "最終回答を確認してください: {{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "回答",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "初期値"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "承認", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "却下", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='Human Input フォームを送信'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
保留中の Human-in-the-Loop フォームを送信します。
|
||||
|
||||
### パス
|
||||
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
|
||||
|
||||
### リクエストボディ
|
||||
- `inputs` (object) 必須、フォーム項目の key/value
|
||||
- `action` (string) 必須、`user_actions` から選択したアクション ID
|
||||
- `user` (string) 必須、エンドユーザー識別子
|
||||
|
||||
### 応答
|
||||
成功時は空オブジェクトを返します。
|
||||
|
||||
### エラー
|
||||
- 400, `invalid_form_data`, 送信データがフォームスキーマに一致しない
|
||||
- 404, フォームが存在しない、または現在のアプリに属していない
|
||||
- 412, `human_input_form_submitted`, 既に送信済み
|
||||
- 412, `human_input_form_expired`, 期限切れ
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "承認済み回答"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```json {{ title: '応答' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='再開後の Workflow イベントを取得'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Human Input フォーム送信後に、ワークフロー再開後のイベントを継続受信します。
|
||||
|
||||
このエンドポイントは `text/event-stream` を返し、完了までイベントを購読できます。
|
||||
|
||||
### パス
|
||||
- `task_id` (string) 必須、workflow 実行 ID(`workflow_run_id`)
|
||||
|
||||
### クエリ
|
||||
- `user` (string) 必須、エンドユーザー識別子
|
||||
- `include_state_snapshot` (bool) 任意、`true` の場合は永続化済み状態スナップショットを先に再生してからリアルタイムイベントへ移行
|
||||
|
||||
### 応答
|
||||
Server-Sent Events ストリーム(`text/event-stream`)。
|
||||
主なイベントは `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished` です。
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```streaming {{ title: '応答' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"承認済み回答","action_id":"approve","action_text":"承認"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/messages/:message_id/feedbacks'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -189,6 +189,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
- `total_price` (decimal) optional 总费用
|
||||
- `currency` (string) optional 货币,如 `USD` / `RMB`
|
||||
- `created_at` (timestamp) 开始时间
|
||||
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
- `event` (string) 固定为 `human_input_required`
|
||||
- `data` (object) 详细内容
|
||||
- `form_id` (string) 人工输入表单 ID
|
||||
- `node_id` (string) Human Input 节点 ID
|
||||
- `node_title` (string) Human Input 节点标题
|
||||
- `form_content` (string) 渲染后的表单内容
|
||||
- `inputs` (array[object]) 表单输入项定义
|
||||
- `actions` (array[object]) 用户可选动作按钮
|
||||
- `id` (string) 动作 ID
|
||||
- `title` (string) 按钮文案
|
||||
- `button_style` (string) 按钮样式
|
||||
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
|
||||
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
|
||||
- `resolved_default_values` (object) 运行时解析后的默认值
|
||||
- `expiration_time` (timestamp) 表单过期时间(Unix 秒级时间戳)
|
||||
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
|
|
@ -572,6 +590,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
</Row>
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='获取人工输入表单'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
通过 `form_token` 获取待处理的 Human-in-the-Loop 表单。
|
||||
|
||||
当流式事件返回 `human_input_required`(包含 `form_token`)时,可调用此接口拉取表单详情。
|
||||
|
||||
### Path
|
||||
- `form_token` (string) 必填,暂停事件返回的表单 token
|
||||
|
||||
### Response
|
||||
- `form_content` (string) 已渲染的表单内容(markdown/plain text)
|
||||
- `inputs` (array[object]) 表单输入项定义
|
||||
- `resolved_default_values` (object) 已解析的默认值(字符串)
|
||||
- `user_actions` (array[object]) 操作按钮列表
|
||||
- `expiration_time` (timestamp) 表单过期时间(Unix 秒)
|
||||
|
||||
### Errors
|
||||
- 404,表单不存在或不属于当前应用
|
||||
- 412,`human_input_form_submitted`,表单已被提交
|
||||
- 412,`human_input_form_expired`,表单已过期
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"form_content": "请确认最终结果:{{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "答案",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "初始值"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "通过", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "拒绝", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='提交人工输入表单'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
提交待处理的 Human-in-the-Loop 表单。
|
||||
|
||||
### Path
|
||||
- `form_token` (string) 必填,暂停事件返回的表单 token
|
||||
|
||||
### Request Body
|
||||
- `inputs` (object) 必填,表单字段的 key/value
|
||||
- `action` (string) 必填,从 `user_actions` 中选择的动作 ID
|
||||
- `user` (string) 必填,终端用户标识
|
||||
|
||||
### Response
|
||||
成功时返回空对象。
|
||||
|
||||
### Errors
|
||||
- 400,`invalid_form_data`,提交数据与表单 schema 不匹配
|
||||
- 404,表单不存在或不属于当前应用
|
||||
- 412,`human_input_form_submitted`,表单已被提交
|
||||
- 412,`human_input_form_expired`,表单已过期
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "已确认答案"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='获取恢复后的 Workflow 事件流'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
在提交人工输入表单后,继续订阅工作流后续执行事件。
|
||||
|
||||
返回 `text/event-stream`,可持续接收直到工作流结束。
|
||||
|
||||
### Path
|
||||
- `task_id` (string) 必填,workflow 运行 ID(`workflow_run_id`)
|
||||
|
||||
### Query
|
||||
- `user` (string) 必填,终端用户标识
|
||||
- `include_state_snapshot` (bool) 可选,设为 `true` 时会先回放持久化状态快照,再继续实时事件
|
||||
|
||||
### Response
|
||||
Server-Sent Events 流(`text/event-stream`)。
|
||||
常见事件包括 `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished`。
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```streaming {{ title: 'Response' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"已确认答案","action_id":"approve","action_text":"通过"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/messages/:message_id/feedbacks'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -146,6 +146,24 @@ Workflow applications offers non-session support and is ideal for translation, a
|
|||
- `total_price` (decimal) optional Total cost
|
||||
- `currency` (string) optional e.g. `USD` / `RMB`
|
||||
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
|
||||
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
|
||||
- `task_id` (string) Task ID, used for request tracking
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
- `event` (string) fixed to `human_input_required`
|
||||
- `data` (object) detail
|
||||
- `form_id` (string) Human input form ID
|
||||
- `node_id` (string) Human input node ID
|
||||
- `node_title` (string) Human input node title
|
||||
- `form_content` (string) Rendered form content
|
||||
- `inputs` (array[object]) Input field definitions
|
||||
- `actions` (array[object]) User action buttons
|
||||
- `id` (string) Action ID
|
||||
- `title` (string) Button text
|
||||
- `button_style` (string) Button style
|
||||
- `display_in_ui` (bool) Whether this form should be shown in UI
|
||||
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
|
||||
- `resolved_default_values` (object) Runtime-resolved default values
|
||||
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
|
||||
- `event: workflow_finished` workflow execution ends, success or failure in different states in the same event
|
||||
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
|
|
@ -457,6 +475,24 @@ Workflow applications offers non-session support and is ideal for translation, a
|
|||
- `total_price` (decimal) optional total cost
|
||||
- `currency` (string) optional currency, such as `USD` / `RMB`
|
||||
- `created_at` (timestamp) timestamp of start, e.g., 1705395332
|
||||
- `event: human_input_required` Workflow paused and requires Human-in-the-Loop input
|
||||
- `task_id` (string) Task ID, used for request tracking
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
- `event` (string) fixed to `human_input_required`
|
||||
- `data` (object) detail
|
||||
- `form_id` (string) Human input form ID
|
||||
- `node_id` (string) Human input node ID
|
||||
- `node_title` (string) Human input node title
|
||||
- `form_content` (string) Rendered form content
|
||||
- `inputs` (array[object]) Input field definitions
|
||||
- `actions` (array[object]) User action buttons
|
||||
- `id` (string) Action ID
|
||||
- `title` (string) Button text
|
||||
- `button_style` (string) Button style
|
||||
- `display_in_ui` (bool) Whether this form should be shown in UI
|
||||
- `form_token` (string) Token used by `/form/human_input/:form_token` APIs
|
||||
- `resolved_default_values` (object) Runtime-resolved default values
|
||||
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
|
||||
- `event: workflow_finished` workflow execution finished, success and failure are different states in the same event
|
||||
- `task_id` (string) Task ID, used for request tracking and the below Stop Generate API
|
||||
- `workflow_run_id` (string) Unique ID of workflow execution
|
||||
|
|
@ -666,6 +702,166 @@ Workflow applications offers non-session support and is ideal for translation, a
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='Get Human Input Form'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Retrieve a pending Human-in-the-Loop form by `form_token`.
|
||||
|
||||
Use this endpoint when a workflow pauses with `human_input_required` and returns a `form_token`.
|
||||
|
||||
### Path
|
||||
- `form_token` (string) Required, token returned by the pause event.
|
||||
|
||||
### Response
|
||||
- `form_content` (string) Rendered form content (markdown/plain text)
|
||||
- `inputs` (array[object]) Form input definitions
|
||||
- `resolved_default_values` (object) Default values resolved to strings
|
||||
- `user_actions` (array[object]) Action buttons
|
||||
- `expiration_time` (timestamp) Form expiration time (Unix seconds)
|
||||
|
||||
### Errors
|
||||
- 404, form not found or does not belong to current app
|
||||
- 412, `human_input_form_submitted`, form already submitted
|
||||
- 412, `human_input_form_expired`, form expired
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"form_content": "Please confirm the final answer: {{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "Answer",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "Initial value"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "Approve", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "Reject", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='Submit Human Input Form'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Submit a pending Human-in-the-Loop form.
|
||||
|
||||
### Path
|
||||
- `form_token` (string) Required, token returned by the pause event.
|
||||
|
||||
### Request Body
|
||||
- `inputs` (object) Required, key/value pairs for form fields.
|
||||
- `action` (string) Required, selected action ID from `user_actions`.
|
||||
- `user` (string) Required, end-user identifier.
|
||||
|
||||
### Response
|
||||
Returns an empty object on success.
|
||||
|
||||
### Errors
|
||||
- 400, `invalid_form_data`, submitted data does not match the form schema
|
||||
- 404, form not found or does not belong to current app
|
||||
- 412, `human_input_form_submitted`, form already submitted
|
||||
- 412, `human_input_form_expired`, form expired
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "Approved answer"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='Get Workflow Resume Events'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Continue receiving workflow events after submitting a human input form.
|
||||
|
||||
This endpoint returns `text/event-stream` and can be used to observe the resumed run until completion.
|
||||
|
||||
### Path
|
||||
- `task_id` (string) Required, workflow run ID (`workflow_run_id`).
|
||||
|
||||
### Query
|
||||
- `user` (string) Required, end-user identifier.
|
||||
- `include_state_snapshot` (bool) Optional, set to `true` to replay from persisted state snapshot before continuing with live events.
|
||||
|
||||
### Response
|
||||
Server-Sent Events stream (`text/event-stream`).
|
||||
Typical events include `node_started`, `node_finished`, `human_input_form_filled`, `human_input_form_timeout`, and `workflow_finished`.
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```streaming {{ title: 'Response' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"Approved answer","action_id":"approve","action_text":"Approve"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -146,6 +146,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
- `total_price` (decimal) オプションの総コスト
|
||||
- `currency` (string) オプション 例:`USD` / `RMB`
|
||||
- `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332
|
||||
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
|
||||
- `task_id` (string) タスク ID、リクエスト追跡に使用
|
||||
- `workflow_run_id` (string) ワークフロー実行の一意の ID
|
||||
- `event` (string) `human_input_required`に固定
|
||||
- `data` (object) 詳細
|
||||
- `form_id` (string) ヒューマン入力フォーム ID
|
||||
- `node_id` (string) Human Input ノード ID
|
||||
- `node_title` (string) Human Input ノードタイトル
|
||||
- `form_content` (string) レンダリング済みフォーム内容
|
||||
- `inputs` (array[object]) フォーム入力項目の定義
|
||||
- `actions` (array[object]) ユーザーが選択できるアクションボタン
|
||||
- `id` (string) アクション ID
|
||||
- `title` (string) ボタンラベル
|
||||
- `button_style` (string) ボタンスタイル
|
||||
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
|
||||
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
|
||||
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
|
||||
- `expiration_time` (timestamp) フォームの有効期限(Unix 秒)
|
||||
- `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗
|
||||
- `task_id` (string) タスク ID、リクエスト追跡と以下の Stop Generate API に使用
|
||||
- `workflow_run_id` (string) ワークフロー実行の一意の ID
|
||||
|
|
@ -452,6 +470,24 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
- `total_price` (decimal) オプション 総費用
|
||||
- `currency` (string) オプション 通貨、例:`USD` / `RMB`
|
||||
- `created_at` (timestamp) 開始時間
|
||||
- `event: human_input_required` ワークフローが一時停止し、Human-in-the-Loop 入力が必要
|
||||
- `task_id` (string) タスク ID、リクエスト追跡に使用
|
||||
- `workflow_run_id` (string) ワークフロー実行 ID
|
||||
- `event` (string) `human_input_required` に固定
|
||||
- `data` (object) 詳細内容
|
||||
- `form_id` (string) ヒューマン入力フォーム ID
|
||||
- `node_id` (string) Human Input ノード ID
|
||||
- `node_title` (string) Human Input ノードタイトル
|
||||
- `form_content` (string) レンダリング済みフォーム内容
|
||||
- `inputs` (array[object]) フォーム入力項目の定義
|
||||
- `actions` (array[object]) ユーザーが選択できるアクションボタン
|
||||
- `id` (string) アクション ID
|
||||
- `title` (string) ボタンラベル
|
||||
- `button_style` (string) ボタンスタイル
|
||||
- `display_in_ui` (bool) UI にこのフォームを表示するかどうか
|
||||
- `form_token` (string) `/form/human_input/:form_token` API で使用するトークン
|
||||
- `resolved_default_values` (object) 実行時に解決されたデフォルト値
|
||||
- `expiration_time` (timestamp) フォームの有効期限(Unix 秒)
|
||||
- `event: workflow_finished` ワークフロー実行終了、成功と失敗は同じイベント内の異なる状態
|
||||
- `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用
|
||||
- `workflow_run_id` (string) ワークフロー実行ID
|
||||
|
|
@ -661,6 +697,166 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='Human Input フォームを取得'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
`form_token` から保留中の Human-in-the-Loop フォームを取得します。
|
||||
|
||||
Workflow が `human_input_required`(`form_token` を含む)で一時停止した際に使用します。
|
||||
|
||||
### パス
|
||||
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
|
||||
|
||||
### 応答
|
||||
- `form_content` (string) レンダリング済みフォーム内容(markdown/plain text)
|
||||
- `inputs` (array[object]) 入力項目定義
|
||||
- `resolved_default_values` (object) 解決済みデフォルト値(文字列)
|
||||
- `user_actions` (array[object]) アクションボタン一覧
|
||||
- `expiration_time` (timestamp) フォーム有効期限(Unix 秒)
|
||||
|
||||
### エラー
|
||||
- 404, フォームが存在しない、または現在のアプリに属していない
|
||||
- 412, `human_input_form_submitted`, 既に送信済み
|
||||
- 412, `human_input_form_expired`, 期限切れ
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```json {{ title: '応答' }}
|
||||
{
|
||||
"form_content": "最終回答を確認してください: {{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "回答",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "初期値"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "承認", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "却下", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='Human Input フォームを送信'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
保留中の Human-in-the-Loop フォームを送信します。
|
||||
|
||||
### パス
|
||||
- `form_token` (string) 必須、一時停止イベントで返されたフォームトークン
|
||||
|
||||
### リクエストボディ
|
||||
- `inputs` (object) 必須、フォーム項目の key/value
|
||||
- `action` (string) 必須、`user_actions` から選択したアクション ID
|
||||
- `user` (string) 必須、エンドユーザー識別子
|
||||
|
||||
### 応答
|
||||
成功時は空オブジェクトを返します。
|
||||
|
||||
### エラー
|
||||
- 400, `invalid_form_data`, 送信データがフォームスキーマに一致しない
|
||||
- 404, フォームが存在しない、または現在のアプリに属していない
|
||||
- 412, `human_input_form_submitted`, 既に送信済み
|
||||
- 412, `human_input_form_expired`, 期限切れ
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "承認済み回答"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```json {{ title: '応答' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='再開後の Workflow イベントを取得'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Human Input フォーム送信後に、ワークフロー再開後のイベントを継続受信します。
|
||||
|
||||
このエンドポイントは `text/event-stream` を返し、完了までイベントを購読できます。
|
||||
|
||||
### パス
|
||||
- `task_id` (string) 必須、workflow 実行 ID(`workflow_run_id`)
|
||||
|
||||
### クエリ
|
||||
- `user` (string) 必須、エンドユーザー識別子
|
||||
- `include_state_snapshot` (bool) 任意、`true` の場合は永続化済み状態スナップショットを先に再生してからリアルタイムイベントへ移行
|
||||
|
||||
### 応答
|
||||
Server-Sent Events ストリーム(`text/event-stream`)。
|
||||
主なイベントは `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished` です。
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="リクエスト"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="応答">
|
||||
```streaming {{ title: '応答' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"承認済み回答","action_id":"approve","action_text":"承認"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
|
|
|
|||
|
|
@ -136,6 +136,24 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
|||
- `total_price` (decimal) optional 总费用
|
||||
- `currency` (string) optional 货币,如 `USD` / `RMB`
|
||||
- `created_at` (timestamp) 开始时间
|
||||
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
- `event` (string) 固定为 `human_input_required`
|
||||
- `data` (object) 详细内容
|
||||
- `form_id` (string) 人工输入表单 ID
|
||||
- `node_id` (string) Human Input 节点 ID
|
||||
- `node_title` (string) Human Input 节点标题
|
||||
- `form_content` (string) 渲染后的表单内容
|
||||
- `inputs` (array[object]) 表单输入项定义
|
||||
- `actions` (array[object]) 用户可选动作按钮
|
||||
- `id` (string) 动作 ID
|
||||
- `title` (string) 按钮文案
|
||||
- `button_style` (string) 按钮样式
|
||||
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
|
||||
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
|
||||
- `resolved_default_values` (object) 运行时解析后的默认值
|
||||
- `expiration_time` (timestamp) 表单过期时间(Unix 秒级时间戳)
|
||||
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
|
|
@ -445,6 +463,24 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
|||
- `total_price` (decimal) optional 总费用
|
||||
- `currency` (string) optional 货币,如 `USD` / `RMB`
|
||||
- `created_at` (timestamp) 开始时间
|
||||
- `event: human_input_required` Workflow 已暂停,等待 Human-in-the-Loop 输入
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
- `event` (string) 固定为 `human_input_required`
|
||||
- `data` (object) 详细内容
|
||||
- `form_id` (string) 人工输入表单 ID
|
||||
- `node_id` (string) Human Input 节点 ID
|
||||
- `node_title` (string) Human Input 节点标题
|
||||
- `form_content` (string) 渲染后的表单内容
|
||||
- `inputs` (array[object]) 表单输入项定义
|
||||
- `actions` (array[object]) 用户可选动作按钮
|
||||
- `id` (string) 动作 ID
|
||||
- `title` (string) 按钮文案
|
||||
- `button_style` (string) 按钮样式
|
||||
- `display_in_ui` (bool) 是否需要在 UI 展示该表单
|
||||
- `form_token` (string) 用于 `/form/human_input/:form_token` 接口的令牌
|
||||
- `resolved_default_values` (object) 运行时解析后的默认值
|
||||
- `expiration_time` (timestamp) 表单过期时间(Unix 秒级时间戳)
|
||||
- `event: workflow_finished` workflow 执行结束,成功失败同一事件中不同状态
|
||||
- `task_id` (string) 任务 ID,用于请求跟踪和下方的停止响应接口
|
||||
- `workflow_run_id` (string) workflow 执行 ID
|
||||
|
|
@ -654,6 +690,166 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
|||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='GET'
|
||||
title='获取人工输入表单'
|
||||
name='#get-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
通过 `form_token` 获取待处理的 Human-in-the-Loop 表单。
|
||||
|
||||
当 Workflow 在流式事件中返回 `human_input_required`(包含 `form_token`)时,可调用此接口拉取表单详情。
|
||||
|
||||
### Path
|
||||
- `form_token` (string) 必填,暂停事件返回的表单 token
|
||||
|
||||
### Response
|
||||
- `form_content` (string) 已渲染的表单内容(markdown/plain text)
|
||||
- `inputs` (array[object]) 表单输入项定义
|
||||
- `resolved_default_values` (object) 已解析的默认值(字符串)
|
||||
- `user_actions` (array[object]) 操作按钮列表
|
||||
- `expiration_time` (timestamp) 表单过期时间(Unix 秒)
|
||||
|
||||
### Errors
|
||||
- 404,表单不存在或不属于当前应用
|
||||
- 412,`human_input_form_submitted`,表单已被提交
|
||||
- 412,`human_input_form_expired`,表单已过期
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X GET '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"form_content": "请确认最终结果:{{#$output.answer#}}",
|
||||
"inputs": [
|
||||
{
|
||||
"label": "答案",
|
||||
"type": "text-input",
|
||||
"required": true,
|
||||
"output_variable_name": "answer"
|
||||
}
|
||||
],
|
||||
"resolved_default_values": {
|
||||
"answer": "初始值"
|
||||
},
|
||||
"user_actions": [
|
||||
{ "id": "approve", "title": "通过", "button_style": "primary" },
|
||||
{ "id": "reject", "title": "拒绝", "button_style": "warning" }
|
||||
],
|
||||
"expiration_time": 1735689600
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/form/human_input/:form_token'
|
||||
method='POST'
|
||||
title='提交人工输入表单'
|
||||
name='#submit-human-input-form'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
提交待处理的 Human-in-the-Loop 表单。
|
||||
|
||||
### Path
|
||||
- `form_token` (string) 必填,暂停事件返回的表单 token
|
||||
|
||||
### Request Body
|
||||
- `inputs` (object) 必填,表单字段的 key/value
|
||||
- `action` (string) 必填,从 `user_actions` 中选择的动作 ID
|
||||
- `user` (string) 必填,终端用户标识
|
||||
|
||||
### Response
|
||||
成功时返回空对象。
|
||||
|
||||
### Errors
|
||||
- 400,`invalid_form_data`,提交数据与表单 schema 不匹配
|
||||
- 404,表单不存在或不属于当前应用
|
||||
- 412,`human_input_form_submitted`,表单已被提交
|
||||
- 412,`human_input_form_expired`,表单已过期
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="POST"
|
||||
label="/form/human_input/:form_token"
|
||||
targetCode={`curl -X POST '${props.appDetail.api_base_url}/form/human_input/{form_token}' \\
|
||||
--header 'Authorization: Bearer {api_key}' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '{
|
||||
"inputs": {"answer": "已确认答案"},
|
||||
"action": "approve",
|
||||
"user": "abc-123"
|
||||
}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflow/:task_id/events'
|
||||
method='GET'
|
||||
title='获取恢复后的 Workflow 事件流'
|
||||
name='#get-workflow-resume-events'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
在提交人工输入表单后,继续订阅工作流后续执行事件。
|
||||
|
||||
返回 `text/event-stream`,可持续接收直到工作流结束。
|
||||
|
||||
### Path
|
||||
- `task_id` (string) 必填,workflow 运行 ID(`workflow_run_id`)
|
||||
|
||||
### Query
|
||||
- `user` (string) 必填,终端用户标识
|
||||
- `include_state_snapshot` (bool) 可选,设为 `true` 时会先回放持久化状态快照,再继续实时事件
|
||||
|
||||
### Response
|
||||
Server-Sent Events 流(`text/event-stream`)。
|
||||
常见事件包括 `node_started`、`node_finished`、`human_input_form_filled`、`human_input_form_timeout`、`workflow_finished`。
|
||||
</Col>
|
||||
<Col sticky>
|
||||
<CodeGroup
|
||||
title="Request"
|
||||
tag="GET"
|
||||
label="/workflow/:task_id/events"
|
||||
targetCode={`curl -N -X GET '${props.appDetail.api_base_url}/workflow/{workflow_run_id}/events?user=abc-123' \\
|
||||
--header 'Authorization: Bearer {api_key}'`}
|
||||
/>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
```streaming {{ title: 'Response' }}
|
||||
data: {"event":"human_input_form_filled","task_id":"run-1","workflow_run_id":"run-1","data":{"node_id":"human_input_1","node_title":"Human Input","rendered_content":"已确认答案","action_id":"approve","action_text":"通过"}}
|
||||
data: {"event":"node_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"node-execution-id","node_id":"llm_1","node_type":"llm","title":"LLM","index":5,"status":"succeeded","created_at":1735689601}}
|
||||
data: {"event":"workflow_finished","task_id":"run-1","workflow_run_id":"run-1","data":{"id":"run-1","workflow_id":"workflow-id","status":"succeeded","outputs":{"text":"Done"},"created_at":1735689590,"finished_at":1735689602,"elapsed_time":12.0,"total_tokens":1000,"total_steps":6}}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/files/upload'
|
||||
method='POST'
|
||||
|
|
|
|||
Loading…
Reference in New Issue