Skip to content

Commit 635b595

Browse files
committed
Merge branch 'main' into chore/zengin-manifest
Signed-off-by: Vincent Biret <[email protected]>
2 parents bf3e1ec + 5d8d738 commit 635b595

28 files changed

+849
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.SemanticKernel;
8+
using Microsoft.SemanticKernel.Plugins.OpenApi;
9+
10+
namespace Plugins;
11+
12+
/// <summary>
13+
/// Sample with demonstration of logging in OpenAPI plugins.
14+
/// </summary>
15+
public sealed class OpenApiPlugin_Telemetry(ITestOutputHelper output) : BaseTest(output)
16+
{
17+
/// <summary>
18+
/// Default logging in OpenAPI plugins.
19+
/// It's possible to use HTTP logging middleware in ASP.NET applications to log information about HTTP request, headers, body, response etc.
20+
/// More information here: <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-logging"/>.
21+
/// For custom logging logic, use <see cref="DelegatingHandler"/>.
22+
/// More information here: <see href="https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers"/>.
23+
/// </summary>
24+
[Fact]
25+
public async Task LoggingAsync()
26+
{
27+
// Arrange
28+
using var stream = File.OpenRead("Resources/Plugins/RepairServicePlugin/repair-service.json");
29+
using HttpClient httpClient = new();
30+
31+
var kernelBuilder = Kernel.CreateBuilder();
32+
33+
// If ILoggerFactory is registered in kernel's DI container, it will be used for logging purposes in OpenAPI functionality.
34+
kernelBuilder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
35+
36+
var kernel = kernelBuilder.Build();
37+
38+
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync(
39+
"RepairService",
40+
stream,
41+
new OpenApiFunctionExecutionParameters(httpClient)
42+
{
43+
IgnoreNonCompliantErrors = true,
44+
EnableDynamicPayload = false,
45+
// For non-DI scenarios, it's possible to set ILoggerFactory in execution parameters when creating a plugin.
46+
// If ILoggerFactory is provided in both ways through the kernel's DI container and execution parameters,
47+
// the one from execution parameters will be used.
48+
LoggerFactory = kernel.LoggerFactory
49+
});
50+
51+
kernel.Plugins.Add(plugin);
52+
53+
var arguments = new KernelArguments
54+
{
55+
["payload"] = """{ "title": "Engine oil change", "description": "Need to drain the old engine oil and replace it with fresh oil.", "assignedTo": "", "date": "", "image": "" }""",
56+
["content-type"] = "application/json"
57+
};
58+
59+
// Create Repair
60+
var result = await plugin["createRepair"].InvokeAsync(kernel, arguments);
61+
Console.WriteLine(result.ToString());
62+
63+
// List All Repairs
64+
result = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
65+
var repairs = JsonSerializer.Deserialize<Repair[]>(result.ToString());
66+
67+
Assert.True(repairs?.Length > 0);
68+
69+
var id = repairs[repairs.Length - 1].Id;
70+
71+
// Update Repair
72+
arguments = new KernelArguments
73+
{
74+
["payload"] = $"{{ \"id\": {id}, \"assignedTo\": \"Karin Blair\", \"date\": \"2024-04-16\", \"image\": \"https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg\" }}",
75+
["content-type"] = "application/json"
76+
};
77+
78+
result = await plugin["updateRepair"].InvokeAsync(kernel, arguments);
79+
Console.WriteLine(result.ToString());
80+
81+
// Delete Repair
82+
arguments = new KernelArguments
83+
{
84+
["payload"] = $"{{ \"id\": {id} }}",
85+
["content-type"] = "application/json"
86+
};
87+
88+
result = await plugin["deleteRepair"].InvokeAsync(kernel, arguments);
89+
Console.WriteLine(result.ToString());
90+
91+
// Output:
92+
// Registering Rest function RepairService.listRepairs
93+
// Created KernelFunction 'listRepairs' for '<CreateRestApiFunction>g__ExecuteAsync|0'
94+
// Registering Rest function RepairService.createRepair
95+
// Created KernelFunction 'createRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
96+
// Registering Rest function RepairService.updateRepair
97+
// Created KernelFunction 'updateRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
98+
// Registering Rest function RepairService.deleteRepair
99+
// Created KernelFunction 'deleteRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
100+
// Function RepairService - createRepair invoking.
101+
// Function RepairService - createRepair arguments: { "payload":"{ \u0022title\u0022: \u0022Engine oil change...
102+
// Function RepairService-createRepair succeeded.
103+
// Function RepairService-createRepair result: { "Content":"New repair created",...
104+
// Function RepairService-createRepair completed. Duration: 0.2793481s
105+
// New repair created
106+
// Function RepairService-listRepairs invoking.
107+
// Function RepairService-listRepairs arguments: { "payload":"{ \u0022title\u0022: \u0022Engine oil change...
108+
// Function RepairService-listRepairs succeeded.
109+
// Function RepairService-listRepairs result: { "Content":"[{\u0022id\u0022:79,\u0022title...
110+
// Function RepairService - updateRepair invoking.
111+
// Function RepairService-updateRepair arguments: { "payload":"{ \u0022id\u0022: 96, ...
112+
// Function RepairService-updateRepair succeeded.
113+
// Function RepairService-updateRepair result: { "Content":"Repair updated",...
114+
// Function RepairService-updateRepair completed. Duration: 0.0430169s
115+
// Repair updated
116+
// Function RepairService - deleteRepair invoking.
117+
// Function RepairService-deleteRepair arguments: { "payload":"{ \u0022id\u0022: 96 ...
118+
// Function RepairService-deleteRepair succeeded.
119+
// Function RepairService-deleteRepair result: { "Content":"Repair deleted",...
120+
// Function RepairService-deleteRepair completed. Duration: 0.049715s
121+
// Repair deleted
122+
}
123+
124+
private sealed class Repair
125+
{
126+
[JsonPropertyName("id")]
127+
public int? Id { get; set; }
128+
129+
[JsonPropertyName("title")]
130+
public string? Title { get; set; }
131+
132+
[JsonPropertyName("description")]
133+
public string? Description { get; set; }
134+
135+
[JsonPropertyName("assignedTo")]
136+
public string? AssignedTo { get; set; }
137+
138+
[JsonPropertyName("date")]
139+
public string? Date { get; set; }
140+
141+
[JsonPropertyName("image")]
142+
public string? Image { get; set; }
143+
}
144+
}

