2121from litellm .types .utils import ModelResponse
2222
2323from effectful .handlers .llm import Template , Tool
24- from effectful .handlers .llm .encoding import type_to_encodable_type
24+ from effectful .handlers .llm .encoding import (
25+ Encodable ,
26+ type_to_encodable_type ,
27+ )
28+ from effectful .internals .unification import TypeExpression , nested_type
2529from effectful .ops .semantics import fwd , handler
2630from effectful .ops .syntax import ObjectInterpretation , defop , implements
2731from effectful .ops .types import Operation
@@ -213,16 +217,18 @@ def call_with_json_args(
213217
214218
215219@defop
216- def compute_response (template : Template , model_input : list [Any ]) -> ModelResponse :
220+ def compute_response [** P , T ](
221+ template : Template [P , T ], model_input_pair : tuple [list [Any ], Encodable [T ]]
222+ ) -> ModelResponse :
217223 """Produce a complete model response for an input message sequence. This may
218224 involve multiple API requests if tools are invoked by the model.
219225
220226 """
221- ret_type = template . __signature__ . return_annotation
227+ model_input , ret_type_encoder = model_input_pair
222228 tools = template .tools
223229
224230 tool_schemas = [function_definition (t ) for t in tools .values ()]
225- response_encoding_type : type | None = type_to_encodable_type ( ret_type ) .t
231+ response_encoding_type : type | None = ret_type_encoder .t
226232 if response_encoding_type == str :
227233 response_encoding_type = None
228234
@@ -262,7 +268,12 @@ def compute_response(template: Template, model_input: list[Any]) -> ModelRespons
262268 )
263269
264270
265- def decode_response [** P , T ](template : Callable [P , T ], response : ModelResponse ) -> T :
271+ type ModelInput [T ] = tuple [list [Any ], Encodable [T ]]
272+
273+
274+ def decode_response [** P , T ](
275+ template : Callable [P , T ], model_input_pair : ModelInput [T ], response : ModelResponse
276+ ) -> T :
266277 """Decode an LLM response into an instance of the template return type. This
267278 operation should raise if the output cannot be decoded.
268279 """
@@ -273,8 +284,7 @@ def decode_response[**P, T](template: Callable[P, T], response: ModelResponse) -
273284 result_str = last_resp .content or last_resp .reasoning_content
274285 assert result_str
275286
276- ret_type = template .__signature__ .return_annotation
277- encodable_ty = type_to_encodable_type (ret_type )
287+ encodable_ty = model_input_pair [1 ]
278288
279289 if encodable_ty .t == str :
280290 # if encoding as a type, value is just directly what the llm returned
@@ -291,28 +301,29 @@ def decode_response[**P, T](template: Callable[P, T], response: ModelResponse) -
291301@defop
292302def format_model_input [** P , T ](
293303 template : Template [P , T ], * args : P .args , ** kwargs : P .kwargs
294- ) -> list [ Any ]:
304+ ) -> ModelInput [ T ]:
295305 """Format a template applied to arguments into a sequence of input
296306 messages.
297307
298308 """
299- bound_args = template .__signature__ .bind (* args , ** kwargs )
309+ signature = template .__signature__
310+
311+ bound_args = signature .bind (* args , ** kwargs )
300312 bound_args .apply_defaults ()
301313 # encode arguments
302314 arguments = {}
303- for param in bound_args .arguments :
304- encoder = type_to_encodable_type (
305- template .__signature__ .parameters [param ].annotation
306- )
307- encoded = encoder .encode (bound_args .arguments [param ], template .__context__ )
315+ for param , value in bound_args .arguments .items ():
316+ ty : TypeExpression = nested_type (value ).value
317+ encoder = type_to_encodable_type (ty ) # type: ignore
318+ encoded = encoder .encode (value , template .__context__ )
308319 arguments [param ] = encoder .serialize (encoded )
309320
310321 prompt = _OpenAIPromptFormatter ().format_as_messages (
311322 template .__prompt_template__ , ** arguments
312323 )
313324
314325 # install prefix if the return type has a return annotation
315- ret_type = template .__signature__ . return_annotation
326+ ret_type = template .__type_rule__ ( * args , ** kwargs )
316327 origin = typing .get_origin (ret_type )
317328 ret_type = ret_type if origin is None else origin
318329 ret_type_encoder = type_to_encodable_type (ret_type )
@@ -327,7 +338,7 @@ def format_model_input[**P, T](
327338 # Note: The OpenAI api only seems to accept images in the 'user' role. The
328339 # effect of different roles on the model's response is currently unclear.
329340 messages = [{"type" : "message" , "content" : prompt , "role" : "user" }]
330- return messages
341+ return ( messages , ret_type_encoder )
331342
332343
333344class InstructionHandler (ObjectInterpretation ):
@@ -347,12 +358,16 @@ def __init__(self, instruction: str):
347358 self .instruction = instruction
348359
349360 @implements (format_model_input )
350- def _inject_instruction (self , template : Template , * args , ** kwargs ) -> list [Any ]:
361+ def _inject_instruction (
362+ self , template : Template , * args , ** kwargs
363+ ) -> tuple [list [Any ], Any ]:
351364 """Append instruction message to the formatted model input."""
352- messages = fwd ()
353- return messages + [
354- {"type" : "message" , "content" : self .instruction , "role" : "user" }
355- ]
365+ (messages , decode_ty ) = fwd ()
366+ return (
367+ messages
368+ + [{"type" : "message" , "content" : self .instruction , "role" : "user" }],
369+ decode_ty ,
370+ )
356371
357372
358373class RetryLLMHandler (ObjectInterpretation ):
@@ -421,4 +436,4 @@ def _call[**P, T](
421436 ) -> T :
422437 model_input = format_model_input (template , * args , ** kwargs )
423438 resp = compute_response (template , model_input )
424- return decode_response (template , resp )
439+ return decode_response (template , model_input , resp )
0 commit comments