Python: Raise clear handler registration error for unresolved TypeVar annotations (#4944)

* Raise clear handler registration error for unresolved TypeVar (#4943)

Detect unresolved TypeVar in message parameter annotations during handler
registration in both _validate_handler_signature (Executor) and
_validate_function_signature (FunctionExecutor). Raises a ValueError with
an actionable message recommending @handler(input=..., output=...) or
@executor(input=..., output=...) instead of letting TypeVar leak through
to a confusing TypeCompatibilityError during workflow edge validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #4943: reorder checks and harden function executor

- Move TypeVar check before validate_workflow_context_annotation in
  _executor.py so users see the more actionable error first
- Wrap get_type_hints in try/except in _function_executor.py matching
  the defensive pattern in _executor.py
- Repurpose duplicate test to cover bounded TypeVar rejection
- Add test_function_executor_allows_concrete_types for test symmetry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Narrow get_type_hints except clause and add missing tests (#4943)

- Narrow `except Exception` to `except (NameError, AttributeError, RecursionError)`
  in both _executor.py and _function_executor.py so unexpected failures in
  get_type_hints are not silently swallowed.
- Add test_handler_unresolvable_annotation_raises to test_function_executor_future.py
  exercising the except branch of get_type_hints in the function executor path.
- Add test_function_executor_rejects_bounded_typevar_in_message_annotation to
  test_function_executor.py for parity with the Executor bounded TypeVar test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add error ordering test for TypeVar vs WorkflowContext priority (#4943)

Add test_handler_typevar_error_takes_priority_over_context_error to verify
that when a handler has both a TypeVar message and an unannotated ctx, the
TypeVar error is raised first (the more actionable issue).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Fix image content serialization sending null file_id to Foundry API

Omit file_id from input_image dict when not present instead of including
it as null, which Azure AI Foundry's stricter schema validation rejects.

* Python: Fix Foundry API rejecting rich content in function_call_output

Azure AI Foundry does not support list-format output in function_call_output
items. Add SUPPORTS_RICH_FUNCTION_OUTPUT flag (default True) to
RawOpenAIChatClient, set to False in RawFoundryChatClient so Foundry
falls back to string output for tool results with images/files.

Also omit file_id from input_image dicts when not set, since Foundry
rejects explicit nulls.

* Python: Surface rich tool content as user message when Foundry lacks support

When SUPPORTS_RICH_FUNCTION_OUTPUT is False, image/file items from tool
results are injected as a follow-up user message so the model can still
process the visual content via Foundry's supported user message format.

* Xfail Foundry image integration test for the meantime

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-04-08 01:59:17 +09:00
committed by GitHub
Unverified
parent 942cb04ccb
commit 36cafe4e5a
9 changed files with 219 additions and 13 deletions
@@ -265,6 +265,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
INJECTABLE: ClassVar[set[str]] = {"client"}
STORES_BY_DEFAULT: ClassVar[bool] = True # type: ignore[reportIncompatibleVariableOverride, misc]
SUPPORTS_RICH_FUNCTION_OUTPUT: ClassVar[bool] = True
FILE_SEARCH_MAX_RESULTS: int = 50
@@ -1353,16 +1354,17 @@ class RawOpenAIChatClient( # type: ignore[misc]
return ret
case "data" | "uri":
if content.has_top_level_media_type("image"):
return {
result: dict[str, Any] = {
"type": "input_image",
"image_url": content.uri,
"detail": content.additional_properties.get("detail", "auto")
if content.additional_properties
else "auto",
"file_id": content.additional_properties.get("file_id", None)
if content.additional_properties
else None,
}
file_id = content.additional_properties.get("file_id") if content.additional_properties else None
if file_id:
result["file_id"] = file_id
return result
if content.has_top_level_media_type("audio"):
if content.media_type and "wav" in content.media_type:
format = "wav"
@@ -1444,7 +1446,11 @@ class RawOpenAIChatClient( # type: ignore[misc]
}
# call_id for the result needs to be the same as the call_id for the function call
output: str | list[dict[str, Any]] = content.result or ""
if content.items and any(item.type in ("data", "uri") for item in content.items):
if (
self.SUPPORTS_RICH_FUNCTION_OUTPUT
and content.items
and any(item.type in ("data", "uri") for item in content.items)
):
output_parts: list[dict[str, Any]] = []
for item in content.items:
if item.type == "text":
@@ -2577,7 +2577,7 @@ def test_prepare_content_for_openai_image_content() -> None:
result = client._prepare_content_for_openai("user", image_content_basic)
assert result["type"] == "input_image"
assert result["detail"] == "auto"
assert result["file_id"] is None
assert "file_id" not in result
def test_prepare_content_for_openai_audio_content() -> None: