Skip to content

Commit d65dc4e

Browse files
committed
add dummy tools
1 parent e1cc110 commit d65dc4e

2 files changed

Lines changed: 77 additions & 9 deletions

File tree

effectful/handlers/llm/completions.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,19 @@ def call_assistant[T, U](
260260
includes the raw assistant message for retry handling.
261261
"""
262262
tool_specs = {k: _function_model(t) for k, t in tools.items()}
263-
response_model = pydantic.create_model(
264-
"Response", value=response_format.enc, __config__={"extra": "forbid"}
263+
response_model = (
264+
response_format.enc
265+
if issubclass(response_format.enc, pydantic.BaseModel)
266+
else pydantic.create_model(
267+
"Response", value=response_format.enc, __config__={"extra": "forbid"}
268+
)
265269
)
266270

267271
messages = list(get_message_sequence().values())
268272
response: litellm.types.utils.ModelResponse = completion(
269273
model,
270274
messages=list(messages),
271-
response_format=response_model,
275+
response_format=response_model if response_format.enc is not str else None,
272276
tools=list(tool_specs.values()),
273277
**kwargs,
274278
)
@@ -291,17 +295,26 @@ def call_assistant[T, U](
291295
tool_calls.append(decoded_tool_call)
292296

293297
result = None
294-
if not tool_calls:
298+
if not tool_calls and response_format.enc is not str:
295299
# return response
296300
serialized_result = message.get("content") or message.get("reasoning_content")
297301
assert isinstance(serialized_result, str), (
298302
"final response from the model should be a string"
299303
)
300304
try:
301305
raw_result = response_model.model_validate_json(serialized_result)
302-
result = response_format.decode(raw_result.value) # type: ignore
306+
result = response_format.decode(
307+
raw_result.value
308+
if not issubclass(response_format.enc, pydantic.BaseModel)
309+
else raw_result
310+
) # type: ignore
303311
except (pydantic.ValidationError, TypeError, ValueError, SyntaxError) as e:
304312
raise ResultDecodingError(e, raw_message=raw_message) from e
313+
elif not tool_calls and response_format.enc is str:
314+
# if expecting a string result, return the raw content as the result
315+
content = message.get("content") or message.get("reasoning_content")
316+
assert isinstance(content, str), "Expected content to be a string"
317+
result = content
305318

306319
return (raw_message, tool_calls, result)
307320

@@ -387,7 +400,36 @@ def flush_text() -> None:
387400
@Operation.define
388401
def call_system(template: Template) -> collections.abc.Sequence[Message]:
389402
"""Get system instruction message(s) to prepend to all LLM prompts."""
390-
return ()
403+
404+
assert inspect.getdoc(type(template)) is not None
405+
406+
system_prompt = inspect.cleandoc(f"""
407+
You are responsible for implementing the `Template` '{template.__name__}' defined in the module source code below.
408+
409+
First, as background, here is the class-level documentation for the `Template` class::
410+
411+
{inspect.getdoc(type(template))}
412+
""")
413+
414+
try:
415+
system_prompt += inspect.cleandoc(f"""
416+
Here is the source code of the module defining the `Template` instance '{template.__name__}'::
417+
418+
{inspect.getsource(inspect.getmodule(template))}
419+
""")
420+
except (TypeError, OSError):
421+
system_prompt += inspect.cleandoc(f"""
422+
The source code for the module defining '{template.__name__}' is not available.
423+
Instead, here are the signature and docstring of '{template.__name__}'::
424+
425+
{template.__name__} :: {template.__signature__.format()}
426+
427+
{inspect.cleandoc(template.__prompt_template__)}
428+
""")
429+
430+
msg = _make_message(dict(role="system", content=system_prompt))
431+
append_message(msg)
432+
return (msg,)
391433

392434

393435
class RetryLLMHandler(ObjectInterpretation):

effectful/handlers/llm/template.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ class _BoundInstance[T]:
110110
instance: T
111111

112112

113+
def _make_context_tool[T](name: str, value: T) -> Tool[[], T]:
114+
"""Create a synthetic read-only Tool for a lexical variable."""
115+
from effectful.internals.unification import nested_type
116+
117+
def reader():
118+
return value
119+
120+
reader.__name__ = name
121+
reader.__doc__ = f"Read the value of lexical variable `{name}`"
122+
reader.__annotations__ = {"return": nested_type(value).value}
123+
124+
return Tool.define(reader)
125+
126+
113127
class Template[**P, T](Tool[P, T]):
114128
"""A :class:`Template` is a function that is implemented by a large language model.
115129
@@ -187,21 +201,33 @@ def tools(self) -> Mapping[str, Tool]:
187201
continue
188202

189203
# Collect tools in context
190-
if isinstance(obj, Tool):
204+
elif isinstance(obj, Tool):
191205
result[name] = obj
192206

193-
if isinstance(obj, staticmethod) and isinstance(obj.__func__, Tool):
207+
elif isinstance(obj, staticmethod) and isinstance(obj.__func__, Tool):
194208
result[name] = obj.__func__
195209

196210
# Collect tools as methods on any bound instances
197-
if isinstance(obj, _BoundInstance):
211+
elif isinstance(obj, _BoundInstance):
198212
for instance_name in obj.instance.__dir__():
199213
if instance_name.startswith(INSTANCE_OP_PREFIX):
200214
continue
201215
instance_obj = getattr(obj.instance, instance_name)
202216
if isinstance(instance_obj, Tool):
203217
result[instance_name] = instance_obj
204218

219+
# Make tools for lexical variables
220+
elif not (
221+
name.startswith("__")
222+
or isinstance(obj, Operation)
223+
or inspect.isclass(obj)
224+
or inspect.isbuiltin(obj)
225+
or inspect.ismodule(obj)
226+
or inspect.isroutine(obj)
227+
or inspect.isabstract(obj)
228+
):
229+
result[name] = _make_context_tool(name, obj)
230+
205231
return result
206232

207233
def __get__[S](self, instance: S | None, owner: type[S] | None = None):

0 commit comments

Comments
 (0)