dotnet/samples/Demos/CreateChatGptPlugin/Solution/Program.cs

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using Microsoft.SemanticKernel;
44
using Microsoft.SemanticKernel.ChatCompletion;
55
using Microsoft.SemanticKernel.Connectors.OpenAI;
6-
using Microsoft.SemanticKernel.Plugins.OpenApi;
76

87
// Create kernel
98
var builder = Kernel.CreateBuilder();

dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Contracts/ProcessStartRequest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ public record ProcessStartRequest
1717
/// <summary>
1818
/// The initial event to send to the process.
1919
/// </summary>
20-
public required KernelProcessEvent InitialEvent { get; set; }
20+
public required string InitialEvent { get; set; }
2121
}

dotnet/src/Experimental/Process.IntegrationTestHost.Dapr/Controllers/ProcessTestController.cs

+3-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Dapr.Actors.Client;
55
using Microsoft.AspNetCore.Mvc;
66
using Microsoft.SemanticKernel;
7+
using Microsoft.SemanticKernel.Process.Serialization;
78

89
namespace SemanticKernel.Process.IntegrationTests.Controllers;
910

@@ -41,14 +42,10 @@ public async Task<IActionResult> StartProcessAsync(string processId, [FromBody]
4142
return this.BadRequest("Process already started");
4243
}
4344

44-
if (request.InitialEvent?.Data is JsonElement jsonElement)
45-
{
46-
object? data = jsonElement.Deserialize<string>();
47-
request.InitialEvent = request.InitialEvent with { Data = data };
48-
}
45+
var initialEvent = KernelProcessEventSerializer.ToKernelProcessEvent(request.InitialEvent);
4946

5047
var kernelProcess = request.Process.ToKernelProcess();
51-
var context = await kernelProcess.StartAsync(request.InitialEvent!);
48+
var context = await kernelProcess.StartAsync(initialEvent);
5249
s_processes.Add(processId, context);
5350

5451
return this.Ok();

dotnet/src/Experimental/Process.IntegrationTestRunner.Dapr/DaprTestProcessContext.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text.Json;
55
using Microsoft.SemanticKernel;
66
using Microsoft.SemanticKernel.Process;
7+
using Microsoft.SemanticKernel.Process.Serialization;
78

