13、OpenAPI作为插件

13、OpenAPI作为插件

OpenAPI 规范(OAS),是定义一个标准的、与具体编程语言无关的RESTful API的规范,如果遵循了 OpenAPI 规范来定义 API,那么就可以用文档生成工具来展示您的 API,用代码生成工具来自动生成各种编程语言的服务器端和客户端的代码,用自动测试工具进行测试等等
通常在企业中,已经有一组执行实际工作的 API。这些 API 可以被其他服务或应用程序使用。在SK中,可以将这些 API 添加为插件,根据OpenAPI规范提供的信息,AI可以理解和调用这些API。
如果需要导入已有的OpenAPI作为插件,需要安装Microsoft.SemanticKernel.Plugins.OpenApi Nuget包。可以通过以下方式导入或创建插件。

  1. await kernel.ImportPluginFromOpenApiAsync方法导入
  2. OpenApiKernelPluginFactory.CreateFromOpenApi 进行创建
1
2
//准备
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
1
#r "nuget:Microsoft.SemanticKernel.Plugins.OpenApi"

示例演示

下面基于微软官方提供的一个简单OpenAPI 示例(https://piercerepairsapi.azurewebsites.net)来快速演示如何将OpenAPI 转为SK 插件。其暴露的OpenAPI 规范保存在./Plugins/repaire-openapi_zh-cn.json

1
2
3
4
5
6
7
8
9
using Microsoft.SemanticKernel.Plugins.OpenApi;

kernel.Plugins.Clear();
var repairePlugin = await kernel.ImportPluginFromOpenApiAsync(
pluginName:"RepairePlugin",
filePath: "./Plugins/repaire-openapi_zh-cn.json"
);

repairePlugin.Display();
1
2
3
4
5
//返回
RepairePlugin.listRepairs
RepairePlugin.createRepair
RepairePlugin.updateRepair
RepairePlugin.deleteRepair

通过Function直接调用

1
2
3
4
5
6
7
8
9
private sealed class Repair
{
public int? Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? AssignedTo { get; set; }
public string? Date { get; set; }
public string? Image { get; set; }
}
1
2
3
4
5
6
7
8
9
10
// Create Repair

var newRepaire = new KernelArguments{
["title"] = "Engine oil change",
["description"] = "Need to drain the old engine oil and replace it with fresh oil.",
["assignedTo"] = "shengjie"
};

var result = await repairePlugin["createRepair"].InvokeAsync(kernel, newRepaire);
result.Display();
1
2
3
4
5
6
7
8
9
10
11
// Update Repair
var arguments = new KernelArguments
{
["id"] = 1,
["assignedTo"] = "Karin Blair",
["date"] = "2024-04-16",
["image"] = "https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg"
};

result = await repairePlugin["updateRepair"].InvokeAsync(kernel, arguments);
result.Display();
1
2
3
4
5
6
7
8
var id = repairs.Last().Id;
// Delete Repair
var deleteRepairArgument = new KernelArguments{
["id"] = id
};

var result = await repairePlugin["deleteRepair"].InvokeAsync(kernel, deleteRepairArgument);
result.Display();

通过Function Call调用

1
2
3
4
5
6
7
8
9
using Microsoft.SemanticKernel.Connectors.OpenAI;
OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};


var result = await kernel.InvokePromptAsync("创建一个新的维修记录并分配给小明:添加一个16g的内存条", new(openAIPromptExecutionSettings));
result.Display();

