Skip to content

Commit 02facf3

Browse files
authored
Switch type_to_encodable_type interface to lexically-sensitive Encodable design (#521)
* refactoring encodable to be lexically sensitive * updated tests to use new interface * cleaned up interface and added handler for str * updated completions with new interface * refactored encodables to top level and switched to dataclasses * removed tools field of encodable for later PR * removed dataclass from Encodable and made subclasses include those fields explicitly
1 parent e33873d commit 02facf3

3 files changed

Lines changed: 431 additions & 344 deletions

File tree

effectful/handlers/llm/completions.py

Lines changed: 33 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import litellm
1111
import pydantic
1212
from litellm import (
13-
ChatCompletionTextObject,
1413
Choices,
1514
Message,
1615
OpenAIChatCompletionToolParam,
@@ -20,7 +19,7 @@
2019
from litellm.types.utils import ModelResponse
2120

2221
from effectful.handlers.llm import Template, Tool
23-
from effectful.handlers.llm.encoding import type_to_encodable_type
22+
from effectful.handlers.llm.encoding import Encodable
2423
from effectful.ops.semantics import fwd, handler
2524
from effectful.ops.syntax import ObjectInterpretation, defop, implements
2625

@@ -75,21 +74,25 @@ def completion(*args, **kwargs) -> Any:
7574
return litellm.completion(*args, **kwargs)
7675

7776

78-
def parameter_model(tool: Tool) -> type[pydantic.BaseModel]:
79-
fields = {
80-
name: type_to_encodable_type(param.annotation).t
77+
def parameter_model(
78+
tool: Tool, ctx: Mapping[str, Any] | None = None
79+
) -> type[pydantic.BaseModel]:
80+
fields: dict[str, Any] = {
81+
name: Encodable.define(param.annotation, ctx).enc
8182
for name, param in tool.__signature__.parameters.items()
8283
}
8384
parameter_model = pydantic.create_model(
8485
"Params",
8586
__config__={"extra": "forbid"},
86-
**fields, # type: ignore
87+
**fields,
8788
)
8889
return parameter_model
8990

9091

91-
def function_definition(tool: Tool) -> OpenAIChatCompletionToolParam:
92-
param_model = parameter_model(tool)
92+
def function_definition(
93+
tool: Tool, ctx: Mapping[str, Any] | None = None
94+
) -> OpenAIChatCompletionToolParam:
95+
param_model = parameter_model(tool, ctx)
9396
response_format = litellm.utils.type_to_response_format_param(param_model)
9497
description = tool.__default__.__doc__
9598
assert response_format is not None
@@ -114,25 +117,25 @@ def call_with_json_args(
114117
115118
"""
116119
sig = tool.__signature__
117-
param_model = parameter_model(tool)
120+
param_model = parameter_model(tool, context)
118121
try:
119122
# build dict of raw encodable types U
120123
raw_args = param_model.model_validate_json(json_str)
121124

122125
# use encoders to decode Us to python types T
123126
params: dict[str, Any] = {
124-
param_name: type_to_encodable_type(
125-
sig.parameters[param_name].annotation
126-
).decode(getattr(raw_args, param_name), context)
127+
param_name: Encodable.define(
128+
sig.parameters[param_name].annotation, context
129+
).decode(getattr(raw_args, param_name))
127130
for param_name in raw_args.model_fields_set
128131
}
129132

130133
# call tool with python types
131134
result = tool(**params)
132135

133136
# serialize back to U using encoder for return type
134-
encoded_ty = type_to_encodable_type(sig.return_annotation)
135-
encoded_value = encoded_ty.encode(result, context)
137+
encoded_ty = Encodable.define(sig.return_annotation, context)
138+
encoded_value = encoded_ty.encode(result)
136139

137140
# serialise back to Json
138141
return encoded_ty.serialize(encoded_value)
@@ -149,8 +152,12 @@ def compute_response(template: Template, model_input: list[Any]) -> ModelRespons
149152
ret_type = template.__signature__.return_annotation
150153
tools = template.tools
151154

152-
tool_schemas = [function_definition(t) for t in tools.values()]
153-
response_encoding_type: type | None = type_to_encodable_type(ret_type).t
155+
tool_schemas = [
156+
function_definition(t, template.__context__) for t in tools.values()
157+
]
158+
response_encoding_type: type | None = Encodable.define(
159+
ret_type, template.__context__
160+
).enc
154161
if response_encoding_type == str:
155162
response_encoding_type = None
156163

@@ -202,18 +209,18 @@ def decode_response[**P, T](template: Callable[P, T], response: ModelResponse) -
202209
assert result_str
203210

204211
ret_type = template.__signature__.return_annotation
205-
encodable_ty = type_to_encodable_type(ret_type)
212+
encodable_ty = Encodable.define(ret_type, template.__context__)
206213

207-
if encodable_ty.t == str:
214+
if encodable_ty.enc == str:
208215
# if encoding as a type, value is just directly what the llm returned
209-
value = result_str
216+
value: Any = result_str
217+
return typing.cast(T, encodable_ty.decode(value))
210218
else:
211-
Result = pydantic.create_model("Result", value=encodable_ty.t)
219+
Result = pydantic.create_model("Result", value=encodable_ty.enc)
212220
result = Result.model_validate_json(result_str)
213221
assert isinstance(result, Result)
214-
value = result.value # type: ignore
215-
216-
return encodable_ty.decode(value, template.__context__) # type: ignore
222+
value = getattr(result, "value")
223+
return typing.cast(T, encodable_ty.decode(value))
217224

218225

219226
@defop
@@ -229,29 +236,16 @@ def format_model_input[**P, T](
229236
# encode arguments
230237
arguments = {}
231238
for param in bound_args.arguments:
232-
encoder = type_to_encodable_type(
233-
template.__signature__.parameters[param].annotation
239+
encoder = Encodable.define(
240+
template.__signature__.parameters[param].annotation, template.__context__
234241
)
235-
encoded = encoder.encode(bound_args.arguments[param], template.__context__)
242+
encoded = encoder.encode(bound_args.arguments[param])
236243
arguments[param] = encoder.serialize(encoded)
237244

238245
prompt = _OpenAIPromptFormatter().format_as_messages(
239246
template.__prompt_template__, **arguments
240247
)
241248

242-
# install prefix if the return type has a return annotation
243-
ret_type = template.__signature__.return_annotation
244-
origin = typing.get_origin(ret_type)
245-
ret_type = ret_type if origin is None else origin
246-
ret_type_encoder = type_to_encodable_type(ret_type)
247-
prompt_prefix = ret_type_encoder.encoding_instructions()
248-
249-
if prompt_prefix:
250-
prefix: list[ChatCompletionTextObject] = [
251-
{"type": "text", "text": prompt_prefix}
252-
]
253-
prompt = prefix + prompt
254-
255249
# Note: The OpenAI api only seems to accept images in the 'user' role. The
256250
# effect of different roles on the model's response is currently unclear.
257251
messages = [{"type": "message", "content": prompt, "role": "user"}]

0 commit comments

Comments
 (0)