89
namespace SemanticKernel.Process.IntegrationTests;
910
internal sealed class DaprTestProcessContext : KernelProcessContext
@@ -39,7 +40,7 @@ internal DaprTestProcessContext(KernelProcess process, HttpClient httpClient)
3940
internal async Task StartWithEventAsync(KernelProcessEvent initialEvent)
4041
{
4142
var daprProcess = DaprProcessInfo.FromKernelProcess(this._process);
42-
var request = new ProcessStartRequest { Process = daprProcess, InitialEvent = initialEvent };
43+
var request = new ProcessStartRequest { Process = daprProcess, InitialEvent = initialEvent.ToJson() };
4344

4445
var response = await this._httpClient.PostAsJsonAsync($"http://localhost:5200/processes/{this._processId}", request, options: this._serializerOptions).ConfigureAwait(false);
4546
if (!response.IsSuccessStatusCode)

dotnet/src/Experimental/Process.IntegrationTests.Resources/ProcessCycleTestResources.cs

+58
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,60 @@ await context.EmitEventAsync(new()
202202
}
203203
}
204204

205+
/// <summary>
206+
/// A step that conditionally throws an exception.
207+
/// </summary>
208+
public sealed class ErrorStep : KernelProcessStep<StepState>
209+
{
210+
private StepState? _state;
211+
212+
public override ValueTask ActivateAsync(KernelProcessStepState<StepState> state)
213+
{
214+
this._state = state.State;
215+
return default;
216+
}
217+
218+
[KernelFunction]
219+
public async Task ErrorWhenTrueAsync(KernelProcessStepContext context, bool shouldError)
220+
{
221+
this._state!.InvocationCount++;
222+
223+
if (shouldError)
224+
{
225+
throw new InvalidOperationException("This is an error");
226+
}
227+
228+
await context.EmitEventAsync(new()
229+
{
230+
Id = ProcessTestsEvents.ErrorStepSuccess,
231+
Data = null,
232+
Visibility = KernelProcessEventVisibility.Internal
233+
});
234+
}
235+
}
236+
237+
/// <summary>
238+
/// A step that reports an error sent to it by logging it to the console.
239+
/// </summary>
240+
public sealed class ReportStep : KernelProcessStep<StepState>
241+
{
242+
private StepState? _state;
243+
244+
public override ValueTask ActivateAsync(KernelProcessStepState<StepState> state)
245+
{
246+
this._state = state.State;
247+
return default;
248+
}
249+
250+
[KernelFunction]
251+
public Task ReportError(KernelProcessStepContext context, object error)
252+
{
253+
this._state!.InvocationCount++;
254+
Console.WriteLine(error.ToString());
255+
return Task.CompletedTask;
256+
}
257+
}
258+
205259
/// <summary>
206260
/// The state object for the repeat and fanIn step.
207261
/// </summary>
@@ -210,6 +264,9 @@ public sealed record StepState
210264
{
211265
[DataMember]
212266
public string? LastMessage { get; set; }
267+
268+
[DataMember]
269+
public int InvocationCount { get; set; }
213270
}
214271

215272
/// <summary>
@@ -222,6 +279,7 @@ public static class ProcessTestsEvents
222279
public const string StartInnerProcess = "StartInnerProcess";
223280
public const string OutputReadyPublic = "OutputReadyPublic";
224281
public const string OutputReadyInternal = "OutputReadyInternal";
282+
public const string ErrorStepSuccess = "ErrorStepSuccess";
225283
}
226284

227285
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

dotnet/src/Experimental/Process.IntegrationTests.Shared/ProcessTests.cs

+40
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@ public async Task FanInProcessAsync()
217217
Assert.Equal($"{testInput}-{testInput} {testInput}", outputStep.State.LastMessage);
218218
}
219219