查看日志,可以看到发送了以下请求,从中可以看出SK将导入的Plugin作为tools 发送给了大模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
{
"tools": [
{
"function": {
"description": "返回维修的详细信息和图片列表",
"name": "RepairePlugin-listRepairs",
"strict": false,
"parameters": {
"type": "object",
"required": [],
"properties": {
"assignedTo": {
"type": "string"
}
}
}
},
"type": "function"
},
{
"function": {
"description": "添加具有给定详细信息和图片 URL 的新维修到列表中",
"name": "RepairePlugin-createRepair",
"strict": false,
"parameters": {
"type": "object",
"required": [
"title",
"description",
"assignedTo"
],
"properties": {
"title": {
"type": "string",
"description": "维修的简短摘要"
},
"description": {
"type": "string",
"description": "维修的详细描述"
},
"assignedTo": {
"type": "string",
"description": "负责维修的用户"
},
"date": {
"type": "string",
"description": "维修计划或完成的日期和时间(可选)",
"format": "date-time"
},
"image": {
"type": "string",
"description": "要维修的物品或维修过程的图片 URL",
"format": "uri"
}
}
}
},
"type": "function"
},
{
"function": {
"description": "使用新的更新详细信息和图片 URL 更新现有维修",
"name": "RepairePlugin-updateRepair",
"strict": false,
"parameters": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"description": "要更新的维修的唯一标识符"
},
"title": {
"type": "string",
"description": "维修的简短摘要"
},
"description": {
"type": "string",
"description": "维修的详细描述"
},
"assignedTo": {
"type": "string",
"description": "负责维修的用户"
},
"date": {
"type": "string",
"description": "维修计划或完成的日期和时间",
"format": "date-time"
},
"image": {
"type": "string",
"description": "要维修的物品或维修过程的图片 URL",
"format": "uri"
}
}
}
},
"type": "function"
},
{
"function": {
"description": "使用其 ID 从列表中删除现有维修",
"name": "RepairePlugin-deleteRepair",
"strict": false,
"parameters": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer",
"description": "要删除的维修的唯一标识符"
}
}
}
},
"type": "function"
}
],
"messages": [
{
"role": "user",
"content": "创建一个新的维修工单并分配给小明:添加一个16g的内存条"
}
],
"model": "gpt-4o",
"tool_choice": "auto"
}
1
2
3
4
5
6
7
8
9
using Microsoft.SemanticKernel.Connectors.OpenAI;

OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var response = await kernel.InvokePromptAsync("查询小明的维修单列表", new(openAIPromptExecutionSettings));

response.Display();

限定特定可用的范围:

1
2
3
4
5
6
7
8
9
10
11
using Microsoft.SemanticKernel.Connectors.OpenAI;

var queryFunction = repairePlugin["listRepairs"];

OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto([queryFunction])
};
var response = await kernel.InvokePromptAsync("查询小明的维修单列表", new(openAIPromptExecutionSettings));

response.Display();

请求的数据包中,将仅含有一个Function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"tools": [
{
"function": {
"description": "查询维修列表",
"name": "RepairePlugin-listRepairs",
"strict": false,
"parameters": {
"type": "object",
"required": [],
"properties": {
"assignedTo": {
"type": "string"
}
}
}
},
"type": "function"
}
],
"messages": [
{
"role": "user",
"content": "查询小明的维修单列表"
}
],
"model": "gpt-4o",
"tool_choice": "auto"
}

按需导入指定API

排除不需要的API

SK 中定义有OpenApiFunctionExecutionParameters 类,其中包含参数OperationsToExclude 可直接排除不需要导入的OpenAPI,但该参数目前尚属实验性功能(SKEXP0040)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma warning disable SKEXP0040

using Microsoft.SemanticKernel.Plugins.OpenApi;

kernel.Plugins.Clear();
var repairePlugin = await kernel.ImportPluginFromOpenApiAsync(
pluginName:"RepairePlugin",
filePath: "./Plugins/repaire-openapi_zh-cn.json",
executionParameters: new OpenApiFunctionExecutionParameters()
{
OperationsToExclude = ["listRepairs","updateRepair"]
}
);

repairePlugin.Display();

仅导入满足条件的API

SK 中定义有OpenApiDocumentParser 类,用于解析OpenAPI 文档,通过设置OpenApiDocumentParserOptions 参数来实现特定OpenAPI的过滤,但该参数目前尚属实验性功能(SKEXP0040)。

以下示例中仅导入Get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma warning disable SKEXP0040

kernel.Plugins.Clear();

OpenApiDocumentParser parser = new();
using (StreamReader reader = System.IO.File.OpenText("./Plugins/repaire-openapi_zh-cn.json"))
{
OpenApiDocumentParserOptions parserOptions = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => context.Method == "Get"
};

RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream, options: parserOptions);


kernel.ImportPluginFromOpenApi("RepairService", specification);

kernel.Plugins["RepairService"].Display();
}

以下示例仅导入特定操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma warning disable SKEXP0040

kernel.Plugins.Clear();

