mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
gRPC skills functionality (#814)
### Motivation and Context This PR adds MVP functionality for gRPC skills by introducing a set of classes for importing, parsing and executing gRPC operations defined in the .proto files. This functionality may be useful in scenarios when skills are represented as methods that are hosted on a different machine and need to be called remotely. ### Description The PR adds the following classes: - ProtoDocumentParser - iterates over a all methods of all services declared in .proto file and creates an instance of GrpcOperation model class for each of them. - GrpcOperation - model class representing gRPC operation. - GrpcOperationRunner - accepts an instance of the GrpcOperation class and a collection of arguments. Dynamically creates an instance of gRPC method request and calls the method. Receives, transforms and returns the method call result. - GrpcOperationExtensions - extension methods for GrpcOperation to extend the class with SK SDK domain related functionality. - KernelGrpcExtensions - declares a few register and import methods for importing the gRPC skills from a folder or a file and registering them in SK Kernel. Co-authored-by: Shawn Callegari <[email protected]> Co-authored-by: Govind Kamtamneni <[email protected]>
This commit is contained in:
co-authored by
Shawn Callegari
Govind Kamtamneni
parent
8f6e96ffd6
commit
972d8782a5
@@ -10,26 +10,22 @@
|
||||
<PackageVersion Include="System.Linq.Async" Version="[6.0.1, )" />
|
||||
<PackageVersion Include="System.Text.Json" Version="[6.0.0, )" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="[1.0.0-beta.5, )" />
|
||||
|
||||
<!-- Microsoft.Extensions.Logging -->
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="[6.0.0, )" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[6.0.0, )" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="[6.0.0, )" />
|
||||
|
||||
<!-- Microsoft.Extensions.Configuration -->
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
|
||||
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="xunit" Version="2.4.2" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
|
||||
|
||||
<!-- Skills -->
|
||||
<PackageVersion Include="DocumentFormat.OpenXml" Version="2.20.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="[7.0, )" />
|
||||
@@ -38,8 +34,10 @@
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="[2.28.0, )" />
|
||||
<PackageVersion Include="Microsoft.OpenApi" Version="[1.6.3, )" />
|
||||
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="[1.6.3, )" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="[13.0.3, )" />
|
||||
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="[11.0.1, )" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.52.0" />
|
||||
<PackageVersion Include="protobuf-net" Version="3.2.16" />
|
||||
<PackageVersion Include="protobuf-net.Reflection" Version="3.2.12" />
|
||||
<!-- Symbols -->
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Planning.SequentialPlanner"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.Memory.AzureCognitiveSearch", "src\Connectors\Connectors.Memory.AzureCognitiveSearch\Connectors.Memory.AzureCognitiveSearch.csproj", "{EC3BB6D1-2FB2-4702-84C6-F791DE533ED4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Skills.Grpc", "src\Skills\Skills.Grpc\Skills.Grpc.csproj", "{E52F805C-794A-4CA9-B684-DFF358B18820}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -203,6 +205,10 @@ Global
|
||||
{EC3BB6D1-2FB2-4702-84C6-F791DE533ED4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC3BB6D1-2FB2-4702-84C6-F791DE533ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC3BB6D1-2FB2-4702-84C6-F791DE533ED4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E52F805C-794A-4CA9-B684-DFF358B18820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E52F805C-794A-4CA9-B684-DFF358B18820}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E52F805C-794A-4CA9-B684-DFF358B18820}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E52F805C-794A-4CA9-B684-DFF358B18820}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -213,7 +219,6 @@ Global
|
||||
{47C6F821-5103-431F-B3B8-A2868A68BB78} = {FA3720F1-C99A-49B2-9577-A940257098BF}
|
||||
{3EB61E99-C39B-4620-9482-F8DA18E48525} = {FA3720F1-C99A-49B2-9577-A940257098BF}
|
||||
{34A7F1EF-D243-4160-A413-D713FEABCD94} = {FA3720F1-C99A-49B2-9577-A940257098BF}
|
||||
{E4B777A1-28E1-41BE-96AE-7F3EC61FD5D4} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0}
|
||||
{F94D1938-9DB7-4B24-9FF3-166DDFD96330} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
|
||||
{689A5041-BAE7-448F-9BDC-4672E96249AA} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
|
||||
{EEA87FBC-4ED5-458C-ABD3-BEAEEB535BAF} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
|
||||
@@ -240,6 +245,7 @@ Global
|
||||
{F51017A9-15C8-472D-893C-080046D710A6} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
|
||||
{A350933D-F9D5-4AD3-8C4F-B856B5020297} = {078F96B4-09E1-4E0E-B214-F71A4F4BF633}
|
||||
{EC3BB6D1-2FB2-4702-84C6-F791DE533ED4} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
|
||||
{E52F805C-794A-4CA9-B684-DFF358B18820} = {9ECD1AA0-75B3-4E25-B0B5-9F0945B64974}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Planning.SequentialPlanner" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Skills.OpenAPI" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Connectors.Memory.CosmosDB" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Skills.Grpc" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="SemanticKernel.Skills.UnitTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="SemanticKernel.UnitTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="IntegrationTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Semantic Kernel Core</Title>
|
||||
<Description>Semantic Kernel core orchestration, runtime and skills.
|
||||
This package is automatically installed by 'Microsoft.SemanticKernel' package with other useful packages.
|
||||
Install this package manually only if you are selecting individual Semantic Kernel components.</Description>
|
||||
<Description>
|
||||
Semantic Kernel core orchestration, runtime and skills.
|
||||
This package is automatically installed by 'Microsoft.SemanticKernel' package with other useful packages.
|
||||
Install this package manually only if you are selecting individual Semantic Kernel components.
|
||||
</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -24,6 +26,7 @@ Install this package manually only if you are selecting individual Semantic Kern
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Planning.SequentialPlanner" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Connectors.AI.OpenAI" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Skills.OpenAPI" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="Microsoft.SemanticKernel.Skills.Grpc" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="SemanticKernel.UnitTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="SemanticKernel.Skills.UnitTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
<InternalsVisibleTo Include="SemanticKernel.IntegrationTests" PublicKey="$(StrongNamePublicKey)"/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Class for extensions methods for the <see cref="GrpcOperation"/> class.
|
||||
/// </summary>
|
||||
internal static class GrpcOperationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns list of gRPC operation parameters.
|
||||
/// </summary>
|
||||
/// <returns>The list of parameters.</returns>
|
||||
public static IReadOnlyList<ParameterView> GetParameters(this GrpcOperation operation)
|
||||
{
|
||||
var parameters = new List<ParameterView>();
|
||||
|
||||
//Register the "address" parameter so that it's possible to override it if needed.
|
||||
parameters.Add(new ParameterView(GrpcOperation.AddressArgumentName, "Address for gRPC channel to use.", string.Empty));
|
||||
|
||||
//Register the "payload" parameter to be used as gRPC operation request message.
|
||||
parameters.Add(new ParameterView(GrpcOperation.PayloadArgumentName, "gRPC request message.", string.Empty));
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel.Diagnostics;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Protobuf;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IKernel"/> extensions methods for gRPC functionality.
|
||||
/// </summary>
|
||||
public static class KernelGrpcExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports gRPC document from a directory.
|
||||
/// </summary>
|
||||
/// <param name="kernel">Semantic Kernel instance.</param>
|
||||
/// <param name="parentDirectory">Directory containing the skill directory.</param>
|
||||
/// <param name="skillDirectoryName">Name of the directory containing the selected skill.</param>
|
||||
/// <returns>A list of all the semantic functions representing the skill.</returns>
|
||||
public static IDictionary<string, ISKFunction> ImportGrpcSkillFromDirectory(this IKernel kernel, string parentDirectory, string skillDirectoryName)
|
||||
{
|
||||
const string PROTO_FILE = "grpc.proto";
|
||||
|
||||
Verify.ValidSkillName(skillDirectoryName);
|
||||
|
||||
var skillDir = Path.Combine(parentDirectory, skillDirectoryName);
|
||||
Verify.DirectoryExists(skillDir);
|
||||
|
||||
var filePath = Path.Combine(skillDir, PROTO_FILE);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"No .proto document for the specified path - {filePath} is found.");
|
||||
}
|
||||
|
||||
kernel.Log.LogTrace("Registering gRPC functions from {0} .proto document.", filePath);
|
||||
|
||||
using var stream = File.OpenRead(filePath);
|
||||
|
||||
return kernel.RegisterGrpcSkill(stream, skillDirectoryName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports gRPC document from a file.
|
||||
/// </summary>
|
||||
/// <param name="kernel">Semantic Kernel instance.</param>
|
||||
/// <param name="skillName">Name of the skill to register.</param>
|
||||
/// <param name="filePath">File path to .proto document.</param>
|
||||
/// <returns>A list of all the semantic functions representing the skill.</returns>
|
||||
public static IDictionary<string, ISKFunction> ImportGrpcSkillFromFile(
|
||||
this IKernel kernel,
|
||||
string skillName,
|
||||
string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"No .proto document for the specified path - {filePath} is found.");
|
||||
}
|
||||
|
||||
kernel.Log.LogTrace("Registering gRPC functions from {0} .proto document.", filePath);
|
||||
|
||||
using var stream = File.OpenRead(filePath);
|
||||
|
||||
return kernel.RegisterGrpcSkill(stream, skillName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an gRPC skill.
|
||||
/// </summary>
|
||||
/// <param name="kernel">Semantic Kernel instance.</param>
|
||||
/// <param name="documentStream">.proto document stream.</param>
|
||||
/// <param name="skillName">Skill name.</param>
|
||||
/// <returns>A list of all the semantic functions representing the skill.</returns>
|
||||
public static IDictionary<string, ISKFunction> RegisterGrpcSkill(
|
||||
this IKernel kernel,
|
||||
Stream documentStream,
|
||||
string skillName)
|
||||
{
|
||||
Verify.NotNull(kernel, nameof(kernel));
|
||||
Verify.ValidSkillName(skillName);
|
||||
|
||||
// Parse
|
||||
var parser = new ProtoDocumentParser();
|
||||
|
||||
var operations = parser.Parse(documentStream, skillName);
|
||||
|
||||
var skill = new Dictionary<string, ISKFunction>();
|
||||
|
||||
var runner = new GrpcOperationRunner(new HttpClient());
|
||||
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
try
|
||||
{
|
||||
kernel.Log.LogTrace("Registering gRPC function {0}.{1}", skillName, operation.Name);
|
||||
var function = kernel.RegisterGrpcFunction(runner, skillName, operation);
|
||||
skill[function.Name] = function;
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
//Logging the exception and keep registering other gRPC functions
|
||||
kernel.Log.LogWarning(ex, "Something went wrong while rendering the gRPC function. Function: {0}.{1}. Error: {2}",
|
||||
skillName, operation.Name, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return skill;
|
||||
}
|
||||
|
||||
#region private
|
||||
|
||||
/// <summary>
|
||||
/// Registers SKFunction for a gRPC operation.
|
||||
/// </summary>
|
||||
/// <param name="kernel">Semantic Kernel instance.</param>
|
||||
/// <param name="runner">gRPC operation runner.</param>
|
||||
/// <param name="skillName">Skill name.</param>
|
||||
/// <param name="operation">The gRPC operation.</param>
|
||||
/// <returns>An instance of <see cref="SKFunction"/> class.</returns>
|
||||
private static ISKFunction RegisterGrpcFunction(
|
||||
this IKernel kernel,
|
||||
GrpcOperationRunner runner,
|
||||
string skillName,
|
||||
GrpcOperation operation)
|
||||
{
|
||||
var operationParameters = operation.GetParameters();
|
||||
|
||||
async Task<SKContext> ExecuteAsync(SKContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var arguments = new Dictionary<string, string>();
|
||||
|
||||
//Extract function arguments from context
|
||||
foreach (var parameter in operationParameters)
|
||||
{
|
||||
//A try to resolve argument parameter name.
|
||||
if (context.Variables.Get(parameter.Name, out var value))
|
||||
{
|
||||
arguments.Add(parameter.Name, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"No variable found in context to use as an argument for the '{parameter.Name}' parameter of the '{skillName}.{operation.Name}' gRPC function.");
|
||||
}
|
||||
|
||||
//SKFunction should be extended to pass cancellation token for delegateFunction calls.
|
||||
var result = await runner.RunAsync(operation, arguments, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
context.Variables.Update(result.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
kernel.Log.LogWarning(ex, "Something went wrong while rendering the gRPC function. Function: {0}.{1}. Error: {2}", skillName, operation.Name,
|
||||
ex.Message);
|
||||
context.Fail(ex.Message, ex);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope.
|
||||
var function = new SKFunction(
|
||||
delegateType: SKFunction.DelegateTypes.ContextSwitchInSKContextOutTaskSKContext,
|
||||
delegateFunction: ExecuteAsync,
|
||||
parameters: operationParameters.ToList(),
|
||||
description: operation.Name,
|
||||
skillName: skillName,
|
||||
functionName: operation.Name,
|
||||
isSemantic: false,
|
||||
log: kernel.Log);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
|
||||
return kernel.RegisterCustomFunction(skillName, function);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.SemanticKernel.Diagnostics;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc;
|
||||
|
||||
/// <summary>
|
||||
/// Runs gRPC operation runner.
|
||||
/// </summary>
|
||||
internal class GrpcOperationRunner
|
||||
{
|
||||
/// <summary>
|
||||
/// An instance of the HttpClient class.
|
||||
/// </summary>
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationRunner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">An instance of the HttpClient class.</param>
|
||||
public GrpcOperationRunner(HttpClient httpClient)
|
||||
{
|
||||
this._httpClient = httpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a gRPC operation.
|
||||
/// </summary>
|
||||
/// <param name="operation">The operation to run.</param>
|
||||
/// <param name="arguments">The operation arguments.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The result of the operation run.</returns>
|
||||
public async Task<JsonObject> RunAsync(GrpcOperation operation, IDictionary<string, string> arguments, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Verify.NotNull(operation, $"No operation was provided for {nameof(GrpcOperationRunner)} to run.");
|
||||
Verify.NotNull(arguments, $"No arguments were provided for {operation.Name} gRPC operation.");
|
||||
|
||||
var address = this.GetAddress(operation, arguments);
|
||||
|
||||
var channelOptions = new GrpcChannelOptions { HttpClient = this._httpClient, DisposeHttpClient = false };
|
||||
|
||||
using (var channel = GrpcChannel.ForAddress(address, channelOptions))
|
||||
{
|
||||
var requestType = BuildGrpcOperationDataContractType(operation.Request);
|
||||
|
||||
var responseType = BuildGrpcOperationDataContractType(operation.Response);
|
||||
|
||||
var method = new Method<object, object>
|
||||
(
|
||||
MethodType.Unary,
|
||||
operation.FullServiceName,
|
||||
operation.Name,
|
||||
this.CreateMarshaller<object>(requestType),
|
||||
this.CreateMarshaller<object>(responseType)
|
||||
);
|
||||
|
||||
var invoker = channel.CreateCallInvoker();
|
||||
|
||||
var request = this.GenerateOperationRequest(operation, requestType, arguments);
|
||||
|
||||
var response = await invoker.AsyncUnaryCall(method, null, new CallOptions(cancellationToken: cancellationToken), request).ConfigureAwait(false);
|
||||
|
||||
return ConvertResponse(response, responseType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts gRPC response.
|
||||
/// </summary>
|
||||
/// <param name="response">The response to convert.</param>
|
||||
/// <param name="responseType">The response type info.</param>
|
||||
/// <returns>The converted response.</returns>
|
||||
private static JsonObject ConvertResponse(object response, Type responseType)
|
||||
{
|
||||
var content = JsonSerializer.Serialize(response, responseType, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
//First iteration allowing to associate additional metadata with the returned content.
|
||||
var result = new JsonObject();
|
||||
result.Add("content", content);
|
||||
result.Add("contentType", "application/json; charset=utf-8");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns address of a channel that provides connection to a gRPC server.
|
||||
/// </summary>
|
||||
/// <param name="operation">The gRPC operation.</param>
|
||||
/// <param name="arguments">The gRPC operation arguments.</param>
|
||||
/// <returns>The channel address.</returns>
|
||||
private string GetAddress(GrpcOperation operation, IDictionary<string, string> arguments)
|
||||
{
|
||||
string? address = null;
|
||||
|
||||
if (!arguments.TryGetValue(GrpcOperation.AddressArgumentName, out address))
|
||||
{
|
||||
address = operation.Address;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(address))
|
||||
{
|
||||
throw new GrpcOperationException($"No address provided for the '{operation.Name}' gRPC operation.");
|
||||
}
|
||||
|
||||
return address!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a marshaller - a typed abstraction for gRPC message serialization and deserialization.
|
||||
/// </summary>
|
||||
/// <param name="contractType">The message contract data type.</param>
|
||||
/// <returns>The marshaller.</returns>
|
||||
private Marshaller<T> CreateMarshaller<T>(Type contractType)
|
||||
{
|
||||
byte[] Serialize(T instance)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
Serializer.NonGeneric.Serialize(memoryStream, instance);
|
||||
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
T Deserialize(byte[] source)
|
||||
{
|
||||
using var memoryStream = new MemoryStream(source);
|
||||
|
||||
return (T)Serializer.NonGeneric.Deserialize(contractType, memoryStream);
|
||||
}
|
||||
|
||||
return Marshallers.Create((instance) => Serialize(instance), (bytes) => Deserialize(bytes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a gRPC operation request.
|
||||
/// </summary>
|
||||
/// <param name="operation">The gRPC operation.</param>
|
||||
/// <param name="type">The operation request data type.</param>
|
||||
/// <param name="arguments">The operation arguments.</param>
|
||||
/// <returns>The operation request instance.</returns>
|
||||
private object GenerateOperationRequest(GrpcOperation operation, Type type, IDictionary<string, string> arguments)
|
||||
{
|
||||
//Getting 'payload' argument to by used as gRPC request message
|
||||
if (!arguments.TryGetValue(GrpcOperation.PayloadArgumentName, out var payload))
|
||||
{
|
||||
throw new GrpcOperationException($"No '{GrpcOperation.PayloadArgumentName}' argument representing gRPC request message is found for the '{operation.Name}' gRPC operation.");
|
||||
}
|
||||
|
||||
//Deserializing JSON payload to gRPC request message
|
||||
var instance = JsonSerializer.Deserialize(payload, type, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
if (instance == null)
|
||||
{
|
||||
throw new GrpcOperationException($"Impossible to create gRPC request message for the '{operation.Name}' gRPC operation.");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds gRPC operation data contract type.
|
||||
/// </summary>
|
||||
/// <param name="dataContractMetadata">The data contract type metadata.</param>
|
||||
/// <returns>.NET type representing the data contract type.</returns>
|
||||
private static Type BuildGrpcOperationDataContractType(GrpcOperationDataContractType dataContractMetadata)
|
||||
{
|
||||
var assemblyName = new AssemblyName($"{dataContractMetadata.Name}Assembly");
|
||||
|
||||
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
|
||||
|
||||
var moduleBuilder = assemblyBuilder.DefineDynamicModule($"{dataContractMetadata.Name}Module");
|
||||
|
||||
var typeBuilder = moduleBuilder.DefineType(dataContractMetadata.Name, TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class);
|
||||
|
||||
//Creating and adding a .NET property for each data contract filed
|
||||
foreach (var field in dataContractMetadata.Fields)
|
||||
{
|
||||
var fieldName = field.Name;
|
||||
var propertyName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(field.Name);
|
||||
|
||||
var propertyType = GetNetType(field.TypeName);
|
||||
|
||||
//Creating a private backing field for the property
|
||||
var fieldBuilder = typeBuilder.DefineField(fieldName + "_", propertyType, FieldAttributes.Private);
|
||||
var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, null);
|
||||
|
||||
//Creating the property get method and binding it to the private filed
|
||||
var getterBuilder = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
|
||||
var getterIl = getterBuilder.GetILGenerator();
|
||||
getterIl.Emit(OpCodes.Ldarg_0);
|
||||
getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
|
||||
getterIl.Emit(OpCodes.Ret);
|
||||
|
||||
//Creating the property set method and binding it to the private filed
|
||||
var setterBuilder = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyType });
|
||||
var setterIl = setterBuilder.GetILGenerator();
|
||||
setterIl.Emit(OpCodes.Ldarg_0);
|
||||
setterIl.Emit(OpCodes.Ldarg_1);
|
||||
setterIl.Emit(OpCodes.Stfld, fieldBuilder);
|
||||
setterIl.Emit(OpCodes.Ret);
|
||||
|
||||
//Registering the property get and set methods.
|
||||
propertyBuilder.SetGetMethod(getterBuilder);
|
||||
propertyBuilder.SetSetMethod(setterBuilder);
|
||||
|
||||
//Add ProtoMember attribute to the data contract with tag/number
|
||||
var dataMemberAttributeBuilder = new CustomAttributeBuilder(typeof(ProtoMemberAttribute).GetConstructor(new[] { typeof(int) }), new object[] { field.Number });
|
||||
propertyBuilder.SetCustomAttribute(dataMemberAttributeBuilder);
|
||||
}
|
||||
|
||||
//Add ProtoContract attribute to the data contract
|
||||
var dataContractAttributeBuilder = new CustomAttributeBuilder(typeof(ProtoContractAttribute).GetConstructor(Type.EmptyTypes), Array.Empty<object>());
|
||||
typeBuilder.SetCustomAttribute(dataContractAttributeBuilder);
|
||||
|
||||
var type = typeBuilder.CreateTypeInfo();
|
||||
if (type == null)
|
||||
{
|
||||
throw new GrpcOperationException($"Impossible to create type for '{dataContractMetadata.Name}' data contract.");
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns .net type that corresponds to protobuf data type name.
|
||||
/// </summary>
|
||||
/// <param name="type">The protobuf data type name.</param>
|
||||
/// <returns>The .net type.</returns>
|
||||
private static Type GetNetType(string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "TYPE_DOUBLE":
|
||||
return typeof(double);
|
||||
case "TYPE_FLOAT":
|
||||
return typeof(float);
|
||||
case "TYPE_INT64":
|
||||
return typeof(long);
|
||||
case "TYPE_UINT64":
|
||||
return typeof(ulong);
|
||||
case "TYPE_INT32":
|
||||
return typeof(int);
|
||||
case "TYPE_FIXED64":
|
||||
return typeof(ulong);
|
||||
case "TYPE_FIXED32":
|
||||
return typeof(uint);
|
||||
case "TYPE_BOOL":
|
||||
return typeof(bool);
|
||||
case "TYPE_STRING":
|
||||
return typeof(string);
|
||||
case "TYPE_BYTES":
|
||||
return typeof(byte[]);
|
||||
case "TYPE_UINT32":
|
||||
return typeof(uint);
|
||||
case "TYPE_SFIXED32":
|
||||
return typeof(int);
|
||||
case "TYPE_SFIXED64":
|
||||
return typeof(long);
|
||||
case "TYPE_SINT32":
|
||||
return typeof(int);
|
||||
case "TYPE_SINT64":
|
||||
return typeof(long);
|
||||
default:
|
||||
throw new ArgumentException($"Unknown type {type}", nameof(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The gRPC operation.
|
||||
/// </summary>
|
||||
internal class GrpcOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of 'address' argument used as override for the address provided by gRPC operation.
|
||||
/// </summary>
|
||||
internal const string AddressArgumentName = "address";
|
||||
|
||||
/// <summary>
|
||||
/// Name of 'payload' argument that represents gRPC operation request message.
|
||||
/// </summary>
|
||||
internal const string PayloadArgumentName = "payload";
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperation"/> class.
|
||||
/// <param name="serviceName">The service name.</param>
|
||||
/// <param name="name">The operation name.</param>
|
||||
/// <param name="request">The operation request type metadata.</param>
|
||||
/// <param name="response">The operation response type metadata.</param>
|
||||
/// </summary>
|
||||
public GrpcOperation(
|
||||
string serviceName,
|
||||
string name,
|
||||
GrpcOperationDataContractType request,
|
||||
GrpcOperationDataContractType response)
|
||||
{
|
||||
this.ServiceName = serviceName;
|
||||
this.Name = name;
|
||||
this.Request = request;
|
||||
this.Response = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The service name.
|
||||
/// </summary>
|
||||
public string ServiceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The operation name.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The full service name that includes that 'package' specifier as prefix.
|
||||
/// </summary>
|
||||
public string FullServiceName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(this.Package))
|
||||
{
|
||||
return this.ServiceName;
|
||||
}
|
||||
|
||||
return $"{this.Package}.{this.ServiceName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The gRPC request data contract.
|
||||
/// </summary>
|
||||
public GrpcOperationDataContractType Request { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The gRPC response data contract.
|
||||
/// </summary>
|
||||
public GrpcOperationDataContractType Response { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The address.
|
||||
/// </summary>
|
||||
public string? Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifier to prevent name clashes between types.
|
||||
/// </summary>
|
||||
public string? Package { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The gRPC operation data contract.
|
||||
/// </summary>
|
||||
internal class GrpcOperationDataContractType
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationDataContractType"/> class.
|
||||
/// </summary>
|
||||
public GrpcOperationDataContractType(string name, IList<GrpcOperationDataContractTypeFiled> fields)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Fields = fields;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data contract name;
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IList<GrpcOperationDataContractTypeFiled> Fields { get; } = new List<GrpcOperationDataContractTypeFiled>();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The gRPC operation data contract field.
|
||||
/// </summary>
|
||||
internal class GrpcOperationDataContractTypeFiled
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationDataContractTypeFiled"/> class.
|
||||
/// </summary>
|
||||
public GrpcOperationDataContractTypeFiled(string name, int number, string typeName)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Number = number;
|
||||
this.TypeName = typeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Field name.
|
||||
/// </summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Field number.
|
||||
/// </summary>
|
||||
public int Number { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Field type name.
|
||||
/// </summary>
|
||||
public string TypeName { get; private set; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Exception to be throw if a gRPC operation has failed.
|
||||
/// </summary>
|
||||
public class GrpcOperationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The exception message.</param>
|
||||
internal GrpcOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The exception message.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
internal GrpcOperationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcOperationException"/> class.
|
||||
/// </summary>
|
||||
internal GrpcOperationException()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Google.Protobuf.Reflection;
|
||||
using Microsoft.SemanticKernel.Diagnostics;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
using ProtoBuf;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Protobuf;
|
||||
|
||||
/// <summary>
|
||||
/// Parser for .proto definition documents.
|
||||
/// </summary>
|
||||
internal class ProtoDocumentParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses .proto document.
|
||||
/// </summary>
|
||||
/// <param name="document">The .proto document.</param>
|
||||
/// <param name="name">The .proto file logical name.</param>
|
||||
/// <returns>List of gRPC operations.</returns>
|
||||
public IList<GrpcOperation> Parse(Stream document, string name)
|
||||
{
|
||||
Verify.NotNull(document, $"The {nameof(document)} parameter is not set to an instance of an object.");
|
||||
Verify.NotNullOrWhiteSpace(name, ".proto file name is not provided.");
|
||||
|
||||
using var textReader = new StreamReader(document);
|
||||
|
||||
var descriptor = new FileDescriptorSet();
|
||||
descriptor.Add(name, source: textReader);
|
||||
descriptor.Process();
|
||||
|
||||
var errors = descriptor.GetErrors();
|
||||
if (errors != null && errors.Any())
|
||||
{
|
||||
throw new ProtobufParsingException($"Parsing of '{name}' .proto document has failed. Details: {string.Join(";", errors.AsEnumerable())}");
|
||||
}
|
||||
|
||||
return this.GetGrpcOperations(descriptor.Files.Single());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an .proto document and extracts gRPC operations.
|
||||
/// </summary>
|
||||
/// <param name="model">The .proto document model.</param>
|
||||
/// <returns>List of gRPC operations.</returns>
|
||||
private IList<GrpcOperation> GetGrpcOperations(FileDescriptorProto model)
|
||||
{
|
||||
var operations = new List<GrpcOperation>();
|
||||
|
||||
foreach (var service in model.Services)
|
||||
{
|
||||
foreach (var method in service.Methods)
|
||||
{
|
||||
var requestContract = this.CreateDataContract(model.MessageTypes, method.InputType, model.Package, method.Name);
|
||||
|
||||
var responseContract = this.CreateDataContract(model.MessageTypes, method.OutputType, model.Package, method.Name);
|
||||
|
||||
var operation = new GrpcOperation(service.Name, method.Name, requestContract, responseContract);
|
||||
operation.Package = model.Package;
|
||||
|
||||
operations.Add(operation);
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates gRPC operation data contract.
|
||||
/// </summary>
|
||||
/// <param name="allMessageTypes">Existing ,message types declared in .proto file.</param>
|
||||
/// <param name="messageTypeName">Message type to create the data contract for.</param>
|
||||
/// <param name="package">The .proto file 'package' specifier.</param>
|
||||
/// <param name="methodName">The method to create data contract for.</param>
|
||||
/// <returns>The operation data contract.</returns>
|
||||
private GrpcOperationDataContractType CreateDataContract(IList<DescriptorProto> allMessageTypes, string messageTypeName, string package, string methodName)
|
||||
{
|
||||
var fullTypeName = messageTypeName.TrimStart('.');
|
||||
|
||||
var typeName = fullTypeName;
|
||||
|
||||
if (!string.IsNullOrEmpty(package))
|
||||
{
|
||||
typeName = fullTypeName.Replace($"{package}.", "");
|
||||
}
|
||||
|
||||
var messageType = allMessageTypes.SingleOrDefault(mt => mt.Name == fullTypeName || mt.Name == typeName);
|
||||
if (messageType == null)
|
||||
{
|
||||
throw new ProtobufParsingException($"No '{fullTypeName}' message type is found while resolving data contracts for the '{methodName}' method.");
|
||||
}
|
||||
|
||||
var fields = this.GetDataContractFields(messageType.Fields);
|
||||
|
||||
return new GrpcOperationDataContractType(fullTypeName, fields);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns data contract fields.
|
||||
/// </summary>
|
||||
/// <param name="fields">Message type fields.</param>
|
||||
/// <returns>The data contract fields.</returns>
|
||||
private IList<GrpcOperationDataContractTypeFiled> GetDataContractFields(List<FieldDescriptorProto> fields)
|
||||
{
|
||||
var result = new List<GrpcOperationDataContractTypeFiled>();
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var type = GetProtobufDataTypeName(field.type);
|
||||
|
||||
result.Add(new GrpcOperationDataContractTypeFiled(field.Name, field.Number, type));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns protobuf data type name.
|
||||
/// </summary>
|
||||
/// <param name="type">Type descriptor.</param>
|
||||
/// <returns>The protobuf data type name.</returns>
|
||||
private static string GetProtobufDataTypeName(FieldDescriptorProto.Type type)
|
||||
{
|
||||
var fieldInfo = typeof(FieldDescriptorProto.Type).GetField(type.ToString());
|
||||
|
||||
//Get protobuf type name from enum attribute - [global::ProtoBuf.ProtoEnum(Name = @"TYPE_DOUBLE")]
|
||||
var attribute = (ProtoEnumAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ProtoEnumAttribute));
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
throw new ProtobufParsingException($"Impossible to find protobuf type name corresponding to '{type}' type.");
|
||||
}
|
||||
|
||||
return attribute.Name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.SemanticKernel.Skills.Grpc.Protobuf;
|
||||
|
||||
/// <summary>
|
||||
/// Exception to be throw if a .proto file parsing has failed.
|
||||
/// </summary>
|
||||
public class ProtobufParsingException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="ProtobufParsingException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The exception message.</param>
|
||||
internal ProtobufParsingException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="ProtobufParsingException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The exception message.</param>
|
||||
/// <param name="innerException">The inner exception.</param>
|
||||
internal ProtobufParsingException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="ProtobufParsingException"/> class.
|
||||
/// </summary>
|
||||
internal ProtobufParsingException()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
|
||||
<AssemblyName>Microsoft.SemanticKernel.Skills.Grpc</AssemblyName>
|
||||
<RootNamespace>$(AssemblyName)</RootNamespace>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Semantic Kernel - gRPC Skills</Title>
|
||||
<Description>Semantic Kernel gRPC Skill</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>SemanticKernel.Skills.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.Net.Client" />
|
||||
<PackageReference Include="protobuf-net" />
|
||||
<PackageReference Include="protobuf-net.Reflection" />
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Protobuf\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -9,7 +9,7 @@ using Microsoft.SemanticKernel.Connectors.WebApi.Rest.Model;
|
||||
namespace Microsoft.SemanticKernel.Skills.OpenAPI.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for OpenApi document classes.
|
||||
/// Interface for OpenApi document parser classes.
|
||||
/// </summary>
|
||||
internal interface IOpenApiDocumentParser
|
||||
{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Extensions;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Skills.UnitTests.Grpc.Extensions;
|
||||
public class GrpcOperationExtensionsTests
|
||||
{
|
||||
private readonly GrpcOperationDataContractType _request;
|
||||
|
||||
private readonly GrpcOperationDataContractType _response;
|
||||
|
||||
private readonly GrpcOperation _operation;
|
||||
|
||||
public GrpcOperationExtensionsTests()
|
||||
{
|
||||
this._request = new GrpcOperationDataContractType("fake-name", new List<GrpcOperationDataContractTypeFiled>());
|
||||
|
||||
this._response = new GrpcOperationDataContractType("fake-name", new List<GrpcOperationDataContractTypeFiled>());
|
||||
|
||||
this._operation = new GrpcOperation("fake-service-name", "fake-operation-name", this._response, this._response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThereShouldBeAddressParameter()
|
||||
{
|
||||
// Act
|
||||
var parameters = this._operation.GetParameters();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(parameters);
|
||||
Assert.True(parameters.Any());
|
||||
|
||||
var addressParameter = parameters.SingleOrDefault(p => p.Name == "address");
|
||||
Assert.NotNull(addressParameter);
|
||||
Assert.Equal("Address for gRPC channel to use.", addressParameter.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThereShouldBePayloadParameter()
|
||||
{
|
||||
// Act
|
||||
var parameters = this._operation.GetParameters();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(parameters);
|
||||
Assert.True(parameters.Any());
|
||||
|
||||
var payloadPrameter = parameters.SingleOrDefault(p => p.Name == "payload");
|
||||
Assert.NotNull(payloadPrameter);
|
||||
Assert.Equal("gRPC request message.", payloadPrameter.Description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Model;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Skills.UnitTests.Grpc;
|
||||
|
||||
public sealed class GrpcRunnerTests : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// An instance of HttpMessageHandlerStub class used to get access to various properties of HttpRequestMessage sent by HTTP client.
|
||||
/// </summary>
|
||||
private readonly HttpMessageHandlerStub _httpMessageHandlerStub;
|
||||
|
||||
/// <summary>
|
||||
/// An instance of HttpClient class used by the tests.
|
||||
/// </summary>
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="GrpcRunnerTests"/> class.
|
||||
/// </summary>
|
||||
public GrpcRunnerTests()
|
||||
{
|
||||
this._httpMessageHandlerStub = new HttpMessageHandlerStub();
|
||||
|
||||
this._httpClient = new HttpClient(this._httpMessageHandlerStub);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldUseAddressProvidedInGrpcOperationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Version = new Version(2, 0);
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content = new ByteArrayContent(new byte[] { 0, 0, 0, 0, 14, 10, 12, 72, 101, 108, 108, 111, 32, 97, 117, 116, 104, 111, 114 });
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content.Headers.Add("Content-Type", "application/grpc");
|
||||
this._httpMessageHandlerStub.ResponseToReturn.TrailingHeaders.Add("grpc-status", "0");
|
||||
|
||||
var requestMetadata = new GrpcOperationDataContractType("greet.HelloRequest", new List<GrpcOperationDataContractTypeFiled>() { new("name", 1, "TYPE_STRING") });
|
||||
|
||||
var responseMetadata = new GrpcOperationDataContractType("greet.HelloReply", new List<GrpcOperationDataContractTypeFiled>() { new("message", 1, "TYPE_STRING") });
|
||||
|
||||
var sut = new GrpcOperationRunner(this._httpClient);
|
||||
|
||||
var operation = new GrpcOperation("Greeter", "SayHello", requestMetadata, responseMetadata);
|
||||
operation.Package = "greet";
|
||||
operation.Address = "https://fake-random-test-host";
|
||||
|
||||
var arguments = new Dictionary<string, string>();
|
||||
arguments.Add("payload", JsonSerializer.Serialize(new { name = "author" }));
|
||||
|
||||
// Act
|
||||
var result = await sut.RunAsync(operation, arguments);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._httpMessageHandlerStub.RequestUri);
|
||||
Assert.Equal("https://fake-random-test-host/greet.Greeter/SayHello", this._httpMessageHandlerStub.RequestUri.AbsoluteUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldUseAddressOverrideFromArgumentsAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Version = new Version(2, 0);
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content = new ByteArrayContent(new byte[] { 0, 0, 0, 0, 14, 10, 12, 72, 101, 108, 108, 111, 32, 97, 117, 116, 104, 111, 114 });
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content.Headers.Add("Content-Type", "application/grpc");
|
||||
this._httpMessageHandlerStub.ResponseToReturn.TrailingHeaders.Add("grpc-status", "0");
|
||||
|
||||
var requestMetadata = new GrpcOperationDataContractType("greet.HelloRequest", new List<GrpcOperationDataContractTypeFiled>() { new("name", 1, "TYPE_STRING") });
|
||||
|
||||
var responseMetadata = new GrpcOperationDataContractType("greet.HelloReply", new List<GrpcOperationDataContractTypeFiled>() { new("message", 1, "TYPE_STRING") });
|
||||
|
||||
var sut = new GrpcOperationRunner(this._httpClient);
|
||||
|
||||
var operation = new GrpcOperation("Greeter", "SayHello", requestMetadata, responseMetadata);
|
||||
operation.Package = "greet";
|
||||
operation.Address = "https://fake-random-test-host";
|
||||
|
||||
var arguments = new Dictionary<string, string>();
|
||||
arguments.Add("payload", JsonSerializer.Serialize(new { name = "author" }));
|
||||
arguments.Add("address", "https://fake-random-test-host-from-args");
|
||||
|
||||
// Act
|
||||
var result = await sut.RunAsync(operation, arguments);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(this._httpMessageHandlerStub.RequestUri);
|
||||
Assert.Equal("https://fake-random-test-host-from-args/greet.Greeter/SayHello", this._httpMessageHandlerStub.RequestUri.AbsoluteUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldRunOperationsWithSimpleDataContractAsync()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
//The byte array is copied from intercepted gRPC call to a local gPRC service created using this guide - https://learn.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-7.0&tabs=visual-studio
|
||||
//since there's no simple way to obtain/create serialized content of gRPC response.
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content = new ByteArrayContent(new byte[] { 0, 0, 0, 0, 14, 10, 12, 72, 101, 108, 108, 111, 32, 97, 117, 116, 104, 111, 114 });
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Version = new Version(2, 0);
|
||||
this._httpMessageHandlerStub.ResponseToReturn.Content.Headers.Add("Content-Type", "application/grpc");
|
||||
this._httpMessageHandlerStub.ResponseToReturn.TrailingHeaders.Add("grpc-status", "0");
|
||||
|
||||
var requestMetadata = new GrpcOperationDataContractType("greet.HelloRequest", new List<GrpcOperationDataContractTypeFiled>() { new("name", 1, "TYPE_STRING") });
|
||||
|
||||
var responseMetadata = new GrpcOperationDataContractType("greet.HelloReply", new List<GrpcOperationDataContractTypeFiled>() { new("message", 1, "TYPE_STRING") });
|
||||
|
||||
var sut = new GrpcOperationRunner(this._httpClient);
|
||||
|
||||
var operation = new GrpcOperation("Greeter", "SayHello", requestMetadata, responseMetadata);
|
||||
operation.Package = "greet";
|
||||
operation.Address = "https://fake-random-test-host";
|
||||
|
||||
var arguments = new Dictionary<string, string>();
|
||||
arguments.Add("payload", JsonSerializer.Serialize(new { name = "author" }));
|
||||
|
||||
// Act
|
||||
var result = await sut.RunAsync(operation, arguments);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
var contentProperty = result["content"]?.ToString();
|
||||
Assert.NotNull(contentProperty);
|
||||
|
||||
var jsonContent = JsonObject.Parse(contentProperty);
|
||||
Assert.NotNull(jsonContent);
|
||||
|
||||
var messageProperty = jsonContent["message"]?.ToString();
|
||||
Assert.Equal("Hello author", messageProperty);
|
||||
|
||||
var contentTypeProperty = result["contentType"]?.ToString();
|
||||
Assert.Equal("application/json; charset=utf-8", contentTypeProperty);
|
||||
|
||||
//The byte array is copied from intercepted gRPC call to a local gPRC service created using this guide - https://learn.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-7.0&tabs=visual-studio
|
||||
//since there's no simple way to obtain/create serialized content of gRPC request.
|
||||
Assert.Equal(new byte[] { 0, 0, 0, 0, 8, 10, 6, 97, 117, 116, 104, 111, 114 }, this._httpMessageHandlerStub.RequestContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources used by this class.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this._httpMessageHandlerStub.Dispose();
|
||||
|
||||
this._httpClient.Dispose();
|
||||
}
|
||||
|
||||
private sealed class HttpMessageHandlerStub : DelegatingHandler
|
||||
{
|
||||
public HttpRequestHeaders? RequestHeaders { get; private set; }
|
||||
|
||||
public HttpContentHeaders? ContentHeaders { get; private set; }
|
||||
|
||||
public byte[]? RequestContent { get; private set; }
|
||||
|
||||
public Uri? RequestUri { get; private set; }
|
||||
|
||||
public HttpMethod? Method { get; private set; }
|
||||
|
||||
public HttpResponseMessage ResponseToReturn { get; set; }
|
||||
|
||||
public HttpMessageHandlerStub()
|
||||
{
|
||||
this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||
this.ResponseToReturn.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
this.Method = request.Method;
|
||||
this.RequestUri = request.RequestUri;
|
||||
this.RequestHeaders = request.Headers;
|
||||
this.RequestContent = request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||
this.ContentHeaders = request.Content?.Headers;
|
||||
|
||||
return await Task.FromResult(this.ResponseToReturn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Protobuf;
|
||||
using SemanticKernel.Skills.UnitTests.Grpc.Protobuf.TestSkills;
|
||||
using Xunit;
|
||||
|
||||
namespace SemanticKernel.Skills.UnitTests.Grpc.Protobuf;
|
||||
|
||||
public sealed class ProtoDocumentParserV30Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// System under test - an instance of ProtoDocumentParser class.
|
||||
/// </summary>
|
||||
private readonly ProtoDocumentParser _sut;
|
||||
|
||||
/// <summary>
|
||||
/// .proto document stream.
|
||||
/// </summary>
|
||||
private readonly Stream _protoDocument;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a <see cref="ProtoDocumentParserV30Tests"/> class.
|
||||
/// </summary>
|
||||
public ProtoDocumentParserV30Tests()
|
||||
{
|
||||
this._protoDocument = ResourceSkillsProvider.LoadFromResource("protoV3.proto");
|
||||
|
||||
this._sut = new ProtoDocumentParser();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateOperationsForAllServicesInProtoDocument()
|
||||
{
|
||||
// Act
|
||||
var operations = this._sut.Parse(this._protoDocument, "fake_name");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(operations);
|
||||
Assert.Equal(2, operations.Count);
|
||||
|
||||
var greeterServiceOperations = operations.Where(o => o.ServiceName == "Greeter");
|
||||
Assert.NotNull(greeterServiceOperations);
|
||||
Assert.Contains(greeterServiceOperations, o => o.Name == "SayHello");
|
||||
|
||||
var farewellerServiceOperations = operations.Where(o => o.ServiceName == "Fareweller");
|
||||
Assert.NotNull(farewellerServiceOperations);
|
||||
Assert.Contains(farewellerServiceOperations, o => o.Name == "SayGoodbye");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldParseSimpleOperationRequestDataContract()
|
||||
{
|
||||
// Act
|
||||
var operations = this._sut.Parse(this._protoDocument, "fake_name");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(operations);
|
||||
|
||||
var greeterServiceOperations = operations.Where(o => o.ServiceName == "Greeter");
|
||||
Assert.NotNull(greeterServiceOperations);
|
||||
|
||||
var sayHelloOperation = greeterServiceOperations.SingleOrDefault(o => o.Name == "SayHello");
|
||||
Assert.NotNull(sayHelloOperation);
|
||||
|
||||
var request = sayHelloOperation.Request;
|
||||
Assert.NotNull(request);
|
||||
|
||||
Assert.Equal("greet.HelloRequest", request.Name);
|
||||
Assert.NotNull(request.Fields);
|
||||
|
||||
var nameField = request.Fields.SingleOrDefault(f => f.Name == "name");
|
||||
Assert.NotNull(nameField);
|
||||
|
||||
Assert.Equal(1, nameField.Number);
|
||||
Assert.Equal("TYPE_STRING", nameField.TypeName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldParseSimpleOperationResponseDataContract()
|
||||
{
|
||||
// Act
|
||||
var operations = this._sut.Parse(this._protoDocument, "fake_name");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(operations);
|
||||
|
||||
var greeterServiceOperations = operations.Where(o => o.ServiceName == "Greeter");
|
||||
Assert.NotNull(greeterServiceOperations);
|
||||
|
||||
var sayHelloOperation = greeterServiceOperations.SingleOrDefault(o => o.Name == "SayHello");
|
||||
Assert.NotNull(sayHelloOperation);
|
||||
|
||||
var response = sayHelloOperation.Response;
|
||||
Assert.NotNull(response);
|
||||
|
||||
Assert.Equal("greet.HelloReply", response.Name);
|
||||
Assert.NotNull(response.Fields);
|
||||
|
||||
var messageFeld = response.Fields.SingleOrDefault(f => f.Name == "message");
|
||||
Assert.NotNull(messageFeld);
|
||||
|
||||
Assert.Equal(1, messageFeld.Number);
|
||||
Assert.Equal("TYPE_STRING", messageFeld.TypeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.IO;
|
||||
using System.Resources;
|
||||
|
||||
namespace SemanticKernel.Skills.UnitTests.Grpc.Protobuf.TestSkills;
|
||||
|
||||
internal static class ResourceSkillsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads .proto file from assembly resource.
|
||||
/// </summary>
|
||||
/// <param name="resourceName">The resource name.</param>
|
||||
/// <returns>The OpenApi document resource stream.</returns>
|
||||
public static Stream LoadFromResource(string resourceName)
|
||||
{
|
||||
var type = typeof(ResourceSkillsProvider);
|
||||
|
||||
var stream = type.Assembly.GetManifestResourceStream(type, resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
throw new MissingManifestResourceException($"Unable to load gRPC skill from assembly resource '{resourceName}'.");
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "GrpcClient";
|
||||
|
||||
package greet;
|
||||
|
||||
// The greeting service definition.
|
||||
service Greeter {
|
||||
// Sends a greeting
|
||||
rpc SayHello (HelloRequest) returns (HelloReply);
|
||||
}
|
||||
|
||||
// The fareweller service definition.
|
||||
service Fareweller {
|
||||
// Says goodbye
|
||||
rpc SayGoodbye (GoodbyeRequest) returns (GoodbyeReply);
|
||||
}
|
||||
|
||||
// The request message containing the user's name.
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// The response message containing the greetings.
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
// The request message containing the user's name.
|
||||
message GoodbyeRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// The response message containing the farewell.
|
||||
message GoodbyeReply {
|
||||
string message = 1;
|
||||
}
|
||||
@@ -11,11 +11,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Grpc\protoV3.proto" />
|
||||
<None Remove="OpenAPI\TestSkills\documentV3_1.yaml" />
|
||||
<None Remove="OpenAPI\TestSkills\openapi.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Grpc\Protobuf\TestSkills\protoV3.proto" />
|
||||
<EmbeddedResource Include="OpenAPI\TestSkills\documentV2_0.json" />
|
||||
<EmbeddedResource Include="OpenAPI\TestSkills\documentV3_1.yaml" />
|
||||
<EmbeddedResource Include="OpenAPI\TestSkills\documentV3_0.json" />
|
||||
@@ -35,6 +37,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SemanticKernel\SemanticKernel.csproj" />
|
||||
<ProjectReference Include="..\Skills.Document\Skills.Document.csproj" />
|
||||
<ProjectReference Include="..\Skills.Grpc\Skills.Grpc.csproj" />
|
||||
<ProjectReference Include="..\Skills.MsGraph\Skills.MsGraph.csproj" />
|
||||
<ProjectReference Include="..\Skills.OpenAPI\Skills.OpenAPI.csproj" />
|
||||
<ProjectReference Include="..\Skills.Web\Skills.Web.csproj" />
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.Skills.Grpc.Extensions;
|
||||
using RepoUtils;
|
||||
|
||||
/**
|
||||
* This example shows how to use gRPC skills.
|
||||
*/
|
||||
|
||||
public static class Example35_GrpcSkills
|
||||
{
|
||||
public static async Task RunAsync()
|
||||
{
|
||||
var kernel = new KernelBuilder().WithLogger(ConsoleLogger.Log).Build();
|
||||
|
||||
// Import a gRPC skill using one of the following Kernel extension methods
|
||||
// kernel.RegisterGrpcSkill
|
||||
// kernel.ImportGrpcSkillFromDirectory
|
||||
var skill = kernel.ImportGrpcSkillFromFile("<skill-name>", "<path-to-.proto-file>");
|
||||
|
||||
// Add arguments for required parameters, arguments for optional ones can be skipped.
|
||||
var contextVariables = new ContextVariables();
|
||||
contextVariables.Set("address", "<gRPC-server-address>");
|
||||
contextVariables.Set("payload", "<gRPC-request-message-as-json>");
|
||||
|
||||
// Run
|
||||
var result = await kernel.RunAsync(contextVariables, skill["<operation-name>"]);
|
||||
|
||||
Console.WriteLine("Skill response: {0}", result);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Extensions\Planning.ActionPlanner\Planning.ActionPlanner.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Skills\Skills.Grpc\Skills.Grpc.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\Skills\Skills.Web\Skills.Web.csproj" />
|
||||
<ProjectReference Include="..\..\..\dotnet\src\SemanticKernel\SemanticKernel.csproj" />
|
||||
|
||||
@@ -109,5 +109,8 @@ public static class Program
|
||||
|
||||
await Example34_CustomChatModel.RunAsync();
|
||||
Console.WriteLine("== DONE ==");
|
||||
|
||||
await Example35_GrpcSkills.RunAsync();
|
||||
Console.WriteLine("== DONE ==");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
@@ -0,0 +1,23 @@
|
||||
# Java Reference Skill gRPC Server
|
||||
This is a sample Java gRPC server that can be invoked via SK's gRPC client as a Native Skill/Function. The purpose of this project is to demonstrate how Polyglot skills can be supported using either REST or gRPC.
|
||||
|
||||
## Prerequisites
|
||||
* Java 17
|
||||
* Maven
|
||||
|
||||
## Build
|
||||
To build the project, run the following command:
|
||||
```
|
||||
mvn clean package
|
||||
```
|
||||
To generate the gRPC classes, run the following command:
|
||||
```
|
||||
mvn protobuf:compile
|
||||
```
|
||||
|
||||
## Run
|
||||
To run the project, run the following command:
|
||||
```
|
||||
java -jar ./target/JavaReferenceSkill-1.0-SNAPSHOT-jar-with-dependencies.jar
|
||||
```
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.microsoft.semantickernel.skills.random</groupId>
|
||||
<artifactId>JavaReferenceSkill</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<grpc.version>1.54.0</grpc.version>
|
||||
<javax.annotation-api.version>1.2</javax.annotation-api.version>
|
||||
<os-maven-plugin.version>1.7.1</os-maven-plugin.version>
|
||||
<protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
|
||||
<protoc.version>3.22.2</protoc.version>
|
||||
<mockito-core.version>5.2.0</mockito-core.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-testing</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito-core.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<version>${javax.annotation-api.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>${os-maven-plugin.version}</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>${protobuf-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.microsoft.semantickernel.skills.random.Main</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.microsoft.semantickernel.skills.random;
|
||||
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerBuilder;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class Main {
|
||||
|
||||
private static final int PORT = 50051;
|
||||
|
||||
public static void main(String[] args) {
|
||||
Logger logger = java.util.logging.Logger.getLogger(Main.class.getName());
|
||||
|
||||
Server server = ServerBuilder.forPort(PORT)
|
||||
.addService(new RandomActivitySkill()).build();
|
||||
|
||||
System.out.println("Starting server...");
|
||||
try {
|
||||
server.start();
|
||||
System.out.println("gRPC Server for random activity started on port " + PORT);
|
||||
server.awaitTermination();
|
||||
} catch (Exception e) {
|
||||
logger.severe("Error with request: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.microsoft.semantickernel.skills.random;
|
||||
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import reference_skill.ActivityOuterClass;
|
||||
import reference_skill.RandomActivitySkillGrpc;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class RandomActivitySkill extends RandomActivitySkillGrpc.RandomActivitySkillImplBase {
|
||||
|
||||
public static final String API_ACTIVITY_URL = "https://www.boredapi.com/api/activity";
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* GetRandomActivity is an RPC method that retrieves a random activity from an API.
|
||||
* </pre>
|
||||
*
|
||||
* @param request
|
||||
* @param responseObserver
|
||||
*/
|
||||
@Override
|
||||
public void getRandomActivity(ActivityOuterClass.GetRandomActivityRequest request, StreamObserver<ActivityOuterClass.GetRandomActivityResponse> responseObserver) {
|
||||
Logger logger = java.util.logging.Logger.getLogger(this.getClass().getName());
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
HttpRequest httpRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(API_ACTIVITY_URL))
|
||||
.build();
|
||||
try {
|
||||
CompletableFuture<HttpResponse<String>> response = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString());
|
||||
logger.info("Response: " + response.get().body());
|
||||
responseObserver.onNext(ActivityOuterClass.GetRandomActivityResponse.newBuilder().setActivity(response.get().body()).build());
|
||||
responseObserver.onCompleted();
|
||||
} catch (Exception e) {
|
||||
logger.severe("Error with request: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package reference_skill;
|
||||
|
||||
// GetRandomActivityRequest is a message that contains input for the GetRandomActivity RPC method.
|
||||
message GetRandomActivityRequest {
|
||||
string input = 1; // Input is a hobby that is use to generate a random activity.
|
||||
}
|
||||
|
||||
// GetRandomActivityResponse is a message that contains the activity returned by the GetRandomActivity RPC method.
|
||||
message GetRandomActivityResponse {
|
||||
string activity = 1; // Activity is a description of the random activity.
|
||||
}
|
||||
|
||||
// RandomActivitySkill is a service that provides methods related to random activities.
|
||||
service RandomActivitySkill {
|
||||
// GetRandomActivity is an RPC method that retrieves a random activity from an API.
|
||||
rpc GetRandomActivity (GetRandomActivityRequest) returns (GetRandomActivityResponse);
|
||||
}
|
||||
|
||||
// Activity is a message that represents an activity with its various properties.
|
||||
message Activity {
|
||||
string activity = 1; // A description of the activity.
|
||||
string type = 2; // The type or category of the activity.
|
||||
int32 participants = 3; // The number of participants required for the activity.
|
||||
double price = 4; // The cost associated with the activity, from 0 (free) to 1 (most expensive).
|
||||
string link = 5; // A URL providing more information about the activity.
|
||||
string key = 6; // A unique identifier for the activity.
|
||||
float accessibility = 7; // The accessibility of the activity, from 0 (most accessible) to 1 (least accessible).
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.microsoft.semantickernel.skills.random;
|
||||
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import io.grpc.testing.GrpcServerRule;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import reference_skill.ActivityOuterClass;
|
||||
import reference_skill.RandomActivitySkillGrpc;
|
||||
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class RandomActivitySkillTest {
|
||||
|
||||
@Rule
|
||||
public GrpcServerRule grpcServerRule = new GrpcServerRule().directExecutor();
|
||||
|
||||
private RandomActivitySkillGrpc.RandomActivitySkillBlockingStub blockingStub;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
grpcServerRule.getServiceRegistry().addService(new RandomActivitySkill());
|
||||
blockingStub = RandomActivitySkillGrpc.newBlockingStub(grpcServerRule.getChannel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRandomActivity() throws Exception {
|
||||
HttpClient httpClient = mock(HttpClient.class);
|
||||
HttpResponse<String> httpResponse = mock(HttpResponse.class);
|
||||
CompletableFuture<HttpResponse<String>> responseFuture = CompletableFuture.completedFuture(httpResponse);
|
||||
|
||||
when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(responseFuture);
|
||||
when(httpResponse.body()).thenReturn("{\"activity\":\"Test Activity\"}");
|
||||
|
||||
RandomActivitySkill randomActivitySkill = new RandomActivitySkill() {
|
||||
};
|
||||
|
||||
ActivityOuterClass.GetRandomActivityRequest request = ActivityOuterClass.GetRandomActivityRequest.newBuilder().build();
|
||||
StreamObserver<ActivityOuterClass.GetRandomActivityResponse> responseObserver = mock(StreamObserver.class);
|
||||
randomActivitySkill.getRandomActivity(request, responseObserver);
|
||||
|
||||
verify(responseObserver).onNext(any(ActivityOuterClass.GetRandomActivityResponse.class));
|
||||
verify(responseObserver).onCompleted();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user