azure-functions-durable 1.3.2__py3-none-any.whl → 1.4.0__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/models/DurableOrchestrationContext.py +24 -0
- 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.2.dist-info → azure_functions_durable-1.4.0.dist-info}/METADATA +7 -2
- {azure_functions_durable-1.3.2.dist-info → azure_functions_durable-1.4.0.dist-info}/RECORD +26 -9
- tests/models/test_DurableOrchestrationContext.py +8 -0
- 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.2.dist-info → azure_functions_durable-1.4.0.dist-info}/LICENSE +0 -0
- {azure_functions_durable-1.3.2.dist-info → azure_functions_durable-1.4.0.dist-info}/WHEEL +0 -0
- {azure_functions_durable-1.3.2.dist-info → azure_functions_durable-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from azure.durable_functions.openai_agents.context import DurableAIAgentContext
|
|
7
|
+
from azure.durable_functions.openai_agents.task_tracker import TaskTracker
|
|
8
|
+
from azure.durable_functions.models.DurableOrchestrationContext import DurableOrchestrationContext
|
|
9
|
+
from azure.durable_functions.models.RetryOptions import RetryOptions
|
|
10
|
+
|
|
11
|
+
from agents.tool import FunctionTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestDurableAIAgentContext:
|
|
15
|
+
"""Test suite for DurableAIAgentContext class."""
|
|
16
|
+
|
|
17
|
+
def _create_mock_orchestration_context(self):
|
|
18
|
+
"""Create a mock DurableOrchestrationContext for testing."""
|
|
19
|
+
orchestration_context = Mock(spec=DurableOrchestrationContext)
|
|
20
|
+
orchestration_context.call_activity = Mock(return_value="mock_task")
|
|
21
|
+
orchestration_context.call_activity_with_retry = Mock(return_value="mock_task_with_retry")
|
|
22
|
+
orchestration_context.instance_id = "test_instance_id"
|
|
23
|
+
orchestration_context.current_utc_datetime = "2023-01-01T00:00:00Z"
|
|
24
|
+
orchestration_context.is_replaying = False
|
|
25
|
+
return orchestration_context
|
|
26
|
+
|
|
27
|
+
def _create_mock_task_tracker(self):
|
|
28
|
+
"""Create a mock TaskTracker for testing."""
|
|
29
|
+
task_tracker = Mock(spec=TaskTracker)
|
|
30
|
+
task_tracker.record_activity_call = Mock()
|
|
31
|
+
task_tracker.get_activity_call_result = Mock(return_value="activity_result")
|
|
32
|
+
task_tracker.get_activity_call_result_with_retry = Mock(return_value="retry_activity_result")
|
|
33
|
+
return task_tracker
|
|
34
|
+
|
|
35
|
+
def _create_mock_activity_func(self, name="test_activity", input_name=None,
|
|
36
|
+
activity_name=None):
|
|
37
|
+
"""Create a mock activity function with configurable parameters."""
|
|
38
|
+
mock_activity_func = Mock()
|
|
39
|
+
mock_activity_func._function._name = name
|
|
40
|
+
mock_activity_func._function._func = lambda x: x
|
|
41
|
+
|
|
42
|
+
if input_name is not None:
|
|
43
|
+
# Create trigger with input_name
|
|
44
|
+
mock_activity_func._function._trigger = Mock()
|
|
45
|
+
mock_activity_func._function._trigger.activity = activity_name
|
|
46
|
+
mock_activity_func._function._trigger.name = input_name
|
|
47
|
+
else:
|
|
48
|
+
# No trigger means no input_name
|
|
49
|
+
mock_activity_func._function._trigger = None
|
|
50
|
+
|
|
51
|
+
return mock_activity_func
|
|
52
|
+
|
|
53
|
+
def _setup_activity_tool_mocks(self, mock_function_tool, mock_function_schema,
|
|
54
|
+
activity_name="test_activity", description=""):
|
|
55
|
+
"""Setup common mocks for function_schema and FunctionTool."""
|
|
56
|
+
mock_schema = Mock()
|
|
57
|
+
mock_schema.name = activity_name
|
|
58
|
+
mock_schema.description = description
|
|
59
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
60
|
+
mock_function_schema.return_value = mock_schema
|
|
61
|
+
|
|
62
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
63
|
+
mock_function_tool.return_value = mock_tool
|
|
64
|
+
|
|
65
|
+
return mock_tool
|
|
66
|
+
|
|
67
|
+
def _invoke_activity_tool(self, run_activity, input_data):
|
|
68
|
+
"""Helper to invoke the activity tool with asyncio."""
|
|
69
|
+
mock_ctx = Mock()
|
|
70
|
+
import asyncio
|
|
71
|
+
return asyncio.run(run_activity(mock_ctx, input_data))
|
|
72
|
+
|
|
73
|
+
def _test_activity_tool_input_processing(self, input_name=None, input_data="",
|
|
74
|
+
expected_input_parameter_value="",
|
|
75
|
+
retry_options=None,
|
|
76
|
+
activity_name="test_activity"):
|
|
77
|
+
"""Framework method that runs a complete input processing test."""
|
|
78
|
+
with patch('azure.durable_functions.openai_agents.context.function_schema') \
|
|
79
|
+
as mock_function_schema, \
|
|
80
|
+
patch('azure.durable_functions.openai_agents.context.FunctionTool') \
|
|
81
|
+
as mock_function_tool:
|
|
82
|
+
|
|
83
|
+
# Setup
|
|
84
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
85
|
+
task_tracker = self._create_mock_task_tracker()
|
|
86
|
+
mock_activity_func = self._create_mock_activity_func(
|
|
87
|
+
name=activity_name, input_name=input_name)
|
|
88
|
+
self._setup_activity_tool_mocks(
|
|
89
|
+
mock_function_tool, mock_function_schema, activity_name)
|
|
90
|
+
|
|
91
|
+
# Create context and tool
|
|
92
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
93
|
+
ai_context.create_activity_tool(mock_activity_func, retry_options=retry_options)
|
|
94
|
+
|
|
95
|
+
# Get and invoke the run_activity function
|
|
96
|
+
call_args = mock_function_tool.call_args
|
|
97
|
+
run_activity = call_args[1]['on_invoke_tool']
|
|
98
|
+
self._invoke_activity_tool(run_activity, input_data)
|
|
99
|
+
|
|
100
|
+
# Verify the expected call was made
|
|
101
|
+
if retry_options:
|
|
102
|
+
task_tracker.get_activity_call_result_with_retry.assert_called_once_with(
|
|
103
|
+
activity_name, retry_options, expected_input_parameter_value
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
task_tracker.get_activity_call_result.assert_called_once_with(
|
|
107
|
+
activity_name, expected_input_parameter_value
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def test_init_creates_context_successfully(self):
|
|
111
|
+
"""Test that __init__ creates a DurableAIAgentContext successfully."""
|
|
112
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
113
|
+
task_tracker = self._create_mock_task_tracker()
|
|
114
|
+
retry_options = RetryOptions(1000, 3)
|
|
115
|
+
|
|
116
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, retry_options)
|
|
117
|
+
|
|
118
|
+
assert isinstance(ai_context, DurableAIAgentContext)
|
|
119
|
+
assert not isinstance(ai_context, DurableOrchestrationContext)
|
|
120
|
+
|
|
121
|
+
def test_call_activity_delegates_and_records(self):
|
|
122
|
+
"""Test that call_activity delegates to context and records activity call."""
|
|
123
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
124
|
+
task_tracker = self._create_mock_task_tracker()
|
|
125
|
+
|
|
126
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
127
|
+
result = ai_context.call_activity("test_activity", "test_input")
|
|
128
|
+
|
|
129
|
+
orchestration_context.call_activity.assert_called_once_with("test_activity", "test_input")
|
|
130
|
+
task_tracker.record_activity_call.assert_called_once()
|
|
131
|
+
assert result == "mock_task"
|
|
132
|
+
|
|
133
|
+
def test_call_activity_with_retry_delegates_and_records(self):
|
|
134
|
+
"""Test that call_activity_with_retry delegates to context and records activity call."""
|
|
135
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
136
|
+
task_tracker = self._create_mock_task_tracker()
|
|
137
|
+
retry_options = RetryOptions(1000, 3)
|
|
138
|
+
|
|
139
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
140
|
+
result = ai_context.call_activity_with_retry("test_activity", retry_options, "test_input")
|
|
141
|
+
|
|
142
|
+
orchestration_context.call_activity_with_retry.assert_called_once_with(
|
|
143
|
+
"test_activity", retry_options, "test_input"
|
|
144
|
+
)
|
|
145
|
+
task_tracker.record_activity_call.assert_called_once()
|
|
146
|
+
assert result == "mock_task_with_retry"
|
|
147
|
+
|
|
148
|
+
@patch('azure.durable_functions.openai_agents.context.function_schema')
|
|
149
|
+
@patch('azure.durable_functions.openai_agents.context.FunctionTool')
|
|
150
|
+
def test_activity_as_tool_creates_function_tool(self, mock_function_tool, mock_function_schema):
|
|
151
|
+
"""Test that create_activity_tool creates a FunctionTool with correct parameters."""
|
|
152
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
153
|
+
task_tracker = self._create_mock_task_tracker()
|
|
154
|
+
|
|
155
|
+
# Mock the activity function
|
|
156
|
+
mock_activity_func = Mock()
|
|
157
|
+
mock_activity_func._function._name = "test_activity"
|
|
158
|
+
mock_activity_func._function._func = lambda x: x
|
|
159
|
+
|
|
160
|
+
# Mock the schema
|
|
161
|
+
mock_schema = Mock()
|
|
162
|
+
mock_schema.name = "test_activity"
|
|
163
|
+
mock_schema.description = "Test activity description"
|
|
164
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
165
|
+
mock_function_schema.return_value = mock_schema
|
|
166
|
+
|
|
167
|
+
# Mock FunctionTool
|
|
168
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
169
|
+
mock_function_tool.return_value = mock_tool
|
|
170
|
+
|
|
171
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
172
|
+
retry_options = RetryOptions(1000, 3)
|
|
173
|
+
|
|
174
|
+
result = ai_context.create_activity_tool(
|
|
175
|
+
mock_activity_func,
|
|
176
|
+
description="Custom description",
|
|
177
|
+
retry_options=retry_options
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Verify function_schema was called correctly
|
|
181
|
+
mock_function_schema.assert_called_once_with(
|
|
182
|
+
func=mock_activity_func._function._func,
|
|
183
|
+
docstring_style=None,
|
|
184
|
+
description_override="Custom description",
|
|
185
|
+
use_docstring_info=True,
|
|
186
|
+
strict_json_schema=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Verify FunctionTool was created correctly
|
|
190
|
+
mock_function_tool.assert_called_once()
|
|
191
|
+
call_args = mock_function_tool.call_args
|
|
192
|
+
assert call_args[1]['name'] == "test_activity"
|
|
193
|
+
assert call_args[1]['description'] == "Test activity description"
|
|
194
|
+
assert call_args[1]['params_json_schema'] == {"type": "object"}
|
|
195
|
+
assert call_args[1]['strict_json_schema'] is True
|
|
196
|
+
assert callable(call_args[1]['on_invoke_tool'])
|
|
197
|
+
|
|
198
|
+
assert result is mock_tool
|
|
199
|
+
|
|
200
|
+
@patch('azure.durable_functions.openai_agents.context.function_schema')
|
|
201
|
+
@patch('azure.durable_functions.openai_agents.context.FunctionTool')
|
|
202
|
+
def test_activity_as_tool_with_default_retry_options(self, mock_function_tool, mock_function_schema):
|
|
203
|
+
"""Test that create_activity_tool uses default retry options when none provided."""
|
|
204
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
205
|
+
task_tracker = self._create_mock_task_tracker()
|
|
206
|
+
|
|
207
|
+
mock_activity_func = Mock()
|
|
208
|
+
mock_activity_func._function._name = "test_activity"
|
|
209
|
+
mock_activity_func._function._func = lambda x: x
|
|
210
|
+
|
|
211
|
+
mock_schema = Mock()
|
|
212
|
+
mock_schema.name = "test_activity"
|
|
213
|
+
mock_schema.description = "Test description"
|
|
214
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
215
|
+
mock_function_schema.return_value = mock_schema
|
|
216
|
+
|
|
217
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
218
|
+
mock_function_tool.return_value = mock_tool
|
|
219
|
+
|
|
220
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
221
|
+
|
|
222
|
+
# Call with default retry options
|
|
223
|
+
result = ai_context.create_activity_tool(mock_activity_func)
|
|
224
|
+
|
|
225
|
+
# Should still create the tool successfully
|
|
226
|
+
assert result is mock_tool
|
|
227
|
+
mock_function_tool.assert_called_once()
|
|
228
|
+
|
|
229
|
+
@patch('azure.durable_functions.openai_agents.context.function_schema')
|
|
230
|
+
@patch('azure.durable_functions.openai_agents.context.FunctionTool')
|
|
231
|
+
def test_activity_as_tool_run_activity_with_retry(self, mock_function_tool, mock_function_schema):
|
|
232
|
+
"""Test that the run_activity function calls task tracker with retry options."""
|
|
233
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
234
|
+
task_tracker = self._create_mock_task_tracker()
|
|
235
|
+
|
|
236
|
+
mock_activity_func = Mock()
|
|
237
|
+
mock_activity_func._function._name = "test_activity"
|
|
238
|
+
mock_activity_func._function._trigger = None
|
|
239
|
+
mock_activity_func._function._func = lambda x: x
|
|
240
|
+
|
|
241
|
+
mock_schema = Mock()
|
|
242
|
+
mock_schema.name = "test_activity"
|
|
243
|
+
mock_schema.description = ""
|
|
244
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
245
|
+
mock_function_schema.return_value = mock_schema
|
|
246
|
+
|
|
247
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
248
|
+
mock_function_tool.return_value = mock_tool
|
|
249
|
+
|
|
250
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
251
|
+
retry_options = RetryOptions(1000, 3)
|
|
252
|
+
|
|
253
|
+
ai_context.create_activity_tool(mock_activity_func, retry_options=retry_options)
|
|
254
|
+
|
|
255
|
+
# Get the run_activity function that was passed to FunctionTool
|
|
256
|
+
call_args = mock_function_tool.call_args
|
|
257
|
+
run_activity = call_args[1]['on_invoke_tool']
|
|
258
|
+
|
|
259
|
+
# Create a mock context wrapper
|
|
260
|
+
mock_ctx = Mock()
|
|
261
|
+
|
|
262
|
+
# Call the run_activity function
|
|
263
|
+
import asyncio
|
|
264
|
+
result = asyncio.run(run_activity(mock_ctx, "test_input"))
|
|
265
|
+
|
|
266
|
+
# Verify the task tracker was called with retry options
|
|
267
|
+
task_tracker.get_activity_call_result_with_retry.assert_called_once_with(
|
|
268
|
+
"test_activity", retry_options, "test_input"
|
|
269
|
+
)
|
|
270
|
+
assert result == "retry_activity_result"
|
|
271
|
+
|
|
272
|
+
@patch('azure.durable_functions.openai_agents.context.function_schema')
|
|
273
|
+
@patch('azure.durable_functions.openai_agents.context.FunctionTool')
|
|
274
|
+
def test_activity_as_tool_run_activity_without_retry(self, mock_function_tool, mock_function_schema):
|
|
275
|
+
"""Test that the run_activity function calls task tracker without retry when retry_options is None."""
|
|
276
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
277
|
+
task_tracker = self._create_mock_task_tracker()
|
|
278
|
+
|
|
279
|
+
mock_activity_func = Mock()
|
|
280
|
+
mock_activity_func._function._name = "test_activity"
|
|
281
|
+
mock_activity_func._function._trigger = None
|
|
282
|
+
mock_activity_func._function._func = lambda x: x
|
|
283
|
+
|
|
284
|
+
mock_schema = Mock()
|
|
285
|
+
mock_schema.name = "test_activity"
|
|
286
|
+
mock_schema.description = ""
|
|
287
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
288
|
+
mock_function_schema.return_value = mock_schema
|
|
289
|
+
|
|
290
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
291
|
+
mock_function_tool.return_value = mock_tool
|
|
292
|
+
|
|
293
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
294
|
+
|
|
295
|
+
ai_context.create_activity_tool(mock_activity_func, retry_options=None)
|
|
296
|
+
|
|
297
|
+
# Get the run_activity function that was passed to FunctionTool
|
|
298
|
+
call_args = mock_function_tool.call_args
|
|
299
|
+
run_activity = call_args[1]['on_invoke_tool']
|
|
300
|
+
|
|
301
|
+
# Create a mock context wrapper
|
|
302
|
+
mock_ctx = Mock()
|
|
303
|
+
|
|
304
|
+
# Call the run_activity function
|
|
305
|
+
import asyncio
|
|
306
|
+
result = asyncio.run(run_activity(mock_ctx, "test_input"))
|
|
307
|
+
|
|
308
|
+
# Verify the task tracker was called without retry options
|
|
309
|
+
task_tracker.get_activity_call_result.assert_called_once_with(
|
|
310
|
+
"test_activity", "test_input"
|
|
311
|
+
)
|
|
312
|
+
assert result == "activity_result"
|
|
313
|
+
|
|
314
|
+
@patch('azure.durable_functions.openai_agents.context.function_schema')
|
|
315
|
+
@patch('azure.durable_functions.openai_agents.context.FunctionTool')
|
|
316
|
+
def test_activity_as_tool_extracts_activity_name_from_trigger(self, mock_function_tool, mock_function_schema):
|
|
317
|
+
"""Test that the run_activity function calls task tracker with the activity name specified in the trigger."""
|
|
318
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
319
|
+
task_tracker = self._create_mock_task_tracker()
|
|
320
|
+
|
|
321
|
+
mock_activity_func = Mock()
|
|
322
|
+
mock_activity_func._function._name = "test_activity"
|
|
323
|
+
mock_activity_func._function._trigger.activity = "activity_name_from_trigger"
|
|
324
|
+
mock_activity_func._function._func = lambda x: x
|
|
325
|
+
|
|
326
|
+
mock_schema = Mock()
|
|
327
|
+
mock_schema.name = "test_activity"
|
|
328
|
+
mock_schema.description = ""
|
|
329
|
+
mock_schema.params_json_schema = {"type": "object"}
|
|
330
|
+
mock_function_schema.return_value = mock_schema
|
|
331
|
+
|
|
332
|
+
mock_tool = Mock(spec=FunctionTool)
|
|
333
|
+
mock_function_tool.return_value = mock_tool
|
|
334
|
+
|
|
335
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
336
|
+
|
|
337
|
+
ai_context.create_activity_tool(mock_activity_func, retry_options=None)
|
|
338
|
+
|
|
339
|
+
# Get the run_activity function that was passed to FunctionTool
|
|
340
|
+
call_args = mock_function_tool.call_args
|
|
341
|
+
run_activity = call_args[1]['on_invoke_tool']
|
|
342
|
+
|
|
343
|
+
# Create a mock context wrapper
|
|
344
|
+
mock_ctx = Mock()
|
|
345
|
+
|
|
346
|
+
# Call the run_activity function
|
|
347
|
+
import asyncio
|
|
348
|
+
result = asyncio.run(run_activity(mock_ctx, "test_input"))
|
|
349
|
+
|
|
350
|
+
# Verify the task tracker was called without retry options
|
|
351
|
+
task_tracker.get_activity_call_result.assert_called_once_with(
|
|
352
|
+
"activity_name_from_trigger", "test_input"
|
|
353
|
+
)
|
|
354
|
+
assert result == "activity_result"
|
|
355
|
+
|
|
356
|
+
def test_create_activity_tool_parses_json_input_with_input_name(self):
|
|
357
|
+
"""Test JSON input parsing and named value extraction with input_name."""
|
|
358
|
+
self._test_activity_tool_input_processing(
|
|
359
|
+
input_name="max",
|
|
360
|
+
input_data='{"max": 100}',
|
|
361
|
+
expected_input_parameter_value=100,
|
|
362
|
+
activity_name="random_number_tool"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def test_create_activity_tool_handles_non_json_input_gracefully(self):
|
|
366
|
+
"""Test non-JSON input passes through unchanged with input_name."""
|
|
367
|
+
self._test_activity_tool_input_processing(
|
|
368
|
+
input_name="param",
|
|
369
|
+
input_data="not json",
|
|
370
|
+
expected_input_parameter_value="not json"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def test_create_activity_tool_handles_json_missing_named_parameter(self):
|
|
374
|
+
"""Test JSON input without named parameter passes through unchanged."""
|
|
375
|
+
json_input = '{"other_param": 200}'
|
|
376
|
+
self._test_activity_tool_input_processing(
|
|
377
|
+
input_name="expected_param",
|
|
378
|
+
input_data=json_input,
|
|
379
|
+
expected_input_parameter_value=json_input
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def test_create_activity_tool_handles_malformed_json_gracefully(self):
|
|
383
|
+
"""Test malformed JSON passes through unchanged."""
|
|
384
|
+
malformed_json = '{"param": 100' # Missing closing brace
|
|
385
|
+
self._test_activity_tool_input_processing(
|
|
386
|
+
input_name="param",
|
|
387
|
+
input_data=malformed_json,
|
|
388
|
+
expected_input_parameter_value=malformed_json
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def test_create_activity_tool_json_parsing_works_with_retry_options(self):
|
|
392
|
+
"""Test JSON parsing works correctly with retry options."""
|
|
393
|
+
retry_options = RetryOptions(1000, 3)
|
|
394
|
+
self._test_activity_tool_input_processing(
|
|
395
|
+
input_name="value",
|
|
396
|
+
input_data='{"value": "test_data"}',
|
|
397
|
+
expected_input_parameter_value="test_data",
|
|
398
|
+
retry_options=retry_options
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def test_create_activity_tool_no_input_name_passes_through_json(self):
|
|
402
|
+
"""Test JSON input passes through unchanged when no input_name."""
|
|
403
|
+
json_input = '{"param": 100}'
|
|
404
|
+
self._test_activity_tool_input_processing(
|
|
405
|
+
input_name=None, # No input_name
|
|
406
|
+
input_data=json_input,
|
|
407
|
+
expected_input_parameter_value=json_input
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def test_context_delegation_methods_work(self):
|
|
411
|
+
"""Test that common context methods work through delegation."""
|
|
412
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
413
|
+
task_tracker = self._create_mock_task_tracker()
|
|
414
|
+
|
|
415
|
+
# Add some mock methods to the orchestration context
|
|
416
|
+
orchestration_context.wait_for_external_event = Mock(return_value="external_event_task")
|
|
417
|
+
orchestration_context.create_timer = Mock(return_value="timer_task")
|
|
418
|
+
|
|
419
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
420
|
+
|
|
421
|
+
# These should work through delegation
|
|
422
|
+
result1 = ai_context.wait_for_external_event("test_event")
|
|
423
|
+
result2 = ai_context.create_timer("2023-01-01T00:00:00Z")
|
|
424
|
+
|
|
425
|
+
assert result1 == "external_event_task"
|
|
426
|
+
assert result2 == "timer_task"
|
|
427
|
+
orchestration_context.wait_for_external_event.assert_called_once_with("test_event")
|
|
428
|
+
orchestration_context.create_timer.assert_called_once_with("2023-01-01T00:00:00Z")
|
|
429
|
+
|
|
430
|
+
def test_getattr_delegates_to_context(self):
|
|
431
|
+
"""Test that __getattr__ delegates attribute access to the underlying context."""
|
|
432
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
433
|
+
task_tracker = self._create_mock_task_tracker()
|
|
434
|
+
|
|
435
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
436
|
+
|
|
437
|
+
# Test delegation of various attributes
|
|
438
|
+
assert ai_context.instance_id == "test_instance_id"
|
|
439
|
+
assert ai_context.current_utc_datetime == "2023-01-01T00:00:00Z"
|
|
440
|
+
assert ai_context.is_replaying is False
|
|
441
|
+
|
|
442
|
+
def test_getattr_raises_attribute_error_for_nonexistent_attributes(self):
|
|
443
|
+
"""Test that __getattr__ raises AttributeError for non-existent attributes."""
|
|
444
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
445
|
+
task_tracker = self._create_mock_task_tracker()
|
|
446
|
+
|
|
447
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
448
|
+
|
|
449
|
+
with pytest.raises(AttributeError, match="'DurableAIAgentContext' object has no attribute 'nonexistent_attr'"):
|
|
450
|
+
_ = ai_context.nonexistent_attr
|
|
451
|
+
|
|
452
|
+
def test_dir_includes_delegated_attributes(self):
|
|
453
|
+
"""Test that __dir__ includes attributes from the underlying context."""
|
|
454
|
+
orchestration_context = self._create_mock_orchestration_context()
|
|
455
|
+
task_tracker = self._create_mock_task_tracker()
|
|
456
|
+
|
|
457
|
+
ai_context = DurableAIAgentContext(orchestration_context, task_tracker, None)
|
|
458
|
+
dir_result = dir(ai_context)
|
|
459
|
+
|
|
460
|
+
# Should include delegated attributes from the underlying context
|
|
461
|
+
assert 'instance_id' in dir_result
|
|
462
|
+
assert 'current_utc_datetime' in dir_result
|
|
463
|
+
assert 'is_replaying' in dir_result
|
|
464
|
+
# Should also include public methods
|
|
465
|
+
assert 'call_activity' in dir_result
|
|
466
|
+
assert 'create_activity_tool' in dir_result
|