OpenApiDocumentParser parser = new();
using (StreamReader reader = System.IO.File.OpenText("./Plugins/repaire-openapi_zh-cn.json"))
{
List<string> operationsToInclude = ["createRepair", "updateRepair"];
OpenApiDocumentParserOptions parserOptions = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => operationsToInclude.Contains(context.Id!)
};

RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream, options: parserOptions);

kernel.ImportPluginFromOpenApi("RepairService", specification);

kernel.Plugins["RepairService"].Display();
}

还可直接将OpenAPI 文档解析后,再进行修改。以下示例中排除了GET请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma warning disable SKEXP0040

kernel.Plugins.Clear();

OpenApiDocumentParser parser = new();
using (StreamReader reader = System.IO.File.OpenText("./Plugins/repaire-openapi_zh-cn.json"))
{
RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream);

var operationsToExclude = specification.Operations.Where(o => o.Method == HttpMethod.Get).ToList();

operationsToExclude.ToList().ForEach(operation => specification.Operations.Remove(operation));


kernel.ImportPluginFromOpenApi("RepairService", specification);

kernel.Plugins["RepairService"].Display();
}

自定义HttpClient

在使用OpenAPI 插件时,也支持自定义HttpClient,以实现更高的请求控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Text;
using System.Text.Json;
using System.Net.Http.Headers;

