1
- from typing import Any , Callable , Optional
1
+ import json
2
+ from typing import Any , Callable , Optional , TYPE_CHECKING , Union
2
3
3
4
from azure .durable_functions .models .DurableOrchestrationContext import (
4
5
DurableOrchestrationContext ,
8
9
from agents import RunContextWrapper , Tool
9
10
from agents .function_schema import function_schema
10
11
from agents .tool import FunctionTool
12
+
13
+ from azure .durable_functions .models .Task import TaskBase
11
14
from .task_tracker import TaskTracker
12
15
13
16
14
- class DurableAIAgentContext :
15
- """Context for AI agents running in Azure Durable Functions orchestration."""
17
+ if TYPE_CHECKING :
18
+ # At type-check time we want all members / signatures for IDE & linters.
19
+ _BaseDurableContext = DurableOrchestrationContext
20
+ else :
21
+ class _BaseDurableContext : # lightweight runtime stub
22
+ """Runtime stub base class for delegation; real context is wrapped.
23
+
24
+ At runtime we avoid inheriting from DurableOrchestrationContext so that
25
+ attribute lookups for its members are delegated via __getattr__ to the
26
+ wrapped ``_context`` instance.
27
+ """
28
+
29
+ __slots__ = ()
30
+
31
+
32
+ class DurableAIAgentContext (_BaseDurableContext ):
33
+ """Context for AI agents running in Azure Durable Functions orchestration.
34
+
35
+ Design
36
+ ------
37
+ * Static analysis / IDEs: Appears to subclass ``DurableOrchestrationContext`` so
38
+ you get autocompletion and type hints (under TYPE_CHECKING branch).
39
+ * Runtime: Inherits from a trivial stub. All durable orchestration operations
40
+ are delegated to the real ``DurableOrchestrationContext`` instance provided
41
+ as ``context`` and stored in ``_context``.
42
+
43
+ Consequences
44
+ ------------
45
+ * ``isinstance(DurableAIAgentContext, DurableOrchestrationContext)`` is **False** at
46
+ runtime (expected).
47
+ * Delegation via ``__getattr__`` works for every member of the real context.
48
+ * No reliance on internal initialization side-effects of the durable SDK.
49
+ """
16
50
17
51
def __init__ (
18
52
self ,
@@ -24,28 +58,56 @@ def __init__(
24
58
self ._task_tracker = task_tracker
25
59
self ._model_retry_options = model_retry_options
26
60
27
- def call_activity (self , activity_name , input : str ):
28
- """Call an activity function and record the activity call."""
29
- task = self ._context .call_activity (activity_name , input )
61
+ def call_activity (
62
+ self , name : Union [str , Callable ], input_ : Optional [Any ] = None
63
+ ) -> TaskBase :
64
+ """Schedule an activity for execution.
65
+
66
+ Parameters
67
+ ----------
68
+ name: str | Callable
69
+ Either the name of the activity function to call, as a string or,
70
+ in the Python V2 programming model, the activity function itself.
71
+ input_: Optional[Any]
72
+ The JSON-serializable input to pass to the activity function.
73
+
74
+ Returns
75
+ -------
76
+ Task
77
+ A Durable Task that completes when the called activity function completes or fails.
78
+ """
79
+ task = self ._context .call_activity (name , input_ )
30
80
self ._task_tracker .record_activity_call ()
31
81
return task
32
82
33
83
def call_activity_with_retry (
34
- self , activity_name , retry_options : RetryOptions , input : str = None
35
- ):
36
- """Call an activity function with retry options and record the activity call."""
37
- task = self ._context .call_activity_with_retry (activity_name , retry_options , input )
84
+ self ,
85
+ name : Union [str , Callable ],
86
+ retry_options : RetryOptions ,
87
+ input_ : Optional [Any ] = None ,
88
+ ) -> TaskBase :
89
+ """Schedule an activity for execution with retry options.
90
+
91
+ Parameters
92
+ ----------
93
+ name: str | Callable
94
+ Either the name of the activity function to call, as a string or,
95
+ in the Python V2 programming model, the activity function itself.
96
+ retry_options: RetryOptions
97
+ The retry options for the activity function.
98
+ input_: Optional[Any]
99
+ The JSON-serializable input to pass to the activity function.
100
+
101
+ Returns
102
+ -------
103
+ Task
104
+ A Durable Task that completes when the called activity function completes or
105
+ fails completely.
106
+ """
107
+ task = self ._context .call_activity_with_retry (name , retry_options , input_ )
38
108
self ._task_tracker .record_activity_call ()
39
109
return task
40
110
41
- def set_custom_status (self , status : str ):
42
- """Set custom status for the orchestration."""
43
- self ._context .set_custom_status (status )
44
-
45
- def wait_for_external_event (self , event_name : str ):
46
- """Wait for an external event in the orchestration."""
47
- return self ._context .wait_for_external_event (event_name )
48
-
49
111
def create_activity_tool (
50
112
self ,
51
113
activity_func : Callable ,
@@ -77,13 +139,29 @@ def create_activity_tool(
77
139
else :
78
140
activity_name = activity_func ._function ._name
79
141
142
+ input_name = None
143
+ if (activity_func ._function ._trigger is not None
144
+ and hasattr (activity_func ._function ._trigger , 'name' )):
145
+ input_name = activity_func ._function ._trigger .name
146
+
80
147
async def run_activity (ctx : RunContextWrapper [Any ], input : str ) -> Any :
148
+ # Parse JSON input and extract the named value if input_name is specified
149
+ activity_input = input
150
+ if input_name :
151
+ try :
152
+ parsed_input = json .loads (input )
153
+ if isinstance (parsed_input , dict ) and input_name in parsed_input :
154
+ activity_input = parsed_input [input_name ]
155
+ # If parsing fails or the named parameter is not found, pass the original input
156
+ except (json .JSONDecodeError , TypeError ):
157
+ pass
158
+
81
159
if retry_options :
82
160
result = self ._task_tracker .get_activity_call_result_with_retry (
83
- activity_name , retry_options , input
161
+ activity_name , retry_options , activity_input
84
162
)
85
163
else :
86
- result = self ._task_tracker .get_activity_call_result (activity_name , input )
164
+ result = self ._task_tracker .get_activity_call_result (activity_name , activity_input )
87
165
return result
88
166
89
167
schema = function_schema (
@@ -101,3 +179,14 @@ async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any:
101
179
on_invoke_tool = run_activity ,
102
180
strict_json_schema = True ,
103
181
)
182
+
183
+ def __getattr__ (self , name ):
184
+ """Delegate missing attributes to the underlying DurableOrchestrationContext."""
185
+ try :
186
+ return getattr (self ._context , name )
187
+ except AttributeError :
188
+ raise AttributeError (f"'{ type (self ).__name__ } ' object has no attribute '{ name } '" )
189
+
190
+ def __dir__ (self ):
191
+ """Improve introspection and tab-completion by including delegated attributes."""
192
+ return sorted (set (dir (type (self )) + list (self .__dict__ ) + dir (self ._context )))
0 commit comments