Skip to content

Commit 988ef6a

Browse files
authored
.NET: Support InvokeFunctionTool for declarative workflows (microsoft#4014)
* Initial Implementation of InvokeFunctionTool * Added unit test for InvokeFunctionTool executor. * Implemented unit and integration tests for InvokeFunctionTool. * Add sample for InvokeFunctionTool in declarative workflows. * Remove unused sample and updated comments. * Updating to official OM release with InvokeFunctionTool * Fix formatting issues. * Updated PowerFx version * Update test fixture * Cleanup - Removed unused method in InvokeFunctionToolExecutor * Update test based on PR feedback. * Update based on PR comments
1 parent 7cee839 commit 988ef6a

File tree

14 files changed

+1175
-5
lines changed

14 files changed

+1175
-5
lines changed

dotnet/Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@
111111
<!-- Identity -->
112112
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.78.0" />
113113
<!-- Workflows -->
114-
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.2.1" />
115-
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.2.1" />
116-
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.2.2.1" />
117-
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.5.0-build.20251008-1002" />
114+
<PackageVersion Include="Microsoft.Agents.ObjectModel" Version="2026.2.3.1" />
115+
<PackageVersion Include="Microsoft.Agents.ObjectModel.Json" Version="2026.2.3.1" />
116+
<PackageVersion Include="Microsoft.Agents.ObjectModel.PowerFx" Version="2026.2.3.1" />
117+
<PackageVersion Include="Microsoft.PowerFx.Interpreter" Version="1.8.1" />
118118
<!-- Durable Task -->
119119
<PackageVersion Include="Microsoft.DurableTask.Client" Version="1.18.0" />
120120
<PackageVersion Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.18.0" />

dotnet/agent-framework-dotnet.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@
215215
<Project Path="samples/GettingStarted/Workflows/Declarative/Marketing/Marketing.csproj" />
216216
<Project Path="samples/GettingStarted/Workflows/Declarative/StudentTeacher/StudentTeacher.csproj" />
217217
<Project Path="samples/GettingStarted/Workflows/Declarative/ToolApproval/ToolApproval.csproj" />
218+
<Project Path="samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj" />
218219
</Folder>
219220
<Folder Name="/Samples/GettingStarted/Workflows/Declarative/Examples/">
220221
<File Path="../workflow-samples/CustomerSupport.yaml" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net10.0</TargetFrameworks>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
12+
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
13+
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
14+
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.Extensions.Configuration" />
19+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
20+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
21+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
22+
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
23+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
24+
<PackageReference Include="Microsoft.Extensions.Logging" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
29+
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.AzureAI\Microsoft.Agents.AI.Workflows.Declarative.AzureAI.csproj" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<None Include="InvokeFunctionTool.yaml">
34+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
35+
</None>
36+
</ItemGroup>
37+
38+
</Project>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#
2+
# This workflow demonstrates using InvokeFunctionTool to call functions directly
3+
# from the workflow without going through an AI agent first.
4+
#
5+
# InvokeFunctionTool allows workflows to:
6+
# - Pre-fetch data before calling an AI agent
7+
# - Execute operations directly without AI involvement
8+
# - Store function results in workflow variables for later use
9+
#
10+
# Example input:
11+
# What are the specials in the menu?
12+
#
13+
kind: Workflow
14+
trigger:
15+
16+
kind: OnConversationStart
17+
id: workflow_invoke_function_tool_demo
18+
actions:
19+
20+
# Invoke GetSpecials function to get today's specials directly from the workflow
21+
- kind: InvokeFunctionTool
22+
id: invoke_get_specials
23+
conversationId: =System.ConversationId
24+
requireApproval: true
25+
functionName: GetSpecials
26+
output:
27+
autoSend: true
28+
result: Local.Specials
29+
messages: Local.FunctionMessage
30+
31+
# Display a message showing we retrieved the specials
32+
- kind: SendMessage
33+
id: show_specials_intro
34+
message: "Today's specials have been retrieved. Here they are: {Local.Specials}"
35+
36+
# Now use an agent to format and present the specials to the user
37+
- kind: InvokeAzureAgent
38+
id: invoke_menu_agent
39+
conversationId: =System.ConversationId
40+
agent:
41+
name: FunctionMenuAgent
42+
input:
43+
messages: =UserMessage("Please describe today's specials in an appealing way.")
44+
output:
45+
messages: Local.AgentResponse
46+
47+
# Allow the user to ask follow-up questions in a loop
48+
- kind: InvokeAzureAgent
49+
id: invoke_followup
50+
conversationId: =System.ConversationId
51+
agent:
52+
name: FunctionMenuAgent
53+
input:
54+
externalLoop:
55+
when: =Upper(System.LastMessage.Text) <> "EXIT"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ComponentModel;
4+
5+
namespace Demo.Workflows.Declarative.InvokeFunctionTool;
6+
7+
#pragma warning disable CA1822 // Mark members as static
8+
9+
/// <summary>
10+
/// Plugin providing menu-related functions that can be invoked directly by the workflow
11+
/// using the InvokeFunctionTool action.
12+
/// </summary>
13+
public sealed class MenuPlugin
14+
{
15+
[Description("Provides a list items on the menu.")]
16+
public MenuItem[] GetMenu()
17+
{
18+
return s_menuItems;
19+
}
20+
21+
[Description("Provides a list of specials from the menu.")]
22+
public MenuItem[] GetSpecials()
23+
{
24+
return [.. s_menuItems.Where(i => i.IsSpecial)];
25+
}
26+
27+
[Description("Provides the price of the requested menu item.")]
28+
public float? GetItemPrice(
29+
[Description("The name of the menu item.")]
30+
string name)
31+
{
32+
return s_menuItems.FirstOrDefault(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Price;
33+
}
34+
35+
private static readonly MenuItem[] s_menuItems =
36+
[
37+
new()
38+
{
39+
Category = "Soup",
40+
Name = "Clam Chowder",
41+
Price = 4.95f,
42+
IsSpecial = true,
43+
},
44+
new()
45+
{
46+
Category = "Soup",
47+
Name = "Tomato Soup",
48+
Price = 4.95f,
49+
IsSpecial = false,
50+
},
51+
new()
52+
{
53+
Category = "Salad",
54+
Name = "Cobb Salad",
55+
Price = 9.99f,
56+
},
57+
new()
58+
{
59+
Category = "Salad",
60+
Name = "House Salad",
61+
Price = 4.95f,
62+
},
63+
new()
64+
{
65+
Category = "Drink",
66+
Name = "Chai Tea",
67+
Price = 2.95f,
68+
IsSpecial = true,
69+
},
70+
new()
71+
{
72+
Category = "Drink",
73+
Name = "Soda",
74+
Price = 1.95f,
75+
},
76+
];
77+
78+
public sealed class MenuItem
79+
{
80+
public string Category { get; init; } = string.Empty;
81+
public string Name { get; init; } = string.Empty;
82+
public float Price { get; init; }
83+
public bool IsSpecial { get; init; }
84+
}
85+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Azure.AI.Projects;
4+
using Azure.AI.Projects.OpenAI;
5+
using Azure.Identity;
6+
using Microsoft.Extensions.AI;
7+
using Microsoft.Extensions.Configuration;
8+
using OpenAI.Responses;
9+
using Shared.Foundry;
10+
using Shared.Workflows;
11+
12+
namespace Demo.Workflows.Declarative.InvokeFunctionTool;
13+
14+
/// <summary>
15+
/// Demonstrate a workflow that uses InvokeFunctionTool to call functions directly
16+
/// from the workflow without going through an AI agent first.
17+
/// </summary>
18+
/// <remarks>
19+
/// The InvokeFunctionTool action allows workflows to invoke function tools directly,
20+
/// enabling pre-fetching of data or executing operations before calling an AI agent.
21+
/// See the README.md file in the parent folder (../README.md) for detailed
22+
/// information about the configuration required to run this sample.
23+
/// </remarks>
24+
internal sealed class Program
25+
{
26+
public static async Task Main(string[] args)
27+
{
28+
// Initialize configuration
29+
IConfiguration configuration = Application.InitializeConfig();
30+
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
31+
32+
// Create the menu plugin with functions that can be invoked directly by the workflow
33+
MenuPlugin menuPlugin = new();
34+
AIFunction[] functions =
35+
[
36+
AIFunctionFactory.Create(menuPlugin.GetMenu),
37+
AIFunctionFactory.Create(menuPlugin.GetSpecials),
38+
AIFunctionFactory.Create(menuPlugin.GetItemPrice),
39+
];
40+
41+
// Ensure sample agent exists in Foundry
42+
await CreateAgentAsync(foundryEndpoint, configuration);
43+
44+
// Get input from command line or console
45+
string workflowInput = Application.GetInput(args);
46+
47+
// Create the workflow factory.
48+
WorkflowFactory workflowFactory = new("InvokeFunctionTool.yaml", foundryEndpoint);
49+
50+
// Execute the workflow
51+
WorkflowRunner runner = new(functions) { UseJsonCheckpoints = true };
52+
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
53+
}
54+
55+
private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
56+
{
57+
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
58+
AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
59+
60+
await aiProjectClient.CreateAgentAsync(
61+
agentName: "FunctionMenuAgent",
62+
agentDefinition: DefineMenuAgent(configuration, []), // Create Agent with no function tool in the definition.
63+
agentDescription: "Provides information about the restaurant menu");
64+
}
65+
66+
private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
67+
{
68+
PromptAgentDefinition agentDefinition =
69+
new(configuration.GetValue(Application.Settings.FoundryModelMini))
70+
{
71+
Instructions =
72+
"""
73+
Answer the users questions about the menu.
74+
Use the information provided in the conversation history to answer questions.
75+
If the information is already available in the conversation, use it directly.
76+
For questions or input that do not require searching the documentation, inform the
77+
user that you can only answer questions about what's on the menu.
78+
"""
79+
};
80+
81+
foreach (AIFunction function in functions)
82+
{
83+
agentDefinition.Tools.Add(function.AsOpenAIResponseTool());
84+
}
85+
86+
return agentDefinition;
87+
}
88+
}

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,27 @@ protected override void Visit(InvokeAzureAgent item)
390390
this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId);
391391
}
392392

393+
protected override void Visit(InvokeFunctionTool item)
394+
{
395+
this.Trace(item);
396+
397+
// Entry point to invoke function tool - always yields for external execution
398+
InvokeFunctionToolExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
399+
this.ContinueWith(action);
400+
401+
// Define request-port for function tool invocation (always requires external input)
402+
string externalInputPortId = InvokeFunctionToolExecutor.Steps.ExternalInput(action.Id);
403+
RequestPortAction externalInputPort = new(RequestPort.Create<ExternalInputRequest, ExternalInputResponse>(externalInputPortId));
404+
this._workflowModel.AddNode(externalInputPort, action.ParentId);
405+
this._workflowModel.AddLinkFromPeer(action.ParentId, externalInputPortId);
406+
407+
// Capture response when external input is received
408+
string resumeId = InvokeFunctionToolExecutor.Steps.Resume(action.Id);
409+
this.ContinueWith(
410+
new DelegateActionExecutor<ExternalInputResponse>(resumeId, this._workflowState, action.CaptureResponseAsync),
411+
action.ParentId);
412+
}
413+
393414
protected override void Visit(InvokeAzureResponse item)
394415
{
395416
this.NotSupported(item);

dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ protected override void Visit(SendActivity item)
365365

366366
#region Not supported
367367

368+
protected override void Visit(InvokeFunctionTool item) => this.NotSupported(item);
369+
368370
protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
369371

370372
protected override void Visit(DeleteActivity item) => this.NotSupported(item);

0 commit comments

Comments
 (0)