private sealed class StubHttpHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage? request, CancellationToken cancellationToken)
{
// 添加Bearer Token
// request.Headers.Authorization = new AuthenticationHeaderValue("Bearer","your-api-key");

// return await base.SendAsync(request, cancellationToken);
var requestContent = await request!.Content.ReadAsStringAsync();
var requestJson = JsonDocument.Parse(requestContent).RootElement;
requestJson.Display();

return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("Success", Encoding.UTF8, "application/json")
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma warning disable SKEXP0040

using Microsoft.SemanticKernel.Plugins.OpenApi;

kernel.Plugins.Clear();

var httpClient = new HttpClient(new StubHttpHandler());

var repairePlugin = await kernel.ImportPluginFromOpenApiAsync(
pluginName:"RepairePlugin",
filePath: "./Plugins/repaire-openapi_zh-cn.json",
executionParameters: new OpenApiFunctionExecutionParameters(httpClient: httpClient)
);

var newRepaire = new KernelArguments{
["title"] = "升级服务器",
["description"] = "升级服务器系统至最新版本",
["assignedTo"] = "shengjie"
};

var result = await repairePlugin["createRepair"].InvokeAsync(kernel, newRepaire);
result.Display();

复杂传参

使用payload传参

OpenApiFunctionExecutionParameters.EnableDynamicPayload 参数用于:确定是否基于负载元数据动态构建 REST API 操作的负载。
该功能默认启用,支持具有简单负载结构的操作——即不同层级不存在同名属性。为了支持更复杂的负载,应禁用此功能,并通过 payload 参数提供负载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#pragma warning disable SKEXP0040

using Microsoft.SemanticKernel.Plugins.OpenApi;
using Microsoft.SemanticKernel.Connectors.OpenAI;

kernel.Plugins.Clear();

var httpClient = new HttpClient(new StubHttpHandler());

using (Stream stream = File.OpenRead("./Plugins/event-openapi.json"))
{
KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("Event_Utils", stream, new OpenApiFunctionExecutionParameters(httpClient)
{
EnableDynamicPayload = false, // Disable dynamic payload construction,default is true
});
KernelFunction createMeetingFunction = plugin["createMeeting"];

string payload = """
{
"subject": "IT Meeting",
"start": {
"dateTime": "2023-10-01T10:00:00",
"timeZone": "UTC"
},
"end": {
"dateTime": "2023-10-01T11:00:00",
"timeZone": "UTC"
},
"tags": [
{ "name": "IT" },
{ "name": "Meeting" }
]
}
""";

// Create arguments for the createEvent function
KernelArguments arguments = new()
{
["payload"] = payload,
["content-type"] = "application/json"
};


// 直接调用createEvent函数的示例
await kernel.InvokeAsync(createMeetingFunction, arguments);

var meetting = new {
subject = "IT Meeting",
start = new {
dateTime = "2023-10-01T10:00:00",
timeZone = "UTC"
},
end = new {
dateTime = "2023-10-01T11:00:00",
timeZone = "UTC"
},
tags = new [] {
new { name = "IT" },
new { name = "Meeting" }
}
};

// Create arguments for the createEvent function
KernelArguments newArgument = new()
{
["payload"] = JsonSerializer.Serialize(meetting),
["content-type"] = "application/json"
};


// 直接调用createEvent函数的示例
await kernel.InvokeAsync(createMeetingFunction, newArgument);

// 通过AI调用createEvent函数的示例
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
var result = await kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}

启用EnablePayloadNamespacing`解决命名空间冲突

确定是否对负载参数名称添加命名空间。 此功能仅在 EnableDynamicPayload 属性设置为 true 时适用。命名空间通过将父参数名称作为前缀(以点号分隔)来防止命名冲突。例如,如果没有命名空间,senderreceiver 父参数中的 email 参数将从相同的 email 参数中解析,这是不正确的。然而,通过使用命名空间,sender.emailreceiver.email 参数将从具有相同名称的参数中正确解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma warning disable SKEXP0040

using Microsoft.SemanticKernel.Plugins.OpenApi;
using Microsoft.SemanticKernel.Connectors.OpenAI;

kernel.Plugins.Clear();

var httpClient = new HttpClient(new StubHttpHandler());

using (Stream stream = File.OpenRead("./Plugins/event-openapi.json"))
{
KernelPlugin plugin = await kernel.ImportPluginFromOpenApiAsync("Event_Utils", stream, new OpenApiFunctionExecutionParameters(httpClient)
{
EnableDynamicPayload = true, // Enable dynamic payload construction, default is true
EnablePayloadNamespacing = true, // Enable payload namespacing, default is false
});
KernelFunction createMeetingFunction = plugin["createMeeting"];

KernelArguments arguments = new()
{
["subject"] = "IT Meeting",
["start.dateTime"] = "2023-10-01T10:00:00",
["start.timeZone"] = "UTC",
["end.dateTime"] = "2023-10-01T11:00:00",
["end.timeZone"] = "UTC",
["tags"] = """[ { "name": "IT" }, { "name": "Meeting" } ]"""
};
// 直接调用createEvent函数的示例
await kernel.InvokeAsync(createMeetingFunction, arguments);

// 通过AI调用createEvent函数的示例
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
var result = await kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
result.Display();
}

请求改写

OpenApiFunctionExecutionParameters 中提供了RestApiOperationResponseFactory 属性, 它允许对原始响应的各个方面进行修改,例如添加响应头、更改响应内容、调整模式或提供全新的响应。如果未提供自定义工厂,则默认使用内部工厂。
其本质是个委托定义如下:

1
2
3
public delegate Task<RestApiOperationResponse> RestApiOperationResponseFactory(
RestApiOperationResponseFactoryContext context,
CancellationToken cancellationToken = default (CancellationToken));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 #pragma warning disable SKEXP0040

Func<RestApiOperationResponseFactoryContext, CancellationToken, Task<RestApiOperationResponse>>
addHeadersIntoRestApiOperationResponseFactory = async (context, cancellationToken) =>
{
// Create the response using the internal factory
RestApiOperationResponse response = await context.InternalFactory(context, cancellationToken);

Console.WriteLine("Response before modification:");
response.Display();
if (response.Headers == null)
{
response.Headers = new Dictionary<string, IEnumerable<string>>();
}

response.Headers.Add("x-custom-header", ["custom-value"]);

// Return the modified response that will be returned to the caller
return response;
};

// Register the operation response factory and the custom HTTP client
OpenApiFunctionExecutionParameters executionParameters = new()
{
RestApiOperationResponseFactory = (context, cancellationToken) => addHeadersIntoRestApiOperationResponseFactory(context, cancellationToken),
//LoggerFactory = kernel.LoggerFactory
};
// Create OpenAPI plugin
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("RepairService", "./Plugins/repaire-openapi_zh-cn.json", executionParameters);
// Create arguments for a new repair
KernelArguments arguments = new()
{
["title"] = "The Case of the Broken Gizmo",
["description"] = "It's broken. Send help!",
["assignedTo"] = "Tech Magician"
};
// Create the repair
FunctionResult createResult = await plugin["createRepair"].InvokeAsync(kernel, arguments);
// Get operation response that was modified
RestApiOperationResponse response = createResult.GetValue<RestApiOperationResponse>()!;

response.Display();
作者

步步为营

发布于

2025-04-11

更新于

2025-04-11

许可协议