Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
147 commits
Select commit Hold shift + click to select a range
6c4f293
Refactor human input form item types
JzoNgKVO Apr 21, 2026
5faa4f9
Define detailed human input field types
JzoNgKVO Apr 21, 2026
37f79ee
Extend human input runtime data types
JzoNgKVO Apr 21, 2026
63711bf
Refine human input extra content types
JzoNgKVO Apr 21, 2026
acd1641
Infer human input output variable types
JzoNgKVO Apr 21, 2026
93945d6
Show typed human input outputs in panel
JzoNgKVO Apr 21, 2026
71803d7
Add human input field type selector
JzoNgKVO Apr 21, 2026
022d73d
Lock paragraph prepopulate behavior
JzoNgKVO Apr 21, 2026
00f0f6d
Add constant select options editor
JzoNgKVO Apr 21, 2026
82d4103
Support variable-backed select options
JzoNgKVO Apr 21, 2026
eb2eefd
Constrain select option variables to string arrays
JzoNgKVO Apr 21, 2026
cae5315
Guard select fields against default leakage
JzoNgKVO Apr 21, 2026
4002d58
Configure single-file human input fields
JzoNgKVO Apr 21, 2026
49195ff
Configure multi-file human input fields
JzoNgKVO Apr 21, 2026
ccf61c0
Summarize human input field configurations
JzoNgKVO Apr 21, 2026
85d05f5
Preview human input field types in markdown
JzoNgKVO Apr 21, 2026
c2fd595
Fix human input typing regressions
JzoNgKVO Apr 22, 2026
8d3ddee
Extract shared human input field renderer
JzoNgKVO Apr 22, 2026
5309b56
Use shared renderer for human input content
JzoNgKVO Apr 22, 2026
80ede7c
Initialize human input values by field type
JzoNgKVO Apr 22, 2026
e9fb3bd
Cover non-string human input chat submissions
JzoNgKVO Apr 22, 2026
67c832e
Allow typed single-run human input submissions
JzoNgKVO Apr 22, 2026
94b8f8f
Preserve typed human input values in share form
JzoNgKVO Apr 22, 2026
add9260
Align human input submission payload types
JzoNgKVO Apr 22, 2026
2ad35e5
Add typed submitted human input renderer
JzoNgKVO Apr 22, 2026
29cad80
Prefer structured submitted human input data
JzoNgKVO Apr 22, 2026
b879748
Parse human input extra contents by payload
JzoNgKVO Apr 22, 2026
cbe2f66
Track human input selector variable references
JzoNgKVO Apr 22, 2026
703228f
Cover structured human input panel rendering
JzoNgKVO Apr 22, 2026
cfb501c
Localize human input field configuration labels
JzoNgKVO Apr 22, 2026
1b45cdb
Expand human input utils initialization tests
JzoNgKVO Apr 22, 2026
4f63b2b
Cover file type transitions in human input tests
JzoNgKVO Apr 22, 2026
ccb43eb
Cover file-based human input content items
JzoNgKVO Apr 22, 2026
75e5429
Test human input submitted markdown fallback
JzoNgKVO Apr 22, 2026
5d20502
Test status-agnostic human input history parsing
JzoNgKVO Apr 22, 2026
d494e42
Test human input form submissions across entry points
JzoNgKVO Apr 22, 2026
6263bb1
Add missing zh-Hans human input labels
JzoNgKVO Apr 22, 2026
1a3c1a9
fix(web): node handle fix
JzoNgKVO Apr 23, 2026
7c348e9
fix(web): form content style issues
JzoNgKVO Apr 23, 2026
1a0776c
fix(web): type select change
JzoNgKVO Apr 23, 2026
7316a1b
fix(web): style issue
JzoNgKVO Apr 23, 2026
d72794b
fix(web): form content preview
JzoNgKVO Apr 23, 2026
8ce356c
fix(web): fix node handle
JzoNgKVO Apr 23, 2026
b8481f6
fix(web): fix rebase error
JzoNgKVO Apr 23, 2026
4674799
fix(web): file_list type
JzoNgKVO Apr 24, 2026
60f577f
fix(web): step run of file uploader
JzoNgKVO Apr 24, 2026
a8e6638
fix(web): human input step run preview restriction
JzoNgKVO Apr 24, 2026
1c5d877
fix(web): human input form content submittion
JzoNgKVO Apr 24, 2026
cec437b
fix(web): fix human input form filled UI
JzoNgKVO Apr 24, 2026
7a12d46
refactor(web): human input form page
JzoNgKVO Apr 24, 2026
74f17d0
refactor(api): rename form definitions fields
QuantumGhost Apr 26, 2026
9ad5d89
feat(web): Use `number_limits` for file uploading limit
QuantumGhost Apr 26, 2026
bdecea3
Merge branch 'main' into tp
JzoNgKVO Apr 27, 2026
4da8afa
fix(web): use number_limits for file_list type
JzoNgKVO Apr 27, 2026
e994009
Revert "fix(web): file_list type"
JzoNgKVO Apr 27, 2026
6e4fa39
Merge branch 'main' into tp
JzoNgKVO Apr 27, 2026
e5b5c1f
Merge branch 'main' into tp
JzoNgKVO Apr 27, 2026
ad43a46
Merge branch 'main' into tp
JzoNgKVO Apr 27, 2026
90ab734
chore(web): replace form_data with submitted_data
JzoNgKVO Apr 27, 2026
4fce9ee
Merge branch 'main' into tp
JzoNgKVO Apr 27, 2026
21a9c8d
Merge branch 'main' into tp
JzoNgKVO May 6, 2026
651dfe5
feat(web): new upload api for human input form page
JzoNgKVO May 6, 2026
cd91757
Merge branch 'main' into tp
JzoNgKVO May 7, 2026
d6f607f
feat(api): expose `sumitted_data` to frontend
QuantumGhost May 7, 2026
51e181c
feat(api): introduce file upload apis for human input page
QuantumGhost May 7, 2026
23e59c6
test(api): update import names in tests
QuantumGhost May 7, 2026
37681bc
test(api): add tests about submission response
QuantumGhost May 7, 2026
a0f8db5
test(api): add tests about file input file uploading api
QuantumGhost May 7, 2026
02e42fd
chore(api): update mock path in tests
QuantumGhost May 7, 2026
b73a4d2
test(api): add file and file list form type in resumption test
QuantumGhost May 7, 2026
c0bedd9
Merge branch 'main' into tp
JzoNgKVO May 7, 2026
3f6559d
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' in…
QuantumGhost May 7, 2026
4c386e3
fix(api): fix missing import and name error
QuantumGhost May 7, 2026
04ed797
merge main
JzoNgKVO May 8, 2026
3f35f35
fix(web): form submmit in human input form page
JzoNgKVO May 8, 2026
23d39be
fix(web): add email configuration check in human input node
JzoNgKVO May 8, 2026
d65cc21
fix(web): support dynamic selector in human input step run & email co…
JzoNgKVO May 8, 2026
a9de4bd
fix(web): form in email test sender
JzoNgKVO May 8, 2026
ed98925
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' in…
QuantumGhost May 8, 2026
58af8aa
refactor(api): use TypedDict to model file mapping
QuantumGhost May 8, 2026
f1adc60
Merge branch 'main' into tp
JzoNgKVO May 8, 2026
7133754
feat(api): bind UploadFile to workflow initiator in unauthenticated f…
QuantumGhost May 8, 2026
c8d6ad1
Merge branch 'main' into tp
JzoNgKVO May 8, 2026
74c4d72
test(web): fix tests of new file uploader
JzoNgKVO May 8, 2026
eadaaa1
fix(web): z-index of shortcut popup
JzoNgKVO May 8, 2026
a75208b
Merge branch 'main' into tp
JzoNgKVO May 8, 2026
e3e4f77
Merge branch 'main' into tp
JzoNgKVO May 8, 2026
62efb66
Merge branch 'main' into tp
JzoNgKVO May 8, 2026
bba7001
fix(api): fix file uploading for delivery test form
QuantumGhost May 8, 2026
343982b
chore(api): improve documentation for HumanInputFormSubmitPayload
QuantumGhost May 8, 2026
c4b2985
test(api): fix broken HITL tests
QuantumGhost May 8, 2026
e099ba8
test(api): add tests for delivery and file inputs
QuantumGhost May 8, 2026
132f80d
Merge branch 'main' into tp
JzoNgKVO May 9, 2026
0e389d2
fix(web): email input
JzoNgKVO May 9, 2026
e9070da
Merge branch 'main' into tp
JzoNgKVO May 9, 2026
f1833fd
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' in…
QuantumGhost May 9, 2026
07fea50
fix(web): remote file upload
JzoNgKVO May 9, 2026
4b5b00c
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' in…
QuantumGhost May 9, 2026
1e6700b
chore(api): replace graphon with development branch
QuantumGhost May 9, 2026
3d445e1
Merge branch 'main' into tp
JzoNgKVO May 9, 2026
17759c0
Merge remote-tracking branch 'upstream/main' into feat/hitl-form-enha…
QuantumGhost May 12, 2026
998201f
fix(web): dynamic select display
JzoNgKVO May 11, 2026
94ad867
fix(web): form content action button disable state
JzoNgKVO May 11, 2026
8bb70e7
fix(web): dynamic select display in form content
JzoNgKVO May 11, 2026
26c14fd
Merge branch 'main' into tp
JzoNgKVO May 12, 2026
c86fd4c
fix(web): tests for component-ui & single-run-form
JzoNgKVO May 12, 2026
3f37235
fix(web): input fields variable name can not be duplicated
JzoNgKVO May 12, 2026
ac05dc9
fix(web): readonly state of form content
JzoNgKVO May 12, 2026
3169ae7
Merge branch 'main' into tp
JzoNgKVO May 12, 2026
b3c0074
chore(api): upgrade grpahon dependency
QuantumGhost May 13, 2026
f9729e8
fix(api): fix HumanInputNode initialization error
QuantumGhost May 13, 2026
fce6d01
test(api): renaming import according to graphon changes
QuantumGhost May 13, 2026
7facbfc
test(api): introduce a test for HumanInputNode initialization
QuantumGhost May 13, 2026
9a65969
test(api): inject file_reference_factory to HumanInputNode in test
QuantumGhost May 13, 2026
698bb90
Merge remote-tracking branch 'upstream/feat/hitl-form-enhancement' in…
QuantumGhost May 13, 2026
070f8bc
Merge branch 'main' into tp
JzoNgKVO May 13, 2026
51b3e63
fix(web): add input field crash
JzoNgKVO May 13, 2026
221a502
fix(web): z-index of add input field
JzoNgKVO May 13, 2026
186fd60
Merge branch 'main' into tp
JzoNgKVO May 13, 2026
5f1b47c
fix(api): fix missing extra_contents in HITL forms
QuantumGhost May 14, 2026
e897e27
fix(api): change values for select input to runtime values
QuantumGhost May 14, 2026
226ce25
chore(api): fix typing errors
QuantumGhost May 14, 2026
da90c93
test(api): add a test to ensure extra_contents exists
QuantumGhost May 14, 2026
c067498
chore(api): upgrade graphon to v0.4.0
QuantumGhost May 14, 2026
c96d108
test(api): ensure select values are replaced in events
QuantumGhost May 14, 2026
4ff9585
fix(api): resolve_variable_select_input_options should raise type err…
QuantumGhost May 14, 2026
f045359
chore(api): adapt new graphon api
QuantumGhost May 14, 2026
68ea88c
fix(api): adapt graphon changes and fix tests
QuantumGhost May 14, 2026
fdc20e4
fix(api): fix tests
QuantumGhost May 14, 2026
f69779f
chore(api): linearize migration
QuantumGhost May 14, 2026
450e19b
fix(api): ensure select options are properly substitued
QuantumGhost May 15, 2026
1c1970e
docs(api): add a design docs for human input form page file upload
QuantumGhost May 15, 2026
e906ad1
fix(web): add input fields
JzoNgKVO May 15, 2026
423edab
fix(web): input field overflow
JzoNgKVO May 16, 2026
ca9e738
Merge remote-tracking branch 'upstream/main' into codex/upgrade-graph…
QuantumGhost May 18, 2026
d2c5539
Merge remote-tracking branch 'upstream/main' into feat/hitl-form-enha…
QuantumGhost May 18, 2026
0b4348e
[autofix.ci] apply automated fixes
autofix-ci[bot] May 18, 2026
45c20a2
Merge remote-tracking branch 'upstream/main' into feat/hitl-form-enha…
QuantumGhost May 19, 2026
41a88a6
[autofix.ci] apply automated fixes
autofix-ci[bot] May 19, 2026
8d200af
fix(web): fix leaked conditional rendering
QuantumGhost May 19, 2026
beafa03
Merge remote-tracking branch 'upstream/main' into feat/hitl-form-enha…
QuantumGhost May 19, 2026
8626e50
test(web): fix test of human input node content
JzoNgKVO May 19, 2026
e26042c
[autofix.ci] apply automated fixes
autofix-ci[bot] May 19, 2026
205ea1b
fix(web): lint error in form status card
JzoNgKVO May 19, 2026
3f8f7a9
chore(web): fix knip
JzoNgKVO May 19, 2026
1150f49
test(web): tests coverage of human input
JzoNgKVO May 19, 2026
4180490
fix(web): fix type check
JzoNgKVO May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ FROM base AS packages
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
# basic environment
g++ \
git g++ \
# for building gmpy2
libmpfr-dev libmpc-dev

Expand Down
34 changes: 32 additions & 2 deletions api/controllers/common/human_input.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
import json

from pydantic import BaseModel, JsonValue
from pydantic import BaseModel, Field, JsonValue

HUMAN_INPUT_FORM_INPUT_EXAMPLE = {
"decision": "approve",
"attachment": {
"transfer_method": "local_file",
"upload_file_id": "4e0d1b87-52f2-49f6-b8c6-95cd9c954b3e",
"type": "document",
},
"attachments": [
{
"transfer_method": "local_file",
"upload_file_id": "1a77f0df-c0e6-461c-987c-e72526f341ee",
"type": "document",
},
{
"transfer_method": "remote_url",
"url": "https://example.com/report.pdf",
"type": "document",
},
],
}


class HumanInputFormSubmitPayload(BaseModel):
inputs: dict[str, JsonValue]
inputs: dict[str, JsonValue] = Field(
description=(
"Submitted human input values keyed by output variable name. "
"Use a string for paragraph or select input values, a file mapping for file inputs, "
"and a list of file mappings for file-list inputs. Local file mappings use "
"`transfer_method=local_file` with `upload_file_id`; remote file mappings use "
"`transfer_method=remote_url` with `url` or `remote_url`."
),
examples=[HUMAN_INPUT_FORM_INPUT_EXAMPLE],
)
action: str


