Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58e7c7a
Add initial OpenAI Agents SDK integration implementation
AnatoliB Sep 2, 2025
455886d
Add basic/hello_world sample
AnatoliB Sep 3, 2025
876a8ee
Port basic/hello_world sample from OpenAI Agents SDK
AnatoliB Sep 3, 2025
a711ed9
Add README.md for OpenAI Agents SDK integration samples
AnatoliB Sep 3, 2025
776ed19
Fix a typo: DefaultAzureCredential
AnatoliB Sep 3, 2025
c4c5c82
Add durable-openai-agent branch to validation workflow triggers
AnatoliB Sep 3, 2025
5197fb5
Merge commit 'c4c5c82064712ded96db762ecc59fa35d7128ed2' into anatolib…
AnatoliB Sep 3, 2025
38bd9e3
Fix linter issues (#3)
AnatoliB Sep 4, 2025
55d0e49
Merge remote-tracking branch 'origin/durable-openai-agent' into anato…
AnatoliB Sep 4, 2025
4da9597
Merge pull request #2 from AnatoliB/anatolib/fix-typo-default-azure-c…
philliphoff Sep 5, 2025
b87819b
Durable OpenAI agent (#4)
greenie-msft Sep 5, 2025
9580998
Scaffold initial orchestration tests (#5)
philliphoff Sep 5, 2025
f769efc
Move OpenAI decorator to `DFApp` class (#6)
philliphoff Sep 9, 2025
0a8b3b0
Improve orchestration output serialization (#7)
philliphoff Sep 12, 2025
1b3ac4c
Enable model activity and tool activity retries (#8)
AnatoliB Sep 12, 2025
0e534bc
Apply new activity name for model invocations (#9)
philliphoff Sep 15, 2025
b9711b3
Rename `activity_as_tool()` to `create_activity_tool()` (#13)
philliphoff Sep 16, 2025
1244b42
Make DurableAIAgentContext delegate to the underlying DurableOrchestr…
AnatoliB Sep 16, 2025
fac5708
Emit OpenAI Agents SDK integration usage telemetry (#14)
AnatoliB Sep 18, 2025
c68fe8d
Match call_activity and call_activity_with_retry signatures and docst…
AnatoliB Sep 18, 2025
3235404
Add a handoffs/message_filter sample, fix passing parameters to activ…
AnatoliB Sep 18, 2025
7aa48a8
Remove model exception handling (#18)
philliphoff Sep 18, 2025
6edbf26
General cleanup and refactoring (#19)
AnatoliB Sep 19, 2025
ccfcffb
Initial Docs for the Durable OpenAI Agents Integration (#17)
greenie-msft Sep 19, 2025
4496dc3
Roll back changes not needed in the Azure repo
AnatoliB Sep 19, 2025
c10ecb5
Update jsonschema dependency to version 4.25.1 in setup.py
AnatoliB Sep 22, 2025
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
8 changes: 8 additions & 0 deletions azure/durable_functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ def validate_extension_bundles():
__all__.append('Blueprint')
except ModuleNotFoundError:
pass

# Import OpenAI Agents integration (optional dependency)
try:
from . import openai_agents # noqa
__all__.append('openai_agents')
except ImportError:
# OpenAI agents integration requires additional dependencies
pass
65 changes: 64 additions & 1 deletion azure/durable_functions/decorators/durable_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from .metadata import OrchestrationTrigger, ActivityTrigger, EntityTrigger,\

from azure.durable_functions.models.RetryOptions import RetryOptions
from .metadata import OrchestrationTrigger, ActivityTrigger, EntityTrigger, \
DurableClient
from typing import Callable, Optional
from azure.durable_functions.entity import Entity
Expand Down Expand Up @@ -45,6 +47,7 @@ def __init__(self,
New instance of a Durable Functions app
"""
super().__init__(auth_level=http_auth_level)
self._is_durable_openai_agent_setup = False

def _configure_entity_callable(self, wrap) -> Callable:
"""Obtain decorator to construct an Entity class from a user-defined Function.
Expand Down Expand Up @@ -250,6 +253,66 @@ def decorator():

return wrap

def _create_invoke_model_activity(self, model_provider, activity_name):
"""Create and register the invoke_model_activity function with the provided FunctionApp."""

@self.activity_trigger(input_name="input", activity=activity_name)
async def run_model_activity(input: str):
from azure.durable_functions.openai_agents.orchestrator_generator\
import durable_openai_agent_activity

return await durable_openai_agent_activity(input, model_provider)

return run_model_activity

def _setup_durable_openai_agent(self, model_provider, activity_name):
if not self._is_durable_openai_agent_setup:
self._create_invoke_model_activity(model_provider, activity_name)
self._is_durable_openai_agent_setup = True

def durable_openai_agent_orchestrator(
self,
_func=None,
*,
model_provider=None,
model_retry_options: Optional[RetryOptions] = RetryOptions(
first_retry_interval_in_milliseconds=2000, max_number_of_attempts=5
),
):
"""Decorate Azure Durable Functions orchestrators that use OpenAI Agents.

Parameters
----------
model_provider: Optional[ModelProvider]
Use a non-default ModelProvider instead of the default OpenAIProvider,
such as when testing.
"""
from agents import ModelProvider
from azure.durable_functions.openai_agents.orchestrator_generator\
import durable_openai_agent_orchestrator_generator

if model_provider is not None and type(model_provider) is not ModelProvider:
raise TypeError("Provided model provider must be of type ModelProvider")

activity_name = "run_model"

self._setup_durable_openai_agent(model_provider, activity_name)

def generator_wrapper_wrapper(func):

@wraps(func)
def generator_wrapper(context):
return durable_openai_agent_orchestrator_generator(
func, context, model_retry_options, activity_name
)

return generator_wrapper

if _func is None:
return generator_wrapper_wrapper
else:
return generator_wrapper_wrapper(_func)


class DFApp(Blueprint, FunctionRegister):
"""Durable Functions (DF) app.
Expand Down
13 changes: 13 additions & 0 deletions azure/durable_functions/openai_agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
"""OpenAI Agents integration for Durable Functions.

This module provides decorators and utilities to integrate OpenAI Agents
with Durable Functions orchestration patterns.
"""

from .context import DurableAIAgentContext

__all__ = [
'DurableAIAgentContext',
]
194 changes: 194 additions & 0 deletions azure/durable_functions/openai_agents/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import json
from typing import Any, Callable, Optional, TYPE_CHECKING, Union

from azure.durable_functions.models.DurableOrchestrationContext import (
DurableOrchestrationContext,
)
from azure.durable_functions.models.RetryOptions import RetryOptions

from agents import RunContextWrapper, Tool
from agents.function_schema import function_schema
from agents.tool import FunctionTool

from azure.durable_functions.models.Task import TaskBase
from .task_tracker import TaskTracker


if TYPE_CHECKING:
# At type-check time we want all members / signatures for IDE & linters.
_BaseDurableContext = DurableOrchestrationContext
else:
class _BaseDurableContext: # lightweight runtime stub
"""Runtime stub base class for delegation; real context is wrapped.

At runtime we avoid inheriting from DurableOrchestrationContext so that
attribute lookups for its members are delegated via __getattr__ to the
wrapped ``_context`` instance.
"""

__slots__ = ()


class DurableAIAgentContext(_BaseDurableContext):
"""Context for AI agents running in Azure Durable Functions orchestration.

Design
------
* Static analysis / IDEs: Appears to subclass ``DurableOrchestrationContext`` so
you get autocompletion and type hints (under TYPE_CHECKING branch).
* Runtime: Inherits from a trivial stub. All durable orchestration operations
are delegated to the real ``DurableOrchestrationContext`` instance provided
as ``context`` and stored in ``_context``.

Consequences
------------
* ``isinstance(DurableAIAgentContext, DurableOrchestrationContext)`` is **False** at
runtime (expected).
* Delegation via ``__getattr__`` works for every member of the real context.
* No reliance on internal initialization side-effects of the durable SDK.
"""

def __init__(
self,
context: DurableOrchestrationContext,
task_tracker: TaskTracker,
model_retry_options: Optional[RetryOptions],
):
self._context = context
self._task_tracker = task_tracker
self._model_retry_options = model_retry_options

def call_activity(
self, name: Union[str, Callable], input_: Optional[Any] = None
) -> TaskBase:
"""Schedule an activity for execution.

Parameters
----------
name: str | Callable
Either the name of the activity function to call, as a string or,
in the Python V2 programming model, the activity function itself.
input_: Optional[Any]
The JSON-serializable input to pass to the activity function.

Returns
-------
Task
A Durable Task that completes when the called activity function completes or fails.
"""
task = self._context.call_activity(name, input_)
self._task_tracker.record_activity_call()
return task

def call_activity_with_retry(
self,
name: Union[str, Callable],
retry_options: RetryOptions,
input_: Optional[Any] = None,
) -> TaskBase:
"""Schedule an activity for execution with retry options.

Parameters
----------
name: str | Callable
Either the name of the activity function to call, as a string or,
in the Python V2 programming model, the activity function itself.
retry_options: RetryOptions
The retry options for the activity function.
input_: Optional[Any]
The JSON-serializable input to pass to the activity function.

Returns
-------
Task
A Durable Task that completes when the called activity function completes or
fails completely.
"""
task = self._context.call_activity_with_retry(name, retry_options, input_)
self._task_tracker.record_activity_call()
return task

def create_activity_tool(
self,
activity_func: Callable,
*,
description: Optional[str] = None,
retry_options: Optional[RetryOptions] = RetryOptions(
first_retry_interval_in_milliseconds=2000, max_number_of_attempts=5
),
) -> Tool:
"""Convert an Azure Durable Functions activity to an OpenAI Agents SDK Tool.

Args
----
activity_func: The Azure Functions activity function to convert
description: Optional description override for the tool
retry_options: The retry options for the activity function

Returns
-------
Tool: An OpenAI Agents SDK Tool object

"""
if activity_func._function is None:
raise ValueError("The provided function is not a valid Azure Function.")

if (activity_func._function._trigger is not None
and activity_func._function._trigger.activity is not None):
activity_name = activity_func._function._trigger.activity
else:
activity_name = activity_func._function._name

input_name = None
if (activity_func._function._trigger is not None
and hasattr(activity_func._function._trigger, 'name')):
input_name = activity_func._function._trigger.name

async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any:
# Parse JSON input and extract the named value if input_name is specified
activity_input = input
if input_name:
try:
parsed_input = json.loads(input)
if isinstance(parsed_input, dict) and input_name in parsed_input:
activity_input = parsed_input[input_name]
# If parsing fails or the named parameter is not found, pass the original input
except (json.JSONDecodeError, TypeError):
pass

if retry_options:
result = self._task_tracker.get_activity_call_result_with_retry(
activity_name, retry_options, activity_input
)
else:
result = self._task_tracker.get_activity_call_result(activity_name, activity_input)
return result

schema = function_schema(
func=activity_func._function._func,
docstring_style=None,
description_override=description,
use_docstring_info=True,
strict_json_schema=True,
)

return FunctionTool(
name=schema.name,
description=schema.description or "",
params_json_schema=schema.params_json_schema,
on_invoke_tool=run_activity,
strict_json_schema=True,
)

def __getattr__(self, name):
"""Delegate missing attributes to the underlying DurableOrchestrationContext."""
try:
return getattr(self._context, name)
except AttributeError:
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

def __dir__(self):
"""Improve introspection and tab-completion by including delegated attributes."""
return sorted(set(dir(type(self)) + list(self.__dict__) + dir(self._context)))
17 changes: 17 additions & 0 deletions azure/durable_functions/openai_agents/event_loop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import asyncio


def ensure_event_loop():
"""Ensure an event loop is available for sync execution context.

This is necessary when calling Runner.run_sync from Azure Functions
Durable orchestrators, which run in a synchronous context but need
an event loop for internal async operations.
"""
try:
asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
11 changes: 11 additions & 0 deletions azure/durable_functions/openai_agents/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from azure.durable_functions.models.Task import TaskBase


class YieldException(BaseException):
"""Exception raised when an orchestrator should yield control."""

def __init__(self, task: TaskBase):
super().__init__("Orchestrator should yield.")
self.task = task
Loading