-
Notifications
You must be signed in to change notification settings - Fork 64
Durable OpenAI Agents #574
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
AnatoliB
merged 26 commits into
Azure:dev
from
AnatoliB:anatolib/durable-openai-agent-pr-to-azure
Sep 22, 2025
Merged
Changes from all 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 455886d
Add basic/hello_world sample
AnatoliB 876a8ee
Port basic/hello_world sample from OpenAI Agents SDK
AnatoliB a711ed9
Add README.md for OpenAI Agents SDK integration samples
AnatoliB 776ed19
Fix a typo: DefaultAzureCredential
AnatoliB c4c5c82
Add durable-openai-agent branch to validation workflow triggers
AnatoliB 5197fb5
Merge commit 'c4c5c82064712ded96db762ecc59fa35d7128ed2' into anatolib…
AnatoliB 38bd9e3
Fix linter issues (#3)
AnatoliB 55d0e49
Merge remote-tracking branch 'origin/durable-openai-agent' into anato…
AnatoliB 4da9597
Merge pull request #2 from AnatoliB/anatolib/fix-typo-default-azure-c…
philliphoff b87819b
Durable OpenAI agent (#4)
greenie-msft 9580998
Scaffold initial orchestration tests (#5)
philliphoff f769efc
Move OpenAI decorator to `DFApp` class (#6)
philliphoff 0a8b3b0
Improve orchestration output serialization (#7)
philliphoff 1b3ac4c
Enable model activity and tool activity retries (#8)
AnatoliB 0e534bc
Apply new activity name for model invocations (#9)
philliphoff b9711b3
Rename `activity_as_tool()` to `create_activity_tool()` (#13)
philliphoff 1244b42
Make DurableAIAgentContext delegate to the underlying DurableOrchestr…
AnatoliB fac5708
Emit OpenAI Agents SDK integration usage telemetry (#14)
AnatoliB c68fe8d
Match call_activity and call_activity_with_retry signatures and docst…
AnatoliB 3235404
Add a handoffs/message_filter sample, fix passing parameters to activ…
AnatoliB 7aa48a8
Remove model exception handling (#18)
philliphoff 6edbf26
General cleanup and refactoring (#19)
AnatoliB ccfcffb
Initial Docs for the Durable OpenAI Agents Integration (#17)
greenie-msft 4496dc3
Roll back changes not needed in the Azure repo
AnatoliB c10ecb5
Update jsonschema dependency to version 4.25.1 in setup.py
AnatoliB File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.