Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8cfe854
Adding lexical context collection
datvo06 Dec 8, 2025
4122acc
Allow model input to refer to anything within the lexical context
datvo06 Dec 8, 2025
28fc2c9
Different handling between representable object and other types when …
datvo06 Dec 8, 2025
126cab4
More edge case handling
datvo06 Dec 9, 2025
bec5e06
More edge case handling
datvo06 Dec 9, 2025
a768749
More edge case handling
datvo06 Dec 9, 2025
aabb0b7
Adding mypy check and test
datvo06 Dec 4, 2025
55cd283
SynthesizedFunction for constrained decoding, adapt the tests
datvo06 Dec 4, 2025
a232926
Linting
datvo06 Dec 4, 2025
61a9432
Let LLM generate param names
datvo06 Dec 4, 2025
6135eee
Pydantic field annotation
datvo06 Dec 4, 2025
cb3cb07
Merging notebook
datvo06 Dec 9, 2025
40413c5
Linting
datvo06 Dec 4, 2025
4d7867b
Fix minor formatting for type context
datvo06 Dec 5, 2025
ce8ebcf
Update llm dependencies to include mypy
datvo06 Dec 5, 2025
246b980
More comprehensive error message
datvo06 Dec 5, 2025
90d933c
linting
datvo06 Dec 5, 2025
ac2c953
More comprehensive import
datvo06 Dec 5, 2025
403a46d
Merging lexical context
datvo06 Dec 9, 2025
bce39c9
Merging lexical context
datvo06 Dec 9, 2025
64ecc63
Merging lexical context
datvo06 Dec 9, 2025
04d5622
Lint
datvo06 Dec 9, 2025
e3741aa
More refractoring to use the lexical context instead of collected typ…
datvo06 Dec 9, 2025
362e773
Bring constrained decoding back to synthesized function
datvo06 Dec 9, 2025
81a993b
Remove redundant format signature and rely on mypy to check for type …
datvo06 Dec 9, 2025
a2ada49
Revert change
datvo06 Dec 15, 2025
3d4aac7
Using lexical context
datvo06 Dec 16, 2025
80d0353
removing lexical context docstring
datvo06 Dec 16, 2025
79f07a4
Using Encodable as interface and resolve merge conflicts
datvo06 Dec 16, 2025
d2abb9e
Linting
datvo06 Dec 16, 2025
e3374f6
Attaching source code to the function
datvo06 Dec 16, 2025
535d9fc
Attaching source code to the function
datvo06 Dec 16, 2025
3d1074c
Attaching source code to the function
datvo06 Dec 16, 2025
a366598
Factor out program synthesis test to be less flaky
datvo06 Dec 16, 2025
aa4a98c
Linting
datvo06 Dec 16, 2025
ad4d603
Factor out image LLM test for less flaky
datvo06 Dec 16, 2025
7aaa173
Linting
datvo06 Dec 16, 2025
8dd6f16
Include factored out test
datvo06 Dec 16, 2025
8f8c614
Fix TypeError weak reference to str
datvo06 Dec 16, 2025
8b0251c
Fix misisng collections import
datvo06 Dec 16, 2025
5a6a8ea
Linting
datvo06 Dec 16, 2025
53b2b95
Add immutable lexical context test
datvo06 Dec 16, 2025
62c649a
Readding type check mypy
datvo06 Dec 16, 2025
23309f1
Update synthesis prompt so that it will not misunderstand and return …
datvo06 Dec 16, 2025
54b72ea
Update
datvo06 Dec 16, 2025
59ecdd0
Update prompt to avoid factory generation again
datvo06 Dec 16, 2025
7ac9471
Remove unnecesary redefinition
datvo06 Dec 17, 2025
3b07059
Use type_to_encodable_type for Lexical context encoding
datvo06 Dec 18, 2025
dec0728
Remove ability to "decode" LexicalContext
datvo06 Dec 18, 2025
170964e
Separating type check to a separate operation
datvo06 Dec 21, 2025
4978a45
Linting
datvo06 Dec 21, 2025
70cb3ee
Successfully move type check to a separate ops
datvo06 Dec 21, 2025
afcc03f
Update notebook
datvo06 Dec 21, 2025
11dfa52
Lint
datvo06 Dec 21, 2025
eff6e2b
Remove unnecessary encoding
datvo06 Dec 21, 2025
3067a34
defaulting program synthesis to not include_body
datvo06 Dec 21, 2025
02c3237
Lint
datvo06 Dec 21, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/test_llm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
uv run pytest tests/test_handlers_llm_provider.py -v --tb=short
uv run pytest tests/test_handlers_llm_provider*.py -v --tb=short
239 changes: 136 additions & 103 deletions docs/source/llm.ipynb

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions effectful/handlers/llm/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ def encode(cls, vl: T) -> U:

