Skip to content

Support for method activities conversion into tools #968

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 16, 2025
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
35 changes: 31 additions & 4 deletions temporalio/contrib/openai_agents/workflow.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
"""Workflow-specific primitives for working with the OpenAI Agents SDK in a workflow context"""

import functools
import inspect
import json
from datetime import timedelta
from typing import Any, Callable, Optional, Type
from typing import Any, Callable, Optional, Type, Union, overload

import nexusrpc
from agents import (
Agent,
RunContextWrapper,
Tool,
)
from agents.function_schema import function_schema
from agents.function_schema import DocstringStyle, function_schema
from agents.tool import (
FunctionTool,
ToolErrorFunction,
ToolFunction,
ToolParams,
default_tool_error_function,
function_tool,
)
from agents.util._types import MaybeAwaitable

from temporalio import activity
from temporalio import workflow as temporal_workflow
Expand Down Expand Up @@ -78,6 +87,25 @@ def activity_as_tool(
"Bare function without tool and activity decorators is not supported",
"invalid_tool",
)
if ret.name is None:
raise ApplicationError(
"Input activity must have a name to be made into a tool",
"invalid_tool",
)
# If the provided callable has a first argument of `self`, partially apply it with the same metadata
# The actual instance will be picked up by the activity execution, the partially applied function will never actually be executed
params = list(inspect.signature(fn).parameters.keys())
if len(params) > 0 and params[0] == "self":
partial = functools.partial(fn, None)
setattr(partial, "__name__", fn.__name__)
partial.__annotations__ = getattr(fn, "__annotations__")
setattr(
partial,
"__temporal_activity_definition",
getattr(fn, "__temporal_activity_definition"),
)
partial.__doc__ = fn.__doc__
fn = partial
schema = function_schema(fn)

async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any:
Expand All @@ -94,9 +122,8 @@ async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any:
# Add the context to the arguments if it takes that
if schema.takes_context:
args = [ctx] + args

result = await temporal_workflow.execute_activity(
fn,
ret.name, # type: ignore
args=args,
task_queue=task_queue,
schedule_to_close_timeout=schedule_to_close_timeout,
Expand Down
50 changes: 46 additions & 4 deletions tests/contrib/openai_agents/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ async def get_weather_context(ctx: RunContextWrapper[str], city: str) -> Weather
return Weather(city=city, temperature_range="14-20C", conditions=ctx.context)


class ActivityWeatherService:
@activity.defn
async def get_weather_method(self, city: str) -> Weather:
"""
Get the weather for a given city.
"""
return Weather(
city=city, temperature_range="14-20C", conditions="Sunny with wind."
)


@nexusrpc.service
class WeatherService:
get_weather_nexus_operation: nexusrpc.Operation[WeatherInput, Weather]
Expand Down Expand Up @@ -269,6 +280,20 @@ class TestWeatherModel(StaticTestModel):
usage=Usage(),
response_id=None,
),
ModelResponse(
output=[
ResponseFunctionToolCall(
arguments='{"city":"Tokyo"}',
call_id="call",
name="get_weather_method",
type="function_call",
id="id",
status="completed",
)
],
usage=Usage(),
response_id=None,
),
ModelResponse(
output=[
ResponseOutputMessage(
Expand Down Expand Up @@ -333,7 +358,7 @@ class TestNexusWeatherModel(StaticTestModel):
class ToolsWorkflow:
@workflow.run
async def run(self, question: str) -> str:
agent = Agent(
agent: Agent = Agent(
name="Tools Workflow",
instructions="You are a helpful agent.",
tools=[
Expand All @@ -349,8 +374,12 @@ async def run(self, question: str) -> str:
openai_agents.workflow.activity_as_tool(
get_weather_context, start_to_close_timeout=timedelta(seconds=10)
),
openai_agents.workflow.activity_as_tool(
ActivityWeatherService.get_weather_method,
start_to_close_timeout=timedelta(seconds=10),
),
],
) # type: Agent
)
result = await Runner.run(
starting_agent=agent, input=question, context="Stormy"
)
Expand Down Expand Up @@ -406,6 +435,7 @@ async def test_tool_workflow(client: Client, use_local_model: bool):
get_weather_object,
get_weather_country,
get_weather_context,
ActivityWeatherService().get_weather_method,
],
interceptors=[OpenAIAgentsTracingInterceptor()],
) as worker:
Expand All @@ -426,7 +456,7 @@ async def test_tool_workflow(client: Client, use_local_model: bool):
if e.HasField("activity_task_completed_event_attributes"):
events.append(e)

assert len(events) == 9
assert len(events) == 11
assert (
"function_call"
in events[0]
Expand Down Expand Up @@ -476,11 +506,23 @@ async def test_tool_workflow(client: Client, use_local_model: bool):
.data.decode()
)
assert (
"Test weather result"
"function_call"
in events[8]
.activity_task_completed_event_attributes.result.payloads[0]
.data.decode()
)
assert (
"Sunny with wind"
in events[9]
.activity_task_completed_event_attributes.result.payloads[0]
.data.decode()
)
assert (
"Test weather result"
in events[10]
.activity_task_completed_event_attributes.result.payloads[0]
.data.decode()
)


@pytest.mark.parametrize("use_local_model", [True, False])
Expand Down