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|