Skip to content

Commit 961ff8c

Browse files
authored
Fix tuple schema compatibility with OpenAI structured output API (#619)
* Make LLM tests fully parametric in model name via EFFECTFUL_LLM_MODEL env var Replace provider-specific environment variable checks (OPENAI_API_KEY, ANTHROPIC_API_KEY) and skip markers (requires_openai, requires_anthropic) with a single EFFECTFUL_LLM_MODEL environment variable that controls which model is used for all LLM integration tests. - Remove hardcoded model names from all test files in favor of LLM_MODEL read from EFFECTFUL_LLM_MODEL env var - Replace requires_openai/requires_anthropic markers with requires_llm - Remove model parametrization that was cross-provider; tests now use whichever model the env var specifies - Use litellm.supports_vision() to conditionally skip vision tests - Remove default model from LiteLLMProvider (make model required) - Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter, making it easy to add parallel CI stages for different providers - Rename/remove fixture files to match updated test names Closes #589 * Deduplicate LLM_MODEL/requires_llm into conftest, fix import order, rename test - Move LLM_MODEL and requires_llm definitions to tests/conftest.py; all four LLM test files now import from there - Fix import ordering in test_handlers_llm_encoding.py (stdlib before local) - Rename test_agent_tool_names_are_openai_compatible_integration to test_agent_tool_names_are_valid_integration since it's no longer OpenAI-specific * Address review feedback: fix breaking change, import hygiene, redundancy - Restore LiteLLMProvider default model via env var fallback so existing callers (template.py, test_handlers_llm_template.py, notebook) are not broken: model=os.environ.get("EFFECTFUL_LLM_MODEL", "gpt-4o") - Move LLM_MODEL/requires_llm from conftest.py to tests/_llm_helpers.py since conftest.py should not be imported directly - Fix import placement in test_handlers_llm_provider.py (was between module-level constants instead of grouped with imports) - Remove redundant @requires_llm on vision tests where the skipif condition already covers the not-LLM_MODEL case * Revert LiteLLMProvider default to plain literal "gpt-4o" The env var belongs in test infrastructure, not the library API. LiteLLMProvider should have a clean, explicit default. * Minor fix * minor fixes * minor * Make LLM test suite parametric in model name (#589) Remove provider-specific environment variable checks and skip markers from LLM tests in favor of a single EFFECTFUL_LLM_MODEL env var. - Add LLM_MODEL and requires_llm to tests/conftest.py; LLM_MODEL defaults to "gpt-4o-mini" and is overridable via EFFECTFUL_LLM_MODEL - Live tests (tool calling, encoding, agent tool names) use LLM_MODEL and are gated by requires_llm which checks for any provider API key - Integration tests use make_provider() which returns a live LiteLLMProvider when API keys are available, else falls back to ReplayLiteLLMProvider for offline replay from fixtures - Replay-only tests (simple_prompt_multiple_models, cross_endpoint, caching) keep hardcoded model names and always run since they never call the API - Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter for easy parallel stages with different providers Closes #589 * Minor fix * Minor fix * Fix tuple type schemas for OpenAI structured output compatibility Three fixes in encoding.py: 1. TupleEncodable.encode() now returns a TupleItems model instance (not a raw tuple), and deserialize() returns the model directly. This fixes pydantic validation in litellm integration tests for NamedTuple and fixed-tuple types. 2. Add _TupleSafeJsonSchema that overrides pydantic's tuple_schema() to produce object schemas (item_0, item_1 properties) instead of prefixItems arrays. Applied via _BoxEncoding.model_json_schema() so dataclasses containing tuple fields produce OpenAI-compatible schemas. 3. SequenceEncodable.encode() returns a list (not tuple) to preserve encode idempotency — nested_type on a list dispatches to the sequence encoder, avoiding a mismatch with TupleEncodable. Also adds test_handlers_llm_encoding.py back to CI workflow. * Clean up * Split out tuple encoding fixes to separate branch Revert encoding.py to master and remove encoding tests from CI workflow. Tuple schema fixes will be in a dedicated PR. * Fix tuple type schemas for OpenAI structured output compatibility Three fixes in encoding.py: 1. TupleEncodable.encode() returns a TupleItems model instance (not a raw tuple), and deserialize() returns the model directly. This fixes pydantic validation in litellm integration tests for NamedTuple and fixed-tuple types. Extract shared field access into _extract_items(). 2. Add _TupleSafeJsonSchema that overrides pydantic's tuple_schema() to produce object schemas (item_0, item_1 properties) instead of prefixItems arrays. Applied via _BoxEncoding.model_json_schema() so dataclasses containing tuple fields produce OpenAI-compatible schemas. Can be removed once #584 replaces the Encodable system. 3. SequenceEncodable.encode()/deserialize() return lists (not tuples) to preserve encode idempotency — nested_type on a list dispatches to the sequence encoder, avoiding a mismatch with TupleEncodable. * Fix tuple type schemas for OpenAI structured output compatibility Use Pydantic's Annotated extension points (PlainValidator, PlainSerializer, WithJsonSchema) to handle tuples — adapted from _pydantic_type_tuple in #584. - _safe_tuple_type: rewrites fixed-length tuple[T1, T2, ...] into an Annotated type with custom validation (item_N dict → tuple), serialization (tuple → item_N dict), and JSON schema (object with item_0/item_1 properties instead of prefixItems). Single mechanism for schema, validation, and serialization. - _rewrite_tuple_annotations: recursively applies _safe_tuple_type to dataclass fields, creating an Annotated proxy so nested tuples inside objects also get safe schemas. - TupleEncodable.encode() returns TupleItems model instances; deserialize() returns the model directly. Shared field access via _extract_items(). - SequenceEncodable.encode()/deserialize() return lists to preserve encode idempotency with the new TupleEncodable return type. * Replace Encodable ABC with TypeToPydanticType Replace the hand-rolled Encodable ABC and its 10+ subclasses with a recursive type rewriter (TypeToPydanticType) that produces Annotated wrappers with Pydantic validators/serializers. This fixes #626 (tuple fields in dataclasses) and #631 (Callable fields in dataclasses) by recursively rewriting field annotations before Pydantic sees them. - Add TypeEvaluator ABC to unification.py for recursive type traversal - Rewrite encoding.py: Encodable[T] now returns Annotated types via TypeToPydanticType, used with pydantic.TypeAdapter - Update completions.py: Encodable.define() -> TypeAdapter(Encodable[T]) - Update template.py: extract _collect_tools to completions.py - Update all test files for new API * Fix composite type encoding and update provider tests - Add field rewriting for dataclass and BaseModel types whose fields contain special types (Callable, tuple, Image, etc.). Uses a proxy Pydantic model for validation/serialization while preserving the original type identity on roundtrip. Fixes #631. - Add additionalProperties:false to all object schemas in _inline_refs for OpenAI strict mode compatibility. - Update test_handlers_llm_provider.py for new encoding API: response_format=Encodable.define(T) -> response_type=T, tools= -> env= - Add composite type regression tests for #626 and #631. - Fix ruff formatting in test_internals_unification.py. * Add encoding integration tests to LLM CI workflow Include test_handlers_llm_encoding.py in the LLM integration test workflow so the litellm_completion tests (which need API keys) run in CI. * Remove unused type: ignore comment in encoding.py * Removing unnecessary files * Minor fix * Fix CI failures: mypy, test mocks, and rebuild fixtures - Remove unused type: ignore in unification.py - Fix _call_assistant mock signature (tools/response_format -> env/response_type) - Fix mock tool call args to remove old {"value": ...} wrapping - Fix test_synthesized_function_roundtrip to use dump_python(mode="json") - Rebuild all provider test fixtures with gpt-4o for new API format - Fix caching test fixtures for deterministic replay * Fix template test expectations for unwrapped encoding format - Update template formatting tests: int values no longer wrapped in {"value": N} - Fix tool call mock args in template test (remove {"value": ...} wrapping) * Add _strict_json_schema for OpenAI strict mode compliance - Add _strict_json_schema() to completions.py (provider boundary) - Apply to tool specs in call_assistant - Wrap response_format schema in object wrapper for encoding integration tests - Apply _strict_json_schema to tool spec tests * Fix lint: restore type: ignore, sort imports, remove unused import * Fix strict schema: inline $refs, handle arrays, dual mypy ignore - Enhance _strict_json_schema to inline $ref/$defs recursively - Add items fallback for arrays (required by OpenAI strict mode) - Handle prefixItems → items conversion for fixed-length tuples - Use dual type: ignore[attr-defined,unused-ignore] for CI/local mypy compat - Fix xfail pattern matching to use startswith for precise case filtering * Fix ruff formatting in test files * Xfail image/tool/dtc types in LLM provider integration tests Image types can't roundtrip through LLM (returns URLs, not data URIs). Tool/DecodedToolCall types have schemas incompatible with OpenAI strict mode when used as nested parameter types. Apply PROVIDER_CASES (with xfails) to tool-as-param and tool-as-return tests, and extend the pattern to catch composite image types (e.g. tuple-img-str, list-img). * Use ROUNDTRIP_CASES for tool-as-return test (no xfails needed) Return types don't affect the tool spec schema sent to OpenAI, so all cases pass without xfails. Only tool-as-param needs PROVIDER_CASES. * Separate xfail sets for response_model vs tool-as-param tests - response_model: xfail img/tool/dtc (LLM can't return images, Tool schema incompatible with strict mode) - tool-as-param: xfail only tool/dtc (image types produce valid param schemas) - tool-as-return: no xfails needed (return type not in tool spec) * Fix mypy segfault under xdist: group mypy tests on same worker The mypy type-check tests call mypy in-process. When xdist schedules them on separate workers simultaneously, mypy's C extensions segfault. Use xdist_group("mypy") to ensure all mypy tests run on the same worker, and set --dist loadgroup as default to respect the grouping. * Removing a bunch of duplicated features, change tests so that Dataclass encodable is manually defined * Remove strict json schema in favor of openai internal tools * Inline named tuple to pydantic_type_tuple * Replace _force_additional_properties_false with _remove_additional_properties_true Symmetric to litellm.utils._remove_additional_properties (which removes false for Vertex AI). Strips additionalProperties: true from litellm/OpenAI models that use extra="allow", then lets _ensure_strict_json_schema apply false. Also switch DecodedToolCall schema to OpenAI's ChatCompletionMessageToolCall (has actual fields: id, function, type) instead of litellm's (empty dict). Remove tool/dtc xfails — schemas are now strict-mode compatible. * Use OpenAI's ChatCompletionMessageToolCall for tool call validation Litellm's ChatCompletionMessageToolCall has no fields (extra="allow"), so model_validate is a no-op. Switch to OpenAI's type which validates id, function.name, function.arguments fields. Also fix serializer to use type="function" (OpenAI const) and json.dumps for arguments. * Replace _ensure_strict_json_schema with _make_strict_safe for Tool/DecodedToolCall _ensure_strict_json_schema forces all properties into required, breaking schemas with optional fields (e.g. ChatCompletionToolParam's parameters, description, strict, cache_control). _make_strict_safe makes optional properties nullable before requiring them, so they're safe for OpenAI strict mode. Also removes _ensure_strict_json_schema from Image and Callable handlers where it was unnecessary, and provides encoded tool context in the response_model integration test so the LLM can produce valid DecodedToolCall instances. * Suppress mypy error for runtime Encodable[type(v)] in test * Xfail Tool/DecodedToolCall integration tests, revert _make_strict_safe Tool/DecodedToolCall as response_model or tool parameter are fundamentally incompatible with OpenAI strict mode (optional fields, bare "type": "object" without properties). This matches master's behavior which also xfailed these cases. Removes _make_strict_safe, restores _ensure_strict_json_schema from openai for Tool/DecodedToolCall WithJsonSchema schemas. * Remove _remove_additional_properties_true helper Only used for Tool/DecodedToolCall WithJsonSchema schemas which are already xfailed in integration tests. No test depends on it. * formatting
1 parent c67f1f5 commit 961ff8c

46 files changed

Lines changed: 1606 additions & 1469 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test_llm.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ jobs:
3737
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
3838
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
3939
run: |
40-
uv run pytest tests/test_handlers_llm_provider.py tests/test_handlers_llm_tool_calling_poem.py tests/test_handlers_llm_tool_calling_book.py -v --tb=short
40+
uv run pytest tests/test_handlers_llm_provider.py tests/test_handlers_llm_tool_calling_poem.py tests/test_handlers_llm_tool_calling_book.py tests/test_handlers_llm_encoding.py -v --tb=short

effectful/handlers/llm/completions.py

Lines changed: 116 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import dataclasses
55
import functools
66
import inspect
7+
import json
78
import string
89
import textwrap
910
import traceback
@@ -24,8 +25,17 @@
2425
OpenAIMessageContentListBlock,
2526
)
2627

