mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-24 10:03:55 +08:00
Add server-side TypeResolver for prompt-graph type resolution
Resolves the concrete io_type of any output/input slot in a prompt by walking the graph, so API-submitted workflows (no frontend) and the execution engine agree on resolved types even when MatchType chains are involved. * New comfy_execution/type_resolver.py: TypeResolver class with output resolution (incl. MatchType template walking, cycle detection, depth cap, AnyType fallback + one-shot warning), input resolution (links and literals), is_output_list / is_input_list helpers, effective slot io_type peeling for dynamic wrappers (Autogrow -> wrapped element type, DynamicSlot -> underlying slot type), and bulk compute_live_input_types. * DynamicPrompt now lazily exposes get_type_resolver() and invalidates the resolver cache on add_ephemeral_node. * get_finalized_class_inputs / parse_class_inputs / DYNAMIC_INPUT_LOOKUP callable signature accept an optional live_input_types dict. Existing Autogrow/DynamicSlot/DynamicCombo expansions accept and ignore it; future per-type dynamic inputs use it as their discriminator. * validate_inputs and get_input_data both build live_input_types via the resolver and pass it through; validate_inputs also uses the resolver to determine received_type for linked inputs so MatchType chains in API workflows validate correctly. * validate_prompt builds one TypeResolver and shares it across all output-node validations to avoid re-walking chains. * tests-unit/execution_test/test_type_resolver.py covers V1 static return types, V1 wildcard warning behavior, MatchType resolution including first-wins, cycle termination, chain walking, input resolution, Autogrow peeling, list info, and cache invalidation. Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
45
execution.py
45
execution.py
@@ -83,8 +83,9 @@ class IsChangedCache:
|
||||
self.is_changed[node_id] = node["is_changed"]
|
||||
return self.is_changed[node_id]
|
||||
|
||||
# Intentionally do not use cached outputs here. We only want constants in IS_CHANGED
|
||||
input_data_all, _, v3_data = get_input_data(node["inputs"], class_def, node_id, None)
|
||||
# Intentionally do not use cached outputs here. We only want constants in IS_CHANGED.
|
||||
# Pass dynprompt so the TypeResolver can resolve link types for V3 dynamic schemas.
|
||||
input_data_all, _, v3_data = get_input_data(node["inputs"], class_def, node_id, None, self.dynprompt)
|
||||
try:
|
||||
is_changed = await _async_map_node_over_list(self.prompt_id, node_id, class_def, input_data_all, is_changed_name, v3_data=v3_data)
|
||||
is_changed = await resolve_map_node_over_list_results(is_changed)
|
||||
@@ -158,7 +159,15 @@ def get_input_data(inputs, class_def, unique_id, execution_list=None, dynprompt=
|
||||
hidden_inputs_v3 = {}
|
||||
valid_inputs = class_def.INPUT_TYPES()
|
||||
if is_v3:
|
||||
valid_inputs, hidden, v3_data = _io.get_finalized_class_inputs(valid_inputs, inputs)
|
||||
# Build the type-resolution map for this node so dynamic schemas can
|
||||
# branch on resolved upstream types (and not only on literal values).
|
||||
# When no DynamicPrompt is available (e.g. some IsChangedCache paths
|
||||
# in tests), live_input_types stays None and only literal-driven
|
||||
# dynamic types continue to work.
|
||||
live_input_types = None
|
||||
if dynprompt is not None and hasattr(dynprompt, "get_type_resolver"):
|
||||
live_input_types = dynprompt.get_type_resolver().compute_live_input_types(unique_id)
|
||||
valid_inputs, hidden, v3_data = _io.get_finalized_class_inputs(valid_inputs, inputs, live_input_types=live_input_types)
|
||||
input_data_all = {}
|
||||
missing_keys = {}
|
||||
for x in inputs:
|
||||
@@ -821,9 +830,19 @@ class PromptExecutor:
|
||||
self._notify_prompt_lifecycle("end", prompt_id)
|
||||
|
||||
|
||||
async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
||||
async def validate_inputs(prompt_id, prompt, item, validated, visiting=None, type_resolver=None):
|
||||
"""Validate inputs for a single node, recursing into upstream nodes.
|
||||
|
||||
``type_resolver`` (a ``comfy_execution.type_resolver.TypeResolver``) is
|
||||
built once at the top of the recursion and reused so MatchType chains are
|
||||
only walked once. It also gives V3 dynamic schemas an accurate map of
|
||||
resolved upstream types for API-submitted workflows.
|
||||
"""
|
||||
if visiting is None:
|
||||
visiting = []
|
||||
if type_resolver is None:
|
||||
from comfy_execution.type_resolver import TypeResolver
|
||||
type_resolver = TypeResolver(prompt)
|
||||
|
||||
unique_id = item
|
||||
if unique_id in validated:
|
||||
@@ -858,7 +877,8 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
||||
if issubclass(obj_class, _ComfyNodeInternal):
|
||||
obj_class: _io._ComfyNodeBaseInternal
|
||||
class_inputs = obj_class.INPUT_TYPES()
|
||||
class_inputs, _, v3_data = _io.get_finalized_class_inputs(class_inputs, inputs)
|
||||
live_input_types = type_resolver.compute_live_input_types(unique_id)
|
||||
class_inputs, _, v3_data = _io.get_finalized_class_inputs(class_inputs, inputs, live_input_types=live_input_types)
|
||||
validate_function_name = "validate_inputs"
|
||||
validate_function = first_real_override(obj_class, validate_function_name)
|
||||
else:
|
||||
@@ -909,8 +929,11 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
||||
|
||||
o_id = val[0]
|
||||
o_class_type = prompt[o_id]['class_type']
|
||||
r = nodes.NODE_CLASS_MAPPINGS[o_class_type].RETURN_TYPES
|
||||
received_type = r[val[1]]
|
||||
# Resolve the upstream output's effective type through the
|
||||
# TypeResolver. This walks MatchType/template chains, so an API
|
||||
# workflow without frontend-injected type metadata still gets the
|
||||
# same answer the UI does.
|
||||
received_type = type_resolver.resolve_output_type(o_id, val[1])
|
||||
received_types[x] = received_type
|
||||
if 'input_types' not in validate_function_inputs and not validate_node_input(received_type, input_type):
|
||||
details = f"{x}, received_type({received_type}) mismatch input_type({input_type})"
|
||||
@@ -930,7 +953,7 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
||||
try:
|
||||
visiting.append(unique_id)
|
||||
try:
|
||||
r = await validate_inputs(prompt_id, prompt, o_id, validated, visiting)
|
||||
r = await validate_inputs(prompt_id, prompt, o_id, validated, visiting, type_resolver)
|
||||
finally:
|
||||
visiting.pop()
|
||||
if r[0] is False:
|
||||
@@ -1155,11 +1178,15 @@ async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[
|
||||
errors = []
|
||||
node_errors = {}
|
||||
validated = {}
|
||||
# Share one TypeResolver across all output validations so MatchType chains
|
||||
# are only walked once per prompt.
|
||||
from comfy_execution.type_resolver import TypeResolver
|
||||
type_resolver = TypeResolver(prompt)
|
||||
for o in outputs:
|
||||
valid = False
|
||||
reasons = []
|
||||
try:
|
||||
m = await validate_inputs(prompt_id, prompt, o, validated)
|
||||
m = await validate_inputs(prompt_id, prompt, o, validated, None, type_resolver)
|
||||
valid = m[0]
|
||||
reasons = m[1]
|
||||
except Exception as ex:
|
||||
|
||||
Reference in New Issue
Block a user