feat(api): add human input data to extra contents

This commit is contained in:
QuantumGhost
2026-01-08 10:20:24 +08:00
parent dac94b573e
commit de428bc9bb
14 changed files with 652 additions and 58 deletions

View File

@ -20,6 +20,8 @@ def test_get_by_message_ids_returns_human_input_content(db_session_with_containe
assert len(results) == 1
assert len(results[0]) == 1
content = results[0][0]
assert content.action_id == fixture.action_id
assert content.action_text == fixture.action_text
assert content.rendered_content == fixture.form.rendered_content
assert content.submitted is True
assert content.form_submission_data is not None
assert content.form_submission_data.action_id == fixture.action_id
assert content.form_submission_data.action_text == fixture.action_text
assert content.form_submission_data.rendered_content == fixture.form.rendered_content

View File

@ -24,9 +24,14 @@ def test_pagination_returns_extra_contents(db_session_with_containers):
message = pagination.data[0]
assert message.extra_contents == [
{
"type": "human_input_result",
"action_id": fixture.action_id,
"action_text": fixture.action_text,
"rendered_content": fixture.form.rendered_content,
"type": "human_input",
"submitted": True,
"form_submission_data": {
"node_id": fixture.form.node_id,
"node_title": fixture.node_title,
"rendered_content": fixture.form.rendered_content,
"action_id": fixture.action_id,
"action_text": fixture.action_text,
},
}
]

View File

@ -0,0 +1,156 @@
import json
import time
import uuid
from typing import Any
from unittest.mock import patch
from flask.testing import FlaskClient
from core.app.apps.message_based_app_generator import MessageBasedAppGenerator
from core.app.entities.task_entities import StreamEvent
from models import Account, Tenant, TenantAccountJoin
from models.account import TenantAccountRole
from models.model import ApiToken, App, AppMode
from models.workflow import Workflow
def _create_tenant_and_owner() -> tuple[Tenant, Account, TenantAccountJoin]:
tenant = Tenant(
name="Test Tenant",
status="normal",
)
tenant.id = str(uuid.uuid4())
account = Account(
email=f"owner-{uuid.uuid4()}@example.com",
name="Owner",
interface_language="en-US",
status="active",
)
account.id = str(uuid.uuid4())
tenant_join = TenantAccountJoin(
tenant_id=tenant.id,
account_id=account.id,
role=TenantAccountRole.OWNER,
current=True,
)
return tenant, account, tenant_join
def _create_workflow_app(tenant: Tenant, account: Account) -> tuple[App, Workflow]:
app = App(
id=str(uuid.uuid4()),
tenant_id=tenant.id,
name="Test Workflow App",
description="",
mode=AppMode.WORKFLOW,
icon_type="emoji",
icon="robot",
icon_background="#FFFFFF",
enable_site=True,
enable_api=True,
created_by=account.id,
updated_by=account.id,
)
workflow = Workflow(
id=str(uuid.uuid4()),
tenant_id=tenant.id,
app_id=app.id,
type="workflow",
version="v1",
graph=json.dumps({"nodes": [], "edges": []}),
features=json.dumps({"features": []}),
created_by=account.id,
updated_by=account.id,
environment_variables=[],
conversation_variables=[],
)
app.workflow_id = workflow.id
return app, workflow
def _create_api_token(app: App) -> ApiToken:
return ApiToken(
app_id=app.id,
tenant_id=app.tenant_id,
type="app",
token=f"app-token-{uuid.uuid4()}",
)
def _collect_sse_events(response, max_events: int = 2, timeout: float = 3.0) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
buffer = ""
start_time = time.time()
for chunk in response.response:
if not chunk:
if time.time() - start_time > timeout:
break
continue
buffer += chunk.decode("utf-8")
while "\n\n" in buffer:
block, buffer = buffer.split("\n\n", 1)
for line in block.splitlines():
if not line.startswith("data: "):
continue
payload = line[len("data: ") :]
events.append(json.loads(payload))
if len(events) >= max_events:
return events
if time.time() - start_time > timeout:
break
return events
class TestSSEStartGateIntegration:
def test_workflow_streaming_sse_starts_after_subscribe(
self,
db_session_with_containers,
test_client_with_containers: FlaskClient,
):
tenant, account, tenant_join = _create_tenant_and_owner()
app, workflow = _create_workflow_app(tenant, account)
api_token = _create_api_token(app)
db_session_with_containers.add_all([tenant, account, tenant_join, app, workflow, api_token])
db_session_with_containers.commit()
def _fake_delay(payload_json: str):
payload = json.loads(payload_json)
workflow_run_id = payload["workflow_run_id"]
app_mode = AppMode.value_of(payload["app_mode"])
topic = MessageBasedAppGenerator.get_response_topic(app_mode, uuid.UUID(workflow_run_id))
events = [
{
"event": StreamEvent.WORKFLOW_STARTED.value,
"workflow_run_id": workflow_run_id,
"created_at": int(time.time()),
},
{
"event": StreamEvent.WORKFLOW_FINISHED.value,
"workflow_run_id": workflow_run_id,
"created_at": int(time.time()),
},
]
for event in events:
topic.publish(json.dumps(event).encode())
payload = {
"inputs": {},
"response_mode": "streaming",
"user": "test-end-user",
}
with patch("services.app_generate_service.chatflow_execute_task.delay", side_effect=_fake_delay):
response = test_client_with_containers.post(
"/v1/workflows/run",
json=payload,
headers={"Authorization": f"Bearer {api_token.token}"},
buffered=False,
)
assert response.status_code == 200
events = _collect_sse_events(response)
assert len(events) == 2
assert events[0]["event"] == StreamEvent.WORKFLOW_STARTED.value
assert events[1]["event"] == StreamEvent.WORKFLOW_FINISHED.value