27-
from effectful.handlers.llm.encoding import DecodedToolCall, Encodable
28-
from effectful.handlers.llm.template import Template, Tool
28+
from effectful.handlers.llm.encoding import (
29+
DecodedToolCall,
30+
Encodable,
31+
to_content_blocks,
32+
)
33+
from effectful.handlers.llm.template import (
34+
Agent,
35+
Template,
36+
Tool,
37+
_is_recursive_signature,
38+
)
2939
from effectful.internals.unification import nested_type
3040
from effectful.ops.semantics import fwd, handler
3141
from effectful.ops.syntax import ObjectInterpretation, implements
@@ -59,15 +69,23 @@ class UserMessage(OpenAIChatCompletionUserMessage):
5969
)
6070

6171

72+
class _NoActiveHistoryException(Exception):
73+
"""Raised when there is no active message history to append to."""
74+
75+
6276
@Operation.define
6377
def _get_history() -> collections.OrderedDict[str, Message]:
64-
raise NotImplementedError
78+
raise _NoActiveHistoryException(
79+
"No active message history. This operation should only be used within a handler that provides a message history."
80+
)
6581

6682

67-
def append_message(message: Message):
83+
def append_message(message: Message, last: bool = True) -> None:
6884
try:
6985
_get_history()[message["id"]] = message
70-
except NotImplementedError:
86+
if not last:
87+
_get_history().move_to_end(message["id"], last=False)
88+
except _NoActiveHistoryException:
7189
pass
7290

