Skip to content

Commit 5631058

Browse files
authored
Add RunHandler to support MCP toolcall approval and manual function tool call for runs.create_and_process (#42855)
* Add new event handler for create_and_process * Resolved comments
1 parent cffae18 commit 5631058

13 files changed

+1398
-115
lines changed

sdk/ai/azure-ai-agents/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
## 1.2.0b5 (Unreleased)
66

77
### Features Added
8+
- Added `run_handler` parameter to `runs.create_and_process` allowing to make function tool calls manually or approve mcp tool calls.
89

910
### Bugs Fixed
10-
- Added `RunStepDeltaChunk` to `StreamEventData` model (GitHub issues [43022](https://github.com/Azure/azure-sdk-for-python/issues/43022))
1111
- Fixed regression, reverted ToolOutput type signature and usage in tool_output submission.
1212
- Added `RunStepDeltaComputerUseDetails` and `RunStepDeltaComputerUseToolCall` classes for streaming computer use scenarios.
13+
- Added `RunStepDeltaChunk` to `StreamEventData` model (GitHub issues [43022](https://github.com/Azure/azure-sdk-for-python/issues/43022))
14+
15+
### Sample updates
16+
- Added `sample_agents_mcp_in_create_and_process.py` abd `sample_agents_mcp_in_create_and_process_async.py` demonstrating MCP tool call approvals in `runs.create_and_process`.
17+
- Added `sample_agents_functions_in_create_and_process.py` and `sample_agents_functions_in_create_and_process_async.py` demonstrating manual function tool calls in `runs.create_and_process`.
1318

1419
## 1.2.0b4 (2025-09-12)
1520

sdk/ai/azure-ai-agents/README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,38 @@ while run.status in ["queued", "in_progress", "requires_action"]:
514514

515515
<!-- END SNIPPET -->
516516

517+
For scenarios requiring custom approval logic or additional control over MCP tool calls, you can use a `RunHandler` with the `create_and_process` method. This approach allows you to implement custom logic for approving or denying MCP tool calls.
518+
519+
Here's an example that demonstrates manual MCP tool call approval using a `RunHandler`:
520+
521+
<!-- SNIPPET:sample_agents_mcp_in_create_and_process.run_handler -->
522+
523+
```python
524+
class MyRunHandler(RunHandler):
525+
def submit_mcp_tool_approval(
526+
self, *, run: ThreadRun, tool_call: RequiredMcpToolCall, **kwargs: Any
527+
) -> ToolApproval:
528+
return ToolApproval(
529+
tool_call_id=tool_call.id,
530+
approve=True,
531+
headers=mcp_tool.headers,
532+
)
533+
```
534+
535+
<!-- END SNIPPET -->
536+
537+
To use the RunHandler with `create_and_process` for MCP tools:
538+
539+
<!-- SNIPPET:sample_agents_mcp_in_create_and_process.create_and_process -->
540+
541+
```python
542+
run = agents_client.runs.create_and_process(thread_id=thread.id, agent_id=agent.id, run_handler=MyRunHandler())
543+
```
544+
545+
<!-- END SNIPPET -->
546+
547+
For a complete example, see [`sample_agents_mcp_in_create_and_process.py`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_tools/sample_agents_mcp_in_create_and_process.py).
548+
517549
### Create Agent with Azure AI Search
518550

519551
Azure AI Search is an enterprise search system for high-performance applications. It integrates with Azure OpenAI Service and Azure Machine Learning, offering advanced search technologies like vector search and full-text search. Ideal for knowledge base insights, information discovery, and automation. Creating an Agent with Azure AI Search requires an existing Azure AI Search Index. For more information and setup guides, see [Azure AI Search Tool Guide](https://learn.microsoft.com/azure/ai-services/agents/how-to/tools/azure-ai-search?tabs=azurecli%2Cpython&pivots=overview-azure-ai-search).
@@ -634,7 +666,6 @@ When `enable_auto_function_calls` is called, the SDK will automatically invoke f
634666
- If you prefer to manage function execution manually, refer to [`sample_agents_stream_eventhandler_with_functions.py`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_streaming/sample_agents_stream_eventhandler_with_functions.py) or
635667
[`sample_agents_functions.py`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_tools/sample_agents_functions.py).
636668

637-
638669
### Create Agent With Azure Function Call
639670

640671
The AI agent leverages Azure Functions triggered asynchronously via Azure Storage Queues. To enable the agent to perform Azure Function calls, you must set up the corresponding `AzureFunctionTool`, specifying input and output queues as well as parameter definitions.
@@ -1366,6 +1397,44 @@ run = agents_client.runs.create_and_process(thread_id=thread.id, agent_id=agent.
13661397

13671398
<!-- END SNIPPET -->
13681399

1400+
For scenarios requiring manual approval or custom control over function execution (such as security-sensitive operations), you can use a `RunHandler` with the `create_and_process` method. This approach allows you to implement custom logic to decide whether, when, and how functions should be executed.
1401+
1402+
Here's an example that demonstrates manual function calls using a `RunHandler`:
1403+
1404+
<!-- SNIPPET:sample_agents_functions_in_create_and_process.run_handler -->
1405+
1406+
```python
1407+
class MyRunHandler(RunHandler):
1408+
def submit_function_call_output(
1409+
self,
1410+
*,
1411+
run: ThreadRun,
1412+
tool_call: RequiredFunctionToolCall,
1413+
tool_call_details: RequiredFunctionToolCallDetails,
1414+
**kwargs: Any,
1415+
) -> Any:
1416+
function_name = tool_call_details.name
1417+
if function_name == send_email.__name__:
1418+
# Parse arguments from tool call
1419+
args_dict = json.loads(tool_call_details.arguments) if tool_call_details.arguments else {}
1420+
# Call the function directly with the arguments
1421+
return send_email(**args_dict)
1422+
```
1423+
1424+
<!-- END SNIPPET -->
1425+
1426+
To use the RunHandler with `create_and_process`:
1427+
1428+
<!-- SNIPPET:sample_agents_functions_in_create_and_process.create_and_process -->
1429+
1430+
```python
1431+
run = agents_client.runs.create_and_process(thread_id=thread.id, agent_id=agent.id, run_handler=MyRunHandler())
1432+
```
1433+
1434+
<!-- END SNIPPET -->
1435+
1436+
For a complete example, see [`sample_agents_functions_in_create_and_process.py`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_tools/sample_agents_functions_in_create_and_process.py).
1437+
13691438
With streaming, polling need not be considered. If `function tools` were added to the agents, you should decide to have the function tools called manually or automatically. Please visit [`manual function call sample`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_streaming/sample_agents_stream_eventhandler_with_functions.py) or [`automatic function call sample`](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-agents/samples/agents_streaming/sample_agents_stream_iteration_with_toolset.py).
13701439

13711440
Here is a basic example of streaming:

sdk/ai/azure-ai-agents/azure/ai/agents/aio/operations/_patch.py

Lines changed: 8 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from azure.core.tracing.decorator_async import distributed_trace_async
3333

3434
from ... import models as _models
35-
from ...models._enums import FilePurpose, RunStatus
35+
from ...models._enums import FilePurpose
3636
from ._operations import FilesOperations as FilesOperationsGenerated
3737
from ._operations import MessagesOperations as MessagesOperationsGenerated
3838
from ._operations import RunsOperations as RunsOperationsGenerated
@@ -442,6 +442,7 @@ async def create_and_process(
442442
response_format: Optional["_types.AgentsResponseFormatOption"] = None,
443443
parallel_tool_calls: Optional[bool] = None,
444444
metadata: Optional[Dict[str, str]] = None,
445+
run_handler: Optional[_models.AsyncRunHandler] = None,
445446
polling_interval: int = 1,
446447
**kwargs: Any,
447448
) -> _models.ThreadRun:
@@ -522,6 +523,9 @@ async def create_and_process(
522523
64 characters in length and values may be up to 512 characters in length. Default value is
523524
None.
524525
:paramtype metadata: dict[str, str]
526+
:keyword run_handler: Optional handler to customize run processing and tool execution.
527+
Default value is None.
528+
:paramtype run_handler: ~azure.ai.agents.models.AsyncRunHandler
525529
:keyword polling_interval: The time in seconds to wait between polling the service for run status.
526530
Default value is 1.
527531
:paramtype polling_interval: int
@@ -553,51 +557,9 @@ async def create_and_process(
553557
)
554558

555559
# Monitor and process the run status
556-
current_retry = 0
557-
while run.status in [
558-
RunStatus.QUEUED,
559-
RunStatus.IN_PROGRESS,
560-
RunStatus.REQUIRES_ACTION,
561-
]:
562-
await asyncio.sleep(polling_interval)
563-
run = await self.get(thread_id=thread_id, run_id=run.id)
564-
565-
if run.status == "requires_action" and isinstance(run.required_action, _models.SubmitToolOutputsAction):
566-
tool_calls = run.required_action.submit_tool_outputs.tool_calls
567-
if not tool_calls:
568-
logger.warning("No tool calls provided - cancelling run")
569-
await self.cancel(thread_id=thread_id, run_id=run.id)
570-
break
571-
# We need tool set only if we are executing local function. In case if
572-
# the tool is azure_function we just need to wait when it will be finished.
573-
if any(tool_call.type == "function" for tool_call in tool_calls):
574-
toolset = _models.AsyncToolSet()
575-
toolset.add(self._function_tool)
576-
tool_outputs = await toolset.execute_tool_calls(tool_calls)
577-
578-
if _has_errors_in_toolcalls_output(tool_outputs):
579-
if current_retry >= self._function_tool_max_retry: # pylint:disable=no-else-return
580-
logger.warning(
581-
"Tool outputs contain errors - reaching max retry %s", self._function_tool_max_retry
582-
)
583-
return await self.cancel(thread_id=thread_id, run_id=run.id)
584-
else:
585-
logger.warning("Tool outputs contain errors - retrying")
586-
current_retry += 1
587-
588-
logger.debug("Tool outputs: %s", tool_outputs)
589-
if tool_outputs:
590-
run2 = await self.submit_tool_outputs(
591-
thread_id=thread_id, run_id=run.id, tool_outputs=tool_outputs
592-
)
593-
logger.debug("Tool outputs submitted to run: %s", run2.id)
594-
elif isinstance(run.required_action, _models.SubmitToolApprovalAction):
595-
logger.warning("Automatic MCP tool approval is not supported.")
596-
await self.cancel(thread_id=thread_id, run_id=run.id)
597-
598-
logger.debug("Current run ID: %s with status: %s", run.id, run.status)
599-
600-
return run
560+
run_handler_obj = run_handler or _models.AsyncRunHandler()
561+
562+
return await run_handler_obj._start(self, run, polling_interval) # pylint: disable=protected-access
601563

602564
@overload
603565
async def stream(

0 commit comments

Comments
 (0)