Skip to content
Open
Changes from all commits
Commits
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
39 changes: 38 additions & 1 deletion effectful/handlers/llm/harness/synthesis/body.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,21 @@ def _callable_type_from_signature(
return collections.abc.Callable[param_types, return_type] # type: ignore


def _has_plain_validator(ty: typing.Any) -> bool:
"""Whether an `Encodable` annotation decodes from a wire form anywhere.

A `PlainValidator` in the encoding (a synthesized callable's source, an
image's data URL) replaces core validation with a decode step, so an
already-built value of the type cannot be re-validated through it.
"""
if any(
isinstance(m, pydantic.PlainValidator)
for m in getattr(ty, "__metadata__", ())
):
return True
return any(_has_plain_validator(arg) for arg in typing.get_args(ty))


class FinalBodySynthesizer(ObjectInterpretation):
"""You may "answer" a Template by writing code instead of producing the value
directly. The `submit_solution` tool accepts a single argument: a Python
Expand Down Expand Up @@ -555,7 +570,29 @@ def submit_solution(implementation: body_type) -> return_type: # type: ignore
it (see the "Code synthesis" section); its return value on the
original arguments becomes the answer.
"""
return implementation(*args, **kwargs) # type: ignore
answer = implementation(*args, **kwargs) # type: ignore
if return_type is inspect.Signature.empty or return_type is str:
return answer # type: ignore
encodable = TypeToPydanticType().evaluate(return_type)
context = {**dict(template.__context__), **bound_args.arguments}
if _has_plain_validator(encodable):
# The answer is already in decoded form; a `PlainValidator`
# encoding expects the wire form, so run only the
# post-decode checks.
afters = [
m
for m in getattr(encodable, "__metadata__", ())
if isinstance(m, pydantic.AfterValidator)
]
if afters:
answer = pydantic.TypeAdapter(
typing.Annotated[typing.Any, *afters]
).validate_python(answer, context=context)
else:
answer = pydantic.TypeAdapter(encodable).validate_python(
answer, context=context
)
return answer # type: ignore

return super().define(submit_solution, name=cls.__toolname__)

Expand Down