Expand Down
11 changes: 7 additions & 4 deletions api/controllers/service_api/app/human_input_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import json
import logging
from collections.abc import Sequence

from flask import Response
from flask_restx import Resource
Expand All @@ -18,6 +19,7 @@
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
from core.workflow.human_input_policy import HumanInputSurface, is_recipient_type_allowed_for_surface
from extensions.ext_database import db
from graphon.nodes.human_input.entities import FormInputConfig
from libs.helper import to_timestamp
from models.model import App, EndUser
from services.human_input_service import Form, FormNotFoundError, HumanInputService
Expand All @@ -28,11 +30,11 @@
register_schema_models(service_api_ns, HumanInputFormSubmitPayload)


def _jsonify_form_definition(form: Form) -> Response:
definition_payload = form.get_definition().model_dump()
def _jsonify_form_definition(form: Form, *, inputs: Sequence[FormInputConfig] = ()) -> Response:
definition_payload = form.get_definition().model_dump(mode="json")
payload = {
"form_content": definition_payload["rendered_content"],
"inputs": definition_payload["inputs"],
"inputs": [form_input.model_dump(mode="json") for form_input in inputs],
"resolved_default_values": stringify_form_default_values(definition_payload["default_values"]),
"user_actions": definition_payload["user_actions"],
"expiration_time": to_timestamp(form.expiration_time),
Expand Down Expand Up @@ -75,7 +77,8 @@ def get(self, app_model: App, form_token: str):
_ensure_form_belongs_to_app(form, app_model)
_ensure_form_is_allowed_for_service_api(form)
service.ensure_form_active(form)
return _jsonify_form_definition(form)
inputs = service.resolve_form_inputs(form)
return _jsonify_form_definition(form, inputs=inputs)

@service_api_ns.expect(service_api_ns.models[HumanInputFormSubmitPayload.__name__])
@service_api_ns.doc("submit_human_input_form")
Expand Down
2 changes: 2 additions & 0 deletions api/controllers/web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
feature,
files,
forgot_password,
human_input_file_upload,
human_input_form,
login,
message,
Expand All @@ -46,6 +47,7 @@
"feature",
"files",
"forgot_password",
"human_input_file_upload",
"human_input_form",
"login",
"message",
Expand Down
181 changes: 181 additions & 0 deletions api/controllers/web/human_input_file_upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import httpx
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, HttpUrl

import services
from controllers.common import helpers
from controllers.common.errors import (
BlockedFileExtensionError,
FileTooLargeError,
NoFileUploadedError,
RemoteFileUploadError,
TooManyFilesError,
UnsupportedFileTypeError,
)
from controllers.common.schema import register_schema_models
from controllers.web import web_ns
from core.helper import ssrf_proxy
from extensions.ext_database import db
from fields.file_fields import FileResponse, FileWithSignedUrl
from graphon.file import helpers as file_helpers
from libs.exception import BaseHTTPException
from services.file_service import FileService
from services.human_input_file_upload_service import (
HITL_UPLOAD_TOKEN_PREFIX,
HumanInputFileUploadService,
InvalidUploadTokenError,
)


class InvalidUploadTokenBadRequestError(BaseHTTPException):
error_code = "invalid_upload_token"
description = "Invalid upload token."
code = 400


class InvalidUploadTokenUnauthorizedError(BaseHTTPException):
error_code = "invalid_upload_token"
description = "Upload token is required."
code = 401


class InvalidUploadTokenForbiddenError(BaseHTTPException):
error_code = "invalid_upload_token"
description = "Upload token is invalid or expired."
code = 403


class HumanInputRemoteFileUploadPayload(BaseModel):
url: HttpUrl = Field(description="Remote file URL")


register_schema_models(web_ns, HumanInputRemoteFileUploadPayload, FileResponse, FileWithSignedUrl)


def _extract_hitl_upload_token() -> str:
"""Read HITL upload token from Authorization without invoking other bearer auth chains."""

authorization = request.headers.get("Authorization")
if authorization is None:
raise InvalidUploadTokenUnauthorizedError()

parts = authorization.split()
if len(parts) != 2:
raise InvalidUploadTokenUnauthorizedError()

scheme, token = parts
if scheme.lower() != "bearer":
raise InvalidUploadTokenBadRequestError()
if not token:
raise InvalidUploadTokenUnauthorizedError()
if not token.startswith(HITL_UPLOAD_TOKEN_PREFIX):
raise InvalidUploadTokenBadRequestError()
return token


def _validate_context(service: HumanInputFileUploadService, token: str):
try:
return service.validate_upload_token(token)
except InvalidUploadTokenError as exc:
raise InvalidUploadTokenForbiddenError() from exc


def _parse_local_upload_file():
if "file" not in request.files:
raise NoFileUploadedError()
if len(request.files) > 1:
raise TooManyFilesError()

file = request.files["file"]
if not file.filename:
from controllers.common.errors import FilenameNotExistsError

raise FilenameNotExistsError()

return file


@web_ns.route("/form/human_input/files/upload")
class HumanInputFileUploadApi(Resource):
def post(self):
"""Upload one local file for a HITL human input form."""

token = _extract_hitl_upload_token()
upload_service = HumanInputFileUploadService(db.engine)
context = _validate_context(upload_service, token)
file = _parse_local_upload_file()

try:
upload_file = FileService(db.engine).upload_file(
filename=file.filename or "",
content=file.read(),
mimetype=file.mimetype,
user=context.owner,
source=None,
)
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
except services.errors.file.BlockedFileExtensionError as exc:
raise BlockedFileExtensionError() from exc

upload_service.record_upload_file(context=context, file_id=upload_file.id)
response = FileResponse.model_validate(upload_file, from_attributes=True)
return response.model_dump(mode="json"), 201


@web_ns.route("/form/human_input/files/remote-upload")
class HumanInputRemoteFileUploadApi(Resource):
def post(self):
"""Upload one remote URL file for a HITL human input form."""

token = _extract_hitl_upload_token()
upload_service = HumanInputFileUploadService(db.engine)
context = _validate_context(upload_service, token)
payload = HumanInputRemoteFileUploadPayload.model_validate(request.get_json(silent=True) or {})
url = str(payload.url)

try:
resp = ssrf_proxy.head(url=url)
if resp.status_code != httpx.codes.OK:
resp = ssrf_proxy.get(url=url, timeout=3, follow_redirects=True)
if resp.status_code != httpx.codes.OK:
raise RemoteFileUploadError(f"Failed to fetch file from {url}: {resp.text}")
except httpx.RequestError as exc:
raise RemoteFileUploadError(f"Failed to fetch file from {url}: {str(exc)}")

file_info = helpers.guess_file_info_from_response(resp)
if not FileService.is_file_size_within_limit(extension=file_info.extension, file_size=file_info.size):
raise FileTooLargeError()

content = resp.content if resp.request.method == "GET" else ssrf_proxy.get(url).content

try:
upload_file = FileService(db.engine).upload_file(
filename=file_info.filename,
content=content,
mimetype=file_info.mimetype,
user=context.owner,
source_url=url,
)
except services.errors.file.FileTooLargeError as file_too_large_error:
raise FileTooLargeError(file_too_large_error.description)
except services.errors.file.UnsupportedFileTypeError:
raise UnsupportedFileTypeError()
except services.errors.file.BlockedFileExtensionError as exc:
raise BlockedFileExtensionError() from exc

upload_service.record_upload_file(context=context, file_id=upload_file.id)
payload1 = FileWithSignedUrl(
id=upload_file.id,
name=upload_file.name,
size=upload_file.size,
extension=upload_file.extension,
url=file_helpers.get_signed_file_url(upload_file_id=upload_file.id),
mime_type=upload_file.mime_type,
created_by=upload_file.created_by,
created_at=int(upload_file.created_at.timestamp()),
)
return payload1.model_dump(mode="json"), 201
Loading
Loading