diff --git a/azure/durable_functions/openai_agents/decorators.py b/azure/durable_functions/openai_agents/decorators.py index baa1604b..6420a3d0 100644 --- a/azure/durable_functions/openai_agents/decorators.py +++ b/azure/durable_functions/openai_agents/decorators.py @@ -1,7 +1,9 @@ from functools import wraps import inspect import sys +from typing import Optional import azure.functions as func +from agents import ModelProvider from agents.run import set_default_agent_runner from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext from .runner import DurableOpenAIRunner @@ -15,14 +17,14 @@ _registered_apps = set() -def _setup_durable_openai_agent(app: func.FunctionApp): +def _setup_durable_openai_agent(app: func.FunctionApp, model_provider: Optional[ModelProvider]): """Set up the Durable OpenAI Agent framework for the given FunctionApp. This is automatically called when using the framework decorators. """ app_id = id(app) if app_id not in _registered_apps: - create_invoke_model_activity(app) + create_invoke_model_activity(app, model_provider) _registered_apps.add(app_id) @@ -40,7 +42,7 @@ def _find_function_app_in_module(module): return None -def _auto_setup_durable_openai_agent(decorated_func): +def _auto_setup_durable_openai_agent(decorated_func, model_provider: Optional[ModelProvider]): """Automatically detect and setup the FunctionApp for Durable OpenAI Agents. This finds the FunctionApp in the same module as the decorated function. @@ -54,67 +56,75 @@ def _auto_setup_durable_openai_agent(decorated_func): # Find the FunctionApp instance in that module app = _find_function_app_in_module(func_module) if app is not None: - _setup_durable_openai_agent(app) + _setup_durable_openai_agent(app, model_provider) except Exception: # Silently fail if auto-setup doesn't work # The user can still manually call create_invoke_model_activity if needed pass -def durable_openai_agent_orchestrator(func): +def durable_openai_agent_orchestrator(_func=None, + *, + model_provider: Optional[ModelProvider] = None): """Decorate Azure Durable Functions orchestrators that use OpenAI Agents.""" # Auto-setup: Find and configure the FunctionApp when decorator is applied - _auto_setup_durable_openai_agent(func) - - @wraps(func) - def wrapper(durable_orchestration_context: DurableOrchestrationContext): - ensure_event_loop() - durable_ai_agent_context = DurableAIAgentContext(durable_orchestration_context) - durable_openai_runner = DurableOpenAIRunner(context=durable_ai_agent_context) - set_default_agent_runner(durable_openai_runner) - - if inspect.isgeneratorfunction(func): - gen = iter(func(durable_ai_agent_context)) - try: - # prime the subiterator - value = next(gen) - yield from durable_ai_agent_context._yield_and_clear_tasks() - while True: - try: - # send whatever was sent into us down to the subgenerator - yield from durable_ai_agent_context._yield_and_clear_tasks() - sent = yield value - except GeneratorExit: - # ensure the subgenerator is closed - if hasattr(gen, "close"): - gen.close() - raise - except BaseException as exc: - # forward thrown exceptions if possible - if hasattr(gen, "throw"): - value = gen.throw(type(exc), exc, exc.__traceback__) - else: + def wrapper_wrapper(func): + _auto_setup_durable_openai_agent(func, model_provider) + + @wraps(func) + def wrapper(durable_orchestration_context: DurableOrchestrationContext): + ensure_event_loop() + durable_ai_agent_context = DurableAIAgentContext(durable_orchestration_context) + durable_openai_runner = DurableOpenAIRunner(context=durable_ai_agent_context) + set_default_agent_runner(durable_openai_runner) + + if inspect.isgeneratorfunction(func): + gen = iter(func(durable_ai_agent_context)) + try: + # prime the subiterator + value = next(gen) + yield from durable_ai_agent_context._yield_and_clear_tasks() + while True: + try: + # send whatever was sent into us down to the subgenerator + yield from durable_ai_agent_context._yield_and_clear_tasks() + sent = yield value + except GeneratorExit: + # ensure the subgenerator is closed + if hasattr(gen, "close"): + gen.close() raise - else: - # normal path: forward .send (or .__next__) - if hasattr(gen, "send"): - value = gen.send(sent) + except BaseException as exc: + # forward thrown exceptions if possible + if hasattr(gen, "throw"): + value = gen.throw(type(exc), exc, exc.__traceback__) + else: + raise else: - value = next(gen) - except StopIteration as e: - yield from durable_ai_agent_context._yield_and_clear_tasks() - return e.value - except YieldException as e: - yield from durable_ai_agent_context._yield_and_clear_tasks() - yield e.task - else: - try: - result = func(durable_ai_agent_context) - return result - except YieldException as e: - yield from durable_ai_agent_context._yield_and_clear_tasks() - yield e.task - finally: - yield from durable_ai_agent_context._yield_and_clear_tasks() - - return wrapper + # normal path: forward .send (or .__next__) + if hasattr(gen, "send"): + value = gen.send(sent) + else: + value = next(gen) + except StopIteration as e: + yield from durable_ai_agent_context._yield_and_clear_tasks() + return e.value + except YieldException as e: + yield from durable_ai_agent_context._yield_and_clear_tasks() + yield e.task + else: + try: + result = func(durable_ai_agent_context) + return result + except YieldException as e: + yield from durable_ai_agent_context._yield_and_clear_tasks() + yield e.task + finally: + yield from durable_ai_agent_context._yield_and_clear_tasks() + + return wrapper + + if _func is None: + return wrapper_wrapper + else: + return wrapper_wrapper(_func) diff --git a/azure/durable_functions/openai_agents/model_invocation_activity.py b/azure/durable_functions/openai_agents/model_invocation_activity.py index 8b65a57d..9f4f1287 100644 --- a/azure/durable_functions/openai_agents/model_invocation_activity.py +++ b/azure/durable_functions/openai_agents/model_invocation_activity.py @@ -1,4 +1,3 @@ -from __future__ import annotations import enum import json import logging @@ -394,12 +393,12 @@ def stream_response( tracing: ModelTracing, *, previous_response_id: Optional[str], - prompt: ResponsePromptParam | None, + prompt: Optional[ResponsePromptParam], ) -> AsyncIterator[TResponseStreamEvent]: raise NotImplementedError("Durable model doesn't support streams yet") -def create_invoke_model_activity(app: func.FunctionApp): +def create_invoke_model_activity(app: func.FunctionApp, model_provider: Optional[ModelProvider]): """Create and register the invoke_model_activity function with the provided FunctionApp.""" @app.activity_trigger(input_name="input") @@ -407,7 +406,7 @@ async def invoke_model_activity(input: str): """Activity that handles OpenAI model invocations.""" activity_input = ActivityModelInput.from_json(input) - model_invoker = ModelInvoker() + model_invoker = ModelInvoker(model_provider=model_provider) result = await model_invoker.invoke_model_activity(activity_input) json_obj = ModelResponse.__pydantic_serializer__.to_json(result) diff --git a/requirements.txt b/requirements.txt index a853b88a..42f4630d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ flake8-docstrings==1.5.0 pytest==7.1.2 python-dateutil==2.8.0 requests==2.32.4 -jsonschema==3.2.0 +jsonschema==4.25.1 aiohttp==3.12.14 azure-functions>=1.11.3b3 nox==2019.11.9 @@ -12,4 +12,7 @@ pytest-asyncio==0.20.2 autopep8 types-python-dateutil opentelemetry-api==1.32.1 -opentelemetry-sdk==1.32.1 \ No newline at end of file +opentelemetry-sdk==1.32.1 +openai==1.98.0 +openai-agents==0.2.4 +eval_type_backport diff --git a/tests/orchestrator/openai_agents/__init__.py b/tests/orchestrator/openai_agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/orchestrator/openai_agents/test_openai_agents.py b/tests/orchestrator/openai_agents/test_openai_agents.py new file mode 100644 index 00000000..e8ab3d16 --- /dev/null +++ b/tests/orchestrator/openai_agents/test_openai_agents.py @@ -0,0 +1,155 @@ +import azure.durable_functions as df +import azure.functions as func +import json +from agents import Agent, Runner +from azure.durable_functions.models import OrchestratorState +from azure.durable_functions.models.actions import CallActivityAction +from azure.durable_functions.models.ReplaySchema import ReplaySchema +from azure.durable_functions.openai_agents import durable_openai_agent_orchestrator +from openai import BaseModel +from tests.orchestrator.orchestrator_test_utils import get_orchestration_state_result, assert_valid_schema, \ + assert_orchestration_state_equals +from tests.test_utils.ContextBuilder import ContextBuilder + +app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +@app.function_name("openai_agent_hello_world") +@app.orchestration_trigger(context_name="context") +@durable_openai_agent_orchestrator +def openai_agent_hello_world(context): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus." + ) + + result = Runner.run_sync(agent, "Tell me about recursion in programming.") + + return result.final_output; + +# +# Run an agent that uses various tools. +# +class Weather(BaseModel): + city: str + temperature_range: str + conditions: str + + @staticmethod + def from_json(data: str) -> "Weather": + return Weather(**json.loads(data)) + +@app.activity_trigger(input_name="city") +def get_weather(city: str) -> Weather: + print("[debug] get_weather called") + return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") + +@app.function_name("openai_agent_use_tool") +@app.orchestration_trigger(context_name="context") +@durable_openai_agent_orchestrator +def openai_agent_use_tool(context): + agent = Agent( + name="Assistant", + instructions="You only respond in haikus.", + tools=[context.activity_as_tool(get_weather)] + ) + + result = Runner.run_sync(agent, "Tell me the weather in Seattle.", ) + + return result.final_output; + +model_activity_name = "invoke_model_activity" + +def base_expected_state(output=None, replay_schema: ReplaySchema = ReplaySchema.V1) -> OrchestratorState: + return OrchestratorState(is_done=False, actions=[], output=output, replay_schema=replay_schema) + +def add_activity_action(state: OrchestratorState, input_: str, activity_name=model_activity_name): + action = CallActivityAction(function_name=activity_name, input_=input_) + state.actions.append([action]) + +def add_activity_completed_events( + context_builder: ContextBuilder, id_: int, result: str, is_played=False, activity_name=model_activity_name): + context_builder.add_task_scheduled_event(name=activity_name, id_=id_) + context_builder.add_orchestrator_completed_event() + context_builder.add_orchestrator_started_event() + context_builder.add_task_completed_event(id_=id_, result=json.dumps(result), is_played=is_played) + +def test_openai_agent_hello_world_start(): + context_builder = ContextBuilder('test_openai_agent_hello_world_start') + + result = get_orchestration_state_result( + context_builder, openai_agent_hello_world, uses_pystein=True) + + expected_state = base_expected_state() + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me about recursion in programming.\",\"role\":\"user\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + expected = expected_state.to_json() + + assert_valid_schema(result) + assert_orchestration_state_equals(expected, result) + +def test_openai_agent_hello_world_completed(): + context_builder = ContextBuilder('test_openai_agent_hello_world_completed') + add_activity_completed_events(context_builder, 0, '{"output":[{"id":"msg_68b9b2a9c67c81a38559c20c18fe86040a86c28ba39b53e8","content":[{"annotations":[],"text":"Skyscrapers whisper— \\nTaxis hum beneath the lights, \\nCity dreams don’t sleep.","type":"output_text","logprobs":null}],"role":"assistant","status":"completed","type":"message"}],"usage":{"requests":1,"input_tokens":27,"input_tokens_details":{"cached_tokens":0},"output_tokens":21,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"response_id":"resp_68b9b2a9461481a3984d0f790dd33f7b0a86c28ba39b53e8"}') + + result = get_orchestration_state_result( + context_builder, openai_agent_hello_world, uses_pystein=True) + + expected_state = base_expected_state() + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me about recursion in programming.\",\"role\":\"user\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + expected_state._is_done = True + expected_state._output = 'Skyscrapers whisper— \nTaxis hum beneath the lights, \nCity dreams don’t sleep.' + expected = expected_state.to_json() + + assert_valid_schema(result) + assert_orchestration_state_equals(expected, result) + +def test_openai_agent_use_tool_activity_start(): + context_builder = ContextBuilder('test_openai_agent_use_tool_start') + add_activity_completed_events(context_builder, 0, '{"output":[{"arguments":"{\\"args\\":\\"Seattle, WA\\"}","call_id":"call_mEdywElQTNpxAdivuEFjO0cT","name":"get_weather","type":"function_call","id":"fc_68b9ecc0ff9c819f863d6cf9e0a1b4e101011fd6f5f8c0a6","status":"completed"}],"usage":{"requests":1,"input_tokens":57,"input_tokens_details":{"cached_tokens":0},"output_tokens":17,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":74},"response_id":"resp_68b9ecc092e0819fb79b97c11aacef2001011fd6f5f8c0a6"}') + + result = get_orchestration_state_result( + context_builder, openai_agent_use_tool, uses_pystein=True) + + expected_state = base_expected_state() + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me the weather in Seattle.\",\"role\":\"user\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[{\"name\":\"get_weather\",\"description\":\"\",\"params_json_schema\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict_json_schema\":true}],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + add_activity_action(expected_state, "{\"args\":\"Seattle, WA\"}", activity_name="get_weather") + expected = expected_state.to_json() + + assert_valid_schema(result) + assert_orchestration_state_equals(expected, result) + +def test_openai_agent_use_tool_activity_completed(): + context_builder = ContextBuilder('test_openai_agent_use_tool_start') + add_activity_completed_events(context_builder, 0, '{"output":[{"arguments":"{\\"args\\":\\"Seattle, WA\\"}","call_id":"call_mEdywElQTNpxAdivuEFjO0cT","name":"get_weather","type":"function_call","id":"fc_68b9ecc0ff9c819f863d6cf9e0a1b4e101011fd6f5f8c0a6","status":"completed"}],"usage":{"requests":1,"input_tokens":57,"input_tokens_details":{"cached_tokens":0},"output_tokens":17,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":74},"response_id":"resp_68b9ecc092e0819fb79b97c11aacef2001011fd6f5f8c0a6"}') + add_activity_completed_events(context_builder, 1, '{"__class__":"Weather","__module__":"function_app","__data__":"{\n \"city\": \"{\\\"args\\\":\\\"Seattle, WA\\\"}\",\n \"temperature_range\": \"14-20C\",\n \"conditions\": \"Sunny with wind.\"\n}"}') + + result = get_orchestration_state_result( + context_builder, openai_agent_use_tool, uses_pystein=True) + + expected_state = base_expected_state() + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me the weather in Seattle.\",\"role\":\"user\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[{\"name\":\"get_weather\",\"description\":\"\",\"params_json_schema\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict_json_schema\":true}],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + add_activity_action(expected_state, "{\"args\":\"Seattle, WA\"}", activity_name="get_weather") + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me the weather in Seattle.\",\"role\":\"user\"},{\"arguments\":\"{\\\"args\\\":\\\"Seattle, WA\\\"}\",\"call_id\":\"call_mEdywElQTNpxAdivuEFjO0cT\",\"name\":\"get_weather\",\"type\":\"function_call\",\"id\":\"fc_68b9ecc0ff9c819f863d6cf9e0a1b4e101011fd6f5f8c0a6\",\"status\":\"completed\"},{\"call_id\":\"call_mEdywElQTNpxAdivuEFjO0cT\",\"output\":\"{\\\"__class__\\\":\\\"Weather\\\",\\\"__module__\\\":\\\"function_app\\\",\\\"__data__\\\":\\\"{\\n \\\"city\\\": \\\"{\\\\\\\"args\\\\\\\":\\\\\\\"Seattle, WA\\\\\\\"}\\\",\\n \\\"temperature_range\\\": \\\"14-20C\\\",\\n \\\"conditions\\\": \\\"Sunny with wind.\\\"\\n}\\\"}\",\"type\":\"function_call_output\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[{\"name\":\"get_weather\",\"description\":\"\",\"params_json_schema\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict_json_schema\":true}],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + expected = expected_state.to_json() + + assert_valid_schema(result) + assert_orchestration_state_equals(expected, result) + +def test_openai_agent_use_tool_analysis_completed(): + context_builder = ContextBuilder('test_openai_agent_use_tool_start') + add_activity_completed_events(context_builder, 0, '{"output":[{"arguments":"{\\"args\\":\\"Seattle, WA\\"}","call_id":"call_mEdywElQTNpxAdivuEFjO0cT","name":"get_weather","type":"function_call","id":"fc_68b9ecc0ff9c819f863d6cf9e0a1b4e101011fd6f5f8c0a6","status":"completed"}],"usage":{"requests":1,"input_tokens":57,"input_tokens_details":{"cached_tokens":0},"output_tokens":17,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":74},"response_id":"resp_68b9ecc092e0819fb79b97c11aacef2001011fd6f5f8c0a6"}') + add_activity_completed_events(context_builder, 1, '{"__class__":"Weather","__module__":"function_app","__data__":"{\n \"city\": \"{\\\"args\\\":\\\"Seattle, WA\\\"}\",\n \"temperature_range\": \"14-20C\",\n \"conditions\": \"Sunny with wind.\"\n}"}') + add_activity_completed_events(context_builder, 2, '{"output":[{"id":"msg_68b9f4b09c14819faa62abfd69cb53e501011fd6f5f8c0a6","content":[{"annotations":[],"text":"The weather in Seattle, WA is currently sunny with some wind. Temperatures are ranging from 14°C to 20°C.","type":"output_text","logprobs":null}],"role":"assistant","status":"completed","type":"message"}],"usage":{"requests":1,"input_tokens":107,"input_tokens_details":{"cached_tokens":0},"output_tokens":28,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":135},"response_id":"resp_68b9f4b00804819f9fe99eac95bd198e01011fd6f5f8c0a6"}') + + result = get_orchestration_state_result( + context_builder, openai_agent_use_tool, uses_pystein=True) + + expected_state = base_expected_state() + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me the weather in Seattle.\",\"role\":\"user\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[{\"name\":\"get_weather\",\"description\":\"\",\"params_json_schema\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict_json_schema\":true}],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + add_activity_action(expected_state, "{\"args\":\"Seattle, WA\"}", activity_name="get_weather") + add_activity_action(expected_state, "{\"input\":[{\"content\":\"Tell me the weather in Seattle.\",\"role\":\"user\"},{\"arguments\":\"{\\\"args\\\":\\\"Seattle, WA\\\"}\",\"call_id\":\"call_mEdywElQTNpxAdivuEFjO0cT\",\"name\":\"get_weather\",\"type\":\"function_call\",\"id\":\"fc_68b9ecc0ff9c819f863d6cf9e0a1b4e101011fd6f5f8c0a6\",\"status\":\"completed\"},{\"call_id\":\"call_mEdywElQTNpxAdivuEFjO0cT\",\"output\":\"{\\\"__class__\\\":\\\"Weather\\\",\\\"__module__\\\":\\\"function_app\\\",\\\"__data__\\\":\\\"{\\n \\\"city\\\": \\\"{\\\\\\\"args\\\\\\\":\\\\\\\"Seattle, WA\\\\\\\"}\\\",\\n \\\"temperature_range\\\": \\\"14-20C\\\",\\n \\\"conditions\\\": \\\"Sunny with wind.\\\"\\n}\\\"}\",\"type\":\"function_call_output\"}],\"model_settings\":{\"temperature\":null,\"top_p\":null,\"frequency_penalty\":null,\"presence_penalty\":null,\"tool_choice\":null,\"parallel_tool_calls\":null,\"truncation\":null,\"max_tokens\":null,\"reasoning\":null,\"metadata\":null,\"store\":null,\"include_usage\":null,\"response_include\":null,\"extra_query\":null,\"extra_body\":null,\"extra_headers\":null,\"extra_args\":null},\"tracing\":0,\"model_name\":null,\"system_instructions\":\"You only respond in haikus.\",\"tools\":[{\"name\":\"get_weather\",\"description\":\"\",\"params_json_schema\":{\"properties\":{\"city\":{\"title\":\"City\",\"type\":\"string\"}},\"required\":[\"city\"],\"title\":\"get_weather_args\",\"type\":\"object\",\"additionalProperties\":false},\"strict_json_schema\":true}],\"output_schema\":null,\"handoffs\":[],\"previous_response_id\":null,\"prompt\":null}") + expected_state._is_done = True + expected_state._output = 'The weather in Seattle, WA is currently sunny with some wind. Temperatures are ranging from 14°C to 20°C.' + expected = expected_state.to_json() + + assert_valid_schema(result) + assert_orchestration_state_equals(expected, result)