azure-functions-durable 1.3.3__py3-none-any.whl → 1.4.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure/durable_functions/__init__.py +8 -0
- azure/durable_functions/decorators/durable_app.py +64 -1
- azure/durable_functions/openai_agents/__init__.py +13 -0
- azure/durable_functions/openai_agents/context.py +194 -0
- azure/durable_functions/openai_agents/event_loop.py +17 -0
- azure/durable_functions/openai_agents/exceptions.py +11 -0
- azure/durable_functions/openai_agents/handoffs.py +67 -0
- azure/durable_functions/openai_agents/model_invocation_activity.py +268 -0
- azure/durable_functions/openai_agents/orchestrator_generator.py +67 -0
- azure/durable_functions/openai_agents/runner.py +103 -0
- azure/durable_functions/openai_agents/task_tracker.py +171 -0
- azure/durable_functions/openai_agents/tools.py +148 -0
- azure/durable_functions/openai_agents/usage_telemetry.py +69 -0
- {azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/METADATA +6 -1
- {azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/RECORD +24 -7
- tests/openai_agents/__init__.py +0 -0
- tests/openai_agents/test_context.py +466 -0
- tests/openai_agents/test_task_tracker.py +290 -0
- tests/openai_agents/test_usage_telemetry.py +99 -0
- tests/orchestrator/openai_agents/__init__.py +0 -0
- tests/orchestrator/openai_agents/test_openai_agents.py +316 -0
- {azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/LICENSE +0 -0
- {azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/WHEEL +0 -0
- {azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from agents import ModelProvider, ModelResponse
|
|
6
|
+
from agents.run import set_default_agent_runner
|
|
7
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
8
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
9
|
+
from .model_invocation_activity import DurableModelActivityInput, ModelInvoker
|
|
10
|
+
from .task_tracker import TaskTracker
|
|
11
|
+
from .runner import DurableOpenAIRunner
|
|
12
|
+
from .context import DurableAIAgentContext
|
|
13
|
+
from .event_loop import ensure_event_loop
|
|
14
|
+
from .usage_telemetry import UsageTelemetry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def durable_openai_agent_activity(input: str, model_provider: ModelProvider) -> str:
|
|
18
|
+
"""Activity logic that handles OpenAI model invocations."""
|
|
19
|
+
activity_input = DurableModelActivityInput.from_json(input)
|
|
20
|
+
|
|
21
|
+
model_invoker = ModelInvoker(model_provider=model_provider)
|
|
22
|
+
result = await model_invoker.invoke_model_activity(activity_input)
|
|
23
|
+
|
|
24
|
+
# Use safe/public Pydantic API when possible. Prefer model_dump_json if result is a BaseModel
|
|
25
|
+
# Otherwise handle common types (str/bytes/dict/list) and fall back to json.dumps.
|
|
26
|
+
import json as _json
|
|
27
|
+
|
|
28
|
+
if hasattr(result, "model_dump_json"):
|
|
29
|
+
# Pydantic v2 BaseModel
|
|
30
|
+
json_str = result.model_dump_json()
|
|
31
|
+
else:
|
|
32
|
+
if isinstance(result, bytes):
|
|
33
|
+
json_str = result.decode()
|
|
34
|
+
elif isinstance(result, str):
|
|
35
|
+
json_str = result
|
|
36
|
+
else:
|
|
37
|
+
# Try the internal serializer as a last resort, but fall back to json.dumps
|
|
38
|
+
try:
|
|
39
|
+
json_bytes = ModelResponse.__pydantic_serializer__.to_json(result)
|
|
40
|
+
json_str = json_bytes.decode()
|
|
41
|
+
except Exception:
|
|
42
|
+
json_str = _json.dumps(result)
|
|
43
|
+
|
|
44
|
+
return json_str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def durable_openai_agent_orchestrator_generator(
|
|
48
|
+
func,
|
|
49
|
+
durable_orchestration_context: DurableOrchestrationContext,
|
|
50
|
+
model_retry_options: Optional[RetryOptions],
|
|
51
|
+
activity_name: str,
|
|
52
|
+
):
|
|
53
|
+
"""Adapts the synchronous OpenAI Agents function to an Durable orchestrator generator."""
|
|
54
|
+
# Log versions the first time this generator is invoked
|
|
55
|
+
UsageTelemetry.log_usage_once()
|
|
56
|
+
|
|
57
|
+
ensure_event_loop()
|
|
58
|
+
task_tracker = TaskTracker(durable_orchestration_context)
|
|
59
|
+
durable_ai_agent_context = DurableAIAgentContext(
|
|
60
|
+
durable_orchestration_context, task_tracker, model_retry_options
|
|
61
|
+
)
|
|
62
|
+
durable_openai_runner = DurableOpenAIRunner(
|
|
63
|
+
context=durable_ai_agent_context, activity_name=activity_name)
|
|
64
|
+
set_default_agent_runner(durable_openai_runner)
|
|
65
|
+
|
|
66
|
+
func_with_context = partial(func, durable_ai_agent_context)
|
|
67
|
+
return task_tracker.execute_orchestrator_function(func_with_context)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
from agents import (
|
|
8
|
+
Agent,
|
|
9
|
+
RunConfig,
|
|
10
|
+
RunResult,
|
|
11
|
+
RunResultStreaming,
|
|
12
|
+
TContext,
|
|
13
|
+
TResponseInputItem,
|
|
14
|
+
)
|
|
15
|
+
from agents.run import DEFAULT_AGENT_RUNNER, DEFAULT_MAX_TURNS, AgentRunner
|
|
16
|
+
from pydantic_core import to_json
|
|
17
|
+
|
|
18
|
+
from .context import DurableAIAgentContext
|
|
19
|
+
from .model_invocation_activity import DurableActivityModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DurableOpenAIRunner:
|
|
23
|
+
"""Runner for OpenAI agents using Durable Functions orchestration."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, context: DurableAIAgentContext, activity_name: str) -> None:
|
|
26
|
+
self._runner = DEFAULT_AGENT_RUNNER or AgentRunner()
|
|
27
|
+
self._context = context
|
|
28
|
+
self._activity_name = activity_name
|
|
29
|
+
|
|
30
|
+
def _prepare_run_config(
|
|
31
|
+
self,
|
|
32
|
+
starting_agent: Agent[TContext],
|
|
33
|
+
input: Union[str, list[TResponseInputItem]],
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
) -> tuple[Union[str, list[TResponseInputItem]], RunConfig, dict[str, Any]]:
|
|
36
|
+
"""Prepare and validate the run configuration and parameters for agent execution."""
|
|
37
|
+
# Avoid https://github.com/pydantic/pydantic/issues/9541
|
|
38
|
+
normalized_input = json.loads(to_json(input))
|
|
39
|
+
|
|
40
|
+
run_config = kwargs.get("run_config") or RunConfig()
|
|
41
|
+
|
|
42
|
+
model_name = run_config.model or starting_agent.model
|
|
43
|
+
if model_name and not isinstance(model_name, str):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
"For agent execution in Durable Functions, model name in run_config or "
|
|
46
|
+
"starting_agent must be a string."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
updated_run_config = replace(
|
|
50
|
+
run_config,
|
|
51
|
+
model=DurableActivityModel(
|
|
52
|
+
model_name=model_name,
|
|
53
|
+
task_tracker=self._context._task_tracker,
|
|
54
|
+
retry_options=self._context._model_retry_options,
|
|
55
|
+
activity_name=self._activity_name,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
run_params = {
|
|
60
|
+
"context": kwargs.get("context"),
|
|
61
|
+
"max_turns": kwargs.get("max_turns", DEFAULT_MAX_TURNS),
|
|
62
|
+
"hooks": kwargs.get("hooks"),
|
|
63
|
+
"previous_response_id": kwargs.get("previous_response_id"),
|
|
64
|
+
"session": kwargs.get("session"),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalized_input, updated_run_config, run_params
|
|
68
|
+
|
|
69
|
+
def run_sync(
|
|
70
|
+
self,
|
|
71
|
+
starting_agent: Agent[TContext],
|
|
72
|
+
input: Union[str, list[TResponseInputItem]],
|
|
73
|
+
**kwargs: Any,
|
|
74
|
+
) -> RunResult:
|
|
75
|
+
"""Run an agent synchronously with the given input and configuration."""
|
|
76
|
+
normalized_input, updated_run_config, run_params = self._prepare_run_config(
|
|
77
|
+
starting_agent, input, **kwargs
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return self._runner.run_sync(
|
|
81
|
+
starting_agent=starting_agent,
|
|
82
|
+
input=normalized_input,
|
|
83
|
+
run_config=updated_run_config,
|
|
84
|
+
**run_params,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def run(
|
|
88
|
+
self,
|
|
89
|
+
starting_agent: Agent[TContext],
|
|
90
|
+
input: Union[str, list[TResponseInputItem]],
|
|
91
|
+
**kwargs: Any,
|
|
92
|
+
) -> RunResult:
|
|
93
|
+
"""Run an agent asynchronously. Not supported in Durable Functions."""
|
|
94
|
+
raise RuntimeError("Durable Functions do not support asynchronous runs.")
|
|
95
|
+
|
|
96
|
+
def run_streamed(
|
|
97
|
+
self,
|
|
98
|
+
starting_agent: Agent[TContext],
|
|
99
|
+
input: Union[str, list[TResponseInputItem]],
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> RunResultStreaming:
|
|
102
|
+
"""Run an agent with streaming. Not supported in Durable Functions."""
|
|
103
|
+
raise RuntimeError("Durable Functions do not support streaming.")
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import json
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from azure.durable_functions.models.DurableOrchestrationContext import (
|
|
8
|
+
DurableOrchestrationContext,
|
|
9
|
+
)
|
|
10
|
+
from azure.durable_functions.models.history.HistoryEventType import HistoryEventType
|
|
11
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
12
|
+
|
|
13
|
+
from .exceptions import YieldException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TaskTracker:
|
|
17
|
+
"""Tracks activity calls and handles task result processing for durable AI agents."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, context: DurableOrchestrationContext):
|
|
20
|
+
self._context = context
|
|
21
|
+
self._activities_called = 0
|
|
22
|
+
self._tasks_to_yield = []
|
|
23
|
+
|
|
24
|
+
def _get_activity_result_or_raise(self, task):
|
|
25
|
+
"""Return the activity result if available; otherwise raise ``YieldException`` to defer.
|
|
26
|
+
|
|
27
|
+
The first time an activity is scheduled its result won't yet exist in the
|
|
28
|
+
orchestration history, so we raise ``YieldException`` with the task so the
|
|
29
|
+
orchestrator can yield it. On replay, once the corresponding TASK_COMPLETED
|
|
30
|
+
history event is present, we capture the result and queue the task for a
|
|
31
|
+
later yield (to preserve ordering) while returning the deserialized value.
|
|
32
|
+
"""
|
|
33
|
+
self.record_activity_call()
|
|
34
|
+
|
|
35
|
+
histories = self._context.histories
|
|
36
|
+
completed_tasks = [
|
|
37
|
+
entry for entry in histories
|
|
38
|
+
if entry.event_type == HistoryEventType.TASK_COMPLETED
|
|
39
|
+
]
|
|
40
|
+
if len(completed_tasks) < self._activities_called:
|
|
41
|
+
# Result not yet available in history -> raise to signal a yield now
|
|
42
|
+
raise YieldException(task)
|
|
43
|
+
# Result exists (replay). Queue task to be yielded after returning value.
|
|
44
|
+
#
|
|
45
|
+
# We cannot just yield it now because this method can be called from
|
|
46
|
+
# deeply nested code paths that we don't control (such as the
|
|
47
|
+
# OpenAI Agents SDK internals), and yielding here would lead to
|
|
48
|
+
# unintended behavior. Instead, we queue the task to be yielded
|
|
49
|
+
# later and return the result recorded in the history, so the
|
|
50
|
+
# code invoking this method can continue executing normally.
|
|
51
|
+
self._tasks_to_yield.append(task)
|
|
52
|
+
|
|
53
|
+
result_json = completed_tasks[self._activities_called - 1].Result
|
|
54
|
+
result = json.loads(result_json)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
def get_activity_call_result(self, activity_name, input: Any):
|
|
58
|
+
"""Call an activity and return its result or raise ``YieldException`` if pending."""
|
|
59
|
+
task = self._context.call_activity(activity_name, input)
|
|
60
|
+
return self._get_activity_result_or_raise(task)
|
|
61
|
+
|
|
62
|
+
def get_activity_call_result_with_retry(
|
|
63
|
+
self, activity_name, retry_options: RetryOptions, input: Any
|
|
64
|
+
):
|
|
65
|
+
"""Call an activity with retry and return its result or raise YieldException if pending."""
|
|
66
|
+
task = self._context.call_activity_with_retry(activity_name, retry_options, input)
|
|
67
|
+
return self._get_activity_result_or_raise(task)
|
|
68
|
+
|
|
69
|
+
def record_activity_call(self):
|
|
70
|
+
"""Record that an activity was called."""
|
|
71
|
+
self._activities_called += 1
|
|
72
|
+
|
|
73
|
+
def _yield_and_clear_tasks(self):
|
|
74
|
+
"""Yield all accumulated tasks and clear the tasks list."""
|
|
75
|
+
for task in self._tasks_to_yield:
|
|
76
|
+
yield task
|
|
77
|
+
self._tasks_to_yield.clear()
|
|
78
|
+
|
|
79
|
+
def execute_orchestrator_function(self, func):
|
|
80
|
+
"""Execute the orchestrator function with comprehensive task and exception handling.
|
|
81
|
+
|
|
82
|
+
The orchestrator function can exhibit any combination of the following behaviors:
|
|
83
|
+
- Execute regular code and return a value or raise an exception
|
|
84
|
+
- Invoke get_activity_call_result or get_activity_call_result_with_retry, which leads to
|
|
85
|
+
either interrupting the orchestrator function immediately (because of YieldException),
|
|
86
|
+
or queueing the task for later yielding while continuing execution
|
|
87
|
+
- Invoke DurableAIAgentContext.call_activity or call_activity_with_retry (which must lead
|
|
88
|
+
to corresponding record_activity_call invocations)
|
|
89
|
+
- Yield tasks (typically produced by DurableAIAgentContext methods like call_activity,
|
|
90
|
+
wait_for_external_event, etc.), which may or may not interrupt orchestrator function
|
|
91
|
+
execution
|
|
92
|
+
- Mix all of the above in any combination
|
|
93
|
+
|
|
94
|
+
This method converts both YieldException and regular yields into a sequence of yields
|
|
95
|
+
preserving the order, while also capturing return values through the generator protocol.
|
|
96
|
+
For example, if the orchestrator function yields task A, then queues task B for yielding,
|
|
97
|
+
then raises YieldException wrapping task C, this method makes sure that the resulting
|
|
98
|
+
sequence of yields is: (A, B, C).
|
|
99
|
+
|
|
100
|
+
Args
|
|
101
|
+
----
|
|
102
|
+
func: The orchestrator function to execute (generator or regular function)
|
|
103
|
+
|
|
104
|
+
Yields
|
|
105
|
+
------
|
|
106
|
+
Tasks yielded by the orchestrator function and tasks wrapped in YieldException
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
The return value from the orchestrator function
|
|
111
|
+
"""
|
|
112
|
+
if inspect.isgeneratorfunction(func):
|
|
113
|
+
gen = iter(func())
|
|
114
|
+
try:
|
|
115
|
+
# prime the subiterator
|
|
116
|
+
value = next(gen)
|
|
117
|
+
yield from self._yield_and_clear_tasks()
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
# send whatever was sent into us down to the subgenerator
|
|
121
|
+
yield from self._yield_and_clear_tasks()
|
|
122
|
+
sent = yield value
|
|
123
|
+
except GeneratorExit:
|
|
124
|
+
# ensure the subgenerator is closed
|
|
125
|
+
if hasattr(gen, "close"):
|
|
126
|
+
gen.close()
|
|
127
|
+
raise
|
|
128
|
+
except BaseException as exc:
|
|
129
|
+
# forward thrown exceptions if possible
|
|
130
|
+
if hasattr(gen, "throw"):
|
|
131
|
+
value = gen.throw(type(exc), exc, exc.__traceback__)
|
|
132
|
+
else:
|
|
133
|
+
raise
|
|
134
|
+
else:
|
|
135
|
+
# normal path: forward .send (or .__next__)
|
|
136
|
+
if hasattr(gen, "send"):
|
|
137
|
+
value = gen.send(sent)
|
|
138
|
+
else:
|
|
139
|
+
value = next(gen)
|
|
140
|
+
except StopIteration as e:
|
|
141
|
+
yield from self._yield_and_clear_tasks()
|
|
142
|
+
return TaskTracker._durable_serializer(e.value)
|
|
143
|
+
except YieldException as e:
|
|
144
|
+
yield from self._yield_and_clear_tasks()
|
|
145
|
+
yield e.task
|
|
146
|
+
else:
|
|
147
|
+
try:
|
|
148
|
+
result = func()
|
|
149
|
+
return TaskTracker._durable_serializer(result)
|
|
150
|
+
except YieldException as e:
|
|
151
|
+
yield from self._yield_and_clear_tasks()
|
|
152
|
+
yield e.task
|
|
153
|
+
finally:
|
|
154
|
+
yield from self._yield_and_clear_tasks()
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _durable_serializer(obj: Any) -> str:
|
|
158
|
+
# Strings are already "serialized"
|
|
159
|
+
if type(obj) is str:
|
|
160
|
+
return obj
|
|
161
|
+
|
|
162
|
+
# Serialize "Durable" and OpenAI models, and typed dictionaries
|
|
163
|
+
if callable(getattr(obj, "to_json", None)):
|
|
164
|
+
return obj.to_json()
|
|
165
|
+
|
|
166
|
+
# Serialize Pydantic models
|
|
167
|
+
if callable(getattr(obj, "model_dump_json", None)):
|
|
168
|
+
return obj.model_dump_json()
|
|
169
|
+
|
|
170
|
+
# Fallback to default JSON serialization
|
|
171
|
+
return json.dumps(obj)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Tool conversion utilities for Azure Durable Functions OpenAI agent operations."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
from agents import (
|
|
8
|
+
CodeInterpreterTool,
|
|
9
|
+
FileSearchTool,
|
|
10
|
+
FunctionTool,
|
|
11
|
+
HostedMCPTool,
|
|
12
|
+
ImageGenerationTool,
|
|
13
|
+
Tool,
|
|
14
|
+
UserError,
|
|
15
|
+
WebSearchTool,
|
|
16
|
+
)
|
|
17
|
+
from openai.types.responses.tool_param import Mcp
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Built-in tool types that can be serialized directly without conversion
|
|
22
|
+
BUILT_IN_TOOL_TYPES = (
|
|
23
|
+
FileSearchTool,
|
|
24
|
+
WebSearchTool,
|
|
25
|
+
ImageGenerationTool,
|
|
26
|
+
CodeInterpreterTool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DurableFunctionTool(BaseModel):
|
|
31
|
+
"""Serializable representation of a FunctionTool.
|
|
32
|
+
|
|
33
|
+
Contains only the data needed by the model execution to
|
|
34
|
+
determine what tool to call, not the actual tool invocation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
description: str
|
|
39
|
+
params_json_schema: dict[str, Any]
|
|
40
|
+
strict_json_schema: bool = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DurableMCPToolConfig(BaseModel):
|
|
44
|
+
"""Serializable representation of a HostedMCPTool.
|
|
45
|
+
|
|
46
|
+
Contains only the data needed by the model execution to
|
|
47
|
+
determine what tool to call, not the actual tool invocation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
tool_config: Mcp
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
DurableTool = Union[
|
|
54
|
+
DurableFunctionTool,
|
|
55
|
+
FileSearchTool,
|
|
56
|
+
WebSearchTool,
|
|
57
|
+
ImageGenerationTool,
|
|
58
|
+
CodeInterpreterTool,
|
|
59
|
+
DurableMCPToolConfig,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_tool_from_durable_tool(
|
|
64
|
+
durable_tool: DurableTool,
|
|
65
|
+
) -> Tool:
|
|
66
|
+
"""Convert a DurableTool to an OpenAI agent Tool for execution.
|
|
67
|
+
|
|
68
|
+
This function transforms Durable Functions tool definitions into actual
|
|
69
|
+
OpenAI agent Tool instances that can be used during model execution.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
durable_tool : DurableTool
|
|
74
|
+
The Durable tool definition to convert
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Tool
|
|
79
|
+
An OpenAI agent Tool instance ready for execution
|
|
80
|
+
|
|
81
|
+
Raises
|
|
82
|
+
------
|
|
83
|
+
UserError
|
|
84
|
+
If the tool type is not supported
|
|
85
|
+
"""
|
|
86
|
+
# Built-in tools that don't need conversion
|
|
87
|
+
if isinstance(durable_tool, BUILT_IN_TOOL_TYPES):
|
|
88
|
+
return durable_tool
|
|
89
|
+
|
|
90
|
+
# Convert Durable MCP tool configuration to HostedMCPTool
|
|
91
|
+
if isinstance(durable_tool, DurableMCPToolConfig):
|
|
92
|
+
return HostedMCPTool(
|
|
93
|
+
tool_config=durable_tool.tool_config,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Convert Durable function tool to FunctionTool
|
|
97
|
+
if isinstance(durable_tool, DurableFunctionTool):
|
|
98
|
+
return FunctionTool(
|
|
99
|
+
name=durable_tool.name,
|
|
100
|
+
description=durable_tool.description,
|
|
101
|
+
params_json_schema=durable_tool.params_json_schema,
|
|
102
|
+
on_invoke_tool=lambda ctx, input: "",
|
|
103
|
+
strict_json_schema=durable_tool.strict_json_schema,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
raise UserError(f"Unsupported tool type: {durable_tool}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def convert_tool_to_durable_tool(tool: Tool) -> DurableTool:
|
|
110
|
+
"""Convert an OpenAI agent Tool to a DurableTool for serialization.
|
|
111
|
+
|
|
112
|
+
This function transforms OpenAI agent Tool instances into Durable Functions
|
|
113
|
+
tool definitions that can be serialized and passed to activities.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
tool : Tool
|
|
118
|
+
The OpenAI agent Tool to convert
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
DurableTool
|
|
123
|
+
A serializable tool definition
|
|
124
|
+
|
|
125
|
+
Raises
|
|
126
|
+
------
|
|
127
|
+
ValueError
|
|
128
|
+
If the tool type is not supported for conversion
|
|
129
|
+
"""
|
|
130
|
+
# Built-in tools that can be serialized directly
|
|
131
|
+
if isinstance(tool, BUILT_IN_TOOL_TYPES):
|
|
132
|
+
return tool
|
|
133
|
+
|
|
134
|
+
# Convert HostedMCPTool to Durable MCP configuration
|
|
135
|
+
elif isinstance(tool, HostedMCPTool):
|
|
136
|
+
return DurableMCPToolConfig(tool_config=tool.tool_config)
|
|
137
|
+
|
|
138
|
+
# Convert FunctionTool to Durable function tool
|
|
139
|
+
elif isinstance(tool, FunctionTool):
|
|
140
|
+
return DurableFunctionTool(
|
|
141
|
+
name=tool.name,
|
|
142
|
+
description=tool.description,
|
|
143
|
+
params_json_schema=tool.params_json_schema,
|
|
144
|
+
strict_json_schema=tool.strict_json_schema,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError(f"Unsupported tool type for Durable Functions: {type(tool).__name__}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UsageTelemetry:
|
|
6
|
+
"""Handles telemetry logging for OpenAI Agents SDK integration usage."""
|
|
7
|
+
|
|
8
|
+
# Class-level flag to ensure logging happens only once across all instances
|
|
9
|
+
_usage_logged = False
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def log_usage_once(cls):
|
|
13
|
+
"""Log OpenAI Agents SDK integration usage exactly once.
|
|
14
|
+
|
|
15
|
+
Fails gracefully if metadata cannot be retrieved.
|
|
16
|
+
"""
|
|
17
|
+
if cls._usage_logged:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# NOTE: Any log line beginning with the special prefix defined below will be
|
|
21
|
+
# captured by the Azure Functions host as a Language Worker console log and
|
|
22
|
+
# forwarded to internal telemetry pipelines.
|
|
23
|
+
# Do not change this constant value without coordinating with the Functions
|
|
24
|
+
# host team.
|
|
25
|
+
LANGUAGE_WORKER_CONSOLE_LOG_PREFIX = "LanguageWorkerConsoleLog"
|
|
26
|
+
|
|
27
|
+
package_versions = cls._collect_openai_agent_package_versions()
|
|
28
|
+
msg = (
|
|
29
|
+
f"{LANGUAGE_WORKER_CONSOLE_LOG_PREFIX}" # Prefix captured by Azure Functions host
|
|
30
|
+
"Detected OpenAI Agents SDK integration with Durable Functions. "
|
|
31
|
+
f"Package versions: {package_versions}"
|
|
32
|
+
)
|
|
33
|
+
print(msg)
|
|
34
|
+
|
|
35
|
+
cls._usage_logged = True
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _collect_openai_agent_package_versions(cls) -> str:
|
|
39
|
+
"""Collect versions of relevant packages for telemetry logging.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
str
|
|
44
|
+
Comma-separated list of name=version entries or "(unavailable)" if
|
|
45
|
+
versions could not be determined.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
try:
|
|
49
|
+
from importlib import metadata # Python 3.8+
|
|
50
|
+
except ImportError: # pragma: no cover - legacy fallback
|
|
51
|
+
import importlib_metadata as metadata # type: ignore
|
|
52
|
+
|
|
53
|
+
package_names = [
|
|
54
|
+
"azure-functions-durable",
|
|
55
|
+
"openai",
|
|
56
|
+
"openai-agents",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
versions = []
|
|
60
|
+
for package_name in package_names:
|
|
61
|
+
try:
|
|
62
|
+
ver = metadata.version(package_name)
|
|
63
|
+
versions.append(f"{package_name}={ver}")
|
|
64
|
+
except Exception: # noqa: BLE001 - swallow and continue
|
|
65
|
+
versions.append(f"{package_name}=(not installed)")
|
|
66
|
+
|
|
67
|
+
return ", ".join(versions) if versions else "(unavailable)"
|
|
68
|
+
except Exception: # noqa: BLE001 - never let version gathering break user code
|
|
69
|
+
return "(unavailable)"
|
{azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: azure-functions-durable
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0rc2
|
|
4
4
|
Summary: Durable Functions For Python
|
|
5
5
|
Home-page: https://github.com/Azure/azure-functions-durable-python
|
|
6
6
|
Author: Azure Functions team at Microsoft Corp.
|
|
@@ -58,4 +58,9 @@ Follow these instructions to get started with Durable Functions in Python:
|
|
|
58
58
|
|
|
59
59
|
* Python Durable Functions requires [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) version 3.0.2630 or higher.
|
|
60
60
|
|
|
61
|
+
## Durable OpenAI Agents (Preview)
|
|
62
|
+
|
|
63
|
+
Build resilient, stateful AI agents backed by Durable Functions orchestration—see the full documentation at [docs/openai_agents/README.md](docs/openai_agents/README.md).
|
|
64
|
+
|
|
65
|
+
|
|
61
66
|
|
{azure_functions_durable-1.3.3.dist-info → azure_functions_durable-1.4.0rc2.dist-info}/RECORD
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
azure/durable_functions/__init__.py,sha256=
|
|
1
|
+
azure/durable_functions/__init__.py,sha256=aTQRMnMb3zT3w28yEbYsEl-_f3x2Rf-kKp91E9nE8nk,3150
|
|
2
2
|
azure/durable_functions/constants.py,sha256=JtknDhaVihMeo-ygY9QNofiO2KEqnQvopdfZ6Qatnik,414
|
|
3
3
|
azure/durable_functions/entity.py,sha256=mUUzb1BZiDrUJjvxOTlnVURnKPyDGPJ3mXXMN0DKT7M,4649
|
|
4
4
|
azure/durable_functions/orchestrator.py,sha256=SZni90Aweq0OZykHyMblfJpUndJ2woJmySarcsDiIK4,2554
|
|
5
5
|
azure/durable_functions/decorators/__init__.py,sha256=wEubgP2rUUISwidZWgKx6mmzEeGKsSnpGmetjIUi1nw,150
|
|
6
|
-
azure/durable_functions/decorators/durable_app.py,sha256=
|
|
6
|
+
azure/durable_functions/decorators/durable_app.py,sha256=5ajbhMPKvZKZhYLCxsEn8J4sZO6Pql9j0nOTCxDaMuY,12599
|
|
7
7
|
azure/durable_functions/decorators/metadata.py,sha256=p91rdCe6OSRYJaKAXnrfR0QCV3PoHK7aGy1m6WAnPIE,2828
|
|
8
8
|
azure/durable_functions/models/DurableEntityContext.py,sha256=cyZmjjZu18oV9S4A2NpnXfjd1JQxPxp9EMmAR424UK0,5830
|
|
9
9
|
azure/durable_functions/models/DurableHttpRequest.py,sha256=a5kgRdg4eA0sgyDcpmQWc0dbwP-o3BwWW2Ive0BYO_Q,2021
|
|
@@ -53,6 +53,17 @@ azure/durable_functions/models/utils/__init__.py,sha256=dQ6-HRUPsCtDIqGjRJ3TA6NX
|
|
|
53
53
|
azure/durable_functions/models/utils/entity_utils.py,sha256=TqNTtRC8VuKFtqWLq9oEAloioV-FyinjgRYVKkCldHo,2881
|
|
54
54
|
azure/durable_functions/models/utils/http_utils.py,sha256=AoCWjCapd_984J_4296iJ8cNJWEG8GIdhRttBPt0HnA,2551
|
|
55
55
|
azure/durable_functions/models/utils/json_utils.py,sha256=zUn62pm3dQw054ZlK7F4uRP-UELjQC8EmZBU1WncHMg,3811
|
|
56
|
+
azure/durable_functions/openai_agents/__init__.py,sha256=pAUkXR5ctS0leHiR0IwBC1aHurzOn70wasL-LDRcRnQ,374
|
|
57
|
+
azure/durable_functions/openai_agents/context.py,sha256=tShhQmlMxvOGHvYu55e10xPp2mVHmeIUf37yUzzjE50,7551
|
|
58
|
+
azure/durable_functions/openai_agents/event_loop.py,sha256=JxezIdw1TL-R77ITlarZZvk3F1IMXHqGUAuCgxKX5Wk,571
|
|
59
|
+
azure/durable_functions/openai_agents/exceptions.py,sha256=AmbFAxpkrQ-LiU_LaU7-aWpb6dR-ETMs_XWkoCfZIoQ,394
|
|
60
|
+
azure/durable_functions/openai_agents/handoffs.py,sha256=R8fuQ-FlZ3DQw1jI0WwU8gWwTlWdRLWW2EVrwsfDEuU,2195
|
|
61
|
+
azure/durable_functions/openai_agents/model_invocation_activity.py,sha256=P94iMKVq9qfZyC_fUQWXb-tuOhvANaGhGvqVwUGfFVk,9783
|
|
62
|
+
azure/durable_functions/openai_agents/orchestrator_generator.py,sha256=dvm5e2pk-wtpTog0XZ4iMy1eskLOgwHcMwswiZ3zCA8,2898
|
|
63
|
+
azure/durable_functions/openai_agents/runner.py,sha256=qfsX52zaTDJpuU3_hxgkrEUtoHQ3GTMm5VYSgUXXsjA,3726
|
|
64
|
+
azure/durable_functions/openai_agents/task_tracker.py,sha256=dBGIGlkIdbwxH2OJ3XJZBFazzC32JZ9hWpN4isxvYLQ,7749
|
|
65
|
+
azure/durable_functions/openai_agents/tools.py,sha256=YahQOV1Duv0217tEwG6SX8gLLTvkQNLFi9E_6KUOjvI,4229
|
|
66
|
+
azure/durable_functions/openai_agents/usage_telemetry.py,sha256=U4nqLJIv_vzxT2Y5FIUi9NkvCyHex1OY4eWY8flydNs,2646
|
|
56
67
|
azure/durable_functions/testing/OrchestratorGeneratorWrapper.py,sha256=cjh-HAq5rVNCoR0pIbfGrqy6cKSf4S1KMQxrBMWU1-s,1728
|
|
57
68
|
azure/durable_functions/testing/__init__.py,sha256=NLbltPtoPXK-0iMTwcKTKPjQlAWrEq55oDYmrhYz6vg,189
|
|
58
69
|
tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -65,6 +76,10 @@ tests/models/test_DurableOrchestrationStatus.py,sha256=fnUZxrHGy771OoaD5TInELhaG
|
|
|
65
76
|
tests/models/test_OrchestrationState.py,sha256=L-k8ScrqoDIZEqIUORbxXA7yCuMbVAUPr-7VmyuQkUc,1272
|
|
66
77
|
tests/models/test_RpcManagementOptions.py,sha256=hvDzlJED8egJloju5nFvKYusgwLgy-o_avJAY6uzfdg,3190
|
|
67
78
|
tests/models/test_TokenSource.py,sha256=wlRn-RPM72U6Ose4sa3Yvu2ng1VbAopDIbea90CYDjk,589
|
|
79
|
+
tests/openai_agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
|
+
tests/openai_agents/test_context.py,sha256=r5nGikUU2j23TmyknruR1ntP4UvxyvYrWil9Ap11qs4,21861
|
|
81
|
+
tests/openai_agents/test_task_tracker.py,sha256=6vjalKmCb-CjpJysx7D0s_3CZaOPycE76TQpcX0ludU,13263
|
|
82
|
+
tests/openai_agents/test_usage_telemetry.py,sha256=gB8efDTZhO9UvCwRTqmCELcgmtwbhSoA6_1Hy7kPCDQ,4825
|
|
68
83
|
tests/orchestrator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
84
|
tests/orchestrator/orchestrator_test_utils.py,sha256=ldgQGGuOVALcI98enRU08nF_VJd5ypJl-hVG-q0kH1o,5359
|
|
70
85
|
tests/orchestrator/test_call_http.py,sha256=CvemeCayrQLjmjl3lpB1fk_CpV-DRPgfxLgD4iNgStQ,9103
|
|
@@ -83,6 +98,8 @@ tests/orchestrator/test_sub_orchestrator.py,sha256=QUb5Q3nLcCdhFwGaEQlYdoNQLUvI8
|
|
|
83
98
|
tests/orchestrator/test_sub_orchestrator_with_retry.py,sha256=bfufQnteEZ9i3q7wzCvd8EwFmaOh89KPOpTPSJvlP1I,6040
|
|
84
99
|
tests/orchestrator/test_task_any.py,sha256=9PQalbQW-Qx7_iO4Yjl1MR2hhsBjst6k00_4veIt97g,2695
|
|
85
100
|
tests/orchestrator/models/OrchestrationInstance.py,sha256=CQ3qyNumjksuFNMujbESsvjadntP3b9VtTpEuI4hzaE,491
|
|
101
|
+
tests/orchestrator/openai_agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
102
|
+
tests/orchestrator/openai_agents/test_openai_agents.py,sha256=qa8DLkJ1IZV9C5kP2mnFfchriv4ppl1nIu9J_3rNlhA,24221
|
|
86
103
|
tests/orchestrator/schemas/OrchetrationStateSchema.py,sha256=EyTsDUZ3K-9mVljLIlg8f_h2zsK228E0PMvvtyEWw24,2718
|
|
87
104
|
tests/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
88
105
|
tests/tasks/tasks_test_utils.py,sha256=Ymc5GESJpzybBq3n2mT4IABDRNTVCerAZrlMilv0Pdk,737
|
|
@@ -96,8 +113,8 @@ tests/test_utils/json_utils.py,sha256=B0q3COMya7TGxbH-7sD_0ypWDSuaF4fpD4QV_oJPgG
|
|
|
96
113
|
tests/test_utils/testClasses.py,sha256=U_u5qKxC9U81SzjLo7ejjPjEn_cE5qjaqoq8edGD6l8,1521
|
|
97
114
|
tests/utils/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
98
115
|
tests/utils/test_entity_utils.py,sha256=kdk5_DV_-bFu_5q2mw9o1yjyzh8Lcxv1jo1Q7is_ukA,748
|
|
99
|
-
azure_functions_durable-1.
|
|
100
|
-
azure_functions_durable-1.
|
|
101
|
-
azure_functions_durable-1.
|
|
102
|
-
azure_functions_durable-1.
|
|
103
|
-
azure_functions_durable-1.
|
|
116
|
+
azure_functions_durable-1.4.0rc2.dist-info/LICENSE,sha256=-VS-Izmxdykuae1Xc4vHtVUx02rNQi6SSQlONvvuYeQ,1090
|
|
117
|
+
azure_functions_durable-1.4.0rc2.dist-info/METADATA,sha256=HOJS4Ub2kDvHZtLcJ1XLNp6P7-yqLO1tDBqGgKpJrHM,3742
|
|
118
|
+
azure_functions_durable-1.4.0rc2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
119
|
+
azure_functions_durable-1.4.0rc2.dist-info/top_level.txt,sha256=h-L8XDVPJ9YzBbHlPvM7FVo1cqNGToNK9ix99ySGOUY,12
|
|
120
|
+
azure_functions_durable-1.4.0rc2.dist-info/RECORD,,
|
|
File without changes
|