7391

@@ -160,6 +178,37 @@ def to_feedback_message(self, include_traceback: bool) -> Message:
160178
type MessageResult[T] = tuple[Message, typing.Sequence[DecodedToolCall], T | None]
161179

162180

181+
def _collect_tools(
182+
env: collections.abc.Mapping[str, typing.Any],
183+
) -> collections.abc.Mapping[str, Tool]:
184+
"""Operations and Templates available as tools. Auto-capture from lexical context."""
185+
result = {}
186+
187+
for name, obj in env.items():
188+
# Collect tools directly in context
189+
if isinstance(obj, Tool | Template):
190+
result[name] = obj
191+
192+
# Collect tools as methods on Agent instances in context
193+
elif isinstance(obj, Agent):
194+
for cls in type(obj).__mro__:
195+
for attr_name in vars(cls):
196+
if isinstance(getattr(obj, attr_name), Tool):
197+
result[f"{name}__{attr_name}"] = getattr(obj, attr_name)
198+
199+
# The same Tool can appear under multiple names when it is both
200+
# visible in the enclosing scope *and* discovered via an Agent
201+
# instance's MRO. Since Tools are hashable Operations and
202+
# instance-method Tools are cached per instance, we keep only
203+
# the last name for each unique tool object.
204+
tool2name = {tool: name for name, tool in sorted(result.items())}
205+
for name, tool in tuple(result.items()):
206+
if tool2name[tool] != name:
207+
del result[name]
208+
209+
return result
210+
211+
163212
@Operation.define
164213
@functools.wraps(litellm.completion)
165214
def completion(*args, **kwargs) -> typing.Any:
@@ -172,10 +221,14 @@ def completion(*args, **kwargs) -> typing.Any:
172221
return litellm.completion(*args, **kwargs)
173222