@classmethod
@abstractmethod
def decode(cls, vl: U) -> T:
def decode(cls, vl: U, template: typing.Any = None) -> T:
"""Decode an encoded value back to the original type.

Args:
vl: The encoded value
template: Optional Template providing context (e.g., lexical scope)
"""
pass

@classmethod
Expand Down Expand Up @@ -67,7 +73,7 @@ def encode(cls, vl: T) -> T:
return vl

@classmethod
def decode(cls, vl: T) -> T:
def decode(cls, vl: T, template: typing.Any = None) -> T:
return vl

return typing.cast(Encodable[T], BaseEncodable())
Expand All @@ -81,7 +87,7 @@ class EncodablePydanticBaseModel(EncodableAs[T, T]):
t: type[T] = ty

@classmethod
def decode(cls, vl: T) -> T:
def decode(cls, vl: T, template: typing.Any = None) -> T:
return vl

@classmethod
Expand All @@ -107,7 +113,9 @@ def encode(cls, image: Image.Image) -> ChatCompletionImageUrlObject:
}

@classmethod
def decode(cls, image: ChatCompletionImageUrlObject) -> Image.Image:
def decode(
cls, image: ChatCompletionImageUrlObject, template: typing.Any = None
) -> Image.Image:
image_url = image["url"]
if not image_url.startswith("data:image/"):
raise RuntimeError(
Expand Down Expand Up @@ -156,13 +164,14 @@ def encode(cls, t: T) -> typing.Any:
return tuple([enc.encode(elem) for enc, elem in zip(element_encoders, t)])

@classmethod
def decode(cls, t: typing.Any) -> T:
def decode(cls, t: typing.Any, template: typing.Any = None) -> T:
if len(t) != len(element_encoders):
raise ValueError(
f"tuple length {len(t)} does not match expected length {len(element_encoders)}"
)
decoded_elements: list[typing.Any] = [
enc.decode(elem) for enc, elem in zip(element_encoders, t)
enc.decode(elem, template=template)
for enc, elem in zip(element_encoders, t)
]
return typing.cast(T, tuple(decoded_elements))

Expand Down Expand Up @@ -217,9 +226,9 @@ def encode(cls, t: T) -> typing.Any:
return [element_encoder.encode(elem) for elem in t]

@classmethod
def decode(cls, t: typing.Any) -> T:
def decode(cls, t: typing.Any, template: typing.Any = None) -> T:
decoded_elements: list[typing.Any] = [
element_encoder.decode(elem) for elem in t
element_encoder.decode(elem, template=template) for elem in t
]
return typing.cast(T, decoded_elements)

Expand Down
24 changes: 21 additions & 3 deletions effectful/handlers/llm/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,23 @@ def tool_call[T](
return tool(*args, **kwargs)


@defop
def type_check[T](value: T, template: Template) -> T:
"""Validate that a value conforms to the template's expected return type.

Default behavior is pass-through (no validation).
Handlers can intercept this to perform type checking (e.g., mypy for Callables).

Args:
value: The value to validate
template: The template providing expected type and context

Returns:
The value unchanged if validation passes
"""
return value


class CacheLLMRequestHandler(ObjectInterpretation):
"""Caches LLM requests."""

Expand Down Expand Up @@ -437,7 +454,7 @@ def decode_response[**P, T](template: Callable[P, T], response: ModelResponse) -
assert isinstance(result, Result)
value = result.value # type: ignore

return encodable_ty.decode(value) # type: ignore
return encodable_ty.decode(value, template=template) # type: ignore


@defop
Expand All @@ -446,7 +463,6 @@ def format_model_input[**P, T](
) -> list[Any]:
"""Format a template applied to arguments into a sequence of input
messages.

"""
bound_args = template.__signature__.bind(*args, **kwargs)
bound_args.apply_defaults()
Expand Down Expand Up @@ -489,4 +505,6 @@ def _call[**P, T](
) -> T:
model_input = format_model_input(template, *args, **kwargs)
resp = compute_response(template, model_input)
return decode_response(template, resp)
result = decode_response(template, resp)
# Validate the result against the expected return type
return type_check(result, template)
Loading
Loading