From 6e205445be51c07257e1ac96f4eff2757cc01300 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Thu, 6 Nov 2025 14:34:27 +0000
Subject: [PATCH] .NET: Add tool calling sample with OpenAPI (#1968)
* Add tool calling sample with OpenAPI
* Address PR comments.
* Rename folders and moved literal to inline.
* Fix broken link.
---
dotnet/Directory.Packages.props | 8 +-
dotnet/agent-framework-dotnet.slnx | 7 +-
.../Agent_Step03.1_UsingFunctionTools.csproj} | 0
.../Program.cs | 0
...03.2_UsingFunctionTools_FromOpenAPI.csproj | 28 ++
.../OpenAPISpec.json | 354 ++++++++++++++++++
.../Program.cs | 33 ++
.../samples/GettingStarted/Agents/README.md | 3 +-
8 files changed, 426 insertions(+), 7 deletions(-)
rename dotnet/samples/GettingStarted/Agents/{Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj => Agent_Step03.1_UsingFunctionTools/Agent_Step03.1_UsingFunctionTools.csproj} (100%)
rename dotnet/samples/GettingStarted/Agents/{Agent_Step03_UsingFunctionTools => Agent_Step03.1_UsingFunctionTools}/Program.cs (100%)
create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj
create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json
create mode 100644 dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 7fb6459906..71eceb7f46 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -68,13 +68,15 @@
-
+
-
-
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 7cbe76b6fc..03f8a910d3 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -47,7 +47,8 @@
-
+
+
@@ -154,8 +155,8 @@
-
+
@@ -309,8 +310,8 @@
-
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Agent_Step03.1_UsingFunctionTools.csproj
similarity index 100%
rename from dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj
rename to dotnet/samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Agent_Step03.1_UsingFunctionTools.csproj
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Program.cs
similarity index 100%
rename from dotnet/samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Program.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Program.cs
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj
new file mode 100644
index 0000000000..e2edbb2f8d
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json
new file mode 100644
index 0000000000..84715914da
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json
@@ -0,0 +1,354 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "Github Versions API",
+ "version": "1.0.0"
+ },
+ "servers": [
+ {
+ "url": "https://api.github.com"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "basic-error": {
+ "title": "Basic Error",
+ "description": "Basic Error",
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "documentation_url": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ }
+ },
+ "label": {
+ "title": "Label",
+ "description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Unique identifier for the label.",
+ "type": "integer",
+ "format": "int64",
+ "example": 208045946
+ },
+ "node_id": {
+ "type": "string",
+ "example": "MDU6TGFiZWwyMDgwNDU5NDY="
+ },
+ "url": {
+ "description": "URL for the label",
+ "example": "https://api.github.com/repositories/42/labels/bug",
+ "type": "string",
+ "format": "uri"
+ },
+ "name": {
+ "description": "The name of the label.",
+ "example": "bug",
+ "type": "string"
+ },
+ "description": {
+ "description": "Optional description of the label, such as its purpose.",
+ "type": "string",
+ "example": "Something isn't working",
+ "nullable": true
+ },
+ "color": {
+ "description": "6-character hex code, without the leading #, identifying the color",
+ "example": "FFFFFF",
+ "type": "string"
+ },
+ "default": {
+ "description": "Whether this label comes by default in a new repository.",
+ "type": "boolean",
+ "example": true
+ }
+ },
+ "required": [
+ "id",
+ "node_id",
+ "url",
+ "name",
+ "description",
+ "color",
+ "default"
+ ]
+ },
+ "tag": {
+ "title": "Tag",
+ "description": "Tag",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "example": "v0.1"
+ },
+ "commit": {
+ "type": "object",
+ "properties": {
+ "sha": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ }
+ },
+ "required": [
+ "sha",
+ "url"
+ ]
+ },
+ "zipball_url": {
+ "type": "string",
+ "format": "uri",
+ "example": "https://github.com/octocat/Hello-World/zipball/v0.1"
+ },
+ "tarball_url": {
+ "type": "string",
+ "format": "uri",
+ "example": "https://github.com/octocat/Hello-World/tarball/v0.1"
+ },
+ "node_id": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "node_id",
+ "commit",
+ "zipball_url",
+ "tarball_url"
+ ]
+ }
+ },
+ "examples": {
+ "label-items": {
+ "value": [
+ {
+ "id": 208045946,
+ "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
+ "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
+ "name": "bug",
+ "description": "Something isn't working",
+ "color": "f29513",
+ "default": true
+ },
+ {
+ "id": 208045947,
+ "node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
+ "url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement",
+ "name": "enhancement",
+ "description": "New feature or request",
+ "color": "a2eeef",
+ "default": false
+ }
+ ]
+ },
+ "tag-items": {
+ "value": [
+ {
+ "name": "v0.1",
+ "commit": {
+ "sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
+ "url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
+ },
+ "zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1",
+ "tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1",
+ "node_id": "MDQ6VXNlcjE="
+ }
+ ]
+ }
+ },
+ "parameters": {
+ "owner": {
+ "name": "owner",
+ "description": "The account owner of the repository. The name is not case sensitive.",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "repo": {
+ "name": "repo",
+ "description": "The name of the repository without the `.git` extension. The name is not case sensitive.",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "per-page": {
+ "name": "per_page",
+ "description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
+ "in": "query",
+ "schema": {
+ "type": "integer",
+ "default": 30
+ }
+ },
+ "page": {
+ "name": "page",
+ "description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
+ "in": "query",
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ }
+ },
+ "responses": {
+ "not_found": {
+ "description": "Resource not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/basic-error"
+ }
+ }
+ }
+ }
+ },
+ "headers": {
+ "link": {
+ "example": "; rel=\"next\", ; rel=\"last\"",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "paths": {
+ "/repos/{owner}/{repo}/tags": {
+ "get": {
+ "summary": "List repository tags",
+ "description": "",
+ "tags": [
+ "repos"
+ ],
+ "operationId": "repos/list-tags",
+ "externalDocs": {
+ "description": "API method documentation",
+ "url": "https://docs.github.com/rest/repos/repos#list-repository-tags"
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/owner"
+ },
+ {
+ "$ref": "#/components/parameters/repo"
+ },
+ {
+ "$ref": "#/components/parameters/per-page"
+ },
+ {
+ "$ref": "#/components/parameters/page"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/tag"
+ }
+ },
+ "examples": {
+ "default": {
+ "$ref": "#/components/examples/tag-items"
+ }
+ }
+ }
+ },
+ "headers": {
+ "Link": {
+ "$ref": "#/components/headers/link"
+ }
+ }
+ }
+ },
+ "x-github": {
+ "githubCloudOnly": false,
+ "enabledForGitHubApps": true,
+ "category": "repos",
+ "subcategory": "repos"
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/labels": {
+ "get": {
+ "summary": "List labels for a repository",
+ "description": "Lists all labels for a repository.",
+ "tags": [
+ "issues"
+ ],
+ "operationId": "issues/list-labels-for-repo",
+ "externalDocs": {
+ "description": "API method documentation",
+ "url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository"
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/owner"
+ },
+ {
+ "$ref": "#/components/parameters/repo"
+ },
+ {
+ "$ref": "#/components/parameters/per-page"
+ },
+ {
+ "$ref": "#/components/parameters/page"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/label"
+ }
+ },
+ "examples": {
+ "default": {
+ "$ref": "#/components/examples/label-items"
+ }
+ }
+ }
+ },
+ "headers": {
+ "Link": {
+ "$ref": "#/components/headers/link"
+ }
+ }
+ },
+ "404": {
+ "$ref": "#/components/responses/not_found"
+ }
+ },
+ "x-github": {
+ "githubCloudOnly": false,
+ "enabledForGitHubApps": true,
+ "category": "issues",
+ "subcategory": "labels"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs
new file mode 100644
index 0000000000..e61c9f845a
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to use a ChatClientAgent with function tools provided via an OpenAPI spec.
+// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.OpenApi;
+using OpenAI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+// Load the OpenAPI Spec from a file.
+KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json");
+
+// Convert the Semantic Kernel plugin to Agent Framework function tools.
+// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one.
+Kernel kernel = new();
+List tools = plugin.Select(x => x.WithKernel(kernel)).Cast().ToList();
+
+// Create the chat client and agent, and provide the OpenAPI function tools to the agent.
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(instructions: "You are a helpful assistant", tools: tools);
+
+// Run the agent with the OpenAPI function tools.
+Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github."));
diff --git a/dotnet/samples/GettingStarted/Agents/README.md b/dotnet/samples/GettingStarted/Agents/README.md
index bf5c291f9e..562b6b2500 100644
--- a/dotnet/samples/GettingStarted/Agents/README.md
+++ b/dotnet/samples/GettingStarted/Agents/README.md
@@ -28,7 +28,8 @@ Before you begin, ensure you have the following prerequisites:
|---|---|
|[Running a simple agent](./Agent_Step01_Running/)|This sample demonstrates how to create and run a basic agent with instructions|
|[Multi-turn conversation with a simple agent](./Agent_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a simple agent|
-|[Using function tools with a simple agent](./Agent_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
+|[Using function tools with a simple agent](./Agent_Step03.1_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
+|[Using OpenAPI function tools with a simple agent](./Agent_Step03.2_UsingFunctionTools_FromOpenAPI/)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a simple agent|
|[Using function tools with approvals](./Agent_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|[Structured output with a simple agent](./Agent_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent|
|[Persisted conversations with a simple agent](./Agent_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later. This is useful for cases where an agent is hosted in a stateless service|