220+
/// <summary>
221+
/// Test with a process that has an error step that emits an error event
222+
/// </summary>
223+
/// <returns></returns>
224+
[Fact]
225+
public async Task ProcessWithErrorEmitsErrorEventAsync()
226+
{
227+
// Arrange
228+
Kernel kernel = this._kernelBuilder.Build();
229+
var process = this.CreateProcessWithError("ProcessWithError").Build();
230+
231+
// Act
232+
bool shouldError = true;
233+
var processHandle = await this._fixture.StartProcessAsync(process, kernel, new() { Id = ProcessTestsEvents.StartProcess, Data = shouldError });
234+
var processInfo = await processHandle.GetStateAsync();
235+
236+
// Assert
237+
var reportStep = processInfo.Steps.Where(s => s.State.Name == nameof(ReportStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
238+
Assert.NotNull(reportStep?.State);
239+
Assert.Equal(1, reportStep.State.InvocationCount);
240+
241+
var repeatStep = processInfo.Steps.Where(s => s.State.Name == nameof(RepeatStep)).FirstOrDefault()?.State as KernelProcessStepState<StepState>;
242+
Assert.NotNull(repeatStep?.State);
243+
Assert.Null(repeatStep.State.LastMessage);
244+
}
245+
220246
/// <summary>
221247
/// Test with a single step that then connects to a nested fan in process with 2 input steps
222248
/// </summary>
@@ -280,4 +306,18 @@ private ProcessBuilder CreateFanInProcess(string name)
280306

281307
return processBuilder;
282308
}
309+
310+
private ProcessBuilder CreateProcessWithError(string name)
311+
{
312+
var processBuilder = new ProcessBuilder(name);
313+
var errorStep = processBuilder.AddStepFromType<ErrorStep>("ErrorStep");
314+
var repeatStep = processBuilder.AddStepFromType<RepeatStep>("RepeatStep");
315+
var reportStep = processBuilder.AddStepFromType<ReportStep>("ReportStep");
316+
317+
processBuilder.OnInputEvent(ProcessTestsEvents.StartProcess).SendEventTo(new ProcessFunctionTargetBuilder(errorStep));
318+
errorStep.OnEvent(ProcessTestsEvents.ErrorStepSuccess).SendEventTo(new ProcessFunctionTargetBuilder(repeatStep, parameterName: "message"));
319+
errorStep.OnFunctionError("ErrorWhenTrue").SendEventTo(new ProcessFunctionTargetBuilder(reportStep));
320+
321+
return processBuilder;
322+
}
283323
}

dotnet/src/Experimental/Process.Runtime.Dapr/Process.Runtime.Dapr.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
<ItemGroup>
2929
<InternalsVisibleTo Include="SemanticKernel.Process.Runtime.Dapr.UnitTests" />
30+
<InternalsVisibleTo Include="SemanticKernel.Process.IntegrationTestHost.Dapr" />
31+
<InternalsVisibleTo Include="SemanticKernel.Process.IntegrationTestRunner.Dapr" />
3032
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
3133
</ItemGroup>
3234

dotnet/src/Functions/Functions.OpenApi/AssemblyInfo.cs

-6
This file was deleted.

dotnet/src/Functions/Functions.OpenApi/DocumentLoader.cs

+8-6
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private static async Task<HttpResponseMessage> LoadDocumentResponseFromUriAsync(
5555
await authCallback(request, cancellationToken).ConfigureAwait(false);
5656
}
5757

58-
logger.LogTrace("Importing document from {0}", uri);
58+
logger.LogTrace("Importing document from '{Uri}'", uri);
5959

6060
return await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
6161
}
@@ -67,9 +67,9 @@ internal static async Task<string> LoadDocumentFromFilePathAsync(
6767
{
6868
cancellationToken.ThrowIfCancellationRequested();
6969

70-
CheckIfFileExists(filePath);
70+
CheckIfFileExists(filePath, logger);
7171

72-
logger.LogTrace("Importing document from {0}", filePath);
72+
logger.LogTrace("Importing document from '{FilePath}'", filePath);
7373

7474
using var sr = File.OpenText(filePath);
7575
return await sr.ReadToEndAsync(
@@ -79,19 +79,21 @@ internal static async Task<string> LoadDocumentFromFilePathAsync(
7979
).ConfigureAwait(false);
8080
}
8181

82-
private static void CheckIfFileExists(string filePath)
82+
private static void CheckIfFileExists(string filePath, ILogger logger)
8383
{
8484
if (!File.Exists(filePath))
8585
{
86-
throw new FileNotFoundException($"Invalid URI. The specified path '{filePath}' does not exist.");
86+
var exception = new FileNotFoundException($"Invalid file path. The specified path '{filePath}' does not exist.");
87+
logger.LogError(exception, "Invalid file path. The specified path '{FilePath}' does not exist.", filePath);
88+
throw exception;
8789
}
8890
}
8991

9092
internal static Stream LoadDocumentFromFilePathAsStream(
9193
string filePath,
9294
ILogger logger)
9395
{
94-
CheckIfFileExists(filePath);
96+
CheckIfFileExists(filePath, logger);
9597

9698
logger.LogTrace("Importing document from {0}", filePath);
9799

0 commit comments

Comments
 (0)