chore(api): upgrade graphon to 0.4.0 (#36124)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
This commit is contained in:
-LAN-
2026-05-18 00:34:17 +00:00
committed by GitHub
co-authored by autofix-ci[bot] QuantumGhost
parent 127fbf2c9a
commit b96f372f45
81 changed files with 3335 additions and 1874 deletions
+5 -5
View File
@@ -10,7 +10,7 @@ from graphon.entities import WorkflowStartReason
from graphon.entities.pause_reason import PauseReasonType
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus
from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import FormInputConfig, UserActionConfig
class AnnotationReplyAccount(BaseModel):
@@ -284,8 +284,8 @@ class HumanInputRequiredResponse(StreamResponse):
node_id: str
node_title: str
form_content: str
inputs: Sequence[FormInput] = Field(default_factory=list)
actions: Sequence[UserAction] = Field(default_factory=list)
inputs: Sequence[FormInputConfig] = Field(default_factory=list)
actions: Sequence[UserActionConfig] = Field(default_factory=list)
display_in_ui: bool = False
form_token: str | None = None
resolved_default_values: Mapping[str, Any] = Field(default_factory=dict)
@@ -307,8 +307,8 @@ class HumanInputRequiredPauseReasonPayload(BaseModel):
node_id: str
node_title: str
form_content: str
inputs: Sequence[FormInput] = Field(default_factory=list)
actions: Sequence[UserAction] = Field(default_factory=list)
inputs: Sequence[FormInputConfig] = Field(default_factory=list)
actions: Sequence[UserActionConfig] = Field(default_factory=list)
display_in_ui: bool = False
form_token: str | None = None
resolved_default_values: Mapping[str, Any] = Field(default_factory=dict)
+3 -3
View File
@@ -5,7 +5,7 @@ from typing import Any, TypeAlias
from pydantic import BaseModel, ConfigDict, Field
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import FormInputConfig, UserActionConfig
from models.execution_extra_content import ExecutionContentType
@@ -16,8 +16,8 @@ class HumanInputFormDefinition(BaseModel):
node_id: str
node_title: str
form_content: str
inputs: Sequence[FormInput] = Field(default_factory=list)
actions: Sequence[UserAction] = Field(default_factory=list)
inputs: Sequence[FormInputConfig] = Field(default_factory=list)
actions: Sequence[UserActionConfig] = Field(default_factory=list)
display_in_ui: bool = False
form_token: str | None = None
resolved_default_values: Mapping[str, Any] = Field(default_factory=dict)
+4 -3
View File
@@ -47,7 +47,7 @@ from graphon.graph.graph import NodeFactory
from graphon.model_runtime.memory import PromptMessageMemory
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
from graphon.nodes.base.node import Node
from graphon.nodes.code.code_node import WorkflowCodeExecutor
from graphon.nodes.code.code_node import CodeExecutorProtocol
from graphon.nodes.code.entities import CodeLanguage
from graphon.nodes.code.limits import CodeNodeLimits
from graphon.nodes.document_extractor import UnstructuredApiConfig
@@ -289,7 +289,7 @@ class DifyNodeFactory(NodeFactory):
self.graph_init_params = graph_init_params
self.graph_runtime_state = graph_runtime_state
self._dify_context = self._resolve_dify_context(graph_init_params.run_context)
self._code_executor: WorkflowCodeExecutor = DefaultWorkflowCodeExecutor()
self._code_executor: CodeExecutorProtocol = DefaultWorkflowCodeExecutor()
self._code_limits = CodeNodeLimits(
max_string_length=dify_config.CODE_MAX_STRING_LENGTH,
max_number=dify_config.CODE_MAX_NUMBER,
@@ -397,6 +397,7 @@ class DifyNodeFactory(NodeFactory):
},
BuiltinNodeTypes.HUMAN_INPUT: lambda: {
"runtime": self._human_input_runtime,
"file_reference_factory": self._file_reference_factory,
"form_repository": self._human_input_runtime.build_form_repository(),
},
BuiltinNodeTypes.LLM: lambda: self._build_llm_compatible_node_init_kwargs(
@@ -434,7 +435,7 @@ class DifyNodeFactory(NodeFactory):
include_jinja2_template_renderer=False,
),
BuiltinNodeTypes.TOOL: lambda: {
"tool_file_manager_factory": self._bound_tool_file_manager_factory(),
"tool_file_manager": self._bound_tool_file_manager_factory(),
"runtime": self._tool_runtime,
},
BuiltinNodeTypes.AGENT: lambda: {
+2 -2
View File
@@ -49,7 +49,7 @@ from graphon.model_runtime.entities.model_entities import AIModelEntity
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
from graphon.nodes.human_input.entities import HumanInputNodeData
from graphon.nodes.llm.runtime_protocols import (
PreparedLLMProtocol,
LLMProtocol,
PromptMessageSerializerProtocol,
RetrieverAttachmentLoaderProtocol,
)
@@ -140,7 +140,7 @@ class DifyFileReferenceFactory(FileReferenceFactoryProtocol):
)
class DifyPreparedLLM(PreparedLLMProtocol):
class DifyPreparedLLM(LLMProtocol):
"""Workflow-layer adapter that hides the full `ModelInstance` API from `graphon` nodes."""
def __init__(self, model_instance: ModelInstance) -> None:
+124 -1
View File
@@ -8,6 +8,7 @@ OpenAPI output early.
from __future__ import annotations
import argparse
import json
import logging
import subprocess
import sys
@@ -28,6 +29,125 @@ CONSOLE_SWAGGER_FILENAME = "console-swagger.json"
STALE_COMBINED_MARKDOWN_FILENAME = "api-reference.md"
def _definition_ref_name(schema: object) -> str | None:
if not isinstance(schema, dict):
return None
ref = schema.get("$ref")
if not isinstance(ref, str) or not ref.startswith("#/definitions/"):
return None
return ref.removeprefix("#/definitions/")
def _markdown_anchor(name: str) -> str:
return name.lower()
def _schema_markdown_type(schema: object) -> str:
if not isinstance(schema, dict):
return ""
ref_name = _definition_ref_name(schema)
if ref_name is not None:
return f"[{ref_name}](#{_markdown_anchor(ref_name)})"
for union_key in ("oneOf", "anyOf"):
variants = schema.get(union_key)
if not isinstance(variants, list):
continue
variant_types = [
variant_type
for variant in variants
if not (isinstance(variant, dict) and variant.get("type") == "null")
for variant_type in [_schema_markdown_type(variant)]
if variant_type
]
if len(variant_types) == 1:
return variant_types[0]
if variant_types:
return "<br>".join(variant_types)
schema_type = schema.get("type")
if schema_type == "array":
item_type = _schema_markdown_type(schema.get("items"))
return f"[ {item_type or 'object'} ]"
if isinstance(schema_type, str):
return schema_type
return ""
def _replace_schema_table_type(markdown: str, definition_name: str, row_name: str, type_markdown: str) -> str:
if not type_markdown:
return markdown
lines = markdown.splitlines()
section_header = f"#### {definition_name}"
in_section = False
for index, line in enumerate(lines):
if line == section_header:
in_section = True
continue
if in_section and line.startswith("#### "):
break
if not in_section or not line.startswith(f"| {row_name} |"):
continue
cells = line.split("|")
if len(cells) < 5:
continue
cells[2] = f" {type_markdown} "
lines[index] = "|".join(cells)
break
return "\n".join(lines)
def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str:
"""Fill Swagger Markdown table cells that `swagger-markdown` leaves blank for union schemas."""
spec = json.loads(spec_path.read_text(encoding="utf-8"))
definitions = spec.get("definitions")
if not isinstance(definitions, dict):
return markdown
for definition_name, schema in definitions.items():
if not isinstance(definition_name, str) or not isinstance(schema, dict):
continue
one_of = schema.get("oneOf")
if not isinstance(one_of, list):
continue
markdown = _replace_schema_table_type(
markdown,
definition_name,
definition_name,
_schema_markdown_type(schema),
)
for variant in one_of:
variant_name = _definition_ref_name(variant)
variant_schema = definitions.get(variant_name) if variant_name is not None else None
if not isinstance(variant_name, str) or not isinstance(variant_schema, dict):
continue
properties = variant_schema.get("properties")
if not isinstance(properties, dict):
continue
for property_name, property_schema in properties.items():
if isinstance(property_name, str):
markdown = _replace_schema_table_type(
markdown,
variant_name,
property_name,
_schema_markdown_type(property_schema),
)
return markdown
def _convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None:
markdown_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f"{markdown_path.stem}-", dir=markdown_path.parent) as temp_dir:
@@ -57,7 +177,10 @@ def _convert_spec_to_markdown(spec_path: Path, markdown_path: Path) -> None:
converter_output = "\n".join(item for item in (result.stdout, result.stderr) if item).strip()
raise RuntimeError(f"swagger-markdown did not write {markdown_path}: {converter_output}")
converted_markdown = temp_markdown_path.read_text(encoding="utf-8")
converted_markdown = _patch_union_schema_markdown(
temp_markdown_path.read_text(encoding="utf-8"),
spec_path,
)
if not converted_markdown.strip():
raise RuntimeError(f"swagger-markdown wrote an empty document for {markdown_path}")
+83 -35
View File
@@ -11805,6 +11805,27 @@ Request payload for bulk downloading documents as a zip archive.
| ---- | ---- | ----------- | -------- |
| file_ids | [ string ] | | Yes |
#### FileInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_extensions | [ string ] | | No |
| allowed_file_types | [ [FileType](#filetype) ] | | No |
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
| output_variable_name | string | | Yes |
| type | string | | No |
#### FileListInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_extensions | [ string ] | | No |
| allowed_file_types | [ [FileType](#filetype) ] | | No |
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
| number_limits | integer | | No |
| output_variable_name | string | | Yes |
| type | string | | No |
#### FileResponse
| Name | Type | Description | Required |
@@ -11824,6 +11845,18 @@ Request payload for bulk downloading documents as a zip archive.
| tenant_id | string | | No |
| user_id | string | | No |
#### FileTransferMethod
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| FileTransferMethod | string | | |
#### FileType
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| FileType | string | | |
#### ForgotPasswordCheckPayload
| Name | Type | Description | Required |
@@ -11869,33 +11902,11 @@ Request payload for bulk downloading documents as a zip archive.
| email | string | | Yes |
| language | string | | No |
#### FormInput
Form input definition.
#### FormInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | [FormInputDefault](#forminputdefault) | | No |
| output_variable_name | string | | Yes |
| type | [FormInputType](#forminputtype) | | Yes |
#### FormInputDefault
Default configuration for form inputs.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| selector | [ string ] | | No |
| type | [PlaceholderType](#placeholdertype) | | Yes |
| value | string | | No |
#### FormInputType
Form input types.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| FormInputType | string | Form input types. | |
| FormInputConfig | [ParagraphInputConfig](#paragraphinputconfig)<br>[SelectInputConfig](#selectinputconfig)<br>[FileInputConfig](#fileinputconfig)<br>[FileListInputConfig](#filelistinputconfig) | | |
#### GenerateSummaryPayload
@@ -12018,13 +12029,13 @@ Form input types.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| actions | [ [UserAction](#useraction) ] | | No |
| actions | [ [UserActionConfig](#useractionconfig) ] | | No |
| display_in_ui | boolean | | No |
| expiration_time | integer | | Yes |
| form_content | string | | Yes |
| form_id | string | | Yes |
| form_token | string | | No |
| inputs | [ [FormInput](#forminput) ] | | No |
| inputs | [ [FormInputConfig](#forminputconfig) ] | | No |
| node_id | string | | Yes |
| node_title | string | | Yes |
| resolved_default_values | object | | No |
@@ -12662,6 +12673,16 @@ Enum class for model type.
| page | integer | | Yes |
| total | integer | | Yes |
#### ParagraphInputConfig
Form input definition.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | [StringSource](#stringsource) | | No |
| output_variable_name | string | | Yes |
| type | string | | No |
#### Parser
| Name | Type | Description | Required |
@@ -12991,14 +13012,6 @@ Enum class for model type.
| unit | string | | No |
| variable | string | | No |
#### PlaceholderType
Default value types for form inputs.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PlaceholderType | string | Default value types for form inputs. | |
#### PluginAutoUpgradeSettingsPayload
| Name | Type | Description | Required |
@@ -13279,6 +13292,14 @@ Default value types for form inputs.
| max_tokens | integer | | Yes |
| separator | string | | No |
#### SelectInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| option_source | [StringListSource](#stringlistsource) | | Yes |
| output_variable_name | string | | Yes |
| type | string | | No |
#### SimpleAccount
| Name | Type | Description | Required |
@@ -13361,6 +13382,24 @@ Default value types for form inputs.
| ---- | ---- | ----------- | -------- |
| StrategySetting | string | | |
#### StringListSource
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| selector | [ string ] | | No |
| type | [ValueSourceType](#valuesourcetype) | | Yes |
| value | [ string ] | | No |
#### StringSource
Default configuration for form inputs.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| selector | [ string ] | | No |
| type | [ValueSourceType](#valuesourcetype) | | Yes |
| value | string | | No |
#### SubscriptionQuery
| Name | Type | Description | Required |
@@ -13792,7 +13831,7 @@ Tag type
| video_file_size_limit | integer | | Yes |
| workflow_file_upload_limit | integer | | Yes |
#### UserAction
#### UserActionConfig
User action configuration.
@@ -13802,6 +13841,15 @@ User action configuration.
| id | string | | Yes |
| title | string | | Yes |
#### ValueSourceType
ValueSourceType records whether the value comes from a static setting
in form definiton, or a variable while the workflow is running.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| ValueSourceType | string | ValueSourceType records whether the value comes from a static setting in form definiton, or a variable while the workflow is running. | |
#### WebhookTriggerResponse
| Name | Type | Description | Required |
+1 -1
View File
@@ -43,7 +43,7 @@ dependencies = [
"resend>=2.27.0,<3.0.0",
# Emerging: newer and fast-moving, use compatible pins
"fastopenapi[flask]~=0.7.0",
"graphon~=0.3.1",
"graphon~=0.4.0",
"httpx-sse~=0.4.0",
"json-repair~=0.59.4",
]
+6 -1
View File
@@ -29,7 +29,11 @@ from core.workflow.node_factory import (
get_node_type_classes_mapping,
is_start_node_type,
)
from core.workflow.node_runtime import DifyHumanInputNodeRuntime, apply_dify_debug_email_recipient
from core.workflow.node_runtime import (
DifyFileReferenceFactory,
DifyHumanInputNodeRuntime,
apply_dify_debug_email_recipient,
)
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables, default_system_variables
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
from core.workflow.workflow_entry import WorkflowEntry
@@ -1259,6 +1263,7 @@ class WorkflowService:
data=node_data,
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
file_reference_factory=DifyFileReferenceFactory(graph_init_params.run_context),
runtime=DifyHumanInputNodeRuntime(run_context),
)
return node
@@ -60,14 +60,14 @@ def init_tool_node(config: dict):
graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
tool_file_manager_factory = MagicMock(spec=ToolFileManagerProtocol)
tool_file_manager = MagicMock(spec=ToolFileManagerProtocol)
node = ToolNode(
node_id=str(uuid.uuid4()),
data=ToolNodeData.model_validate(config["data"]),
graph_init_params=init_params,
graph_runtime_state=graph_runtime_state,
tool_file_manager_factory=tool_file_manager_factory,
tool_file_manager=tool_file_manager,
runtime=DifyToolNodeRuntime(init_params.run_context),
)
return node
@@ -17,7 +17,7 @@ from core.workflow.human_input_adapter import (
MemberRecipient,
WebAppDeliveryMethod,
)
from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserAction
from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserActionConfig
from models.account import (
Account,
AccountStatus,
@@ -69,7 +69,7 @@ def _build_form_params(delivery_methods: list[DeliveryChannelConfig]) -> FormCre
title="Human Approval",
delivery_methods=delivery_methods,
form_content="<p>Approve?</p>",
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
)
return FormCreateParams(
workflow_execution_id=str(uuid4()),
@@ -185,7 +185,7 @@ class TestHumanInputFormRepositoryImplWithContainers:
title="Human Approval",
form_content="<p>Approve?</p>",
inputs=[],
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
),
rendered_content="<p>Approve?</p>",
delivery_methods=[],
@@ -220,7 +220,7 @@ class TestHumanInputFormRepositoryImplWithContainers:
title="Human Approval",
form_content="<p>Approve?</p>",
inputs=[],
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
delivery_methods=[WebAppDeliveryMethod()],
),
rendered_content="<p>Approve?</p>",
@@ -13,7 +13,7 @@ from core.app.workflow.layers import PersistenceWorkflowInfo, WorkflowPersistenc
from core.repositories.human_input_repository import HumanInputFormEntity, HumanInputFormRepository
from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository
from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
from core.workflow.node_runtime import DifyFileReferenceFactory, DifyHumanInputNodeRuntime
from core.workflow.system_variables import build_system_variables
from graphon.enums import WorkflowType
from graphon.graph import Graph
@@ -21,7 +21,7 @@ from graphon.graph_engine import GraphEngine
from graphon.graph_engine.command_channels import InMemoryChannel
from graphon.nodes.end.end_node import EndNode
from graphon.nodes.end.entities import EndNodeData
from graphon.nodes.human_input.entities import HumanInputNodeData, UserAction
from graphon.nodes.human_input.entities import HumanInputNodeData, UserActionConfig
from graphon.nodes.human_input.enums import HumanInputFormStatus
from graphon.nodes.human_input.human_input_node import HumanInputNode
from graphon.nodes.start.entities import StartNodeData
@@ -112,7 +112,7 @@ def _build_graph(
form_content="Awaiting human input",
inputs=[],
user_actions=[
UserAction(id="continue", title="Continue"),
UserActionConfig(id="continue", title="Continue"),
],
)
human_node = HumanInputNode(
@@ -121,6 +121,7 @@ def _build_graph(
graph_init_params=params,
graph_runtime_state=runtime_state,
form_repository=form_repository,
file_reference_factory=DifyFileReferenceFactory(params.run_context),
runtime=DifyHumanInputNodeRuntime(params.run_context),
)
@@ -5,7 +5,7 @@ from datetime import timedelta
from decimal import Decimal
from uuid import uuid4
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from graphon.nodes.human_input.entities import FormDefinition, UserActionConfig
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin
from models.enums import ConversationFromSource, InvokeFrom
@@ -116,7 +116,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
form_definition = FormDefinition(
form_content="content",
inputs=[],
user_actions=[UserAction(id=action_id, title=action_text)],
user_actions=[UserActionConfig(id=action_id, title=action_text)],
rendered_content="Rendered block",
expiration_time=naive_utc_now() + timedelta(days=1),
node_title=node_title,
@@ -17,7 +17,7 @@ from extensions.ext_storage import storage
from graphon.entities import WorkflowExecution
from graphon.entities.pause_reason import HumanInputRequired, PauseReasonType
from graphon.enums import WorkflowExecutionStatus
from graphon.nodes.human_input.entities import FormDefinition, FormInput, UserAction
from graphon.nodes.human_input.entities import FormDefinition, ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
@@ -642,8 +642,8 @@ class TestBuildHumanInputRequiredReason:
expiration_time = naive_utc_now()
form_definition = FormDefinition(
form_content="content",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
user_actions=[UserAction(id="approve", title="Approve")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="name")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
rendered_content="rendered",
expiration_time=expiration_time,
default_values={"name": "Alice"},
@@ -754,8 +754,8 @@ class TestBuildHumanInputRequiredReason:
expiration_time = naive_utc_now()
form_definition = FormDefinition(
form_content="content",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
user_actions=[UserAction(id="approve", title="Approve")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="name")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
rendered_content="rendered",
expiration_time=expiration_time,
default_values={"name": "Alice"},
@@ -15,7 +15,7 @@ import pytest
from sqlalchemy import Engine, delete, select
from sqlalchemy.orm import Session, sessionmaker
from graphon.nodes.human_input.entities import FormDefinition, UserAction
from graphon.nodes.human_input.entities import FormDefinition, UserActionConfig
from graphon.nodes.human_input.enums import HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
@@ -179,7 +179,7 @@ def _create_submitted_form(
form_definition = FormDefinition(
form_content="content",
inputs=[],
user_actions=[UserAction(id=action_id, title=action_title)],
user_actions=[UserActionConfig(id=action_id, title=action_title)],
rendered_content="rendered",
expiration_time=expiration_time,
node_title=node_title,
@@ -212,7 +212,7 @@ def _create_waiting_form(
form_definition = FormDefinition(
form_content="content",
inputs=[],
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
rendered_content="rendered",
expiration_time=expiration_time,
default_values=default_values or {"name": "John"},
@@ -1,6 +1,7 @@
"""Unit tests for the Markdown API docs generator."""
import importlib.util
import json
import sys
from pathlib import Path
@@ -101,3 +102,195 @@ def test_generate_markdown_docs_only_removes_generated_specs_from_separate_swagg
assert existing_file.read_text(encoding="utf-8") == "keep me"
assert not list(swagger_dir.glob("*.json"))
def test_patch_union_schema_markdown_fills_converter_blank_schema_types(tmp_path):
module = _load_generate_swagger_markdown_docs_module()
spec_path = tmp_path / "console-swagger.json"
spec_path.write_text(
json.dumps(
{
"definitions": {
"FormInputConfig": {
"oneOf": [
{"$ref": "#/definitions/ParagraphInputConfig"},
{"$ref": "#/definitions/SelectInputConfig"},
{"$ref": "#/definitions/FileInputConfig"},
],
},
"ParagraphInputConfig": {
"properties": {
"default": {
"anyOf": [
{"$ref": "#/definitions/StringSource"},
{"type": "null"},
],
},
"output_variable_name": {"type": "string"},
},
},
"SelectInputConfig": {
"properties": {
"option_source": {"$ref": "#/definitions/StringListSource"},
},
},
"FileInputConfig": {
"properties": {
"allowed_file_types": {
"type": "array",
"items": {"$ref": "#/definitions/FileType"},
},
},
},
}
}
),
encoding="utf-8",
)
markdown = """#### FormInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| FormInputConfig | | | |
#### ParagraphInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | | | No |
| output_variable_name | string | | Yes |
#### SelectInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| option_source | | | Yes |
#### FileInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_types | | | No |
"""
patched = module._patch_union_schema_markdown(markdown, spec_path)
assert (
"| FormInputConfig | "
"[ParagraphInputConfig](#paragraphinputconfig)<br>"
"[SelectInputConfig](#selectinputconfig)<br>"
"[FileInputConfig](#fileinputconfig) | | |"
) in patched
assert "| default | [StringSource](#stringsource) | | No |" in patched
assert "| output_variable_name | string | | Yes |" in patched
assert "| option_source | [StringListSource](#stringlistsource) | | Yes |" in patched
assert "| allowed_file_types | [ [FileType](#filetype) ] | | No |" in patched
def test_patch_union_schema_markdown_ignores_specs_without_definitions(tmp_path):
module = _load_generate_swagger_markdown_docs_module()
spec_path = tmp_path / "console-swagger.json"
spec_path.write_text("{}", encoding="utf-8")
assert module._patch_union_schema_markdown("unchanged", spec_path) == "unchanged"
def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path):
module = _load_generate_swagger_markdown_docs_module()
spec_path = tmp_path / "console-swagger.json"
spec_path.write_text(
json.dumps(
{
"definitions": {
"NotAMapping": [],
"BrokenUnion": {
"oneOf": [
{},
{"$ref": "#/definitions/Missing"},
{"$ref": "#/definitions/NoPropertyMapping"},
],
},
"NoPropertyMapping": {"properties": []},
}
}
),
encoding="utf-8",
)
assert module._definition_ref_name(None) is None
assert module._schema_markdown_type(None) == ""
assert module._schema_markdown_type({"anyOf": [{"type": "null"}]}) == ""
assert module._replace_schema_table_type("unchanged", "Definition", "field", "") == "unchanged"
assert (
module._replace_schema_table_type(
"#### Definition\n#### Next\n| field | | | No |",
"Definition",
"field",
"string",
)
== "#### Definition\n#### Next\n| field | | | No |"
)
assert (
module._replace_schema_table_type("#### Definition\n| field |", "Definition", "field", "string")
== "#### Definition\n| field |"
)
assert module._patch_union_schema_markdown("#### BrokenUnion\n", spec_path) == "#### BrokenUnion"
def test_convert_spec_to_markdown_patches_generated_union_tables(tmp_path, monkeypatch):
module = _load_generate_swagger_markdown_docs_module()
spec_path = tmp_path / "console-swagger.json"
output_path = tmp_path / "console-swagger.md"
spec_path.write_text(
json.dumps(
{
"definitions": {
"FormInputConfig": {
"oneOf": [
{"$ref": "#/definitions/ParagraphInputConfig"},
],
},
"ParagraphInputConfig": {
"properties": {
"default": {
"anyOf": [
{"$ref": "#/definitions/StringSource"},
{"type": "null"},
],
},
},
},
}
}
),
encoding="utf-8",
)
def run_converter(args, **kwargs):
assert kwargs["check"] is False
markdown_path = Path(args[args.index("-o") + 1])
markdown_path.write_text(
"""#### FormInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| FormInputConfig | | | |
#### ParagraphInputConfig
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| default | | | No |
""",
encoding="utf-8",
)
return module.subprocess.CompletedProcess(args=args, returncode=0, stdout="", stderr="")
monkeypatch.setattr(module.subprocess, "run", run_converter)
module._convert_spec_to_markdown(spec_path, output_path)
converted = output_path.read_text(encoding="utf-8")
assert "| FormInputConfig | [ParagraphInputConfig](#paragraphinputconfig) | | |" in converted
assert "| default | [StringSource](#stringsource) | | No |" in converted
@@ -12,7 +12,7 @@ from controllers.console.app import workflow_run as workflow_run_module
from controllers.web.error import NotFoundError
from graphon.entities.pause_reason import HumanInputRequired
from graphon.enums import WorkflowExecutionStatus
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType
from libs import login as login_lib
from models.account import Account, AccountStatus, TenantAccountRole
@@ -63,8 +63,8 @@ def test_pause_details_returns_backstage_input_url(app: Flask, monkeypatch: pyte
reason = HumanInputRequired(
form_id="form-1",
form_content="content",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
actions=[UserAction(id="approve", title="Approve")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="name")],
actions=[UserActionConfig(id="approve", title="Approve")],
node_id="node-1",
node_title="Ask Name",
)
@@ -32,7 +32,7 @@ from core.workflow.system_variables import build_system_variables
from graphon.entities import WorkflowStartReason
from graphon.entities.pause_reason import HumanInputRequired, PauseReasonType
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType
from graphon.runtime import GraphRuntimeState, VariablePool
from models.account import Account
@@ -450,7 +450,7 @@ class TestHitlServiceApi:
node_title="Approval",
form_content="Need approval",
inputs=[],
actions=[UserAction(id="approve", title="Approve")],
actions=[UserActionConfig(id="approve", title="Approve")],
display_in_ui=True,
form_token="token-1",
resolved_default_values={},
@@ -591,9 +591,9 @@ class TestHitlServiceApi:
form_id="form-1",
form_content="Rendered",
inputs=[
FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="field", default=None),
ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="field", default=None),
],
actions=[UserAction(id="approve", title="Approve")],
actions=[UserActionConfig(id="approve", title="Approve")],
display_in_ui=True,
node_id="node-id",
node_title="Human Step",
@@ -51,7 +51,7 @@ from core.base.tts.app_generator_tts_publisher import AudioTrunk
from core.workflow.system_variables import build_system_variables
from graphon.entities.pause_reason import PauseReasonType
from graphon.enums import BuiltinNodeTypes
from graphon.nodes.human_input.entities import UserAction
from graphon.nodes.human_input.entities import UserActionConfig
from graphon.runtime import GraphRuntimeState, VariablePool
from libs.datetime_utils import naive_utc_now
from models.enums import MessageStatus
@@ -150,7 +150,7 @@ class TestAdvancedChatGenerateTaskPipeline:
node_title="Approval",
form_content="Need approval",
inputs=[],
actions=[UserAction(id="approve", title="Approve")],
actions=[UserActionConfig(id="approve", title="Approve")],
display_in_ui=True,
form_token="token-1",
resolved_default_values={},
@@ -14,7 +14,7 @@ from core.workflow.system_variables import build_system_variables
from graphon.entities import WorkflowStartReason
from graphon.entities.pause_reason import HumanInputRequired
from graphon.graph_events import GraphRunPausedEvent
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType
from models.account import Account
from models.human_input import RecipientType
@@ -157,9 +157,9 @@ def test_queue_workflow_paused_event_to_stream_responses(monkeypatch: pytest.Mon
form_id="form-1",
form_content="Rendered",
inputs=[
FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="field", default=None),
ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="field", default=None),
],
actions=[UserAction(id="approve", title="Approve")],
actions=[UserActionConfig(id="approve", title="Approve")],
node_id="node-id",
node_title="Human Step",
)
@@ -4,7 +4,7 @@ from core.entities.execution_extra_content import (
HumanInputFormDefinition,
HumanInputFormSubmissionData,
)
from graphon.nodes.human_input.entities import FormInput, UserAction
from graphon.nodes.human_input.entities import ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType
from models.execution_extra_content import ExecutionContentType
@@ -16,8 +16,8 @@ def test_human_input_content_defaults_and_domain_alias() -> None:
node_id="node-1",
node_title="Human Input",
form_content="Please confirm",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="answer")],
actions=[UserAction(id="confirm", title="Confirm")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="answer")],
actions=[UserActionConfig(id="confirm", title="Confirm")],
resolved_default_values={"answer": "yes"},
expiration_time=1_700_000_000,
)
@@ -23,7 +23,7 @@ from core.workflow.human_input_adapter import (
)
from graphon.nodes.human_input.entities import (
FormDefinition,
UserAction,
UserActionConfig,
)
from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
@@ -272,7 +272,7 @@ def _make_form_definition() -> str:
return FormDefinition(
form_content="hello",
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now(),
).model_dump_json()
@@ -29,7 +29,7 @@ from core.workflow.human_input_adapter import (
MemberRecipient,
WebAppDeliveryMethod,
)
from graphon.nodes.human_input.entities import HumanInputNodeData, UserAction
from graphon.nodes.human_input.entities import HumanInputNodeData, UserActionConfig
from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
from models.human_input import HumanInputFormRecipient, RecipientType
@@ -467,7 +467,7 @@ def test_create_form_adds_console_and_backstage_recipients(monkeypatch: pytest.M
delivery_methods=[],
form_content="hello",
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
)
params = FormCreateParams(
workflow_execution_id=None,
@@ -81,11 +81,11 @@ class MockNodeMixin:
if isinstance(self, TemplateTransformNode):
kwargs.setdefault("jinja2_template_renderer", _TestJinja2Renderer())
# Provide default tool_file_manager_factory for ToolNode subclasses
# Provide default ToolNode dependencies for ToolNode subclasses.
from graphon.nodes.tool import ToolNode as _ToolNode # local import to avoid cycles
if isinstance(self, _ToolNode):
kwargs.setdefault("tool_file_manager_factory", MagicMock(spec=ToolFileManagerProtocol))
kwargs.setdefault("tool_file_manager", MagicMock(spec=ToolFileManagerProtocol))
kwargs.setdefault("runtime", DifyToolNodeRuntime(graph_init_params.run_context))
if isinstance(self, AgentNode):
@@ -9,7 +9,7 @@ from core.repositories.human_input_repository import (
HumanInputFormEntity,
HumanInputFormRepository,
)
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
from core.workflow.node_runtime import DifyFileReferenceFactory, DifyHumanInputNodeRuntime
from core.workflow.system_variables import build_system_variables
from graphon.entities import WorkflowStartReason
from graphon.graph import Graph
@@ -24,7 +24,7 @@ from graphon.graph_events import (
from graphon.nodes.base.entities import OutputVariableEntity
from graphon.nodes.end.end_node import EndNode
from graphon.nodes.end.entities import EndNodeData
from graphon.nodes.human_input.entities import HumanInputNodeData, UserAction
from graphon.nodes.human_input.entities import HumanInputNodeData, UserActionConfig
from graphon.nodes.human_input.enums import HumanInputFormStatus
from graphon.nodes.human_input.human_input_node import HumanInputNode
from graphon.nodes.start.entities import StartNodeData
@@ -149,7 +149,7 @@ def _build_graph(runtime_state: GraphRuntimeState, repo: HumanInputFormRepositor
title="Human Input",
form_content="Human input required",
inputs=[],
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
)
human_a_config = {"id": "human_a", "data": human_data.model_dump()}
@@ -159,6 +159,7 @@ def _build_graph(runtime_state: GraphRuntimeState, repo: HumanInputFormRepositor
graph_init_params=graph_init_params,
graph_runtime_state=runtime_state,
form_repository=repo,
file_reference_factory=DifyFileReferenceFactory(graph_init_params.run_context),
runtime=DifyHumanInputNodeRuntime(graph_init_params.run_context),
)
@@ -169,6 +170,7 @@ def _build_graph(runtime_state: GraphRuntimeState, repo: HumanInputFormRepositor
graph_init_params=graph_init_params,
graph_runtime_state=runtime_state,
form_repository=repo,
file_reference_factory=DifyFileReferenceFactory(graph_init_params.run_context),
runtime=DifyHumanInputNodeRuntime(graph_init_params.run_context),
)
@@ -30,23 +30,23 @@ from core.workflow.human_input_adapter import (
WebAppDeliveryMethod,
_WebAppDeliveryConfig,
)
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
from core.workflow.node_runtime import DifyFileReferenceFactory, DifyHumanInputNodeRuntime
from core.workflow.system_variables import build_system_variables
from graphon.entities import GraphInitParams
from graphon.node_events import PauseRequestedEvent
from graphon.node_events.node import StreamCompletedEvent
from graphon.nodes.human_input.entities import (
FormInput,
FormInputDefault,
HumanInputNodeData,
UserAction,
ParagraphInputConfig,
StringSource,
UserActionConfig,
)
from graphon.nodes.human_input.enums import (
ButtonStyle,
FormInputType,
HumanInputFormStatus,
PlaceholderType,
TimeoutUnit,
ValueSourceType,
)
from graphon.nodes.human_input.human_input_node import HumanInputNode
from graphon.runtime import GraphRuntimeState, VariablePool
@@ -152,6 +152,7 @@ def _build_human_input_node(
data=typed_node_data,
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
file_reference_factory=DifyFileReferenceFactory(graph_init_params.run_context),
runtime=runtime,
)
@@ -190,32 +191,36 @@ class TestDeliveryMethod:
assert len(delivery_method.config.recipients.items) == 2
class TestFormInput:
"""Test FormInput entity."""
class TestParagraphInputConfig:
"""Test ParagraphInputConfig entity."""
def test_text_input_with_constant_default(self):
"""Test text input with constant default value."""
default = FormInputDefault(type=PlaceholderType.CONSTANT, value="Enter your response here...")
def test_paragraph_input_with_constant_default(self):
"""Test paragraph input with constant default value."""
default = StringSource(type=ValueSourceType.CONSTANT, value="Enter your response here...")
form_input = FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="user_input", default=default)
form_input = ParagraphInputConfig(
type=FormInputType.PARAGRAPH, output_variable_name="user_input", default=default
)
assert form_input.type == FormInputType.TEXT_INPUT
assert form_input.type == FormInputType.PARAGRAPH
assert form_input.output_variable_name == "user_input"
assert form_input.default.type == PlaceholderType.CONSTANT
assert form_input.default.type == ValueSourceType.CONSTANT
assert form_input.default.value == "Enter your response here..."
def test_text_input_with_variable_default(self):
"""Test text input with variable default value."""
default = FormInputDefault(type=PlaceholderType.VARIABLE, selector=["node_123", "output_var"])
def test_paragraph_input_with_variable_default(self):
"""Test paragraph input with variable default value."""
default = StringSource(type=ValueSourceType.VARIABLE, selector=["node_123", "output_var"])
form_input = FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="user_input", default=default)
form_input = ParagraphInputConfig(
type=FormInputType.PARAGRAPH, output_variable_name="user_input", default=default
)
assert form_input.default.type == PlaceholderType.VARIABLE
assert form_input.default.type == ValueSourceType.VARIABLE
assert form_input.default.selector == ["node_123", "output_var"]
def test_form_input_without_default(self):
"""Test form input without default value."""
form_input = FormInput(type=FormInputType.PARAGRAPH, output_variable_name="description")
form_input = ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="description")
assert form_input.type == FormInputType.PARAGRAPH
assert form_input.output_variable_name == "description"
@@ -223,11 +228,11 @@ class TestFormInput:
class TestUserAction:
"""Test UserAction entity."""
"""Test UserActionConfig entity."""
def test_user_action_creation(self):
"""Test user action creation."""
action = UserAction(id="approve", title="Approve", button_style=ButtonStyle.PRIMARY)
action = UserActionConfig(id="approve", title="Approve", button_style=ButtonStyle.PRIMARY)
assert action.id == "approve"
assert action.title == "Approve"
@@ -235,13 +240,13 @@ class TestUserAction:
def test_user_action_default_button_style(self):
"""Test user action with default button style."""
action = UserAction(id="cancel", title="Cancel")
action = UserActionConfig(id="cancel", title="Cancel")
assert action.button_style == ButtonStyle.DEFAULT
def test_user_action_length_boundaries(self):
"""Test user action id and title length boundaries."""
action = UserAction(id="a" * 20, title="b" * 100)
action = UserActionConfig(id="a" * 20, title="b" * 100)
assert action.id == "a" * 20
assert action.title == "b" * 100
@@ -259,7 +264,7 @@ class TestUserAction:
data[field_name] = value
with pytest.raises(ValidationError) as exc_info:
UserAction.model_validate(data)
UserActionConfig.model_validate(data)
errors = exc_info.value.errors()
assert any(error["loc"] == (field_name,) and error["type"] == "string_too_long" for error in errors)
@@ -273,14 +278,14 @@ class TestHumanInputNodeData:
delivery_methods = [WebAppDeliveryMethod(enabled=True, config=_WebAppDeliveryConfig())]
inputs = [
FormInput(
type=FormInputType.TEXT_INPUT,
ParagraphInputConfig(
type=FormInputType.PARAGRAPH,
output_variable_name="content",
default=FormInputDefault(type=PlaceholderType.CONSTANT, value="Enter content..."),
default=StringSource(type=ValueSourceType.CONSTANT, value="Enter content..."),
)
]
user_actions = [UserAction(id="submit", title="Submit", button_style=ButtonStyle.PRIMARY)]
user_actions = [UserActionConfig(id="submit", title="Submit", button_style=ButtonStyle.PRIMARY)]
node_data = HumanInputNodeData(
title="Human Input Test",
@@ -338,8 +343,8 @@ class TestHumanInputNodeData:
def test_duplicate_input_output_variable_name_raises_validation_error(self):
"""Duplicate form input output_variable_name should raise validation error."""
duplicate_inputs = [
FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="content"),
FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="content"),
ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="content"),
ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="content"),
]
with pytest.raises(ValidationError, match="duplicated output_variable_name 'content'"):
@@ -348,8 +353,8 @@ class TestHumanInputNodeData:
def test_duplicate_user_action_ids_raise_validation_error(self):
"""Duplicate user action ids should raise validation error."""
duplicate_actions = [
UserAction(id="submit", title="Submit"),
UserAction(id="submit", title="Submit Again"),
UserActionConfig(id="submit", title="Submit"),
UserActionConfig(id="submit", title="Submit Again"),
]
with pytest.raises(ValidationError, match="duplicated user action id 'submit'"):
@@ -458,18 +463,18 @@ class TestHumanInputNodeVariableResolution:
title="Human Input",
form_content="Provide your name",
inputs=[
FormInput(
type=FormInputType.TEXT_INPUT,
ParagraphInputConfig(
type=FormInputType.PARAGRAPH,
output_variable_name="user_name",
default=FormInputDefault(type=PlaceholderType.VARIABLE, selector=["start", "name"]),
default=StringSource(type=ValueSourceType.VARIABLE, selector=["start", "name"]),
),
FormInput(
type=FormInputType.TEXT_INPUT,
ParagraphInputConfig(
type=FormInputType.PARAGRAPH,
output_variable_name="user_email",
default=FormInputDefault(type=PlaceholderType.CONSTANT, value="foo@example.com"),
default=StringSource(type=ValueSourceType.CONSTANT, value="foo@example.com"),
),
],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
)
config = {"id": "human", "data": node_data.model_dump()}
@@ -534,7 +539,7 @@ class TestHumanInputNodeVariableResolution:
title="Human Input",
form_content="Provide your name",
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
)
config = {"id": "human", "data": node_data.model_dump()}
@@ -661,7 +666,7 @@ class TestHumanInputNodeVariableResolution:
title="Human Input",
form_content="Provide your name",
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
delivery_methods=[
EmailDeliveryMethod(
enabled=True,
@@ -721,7 +726,7 @@ class TestValidation:
def test_invalid_form_input_type(self):
"""Test validation with invalid form input type."""
with pytest.raises(ValidationError):
FormInput(
ParagraphInputConfig(
type="invalid-type", # Invalid type
output_variable_name="test",
)
@@ -729,7 +734,7 @@ class TestValidation:
def test_invalid_button_style(self):
"""Test validation with invalid button style."""
with pytest.raises(ValidationError):
UserAction(
UserActionConfig(
id="test",
title="Test",
button_style="invalid-style", # Invalid style
@@ -778,12 +783,12 @@ class TestHumanInputNodeRenderedContent:
title="Human Input",
form_content="Name: {{#$output.name#}}",
inputs=[
FormInput(
type=FormInputType.TEXT_INPUT,
ParagraphInputConfig(
type=FormInputType.PARAGRAPH,
output_variable_name="name",
)
],
user_actions=[UserAction(id="approve", title="Approve")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
)
config = {"id": "human", "data": node_data.model_dump()}
@@ -810,4 +815,4 @@ class TestHumanInputNodeRenderedContent:
last_event = events[-1]
assert isinstance(last_event, StreamCompletedEvent)
node_run_result = last_event.node_run_result
assert node_run_result.outputs["__rendered_content"] == "Name: Alice"
assert node_run_result.outputs["__rendered_content"].to_object() == "Name: Alice"
@@ -2,7 +2,7 @@ import datetime
from types import SimpleNamespace
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom
from core.workflow.node_runtime import DifyHumanInputNodeRuntime
from core.workflow.node_runtime import DifyFileReferenceFactory, DifyHumanInputNodeRuntime
from core.workflow.system_variables import default_system_variables
from graphon.entities import GraphInitParams
from graphon.enums import BuiltinNodeTypes
@@ -44,6 +44,7 @@ def _create_human_input_node(
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
form_repository=repo,
file_reference_factory=DifyFileReferenceFactory(graph_init_params.run_context),
runtime=DifyHumanInputNodeRuntime(graph_init_params.run_context),
)
@@ -81,7 +82,7 @@ def _build_node(form_content: str = "Please enter your name:\n\n{{#$output.name#
"form_content": form_content,
"inputs": [
{
"type": "text_input",
"type": "paragraph",
"output_variable_name": "name",
"default": {"type": "constant", "value": ""},
}
@@ -148,7 +149,7 @@ def _build_timeout_node() -> HumanInputNode:
"form_content": "Please enter your name:\n\n{{#$output.name#}}",
"inputs": [
{
"type": "text_input",
"type": "paragraph",
"output_variable_name": "name",
"default": {"type": "constant", "value": ""},
}
@@ -111,8 +111,8 @@ def tool_node(monkeypatch) -> ToolNode:
config = graph_config["nodes"][0]
# Provide a stub ToolFileManager to satisfy the updated ToolNode constructor
tool_file_manager_factory = MagicMock(spec=ToolFileManagerProtocol)
# Provide a stub ToolFileManager to satisfy the ToolNode constructor.
tool_file_manager = MagicMock(spec=ToolFileManagerProtocol)
runtime = _StubToolRuntime()
node = ToolNode(
@@ -120,7 +120,7 @@ def tool_node(monkeypatch) -> ToolNode:
data=ToolNodeData.model_validate(config["data"]),
graph_init_params=init_params,
graph_runtime_state=graph_runtime_state,
tool_file_manager_factory=tool_file_manager_factory,
tool_file_manager=tool_file_manager,
runtime=runtime,
)
return node
@@ -215,7 +215,7 @@ def test_image_link_messages_use_tool_file_id_metadata(tool_node: ToolNode):
size=123,
storage_key="file-key",
)
tool_node._tool_file_manager_factory.get_file_generator_by_tool_file_id.return_value = (
tool_node._tool_file_manager.get_file_generator_by_tool_file_id.return_value = (
None,
SimpleNamespace(mime_type="application/pdf"),
)
@@ -228,7 +228,7 @@ def test_image_link_messages_use_tool_file_id_metadata(tool_node: ToolNode):
events, _ = _run_transform(tool_node, message)
tool_node._tool_file_manager_factory.get_file_generator_by_tool_file_id.assert_called_once_with("file-id")
tool_node._tool_file_manager.get_file_generator_by_tool_file_id.assert_called_once_with("file-id")
completed_events = [event for event in events if isinstance(event, StreamCompletedEvent)]
assert len(completed_events) == 1
files_segment = completed_events[0].node_run_result.outputs["files"]
@@ -452,7 +452,7 @@ class TestDifyNodeFactoryCreateNode:
factory._jinja2_template_renderer = sentinel.jinja2_template_renderer
factory._template_transform_max_output_length = 2048
factory._http_request_http_client = sentinel.http_client
factory._bound_tool_file_manager_factory = sentinel.tool_file_manager_factory
factory._bound_tool_file_manager_factory = MagicMock(return_value=sentinel.tool_file_manager)
factory._file_reference_factory = sentinel.file_reference_factory
factory._prompt_message_serializer = sentinel.prompt_message_serializer
factory._retriever_attachment_loader = sentinel.retriever_attachment_loader
@@ -540,6 +540,7 @@ class TestDifyNodeFactoryCreateNode:
(BuiltinNodeTypes.TEMPLATE_TRANSFORM, "TemplateTransformNode"),
(BuiltinNodeTypes.HTTP_REQUEST, "HttpRequestNode"),
(BuiltinNodeTypes.HUMAN_INPUT, "HumanInputNode"),
(BuiltinNodeTypes.TOOL, "ToolNode"),
(KNOWLEDGE_INDEX_NODE_TYPE, "KnowledgeIndexNode"),
(BuiltinNodeTypes.DATASOURCE, "DatasourceNode"),
(BuiltinNodeTypes.KNOWLEDGE_RETRIEVAL, "KnowledgeRetrievalNode"),
@@ -580,13 +581,19 @@ class TestDifyNodeFactoryCreateNode:
elif constructor_name == "HttpRequestNode":
assert kwargs["http_request_config"] is sentinel.http_request_config
assert kwargs["http_client"] is sentinel.http_client
assert kwargs["tool_file_manager_factory"] is sentinel.tool_file_manager_factory
assert kwargs["tool_file_manager_factory"] is factory._bound_tool_file_manager_factory
assert kwargs["file_manager"] is sentinel.file_manager
assert kwargs["file_reference_factory"] is sentinel.file_reference_factory
factory._bound_tool_file_manager_factory.assert_not_called()
elif constructor_name == "HumanInputNode":
assert kwargs["form_repository"] is form_repository
assert kwargs["file_reference_factory"] is sentinel.file_reference_factory
assert kwargs["runtime"] is factory._human_input_runtime
factory._human_input_runtime.build_form_repository.assert_called_once_with()
elif constructor_name == "ToolNode":
assert kwargs["tool_file_manager"] is sentinel.tool_file_manager
assert kwargs["runtime"] is sentinel.tool_runtime
factory._bound_tool_file_manager_factory.assert_called_once_with()
elif constructor_name == "DocumentExtractorNode":
assert kwargs["unstructured_api_config"] is sentinel.unstructured_api_config
assert kwargs["http_client"] is sentinel.http_client
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any
from graphon.nodes.human_input.entities import FormInput
from graphon.nodes.human_input.entities import ParagraphInputConfig
from graphon.nodes.human_input.enums import TimeoutUnit
from libs.datetime_utils import naive_utc_now
@@ -45,7 +45,7 @@ class HumanInputForm:
tenant_id: str
app_id: str | None
form_content: str
inputs: list[FormInput]
inputs: list[ParagraphInputConfig]
user_actions: list[dict[str, Any]]
timeout: int
timeout_unit: TimeoutUnit
@@ -7,8 +7,8 @@ from datetime import timedelta
import pytest
from graphon.nodes.human_input.entities import (
FormInput,
UserAction,
ParagraphInputConfig,
UserActionConfig,
)
from graphon.nodes.human_input.enums import (
FormInputType,
@@ -50,8 +50,8 @@ class TestFormService:
"tenant_id": "tenant-abc",
"app_id": "app-def",
"form_content": "# Test Form\n\nInput: {{#$output.input#}}",
"inputs": [FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="input", default=None)],
"user_actions": [UserAction(id="submit", title="Submit")],
"inputs": [ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="input", default=None)],
"user_actions": [UserActionConfig(id="submit", title="Submit")],
"timeout": 1,
"timeout_unit": TimeoutUnit.HOUR,
"form_token": "token-xyz",
@@ -304,8 +304,10 @@ class TestFormValidation:
"tenant_id": "tenant-abc",
"app_id": "app-def",
"form_content": "Test form",
"inputs": [FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="required_input", default=None)],
"user_actions": [UserAction(id="submit", title="Submit")],
"inputs": [
ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="required_input", default=None)
],
"user_actions": [UserActionConfig(id="submit", title="Submit")],
"timeout": 1,
"timeout_unit": TimeoutUnit.HOUR,
}
@@ -7,8 +7,8 @@ from datetime import datetime, timedelta
import pytest
from graphon.nodes.human_input.entities import (
FormInput,
UserAction,
ParagraphInputConfig,
UserActionConfig,
)
from graphon.nodes.human_input.enums import (
FormInputType,
@@ -32,8 +32,8 @@ class TestHumanInputForm:
"tenant_id": "tenant-abc",
"app_id": "app-def",
"form_content": "# Test Form\n\nInput: {{#$output.input#}}",
"inputs": [FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="input", default=None)],
"user_actions": [UserAction(id="submit", title="Submit")],
"inputs": [ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="input", default=None)],
"user_actions": [UserActionConfig(id="submit", title="Submit")],
"timeout": 2,
"timeout_unit": TimeoutUnit.HOUR,
"form_token": "token-xyz",
@@ -132,7 +132,7 @@ class TestHumanInputForm:
assert "site" not in response
assert response["form_content"] == "# Test Form\n\nInput: {{#$output.input#}}"
assert len(response["inputs"]) == 1
assert response["inputs"][0]["type"] == "text-input"
assert response["inputs"][0]["type"] == "paragraph"
assert response["inputs"][0]["output_variable_name"] == "input"
def test_form_to_response_dict_with_site_info(self, sample_form_data):
@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import UTC, datetime
from types import SimpleNamespace
from graphon.nodes.human_input.entities import FormDefinition, FormInput, UserAction
from graphon.nodes.human_input.entities import FormDefinition, ParagraphInputConfig, UserActionConfig
from graphon.nodes.human_input.enums import FormInputType
from models.human_input import RecipientType
from repositories.sqlalchemy_api_workflow_run_repository import _build_human_input_required_reason
@@ -13,8 +13,8 @@ def _build_form_model() -> SimpleNamespace:
expiration_time = datetime(2024, 1, 1, tzinfo=UTC)
definition = FormDefinition(
form_content="content",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="name")],
user_actions=[UserAction(id="approve", title="Approve")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="name")],
user_actions=[UserActionConfig(id="approve", title="Approve")],
rendered_content="rendered",
expiration_time=expiration_time,
default_values={"name": "Alice"},
@@ -12,8 +12,8 @@ from core.repositories.human_input_repository import (
)
from graphon.nodes.human_input.entities import (
FormDefinition,
FormInput,
UserAction,
ParagraphInputConfig,
UserActionConfig,
)
from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus
from libs.datetime_utils import naive_utc_now
@@ -51,7 +51,7 @@ def sample_form_record():
definition=FormDefinition(
form_content="hello",
inputs=[],
user_actions=[UserAction(id="submit", title="Submit")],
user_actions=[UserActionConfig(id="submit", title="Submit")],
rendered_content="<p>hello</p>",
expiration_time=naive_utc_now() + timedelta(hours=1),
),
@@ -280,7 +280,7 @@ def test_submit_form_by_token_missing_inputs(sample_form_record, mock_session_fa
definition_with_input = FormDefinition(
form_content="hello",
inputs=[FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="content")],
inputs=[ParagraphInputConfig(type=FormInputType.PARAGRAPH, output_variable_name="content")],
user_actions=sample_form_record.definition.user_actions,
rendered_content="<p>hello</p>",
expiration_time=sample_form_record.expiration_time,
@@ -2833,6 +2833,7 @@ class TestWorkflowServiceFreeNodeExecution:
return_value=sentinel.adapted_node_data,
) as mock_adapt_node_data,
patch("services.workflow_service.build_dify_run_context") as mock_build_dify_run_context,
patch("services.workflow_service.DifyFileReferenceFactory") as mock_file_reference_factory_cls,
patch("services.workflow_service.DifyHumanInputNodeRuntime") as mock_runtime_cls,
patch("services.workflow_service.HumanInputNode") as mock_node_cls,
):
@@ -2851,10 +2852,14 @@ class TestWorkflowServiceFreeNodeExecution:
mock_runtime_cls.assert_called_once_with(mock_build_dify_run_context.return_value)
mock_adapt_node_data.assert_called_once_with(node_config["data"])
mock_node_cls.validate_node_data.assert_called_once_with(sentinel.adapted_node_data)
mock_file_reference_factory_cls.assert_called_once_with(
mock_graph_init_context_cls.return_value.to_graph_init_params.return_value.run_context
)
mock_node_cls.assert_called_once_with(
node_id="n-1",
data=sentinel.node_data,
graph_init_params=mock_graph_init_context_cls.return_value.to_graph_init_params.return_value,
graph_runtime_state=ANY,
file_reference_factory=mock_file_reference_factory_cls.return_value,
runtime=mock_runtime_cls.return_value,
)
Generated
+4 -4
View File
@@ -1628,7 +1628,7 @@ requires-dist = [
{ name = "gmpy2", specifier = ">=2.3.0" },
{ name = "google-api-python-client", specifier = ">=2.196.0" },
{ name = "google-cloud-aiplatform", specifier = ">=1.151.0,<2.0.0" },
{ name = "graphon", specifier = "~=0.3.1" },
{ name = "graphon", specifier = "~=0.4.0" },
{ name = "gunicorn", specifier = ">=26.0.0" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0.0" },
{ name = "httpx-sse", specifier = "~=0.4.0" },
@@ -2984,7 +2984,7 @@ httpx = [
[[package]]
name = "graphon"
version = "0.3.1"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "charset-normalizer" },
@@ -3005,9 +3005,9 @@ dependencies = [
{ name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] },
{ name = "webvtt-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/ef/43217842e84160acca64a95858f1689389a50e04a53fc94f2aa836b4eaf7/graphon-0.3.1.tar.gz", hash = "sha256:49971baed1eb16c8e1983f755e659902e4f117a68dc62fad19e91472950b937d", size = 242210, upload-time = "2026-05-07T06:58:21.879Z" }
sdist = { url = "https://files.pythonhosted.org/packages/76/24/eb1e7983404dcac84816b76ea450e1bb97023e55e00c699d609340bc361e/graphon-0.4.0.tar.gz", hash = "sha256:afb0c7a58f89e09cfa585296429b4d08cd0df80b9ac54d550f88e7d76ec48ee0", size = 261812, upload-time = "2026-05-13T11:48:39.198Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/37/bef16ed3d6da7446b36769fa388f4dc79f95337ffa16d6dfc3177152507e/graphon-0.3.1-py3-none-any.whl", hash = "sha256:e6422c7e3f1ce7d2185979c17e08201816ca25d46d400ebdd035c95d501c04fe", size = 349368, upload-time = "2026-05-07T06:58:20.217Z" },
{ url = "https://files.pythonhosted.org/packages/b7/de/bad6b3fd1e4b4defc16e6ea106e55c44725a159f1d191a99877bce1c9931/graphon-0.4.0-py3-none-any.whl", hash = "sha256:b33f95886da823d5b1b53d663a4f5f8fa383c37740f3bd19297b8d140fcb804c", size = 372711, upload-time = "2026-05-13T11:48:37.712Z" },
]
[[package]]