Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions python/deepagents/src/temporalio/deepagents/_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ def __call__(self, argument: Any) -> Any:
return self._factory()


# Binding options that a provider's ``bind_tools`` converts to its own format
# (Anthropic turns ``"any"`` into ``{"type": "any"}``, OpenAI into
# ``"required"``). Passing them to ``.bind()`` instead skips that conversion.
_TOOL_BIND_KWARGS = ("tool_choice", "parallel_tool_calls")


def _default_model_provider(model_name: str) -> Any:
"""Build a chat model from a name string with LLM-SDK retries disabled.

Expand Down Expand Up @@ -295,10 +301,14 @@ def __init__(

def _build_bound_model(self, input: ModelActivityInput) -> Any:
model = self._model_provider(input.model_name)
bind_kwargs = dict(input.bind_kwargs)
if input.tool_schemas:
model = model.bind_tools(input.tool_schemas)
if input.bind_kwargs:
model = model.bind(**input.bind_kwargs)
tool_kwargs = {
k: bind_kwargs.pop(k) for k in _TOOL_BIND_KWARGS if k in bind_kwargs
}
model = model.bind_tools(input.tool_schemas, **tool_kwargs)
if bind_kwargs:
model = model.bind(**bind_kwargs)
return model

@activity.defn(name=INVOKE_MODEL)
Expand Down
68 changes: 68 additions & 0 deletions python/deepagents/tests/test_model_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from collections.abc import Sequence
from datetime import timedelta
from typing import Any
from unittest.mock import Mock

import pytest

Expand Down Expand Up @@ -74,6 +75,73 @@ def test_model_binding_preserves_kwargs_with_tools(bind_kwargs: dict[str, Any])
assert bound.kwargs == {"tools": tool_schemas, **bind_kwargs}


@pytest.mark.parametrize(
"tool_kwargs",
[
{"tool_choice": "any"},
{"parallel_tool_calls": False},
{"tool_choice": "any", "parallel_tool_calls": False},
],
)
def test_tool_options_go_to_bind_tools(tool_kwargs: dict[str, Any]) -> None:
# Providers convert these options only inside bind_tools, so the activity
# must pass them there; other options still go through .bind().
tool_schemas = [
{
"type": "function",
"function": {"name": "lookup", "parameters": {"type": "object"}},
}
]
other_kwargs = {"stop": ["Observation:"]}
model = Mock(wraps=_ToolBindingModel(responses=["answer"]))
activities = DeepAgentActivities(model_provider=lambda _: model)

bound = activities._build_bound_model(
ModelActivityInput(
model_name="fake:model",
messages=[],
tool_schemas=tool_schemas,
bind_kwargs={**tool_kwargs, **other_kwargs},
)
)

model.bind_tools.assert_called_once_with(tool_schemas, **tool_kwargs)
assert bound.kwargs == {"tools": tool_schemas, **tool_kwargs, **other_kwargs}


@pytest.mark.parametrize(
("tool_kwargs", "expected"),
[
({"tool_choice": "any"}, {"tool_choice": {"type": "any"}}),
(
{"parallel_tool_calls": False},
{"tool_choice": {"type": "auto", "disable_parallel_tool_use": True}},
),
],
)
def test_tool_options_use_provider_format(
tool_kwargs: dict[str, Any], expected: dict[str, Any]
) -> None:
# create_agent binds tool_choice="any" for structured output; the provider's
# bind_tools must convert it, as it does without Temporal.
langchain_anthropic = pytest.importorskip("langchain_anthropic")
chat_model = langchain_anthropic.ChatAnthropic(model="test-model", api_key="test")
tool = {
"type": "function",
"function": {"name": "lookup", "parameters": {"type": "object"}},
}
messages = [HumanMessage(content="hi")]
binding = TemporalModel(model="provider:model").bind_tools([tool], **tool_kwargs)
activity_input = binding.bound._build_input(messages, None, **binding.kwargs)
activities = DeepAgentActivities(model_provider=lambda _: chat_model)

model = activities._build_bound_model(activity_input)
payload = model.bound._get_request_payload(messages, **model.kwargs)

keys = ("tool_choice", "parallel_tool_calls")
assert {k: payload[k] for k in keys if k in payload} == expected


@workflow.defn
class ModelWorkflow:
@workflow.run
Expand Down
Loading