fix(agent): support iteration item aliases in child nodes (#14146)

## Summary
This PR fixes the iteration variable mismatch reported in #14142.

Changes:
- restore compatibility for `IterationItem@result` by exposing `result`
alongside `item`
- support bare iteration aliases like `{item}`, `{index}`, and
`{result}` inside iteration child-node inputs
- add focused unit/runtime tests covering both alias styles and
multi-item iteration execution

## Validation
```bash
pytest -q --noconftest \
  test/testcases/test_web_api/test_canvas_app/test_iterationitem_unit.py \
  test/testcases/test_web_api/test_canvas_app/test_iteration_runtime_unit.py \
  test/testcases/test_web_api/test_canvas_app/test_invoke_component_unit.py
```

Result: `12 passed`

Closes #14142
This commit is contained in:
hyl64
2026-05-12 13:05:21 +08:00
committed by GitHub
parent 128a64eae5
commit 02c2587ca4
4 changed files with 576 additions and 1 deletions

View File

@@ -366,6 +366,7 @@ class ComponentBase(ABC):
component_name: str
thread_limiter = asyncio.Semaphore(int(os.environ.get("MAX_CONCURRENT_CHATS", 10)))
variable_ref_patt = r"\{* *\{([a-zA-Z:0-9]+@[A-Za-z0-9_.-]+|sys\.[A-Za-z0-9_.]+|env\.[A-Za-z0-9_.]+)\} *\}*"
iteration_alias_patt = r"\{* *\{(item|index|result)\} *\}*"
def __str__(self):
"""
@@ -501,6 +502,23 @@ class ComponentBase(ABC):
return {var: self.get_input_value(var) for var, o in self.get_input_elements().items()}
def _resolve_iteration_alias_ref(self, exp: str) -> str | None:
if exp not in {"item", "index", "result"}:
return None
parent = self.get_parent()
if not parent or parent.component_name.lower() != "iteration":
return None
for cid, cpn in self._canvas.components.items():
if cpn.get("parent_id") != parent._id:
continue
if cpn["obj"].component_name.lower() != "iterationitem":
continue
return f"{cid}@{exp}"
return None
def get_input_elements_from_text(self, txt: str) -> dict[str, dict[str, str]]:
res = {}
for r in re.finditer(self.variable_ref_patt, txt, flags=re.IGNORECASE | re.DOTALL):
@@ -512,6 +530,20 @@ class ComponentBase(ABC):
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references") if cpn_id else None,
"_cpn_id": cpn_id
}
for r in re.finditer(self.iteration_alias_patt, txt, flags=re.IGNORECASE | re.DOTALL):
exp = r.group(1)
if exp in res:
continue
ref = self._resolve_iteration_alias_ref(exp)
if not ref:
continue
cpn_id, var_nm = ref.split("@", 1)
res[exp] = {
"name": (self._canvas.get_component_name(cpn_id) + f"@{var_nm}"),
"value": self._canvas.get_variable_value(ref),
"_retrieval": self._canvas.get_variable_value(f"{cpn_id}@_references"),
"_cpn_id": cpn_id
}
return res
def get_input_elements(self) -> dict[str, Any]:

View File

@@ -54,7 +54,11 @@ class IterationItem(ComponentBase, ABC):
if self.check_if_canceled("IterationItem processing"):
return
self.set_output("item", arr[self._idx])
current_item = arr[self._idx]
self.set_output("item", current_item)
# Keep `result` as a compatibility alias because existing DSL examples
# and downstream references may still consume IterationItem via `@result`.
self.set_output("result", current_item)
self.set_output("index", self._idx)
self._idx += 1