174223

224+
class _BoxedResponse[T](pydantic.BaseModel):
225+
value: T
226+
227+
175228
@Operation.define
176-
def call_assistant[T, U](
177-
tools: collections.abc.Mapping[str, Tool],
178-
response_format: Encodable[T, U],
229+
def call_assistant[T](
230+
env: collections.abc.Mapping[str, typing.Any],
231+
response_type: type[T],
179232
model: str,
180233
**kwargs,
181234
) -> MessageResult[T]:
@@ -190,15 +243,28 @@ def call_assistant[T, U](
190243
ResultDecodingError: If the result cannot be decoded. The error
191244
includes the raw assistant message for retry handling.
192245
"""
246+
tools = _collect_tools(env)
193247
tool_specs = {
194-
k: Encodable.define(type(t), tools).encode(t) # type: ignore
248+
k: typing.cast(
249+
pydantic.TypeAdapter[typing.Any],
250+
pydantic.TypeAdapter(Encodable[type(t)]), # type: ignore[misc]
251+
).dump_python(t, mode="json", context={k: t})
195252
for k, t in tools.items()
196253
}
197-
messages = list(_get_history().values())
254+
255+
# The OpenAI API requires a wrapper object for non-object structured output types,
256+
# so we create one on the fly here. Using a Pydantic model offloads JSON schema
257+
# generation and validation logic to litellm, and offers better error messages.
258+
response_format: type[_BoxedResponse[T]] = pydantic.create_model(
259+
"BoxedResponse",
260+
value=Encodable[response_type], # type: ignore[valid-type]
261+
__base__=_BoxedResponse,
262+
)
263+
198264
response: litellm.types.utils.ModelResponse = completion(
199265
model,
200-
messages=list(messages),
201-
response_format=None if response_format.enc is str else response_format.enc,
266+
messages=list(_get_history().values()),
267+
response_format=None if response_type is str else response_format,
202268
tools=list(tool_specs.values()),
203269
**kwargs,
204270
)
@@ -212,11 +278,12 @@ def call_assistant[T, U](
212278
append_message(raw_message)
213279

214280
tool_calls: list[DecodedToolCall] = []
215-
raw_tool_calls = message.get("tool_calls") or []
216-
encoding = Encodable.define(DecodedToolCall, tools) # type: ignore
217-
for raw_tool_call in raw_tool_calls:
281+
encoding: pydantic.TypeAdapter[DecodedToolCall] = pydantic.TypeAdapter(
282+
Encodable[DecodedToolCall]
283+
)
284+
for raw_tool_call in message.get("tool_calls") or []:
218285
try:
219-
tool_calls += [encoding.decode(raw_tool_call)] # type: ignore
286+
tool_calls += [encoding.validate_python(raw_tool_call, context=tools)]
220287
except Exception as e:
221288
raise ToolCallDecodingError(
222289
raw_tool_call=raw_tool_call,
@@ -231,12 +298,15 @@ def call_assistant[T, U](
231298
assert isinstance(serialized_result, str), (
232299
"final response from the model should be a string"
233300
)
234-
try:
235-
result = response_format.decode(
236-
response_format.deserialize(serialized_result)
237-
)
238-
except (pydantic.ValidationError, TypeError, ValueError, SyntaxError) as e:
239-
raise ResultDecodingError(e, raw_message=raw_message) from e
301+
if response_type is str:
302+
result = typing.cast(T, serialized_result)
303+
else:
304+
try:
305+
result = response_format.model_validate(
306+
json.loads(serialized_result), context=env
307+
).value
308+
except Exception as e:
309+
raise ResultDecodingError(e, raw_message=raw_message) from e
240310

241311
return (raw_message, tool_calls, result)
242312

@@ -256,10 +326,12 @@ def call_tool(tool_call: DecodedToolCall) -> Message:
256326
except Exception as e:
257327
raise ToolCallExecutionError(raw_tool_call=tool_call, original_error=e) from e
258328

259-
return_type = Encodable.define(
260-
typing.cast(type[typing.Any], nested_type(result).value)
329+
return_type: pydantic.TypeAdapter[typing.Any] = pydantic.TypeAdapter(
330+
Encodable[nested_type(result).value] # type: ignore[misc]
331+
)
332+
encoded_result = to_content_blocks(
333+
return_type.dump_python(result, mode="json", context={})
261334
)
262-
encoded_result = return_type.serialize(return_type.encode(result))
263335
message = _make_message(
264336
dict(role="tool", content=encoded_result, tool_call_id=tool_call.id),
265337
)
@@ -295,13 +367,11 @@ def flush_text() -> None:
295367
continue
296368

297369
obj, _ = formatter.get_field(field_name, (), env)
298-
encoder = Encodable.define(
299-
typing.cast(type[typing.Any], nested_type(obj).value), env
300-
)
301-
encoded_obj: typing.Sequence[OpenAIMessageContentListBlock] = encoder.serialize(
302-
encoder.encode(obj)
370+
encoder: pydantic.TypeAdapter[typing.Any] = pydantic.TypeAdapter(
371+
Encodable[nested_type(obj).value] # type: ignore[misc]
303372
)
304-
for part in encoded_obj:
373+
encoded_obj = encoder.dump_python(obj, mode="json", context=env)
374+
for part in to_content_blocks(encoded_obj):
305375
if part["type"] == "text":
306376
text = (
307377
formatter.convert_field(part["text"], conversion)
@@ -327,21 +397,8 @@ def call_system(template: Template) -> Message:
327397
"""Get system instruction message(s) to prepend to all LLM prompts."""
328398
system_prompt = template.__system_prompt__ or DEFAULT_SYSTEM_PROMPT
329399
message = _make_message(dict(role="system", content=system_prompt))
330-
try:
331-
history: collections.OrderedDict[str, Message] = _get_history()
332-
if any(m["role"] == "system" for m in history.values()):
333-
assert sum(1 for m in history.values() if m["role"] == "system") == 1, (
334-
"There should be at most one system message in the history"
335-
)
336-
assert history[next(iter(history))]["role"] == "system", (
337-
"The system message should be the first message in the history"
338-
)
339-
history.popitem(last=False) # remove existing system message
340-
history[message["id"]] = message
341-
history.move_to_end(message["id"], last=False)
342-
return message
343-
except NotImplementedError:
344-
return message
400+
append_message(message, last=False)
401+
return message
345402

346403

347404
class RetryLLMHandler(ObjectInterpretation):
@@ -404,20 +461,17 @@ def _before_sleep(self, retry_state: tenacity.RetryCallState) -> None:
404461
self._user_before_sleep(retry_state)
405462

406463
@implements(call_assistant)
407-
def _call_assistant[T, U](
464+
def _call_assistant[T](
408465
self,
409-
tools: collections.abc.Mapping[str, Tool],
410-
response_format: Encodable[T, U],
466+
env: collections.abc.Mapping[str, typing.Any],
467+
response_type: type[T],
411468
model: str,
412469
**kwargs,
413470
) -> MessageResult[T]:
414471
_message_sequence = _get_history().copy()
415472

416-
def _attempt() -> MessageResult[T]:
417-
return fwd(tools, response_format, model, **kwargs)
418-
419473
with handler({_get_history: lambda: _message_sequence}):
420-
message, tool_calls, result = self.call_assistant_retryer(_attempt)
474+
message, tool_calls, result = self.call_assistant_retryer(fwd)
421475

422476
append_message(message)
423477
return (message, tool_calls, result)
@@ -461,16 +515,20 @@ def _call[**P, T](
461515
bound_args.apply_defaults()
462516
env = template.__context__.new_child(bound_args.arguments)
463517

464-
# Create response_model with env so tools passed as arguments are available
465-
response_model = Encodable.define(template.__signature__.return_annotation, env)
518+
if not _is_recursive_signature(template.__signature__):
519+
env = env.new_child({k: None for k, v in env.items() if v is template})
466520

467521
history: collections.OrderedDict[str, Message] = getattr(
468522
template, "__history__", collections.OrderedDict()
469523
) # type: ignore
470524
history_copy = history.copy()
471525

472526
with handler({_get_history: lambda: history_copy}):
473-
call_system(template)
527+
if (
528+
not _get_history()
529+
or next(iter(_get_history().values()))["role"] != "system"
530+
):
531+
call_system(template)
474532

475533
message: Message = call_user(template.__prompt_template__, env)
476534

@@ -479,14 +537,14 @@ def _call[**P, T](
479537
result: T | None = None
480538
while message["role"] != "assistant" or tool_calls:
481539
message, tool_calls, result = call_assistant(
482-
template.tools, response_model, **self.config
540+
env, template.__signature__.return_annotation, **self.config
483541
)
484542
for tool_call in tool_calls:
485543
message = call_tool(tool_call)
486544

487545
try:
488546
_get_history()
489-
except NotImplementedError:
547+
except _NoActiveHistoryException:
490548
history.clear()
491549
history.update(history_copy)
492550
return typing.cast(T, result)

0 commit comments

Comments
 (0)