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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ to include examples, links to docs, or any other relevant information.
without separately installing `mcp`. Previously the import failed with an
`ImportError` because `google.adk.tools.mcp_tool` only exports `McpToolset`
when `mcp` is installed.
- `temporalio.contrib.openai_agents` no longer crashes when a plain `dict`
is passed for `run_config`. (openai-agents >= 0.19.0 accepts `dict` run
configs at its public runner API)

### Security

Expand Down
27 changes: 25 additions & 2 deletions temporalio/contrib/openai_agents/_openai_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ def _has_sandbox_agent(agent: Agent[Any], seen: set[int] | None = None) -> bool:
return False


def _coerce_run_config(value: object) -> RunConfig:
"""openai-agents >= 0.19 also accepts a plain dict for ``run_config``.

This function normalizes to a RunConfig instance.
"""
if isinstance(value, RunConfig):
return value
if not isinstance(value, dict):
raise TypeError(
f"run_config must be a RunConfig instance or a dict, got {type(value).__name__}"
)
field_names = {
config_field.name
for config_field in dataclasses.fields(RunConfig)
if config_field.init
}
unknown_fields = sorted(str(name) for name in value if name not in field_names)
if unknown_fields:
raise TypeError(f"Unknown run_config settings: {', '.join(unknown_fields)}")
return RunConfig(**value)


class TemporalOpenAIRunner(AgentRunner):
"""Temporal Runner for OpenAI agents.

Expand Down Expand Up @@ -148,8 +170,9 @@ def _prepare_workflow_run(
raise ValueError("Temporal workflows don't support SQLite sessions.")

run_config = kwargs.get("run_config")
if run_config is None:
run_config = RunConfig()
Comment thread
DABH marked this conversation as resolved.
run_config = (
RunConfig() if run_config is None else _coerce_run_config(run_config)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could move the None check into the function and call it _normalize_run_config() so this is more compact.

)

if run_config.model and not isinstance(run_config.model, _TemporalModelStub):
if not isinstance(run_config.model, str):
Expand Down
90 changes: 86 additions & 4 deletions tests/contrib/openai_agents/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@
)
from temporalio.contrib.openai_agents._invoke_model_activity import _build_tool
from temporalio.contrib.openai_agents._model_parameters import ModelSummaryProvider
from temporalio.contrib.openai_agents._openai_runner import _convert_agent
from temporalio.contrib.openai_agents._openai_runner import (
_coerce_run_config,
_convert_agent,
)
from temporalio.contrib.openai_agents._temporal_model_stub import (
_TemporalModelStub,
)
Expand Down Expand Up @@ -2074,13 +2077,14 @@ def get_model(self, model_name: str | None) -> Model:
return self._model


MULTIPLE_MODELS_FINAL_RESPONSE = "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?"


def multiple_models_mock_model():
return TestModel.returning_responses(
[
ResponseBuilders.tool_call("{}", "transfer_to_underling"),
ResponseBuilders.output_message(
"I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?"
),
ResponseBuilders.output_message(MULTIPLE_MODELS_FINAL_RESPONSE),
]
)

Expand Down Expand Up @@ -2160,6 +2164,84 @@ async def test_run_config_models(client: Client):
assert provider.model_names == {"gpt-4o"}


@workflow.defn
class DictRunConfigWorkflow:
"""Same agents as MultipleModelWorkflow, but passes run_config as a plain
dict, which openai-agents >= 0.19 accepts at its public runner boundaries."""

@workflow.run
async def run(self) -> str:
underling = Agent[None](
name="Underling",
instructions="You do all the work you are told.",
)

starting_agent = Agent[None](
name="Lazy Assistant",
model="gpt-4o-mini",
instructions="You delegate all your work to another agent.",
handoffs=[underling],
)
# Typed as Any so this also type-checks against openai-agents
# versions whose run_config annotation does not include dict.
dict_run_config: Any = {"model": "gpt-4o"}
result = await Runner.run(
starting_agent=starting_agent,
input="Have you cleaned the store room yet?",
run_config=dict_run_config,
)
return result.final_output


async def test_dict_run_config_models(client: Client):
# A dict run_config must behave identically to the equivalent
# RunConfig(model="gpt-4o") in test_run_config_models above.
provider = AssertDifferentModelProvider(multiple_models_mock_model())
async with AgentEnvironment(
model_params=ModelActivityParameters(
start_to_close_timeout=timedelta(seconds=120)
),
model_provider=provider,
) as env:
client = env.applied_on_client(client)

async with new_worker(
client,
DictRunConfigWorkflow,
) as worker:
workflow_handle = await client.start_workflow(
DictRunConfigWorkflow.run,
id=f"dict-run-config-model-{uuid.uuid4()}",
task_queue=worker.task_queue,
execution_timeout=timedelta(seconds=10),
)
result = await workflow_handle.result()

# Only the model from the runconfig override is used
assert provider.model_names == {"gpt-4o"}
assert result == MULTIPLE_MODELS_FINAL_RESPONSE


def test_coerce_run_config_validation():
# Mirrors upstream agents' normalization: equivalent RunConfig out of a
# dict, and the same TypeErrors for invalid input.
coerced = _coerce_run_config({"model": "gpt-4o", "workflow_name": "wf"})
assert isinstance(coerced, RunConfig)
assert coerced.model == "gpt-4o"
assert coerced.workflow_name == "wf"

run_config = RunConfig(model="gpt-4o")
assert _coerce_run_config(run_config) is run_config

with pytest.raises(TypeError, match="Unknown run_config settings: bogus_setting"):
_coerce_run_config({"model": "gpt-4o", "bogus_setting": True})

with pytest.raises(
TypeError, match="run_config must be a RunConfig instance or a dict, got int"
):
_coerce_run_config(42)


async def test_summary_provider(client: Client):
class SummaryProvider(ModelSummaryProvider):
def provide(
Expand Down
Loading