|
1 | 1 | """Support for using Temporal activities as OpenAI agents tools."""
|
2 | 2 |
|
3 | 3 | import json
|
| 4 | +import typing |
4 | 5 | from datetime import timedelta
|
5 |
| -from typing import Any, Callable, Optional |
| 6 | +from typing import Any, Callable, Optional, Type |
6 | 7 |
|
7 | 8 | from temporalio import activity, workflow
|
8 | 9 | from temporalio.common import Priority, RetryPolicy
|
9 | 10 | from temporalio.exceptions import ApplicationError, TemporalError
|
| 11 | +from temporalio.nexus._util import get_operation_factory |
10 | 12 | from temporalio.workflow import ActivityCancellationType, VersioningIntent, unsafe
|
11 | 13 |
|
12 | 14 | with unsafe.imports_passed_through():
|
@@ -115,3 +117,102 @@ async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any:
|
115 | 117 | on_invoke_tool=run_activity,
|
116 | 118 | strict_json_schema=True,
|
117 | 119 | )
|
| 120 | + |
| 121 | + |
| 122 | +def nexus_operation_as_tool( |
| 123 | + fn: Callable, |
| 124 | + *, |
| 125 | + service: Type[Any], |
| 126 | + endpoint: str, |
| 127 | + schedule_to_close_timeout: Optional[timedelta] = None, |
| 128 | +) -> Tool: |
| 129 | + """Convert a Nexus operation into an OpenAI agent tool. |
| 130 | +
|
| 131 | + .. warning:: |
| 132 | + This API is experimental and may change in future versions. |
| 133 | + Use with caution in production environments. |
| 134 | +
|
| 135 | + This function takes a Nexus operation and converts it into an |
| 136 | + OpenAI agent tool that can be used by the agent to execute the operation |
| 137 | + during workflow execution. The tool will automatically handle the conversion |
| 138 | + of inputs and outputs between the agent and the operation. |
| 139 | +
|
| 140 | + Args: |
| 141 | + fn: A Nexus operation to convert into a tool. |
| 142 | + service: The Nexus service class that contains the operation. |
| 143 | + endpoint: The Nexus endpoint to use for the operation. |
| 144 | +
|
| 145 | + Returns: |
| 146 | + An OpenAI agent tool that wraps the provided operation. |
| 147 | +
|
| 148 | + Raises: |
| 149 | + ApplicationError: If the operation is not properly decorated as a Nexus operation. |
| 150 | +
|
| 151 | + Example: |
| 152 | + >>> @service_handler |
| 153 | + >>> class WeatherServiceHandler: |
| 154 | + ... @sync_operation |
| 155 | + ... async def get_weather_object(self, ctx: StartOperationContext, input: WeatherInput) -> Weather: |
| 156 | + ... return Weather( |
| 157 | + ... city=input.city, temperature_range="14-20C", conditions="Sunny with wind." |
| 158 | + ... ) |
| 159 | + >>> |
| 160 | + >>> # Create tool with custom activity options |
| 161 | + >>> tool = nexus_operation_as_tool( |
| 162 | + ... WeatherServiceHandler.get_weather_object, |
| 163 | + ... service=WeatherServiceHandler, |
| 164 | + ... endpoint="weather-service", |
| 165 | + ... ) |
| 166 | + >>> # Use tool with an OpenAI agent |
| 167 | + """ |
| 168 | + if not get_operation_factory(fn): |
| 169 | + raise ApplicationError( |
| 170 | + "Function is not a Nexus operation", |
| 171 | + "invalid_tool", |
| 172 | + ) |
| 173 | + |
| 174 | + schema = function_schema(adapt_nexus_operation_function_schema(fn)) |
| 175 | + |
| 176 | + async def run_operation(ctx: RunContextWrapper[Any], input: str) -> Any: |
| 177 | + try: |
| 178 | + json_data = json.loads(input) |
| 179 | + except Exception as e: |
| 180 | + raise ApplicationError( |
| 181 | + f"Invalid JSON input for tool {schema.name}: {input}" |
| 182 | + ) from e |
| 183 | + |
| 184 | + nexus_client = workflow.NexusClient(service=service, endpoint=endpoint) |
| 185 | + args, _ = schema.to_call_args(schema.params_pydantic_model(**json_data)) |
| 186 | + assert len(args) == 1, "Nexus operations must have exactly one argument" |
| 187 | + [arg] = args |
| 188 | + result = await nexus_client.execute_operation( |
| 189 | + fn, |
| 190 | + arg, |
| 191 | + schedule_to_close_timeout=schedule_to_close_timeout, |
| 192 | + ) |
| 193 | + try: |
| 194 | + return str(result) |
| 195 | + except Exception as e: |
| 196 | + raise ToolSerializationError( |
| 197 | + "You must return a string representation of the tool output, or something we can call str() on" |
| 198 | + ) from e |
| 199 | + |
| 200 | + return FunctionTool( |
| 201 | + name=schema.name, |
| 202 | + description=schema.description or "", |
| 203 | + params_json_schema=schema.params_json_schema, |
| 204 | + on_invoke_tool=run_operation, |
| 205 | + strict_json_schema=True, |
| 206 | + ) |
| 207 | + |
| 208 | + |
| 209 | +def adapt_nexus_operation_function_schema(fn: Callable[..., Any]) -> Callable[..., Any]: |
| 210 | + # Nexus operation start methods look like |
| 211 | + # async def operation(self, ctx: StartOperationContext, input: InputType) -> OutputType |
| 212 | + _, inputT, retT = typing.get_type_hints(fn).values() |
| 213 | + |
| 214 | + def adapted(input: inputT) -> retT: # type: ignore |
| 215 | + pass |
| 216 | + |
| 217 | + adapted.__name__ = fn.__name__ |
| 218 | + return adapted |
0 commit comments