Add .NET client for LLM Conversations support (#1382)

* Updated prototype

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added Dapr.AI project and unit test project to contain the conversational building block (and potentially future other projects)

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Changed default values

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed unnecessary method

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added a few unit tests

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added example project

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added missing copyright headers

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Changed type name -> DaprLlmInput to DaprConversationInput

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Returning read only list

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Update to use IReadOnlyDictionary

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added method to abstract class

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Striving for consistency in how properties are specified on the record

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Refactored enum extensions out to Dapr.Common since it will be used in AI project

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added JSON converter for System.Text.Json to handle enum serialization based on the enum member attributes

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added unit tests to prove out generic enum JSON converter using EnumMember attributes

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added JSON converter to new enum for Dapr Conversation role

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Set up role to map to the string used in grpc call to sidecar

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* No need for the JSON converter after all

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added missing package version to fix build error

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed duplicate using statement breaking build

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed missing [Fact] annotation

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Updated proto types to reflect type name changes in https://github.com/dapr/dapr/pull/8250

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added support for service lifetime

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Building out documentation for Dapr AI

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Simplified registration

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Tweaked package version

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Using IConfiguration to source DaprClient values if provided in service provider

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed Models.* directories, flattened into Conversation namespace

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Swapped out to use IReadOnlyDictionary

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Added suggested optimization

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed bad using statement

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Updates to use uniform method for standing up new Dapr clients

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed duplicate project reference

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed build error

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixing build errors

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed bad references

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed several build errors

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixing more build errors

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Updated to fix several build errors

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixed bad refernce

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Fixing more build errors

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Role is required when submitting conversation input

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed impossible path since the role cannot be nullable

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

* Removed impossible path from logic now that role cannot be null

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>

---------

Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
This commit is contained in:
Whit Waldo 2024-12-10 09:06:44 -06:00 committed by GitHub
parent cfd4fbee84
commit 7b5ca4fb6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 3369 additions and 2316 deletions

View File

@ -1,52 +1,52 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="FluentAssertions" Version="5.9.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="1.1.2" />
<PackageVersion Include="Google.Api.CommonProtos" Version="2.2.0" />
<PackageVersion Include="Google.Protobuf" Version="3.28.2" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.66.0" />
<PackageVersion Include="Grpc.Core.Testing" Version="2.46.6" />
<PackageVersion Include="Grpc.Net.Client" Version="2.66.0" />
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.66.0" />
<PackageVersion Include="Grpc.Tools" Version="2.67.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.35" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="6.0.35" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.3.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MinVer" Version="2.3.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="protobuf-net.Grpc.AspNetCore" Version="1.2.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="System.Formats.Asn1" Version="6.0.1" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.extensibility.core" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="FluentAssertions" Version="5.9.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="1.1.2" />
<PackageVersion Include="Google.Api.CommonProtos" Version="2.2.0" />
<PackageVersion Include="Google.Protobuf" Version="3.28.2" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.66.0" />
<PackageVersion Include="Grpc.Core.Testing" Version="2.46.6" />
<PackageVersion Include="Grpc.Net.Client" Version="2.66.0" />
<PackageVersion Include="Grpc.Net.ClientFactory" Version="2.66.0" />
<PackageVersion Include="Grpc.Tools" Version="2.67.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.35" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="6.0.35" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.3.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MinVer" Version="2.3.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="protobuf-net.Grpc.AspNetCore" Version="1.2.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="System.Formats.Asn1" Version="6.0.1" />
<PackageVersion Include="System.Text.Json" Version="6.0.10" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.extensibility.core" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>

24
all.sln
View File

@ -119,6 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI", "src\Dapr.AI\Dapr.AI.csproj", "{273F2527-1658-4CCF-8DC6-600E921188C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI.Test", "test\Dapr.AI.Test\Dapr.AI.Test.csproj", "{2F3700EF-1CDA-4C15-AC88-360230000ECD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConversationalAI", "examples\AI\ConversationalAI\ConversationalAI.csproj", "{11011FF8-77EA-4B25-96C0-29D4D486EF1C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowExternalInteraction", "examples\Workflow\WorkflowExternalInteraction\WorkflowExternalInteraction.csproj", "{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}"
@ -331,6 +339,18 @@ Global
{CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU
{273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.Build.0 = Release|Any CPU
{2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.Build.0 = Release|Any CPU
{11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.Build.0 = Release|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -439,6 +459,10 @@ Global
{DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}
{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}

View File

@ -84,6 +84,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria
<a href="{{< ref dotnet-jobs >}}" class="stretched-link"></a>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title"><b>AI</b></h5>
<p class="card-text">Create and manage AI operations in .NET</p>
<a href="{{< ref dotnet-ai >}}" class="stretched-link"></a>
</div>
</div>
</div>
## More information

View File

@ -0,0 +1,83 @@
_index.md
---
type: docs
title: "Getting started with the Dapr AI .NET SDK client"
linkTitle: "AI"
weight: 10000
description: How to get up and running with the Dapr AI .NET SDK
no_list: true
---
The Dapr AI client package allows you to interact with the AI capabilities provided by the Dapr sidecar.
## Installation
To get started with the Dapr AI .NET SDK client, install the following package from NuGet:
```sh
dotnet add package Dapr.AI
```
A `DaprConversationClient` holes access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar.
### Dependency Injection
The `AddDaprAiConversation()` method will register the Dapr client ASP.NET Core dependency injection and is the recommended approach
for using this package. This method accepts an optional options delegate for configuring the `DaprConversationClient` and a
`ServiceLifetime` argument, allowing you to specify a different lifetime for the registered services instead of the default `Singleton`
value.
The following example assumes all default values are acceptable and is sufficient to register the `DaprConversationClient`:
```csharp
services.AddDaprAiConversation();
```
The optional configuration delegate is used to configure the `DaprConversationClient` by specifying options on the
`DaprConversationClientBuilder` as in the following example:
```csharp
services.AddSingleton<DefaultOptionsProvider>();
services.AddDaprAiConversation((serviceProvider, clientBuilder) => {
//Inject a service to source a value from
var optionsProvider = serviceProvider.GetRequiredService<DefaultOptionsProvider>();
var standardTimeout = optionsProvider.GetStandardTimeout();
//Configure the value on the client builder
clientBuilder.UseTimeout(standardTimeout);
});
```
### Manual Instantiation
Rather than using dependency injection, a `DaprConversationClient` can also be built using the static client builder.
For best performance, create a single long-lived instance of `DaprConversationClient` and provide access to that shared instance throughout
your application. `DaprConversationClient` instances are thread-safe and intended to be shared.
Avoid creating a `DaprConversationClient` per-operation.
A `DaprConversationClient` can be configured by invoking methods on the `DaprConversationClientBuilder` class before calling `.Build()`
to create the client. The settings for each `DaprConversationClient` are separate and cannot be changed after calling `.Build()`.
```csharp
var daprConversationClient = new DaprConversationClientBuilder()
.UseJsonSerializerSettings( ... ) //Configure JSON serializer
.Build();
```
See the .NET [documentation here]({{< ref dotnet-client >}}) for more information about the options available when configuring the Dapr client via the builder.
## Try it out
Put the Dapr AI .NET SDK to the test. Walk through the samples to see Dapr in action:
| SDK Samples | Description |
| ----------- | ----------- |
| [SDK samples](https://github.com/dapr/dotnet-sdk/tree/master/examples) | Clone the SDK repo to try out some examples and get started. |
## Building Blocks
This part of the .NET SDK allows you to interface with the Conversations API to send and receive messages from
large language models.
### Send messages

View File

@ -0,0 +1,7 @@
---
type: docs
title: "Best practices with the Dapr AI .NET SDK client"
linkTitle: "Best Practices"
weight: 100000
description: How to get up and running with the Dapr .NET SDK
---

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Dapr.AI\Dapr.AI.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using Dapr.AI.Conversation;
using Dapr.AI.Conversation.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprAiConversation();
var app = builder.Build();
var conversationClient = app.Services.GetRequiredService<DaprConversationClient>();
var response = await conversationClient.ConverseAsync("conversation",
new List<DaprConversationInput>
{
new DaprConversationInput(
"Please write a witty haiku about the Dapr distributed programming framework at dapr.io",
DaprConversationRole.Generic)
});
Console.WriteLine("Received the following from the LLM:");
foreach (var resp in response.Outputs)
{
Console.WriteLine($"\t{resp.Result}");
}

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]

View File

@ -0,0 +1,40 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Google.Protobuf.WellKnownTypes;
namespace Dapr.AI.Conversation;
/// <summary>
/// Options used to configure the conversation operation.
/// </summary>
/// <param name="ConversationId">The identifier of the conversation this is a continuation of.</param>
public sealed record ConversationOptions(string? ConversationId = null)
{
/// <summary>
/// Temperature for the LLM to optimize for creativity or predictability.
/// </summary>
public double Temperature { get; init; } = default;
/// <summary>
/// Flag that indicates whether data that comes back from the LLM should be scrubbed of PII data.
/// </summary>
public bool ScrubPII { get; init; } = default;
/// <summary>
/// The metadata passing to the conversation components.
/// </summary>
public IReadOnlyDictionary<string, string> Metadata { get; init; } = new Dictionary<string, string>();
/// <summary>
/// Parameters for all custom fields.
/// </summary>
public IReadOnlyDictionary<string, Any> Parameters { get; init; } = new Dictionary<string, Any>();
}

View File

@ -0,0 +1,116 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Dapr.Common;
using Dapr.Common.Extensions;
using P = Dapr.Client.Autogen.Grpc.v1;
namespace Dapr.AI.Conversation;
/// <summary>
/// Used to interact with the Dapr conversation building block.
/// </summary>
public sealed class DaprConversationClient : DaprAIClient
{
/// <summary>
/// The HTTP client used by the client for calling the Dapr runtime.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly HttpClient HttpClient;
/// <summary>
/// The Dapr API token value.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly string? DaprApiToken;
/// <summary>
/// The autogenerated Dapr client.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal P.Dapr.DaprClient Client { get; }
/// <summary>
/// Used to initialize a new instance of a <see cref="DaprConversationClient"/>.
/// </summary>
/// <param name="client">The Dapr client.</param>
/// <param name="httpClient">The HTTP client used by the client for calling the Dapr runtime.</param>
/// <param name="daprApiToken">An optional token required to send requests to the Dapr sidecar.</param>
public DaprConversationClient(P.Dapr.DaprClient client,
HttpClient httpClient,
string? daprApiToken = null)
{
this.Client = client;
this.HttpClient = httpClient;
this.DaprApiToken = daprApiToken;
}
/// <summary>
/// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
/// </summary>
/// <param name="daprConversationComponentName">The name of the Dapr conversation component.</param>
/// <param name="inputs">The input values to send.</param>
/// <param name="options">Optional options used to configure the conversation.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The response(s) provided by the LLM provider.</returns>
public override async Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName, IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
CancellationToken cancellationToken = default)
{
var request = new P.ConversationRequest
{
Name = daprConversationComponentName
};
if (options is not null)
{
request.ContextID = options.ConversationId;
request.ScrubPII = options.ScrubPII;
foreach (var (key, value) in options.Metadata)
{
request.Metadata.Add(key, value);
}
foreach (var (key, value) in options.Parameters)
{
request.Parameters.Add(key, value);
}
}
foreach (var input in inputs)
{
request.Inputs.Add(new P.ConversationInput
{
ScrubPII = input.ScrubPII,
Message = input.Message,
Role = input.Role.GetValueFromEnumMember()
});
}
var grpCCallOptions =
DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken,
cancellationToken);
var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false);
var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result)
{
Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value)
}).ToList();
return new DaprConversationResponse(outputs);
}
}

View File

@ -0,0 +1,46 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Dapr.Common;
using Microsoft.Extensions.Configuration;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;
namespace Dapr.AI.Conversation;
/// <summary>
/// Used to create a new instance of a <see cref="DaprConversationClient"/>.
/// </summary>
public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder<DaprConversationClient>
{
/// <summary>
/// Used to initialize a new instance of the <see cref="DaprConversationClient"/>.
/// </summary>
/// <param name="configuration"></param>
public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration)
{
}
/// <summary>
/// Builds the client instance from the properties of the builder.
/// </summary>
/// <returns>The Dapr client instance.</returns>
/// <summary>
/// Builds the client instance from the properties of the builder.
/// </summary>
public override DaprConversationClient Build()
{
var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly);
var client = new Autogenerated.DaprClient(daprClientDependencies.channel);
return new DaprConversationClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
}
}

View File

@ -0,0 +1,22 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
namespace Dapr.AI.Conversation;
/// <summary>
/// Represents an input for the Dapr Conversational API.
/// </summary>
/// <param name="Message">The message to send to the LLM.</param>
/// <param name="Role">The role indicating the entity providing the message.</param>
/// <param name="ScrubPII">If true, scrubs the data that goes into the LLM.</param>
public sealed record DaprConversationInput(string Message, DaprConversationRole Role, bool ScrubPII = false);

View File

@ -0,0 +1,21 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
namespace Dapr.AI.Conversation;
/// <summary>
/// The response for a conversation.
/// </summary>
/// <param name="Outputs">The collection of conversation results.</param>
/// <param name="ConversationId">The identifier of an existing or newly created conversation.</param>
public record DaprConversationResponse(IReadOnlyList<DaprConversationResult> Outputs, string? ConversationId = null);

View File

@ -0,0 +1,28 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Google.Protobuf.WellKnownTypes;
namespace Dapr.AI.Conversation;
/// <summary>
/// The result for a single conversational input.
/// </summary>
/// <param name="Result">The result for one conversation input.</param>
public record DaprConversationResult(string Result)
{
/// <summary>
/// Parameters for all custom fields.
/// </summary>
public IReadOnlyDictionary<string, Any> Parameters { get; init; } = new Dictionary<string, Any>();
}

View File

@ -0,0 +1,42 @@
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Dapr.Common.JsonConverters;
namespace Dapr.AI.Conversation;
/// <summary>
/// Represents who
/// </summary>
public enum DaprConversationRole
{
/// <summary>
/// Represents a message sent by an AI.
/// </summary>
[EnumMember(Value="ai")]
AI,
/// <summary>
/// Represents a message sent by a human.
/// </summary>
[EnumMember(Value="human")]
Human,
/// <summary>
/// Represents a message sent by the system.
/// </summary>
[EnumMember(Value="system")]
System,
/// <summary>
/// Represents a message sent by a generic user.
/// </summary>
[EnumMember(Value="generic")]
Generic,
/// <summary>
/// Represents a message sent by a function.
/// </summary>
[EnumMember(Value="function")]
Function,
/// <summary>
/// Represents a message sent by a tool.
/// </summary>
[EnumMember(Value="tool")]
Tool
}

View File

@ -0,0 +1,35 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace Dapr.AI.Conversation.Extensions;
/// <summary>
/// Used by the fluent registration builder to configure a Dapr AI conversational manager.
/// </summary>
public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder
{
/// <summary>
/// The registered services on the builder.
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Used to initialize a new <see cref="DaprAiConversationBuilder"/>.
/// </summary>
public DaprAiConversationBuilder(IServiceCollection services)
{
Services = services;
}
}

View File

@ -0,0 +1,64 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Dapr.AI.Conversation.Extensions;
/// <summary>
/// Contains the dependency injection registration extensions for the Dapr AI Conversation operations.
/// </summary>
public static class DaprAiConversationBuilderExtensions
{
/// <summary>
/// Registers the necessary functionality for the Dapr AI conversation functionality.
/// </summary>
/// <returns></returns>
public static IDaprAiConversationBuilder AddDaprAiConversation(this IServiceCollection services, Action<IServiceProvider, DaprConversationClientBuilder>? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
ArgumentNullException.ThrowIfNull(services, nameof(services));
services.AddHttpClient();
var registration = new Func<IServiceProvider, DaprConversationClient>(provider =>
{
var configuration = provider.GetService<IConfiguration>();
var builder = new DaprConversationClientBuilder(configuration);
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
builder.UseHttpClientFactory(httpClientFactory);
configure?.Invoke(provider, builder);
return builder.Build();
});
switch (lifetime)
{
case ServiceLifetime.Scoped:
services.TryAddScoped(registration);
break;
case ServiceLifetime.Transient:
services.TryAddTransient(registration);
break;
case ServiceLifetime.Singleton:
default:
services.TryAddSingleton(registration);
break;
}
return new DaprAiConversationBuilder(services);
}
}

View File

@ -0,0 +1,23 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Dapr.AI.Extensions;
namespace Dapr.AI.Conversation.Extensions;
/// <summary>
/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration.
/// </summary>
public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder
{
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6;net8</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Dapr.AI</PackageId>
<Title>Dapr AI SDK</Title>
<Description>Dapr AI SDK for performing operations associated with artificial intelligence.</Description>
<VersionSuffix>alpha</VersionSuffix>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dapr.Common\Dapr.Common.csproj" />
<ProjectReference Include="..\Dapr.Protos\Dapr.Protos.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Dapr.AI.Conversation;
namespace Dapr.AI;
/// <summary>
/// The base implementation of a Dapr AI client.
/// </summary>
public abstract class DaprAIClient
{
/// <summary>
/// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
/// </summary>
/// <param name="daprConversationComponentName">The name of the Dapr conversation component.</param>
/// <param name="inputs">The input values to send.</param>
/// <param name="options">Optional options used to configure the conversation.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The response(s) provided by the LLM provider.</returns>
public abstract Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName,
IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,27 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
namespace Dapr.AI.Extensions;
/// <summary>
/// Responsible for registering Dapr AI service functionality.
/// </summary>
public interface IDaprAiServiceBuilder
{
/// <summary>
/// The registered services on the builder.
/// </summary>
public IServiceCollection Services { get; }
}

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
// ------------------------------------------------------------------------
// Copyright 2023 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
#nullable enable
using System;
using System.Reflection;
using System.Runtime.Serialization;
namespace Dapr.Client
{
internal static class EnumExtensions
{
/// <summary>
/// Reads the value of an enum out of the attached <see cref="EnumMemberAttribute"/> attribute.
/// </summary>
/// <typeparam name="T">The enum.</typeparam>
/// <param name="value">The value of the enum to pull the value for.</param>
/// <returns></returns>
public static string GetValueFromEnumMember<T>(this T value) where T : Enum
{
ArgumentNullException.ThrowIfNull(value, nameof(value));
var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly);
if (memberInfo.Length <= 0)
return value.ToString();
var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false);
return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString();
}
}
}

View File

@ -16,9 +16,11 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
@ -27,6 +29,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
@ -40,3 +43,4 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]
[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")]

View File

@ -0,0 +1,65 @@
using System.Net.Http.Headers;
using System.Reflection;
using Grpc.Core;
namespace Dapr.Common;
internal static class DaprClientUtilities
{
/// <summary>
/// Provisions the gRPC call options used to provision the various Dapr clients.
/// </summary>
/// <param name="daprApiToken">The Dapr API token, if any.</param>
/// <param name="assembly">The assembly the user agent is built from.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The gRPC call options.</returns>
internal static CallOptions ConfigureGrpcCallOptions(Assembly assembly, string? daprApiToken, CancellationToken cancellationToken = default)
{
var callOptions = new CallOptions(headers: new Metadata(), cancellationToken: cancellationToken);
//Add the user-agent header to the gRPC call options
var assemblyVersion = assembly
.GetCustomAttributes<AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?
.InformationalVersion;
var userAgent = new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}").ToString();
callOptions.Headers!.Add("User-Agent", userAgent);
//Add the API token to the headers as well if it's populated
if (daprApiToken is not null)
{
var apiTokenHeader = GetDaprApiTokenHeader(daprApiToken);
if (apiTokenHeader is not null)
{
callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value);
}
}
return callOptions;
}
/// <summary>
/// Used to create the user-agent from the assembly attributes.
/// </summary>
/// <param name="assembly">The assembly the client is being built for.</param>
/// <returns>The header value containing the user agent information.</returns>
public static ProductInfoHeaderValue GetUserAgent(Assembly assembly)
{
var assemblyVersion = assembly
.GetCustomAttributes<AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?
.InformationalVersion;
return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}");
}
/// <summary>
/// Used to provision the header used for the Dapr API token on the HTTP or gRPC connection.
/// </summary>
/// <param name="daprApiToken">The value of the Dapr API token.</param>
/// <returns>If a Dapr API token exists, the key/value pair to use for the header; otherwise null.</returns>
public static KeyValuePair<string, string>? GetDaprApiTokenHeader(string? daprApiToken) =>
string.IsNullOrWhiteSpace(daprApiToken)
? null
: new KeyValuePair<string, string>("dapr-api-token", daprApiToken);
}

View File

@ -1,4 +1,18 @@
using System.Text.Json;
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using System.Reflection;
using System.Text.Json;
using Grpc.Net.Client;
using Microsoft.Extensions.Configuration;
@ -170,8 +184,9 @@ public abstract class DaprGenericClientBuilder<TClientBuilder> where TClientBuil
/// Builds out the inner DaprClient that provides the core shape of the
/// runtime gRPC client used by the consuming package.
/// </summary>
/// <param name="assembly">The assembly the dependencies are being built for.</param>
/// <exception cref="InvalidOperationException"></exception>
protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies()
protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly)
{
var grpcEndpoint = new Uri(this.GrpcEndpoint);
if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https")
@ -184,22 +199,48 @@ public abstract class DaprGenericClientBuilder<TClientBuilder> where TClientBuil
// Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel.
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
}
var httpEndpoint = new Uri(this.HttpEndpoint);
if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https")
{
throw new InvalidOperationException("The HTTP endpoint must use http or https.");
}
var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions);
//Configure the HTTP client
var httpClient = ConfigureHttpClient(assembly);
this.GrpcChannelOptions.HttpClient = httpClient;
var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions);
return (channel, httpClient, httpEndpoint, this.DaprApiToken);
}
/// <summary>
/// Configures the HTTP client.
/// </summary>
/// <param name="assembly">The assembly the user agent is built from.</param>
/// <returns>The HTTP client to interact with the Dapr runtime with.</returns>
private HttpClient ConfigureHttpClient(Assembly assembly)
{
var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient();
//Set the timeout as necessary
if (this.Timeout > TimeSpan.Zero)
{
httpClient.Timeout = this.Timeout;
}
//Set the user agent
var userAgent = DaprClientUtilities.GetUserAgent(assembly);
httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString());
//Set the API token
var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken);
if (apiTokenHeader is not null)
{
httpClient.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value);
}
return (channel, httpClient, httpEndpoint);
return httpClient;
}
/// <summary>

View File

@ -1,4 +1,17 @@
using System.Reflection;
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using System.Reflection;
using System.Runtime.Serialization;
namespace Dapr.Common.Extensions;

View File

@ -0,0 +1,70 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapr.Common.Extensions;
namespace Dapr.Common.JsonConverters;
/// <summary>
/// A JsonConverter used to convert from an enum to a string and vice versa, but using the Enum extension written to pull
/// the value from the [EnumMember] attribute, if present.
/// </summary>
/// <typeparam name="T">The enum type to convert.</typeparam>
internal sealed class GenericEnumJsonConverter<T> : JsonConverter<T> where T : struct, Enum
{
private static readonly Dictionary<string, T> enumMemberCache = new();
static GenericEnumJsonConverter()
{
foreach (var enumValue in Enum.GetValues<T>())
{
var enumMemberValue = enumValue.GetValueFromEnumMember();
enumMemberCache[enumMemberValue] = enumValue;
}
}
/// <summary>Reads and converts the JSON to type <typeparamref name="T" />.</summary>
/// <param name="reader">The reader.</param>
/// <param name="typeToConvert">The type to convert.</param>
/// <param name="options">An object that specifies serialization options to use.</param>
/// <returns>The converted value.</returns>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
//Get the string value from the JSON reader
var value = reader.GetString();
//Try pulling the value from the cache
if (value is not null && enumMemberCache.TryGetValue(value, out var enumValue))
{
return enumValue;
}
//If no match found, throw an exception
throw new JsonException($"Invalid valid for {typeToConvert.Name}: {value}");
}
/// <summary>Writes a specified value as JSON.</summary>
/// <param name="writer">The writer to write to.</param>
/// <param name="value">The value to convert to JSON.</param>
/// <param name="options">An object that specifies serialization options to use.</param>
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
//Get the value from the EnumMember attribute, if any
var enumMemberValue = value.GetValueFromEnumMember();
//Write the value to the JSON writer
writer.WriteStringValue(enumMemberValue);
}
}

View File

@ -12,6 +12,7 @@
// ------------------------------------------------------------------------
using Dapr.Common;
using Microsoft.Extensions.Configuration;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1;
namespace Dapr.Jobs;
@ -21,17 +22,22 @@ namespace Dapr.Jobs;
/// </summary>
public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder<DaprJobsClient>
{
/// <summary>
/// Used to initialize a new instance of <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">An optional instance of <see cref="IConfiguration"/>.</param>
public DaprJobsClientBuilder(IConfiguration? configuration = null) : base(configuration)
{
}
/// <summary>
/// Builds the client instance from the properties of the builder.
/// </summary>
/// <returns>The Dapr client instance.</returns>
public override DaprJobsClient Build()
{
var daprClientDependencies = this.BuildDaprClientDependencies();
var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprJobsClient).Assembly);
var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel);
var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null;
return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader);
return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
}
}

View File

@ -11,8 +11,7 @@
// limitations under the License.
// ------------------------------------------------------------------------
using System.Net.Http.Headers;
using System.Reflection;
using Dapr.Common;
using Dapr.Jobs.Models;
using Dapr.Jobs.Models.Responses;
using Google.Protobuf;
@ -28,29 +27,35 @@ namespace Dapr.Jobs;
internal sealed class DaprJobsGrpcClient : DaprJobsClient
{
/// <summary>
/// Present only for testing purposes.
/// The HTTP client used by the client for calling the Dapr runtime.
/// </summary>
internal readonly HttpClient httpClient;
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly HttpClient HttpClient;
/// <summary>
/// Used to populate options headers with API token value.
/// The Dapr API token value.
/// </summary>
internal readonly KeyValuePair<string, string>? apiTokenHeader;
private readonly Autogenerated.Dapr.DaprClient client;
private readonly string userAgent = UserAgent().ToString();
// property exposed for testing purposes
internal Autogenerated.Dapr.DaprClient Client => client;
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly string? DaprApiToken;
/// <summary>
/// The autogenerated Dapr client.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal Autogenerated.Dapr.DaprClient Client { get; }
internal DaprJobsGrpcClient(
Autogenerated.Dapr.DaprClient innerClient,
HttpClient httpClient,
KeyValuePair<string, string>? apiTokenHeader)
string? daprApiToken)
{
this.client = innerClient;
this.httpClient = httpClient;
this.apiTokenHeader = apiTokenHeader;
this.Client = innerClient;
this.HttpClient = httpClient;
this.DaprApiToken = daprApiToken;
}
/// <summary>
@ -107,11 +112,11 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
var envelope = new Autogenerated.ScheduleJobRequest { Job = job };
var callOptions = CreateCallOptions(headers: null, cancellationToken);
var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken);
try
{
await client.ScheduleJobAlpha1Async(envelope, callOptions);
await Client.ScheduleJobAlpha1Async(envelope, grpcCallOptions).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@ -146,8 +151,8 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
try
{
var envelope = new Autogenerated.GetJobRequest { Name = jobName };
var callOptions = CreateCallOptions(headers: null, cancellationToken);
var response = await client.GetJobAlpha1Async(envelope, callOptions);
var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken);
var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions);
return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule))
{
DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null,
@ -190,8 +195,8 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
try
{
var envelope = new Autogenerated.DeleteJobRequest { Name = jobName };
var callOptions = CreateCallOptions(headers: null, cancellationToken);
await client.DeleteJobAlpha1Async(envelope, callOptions);
var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken);
await Client.DeleteJobAlpha1Async(envelope, grpcCallOptions);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@ -213,36 +218,7 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient
{
if (disposing)
{
this.httpClient.Dispose();
this.HttpClient.Dispose();
}
}
private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken)
{
var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken);
callOptions.Headers!.Add("User-Agent", this.userAgent);
if (apiTokenHeader is not null)
{
callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value);
}
return callOptions;
}
/// <summary>
/// Returns the value for the User-Agent.
/// </summary>
/// <returns>A <see cref="ProductInfoHeaderValue"/> containing the value to use for the User-Agent.</returns>
private static ProductInfoHeaderValue UserAgent()
{
var assembly = typeof(DaprJobsClient).Assembly;
var assemblyVersion = assembly
.GetCustomAttributes<AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?
.InformationalVersion;
return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}");
}
}

View File

@ -11,6 +11,7 @@
// limitations under the License.
// ------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@ -25,27 +26,29 @@ public static class DaprJobsServiceCollectionExtensions
/// Adds Dapr Jobs client support to the service collection.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">Optionally allows greater configuration of the <see cref="DaprJobsClient"/>.</param>
/// <param name="configure">Optionally allows greater configuration of the <see cref="DaprJobsClient"/> using injected services.</param>
/// <param name="lifetime">The lifetime of the registered services.</param>
public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action<DaprJobsClientBuilder>? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
/// <returns></returns>
public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action<IServiceProvider, DaprJobsClientBuilder>? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection));
//Register the IHttpClientFactory implementation
serviceCollection.AddHttpClient();
var registration = new Func<IServiceProvider, DaprJobsClient>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var configuration = serviceProvider.GetService<IConfiguration>();
var builder = new DaprJobsClientBuilder();
var builder = new DaprJobsClientBuilder(configuration);
builder.UseHttpClientFactory(httpClientFactory);
configure?.Invoke(builder);
configure?.Invoke(serviceProvider, builder);
return builder.Build();
});
switch (lifetime)
{
case ServiceLifetime.Scoped:
@ -59,35 +62,6 @@ public static class DaprJobsServiceCollectionExtensions
serviceCollection.TryAddSingleton(registration);
break;
}
return serviceCollection;
}
/// <summary>
/// Adds Dapr Jobs client support to the service collection.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">Optionally allows greater configuration of the <see cref="DaprJobsClient"/> using injected services.</param>
/// <param name="lifetime">The lifetime of the registered services.</param>
/// <returns></returns>
public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action<IServiceProvider, DaprJobsClientBuilder>? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection));
//Register the IHttpClientFactory implementation
serviceCollection.AddHttpClient();
serviceCollection.TryAddSingleton(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var builder = new DaprJobsClientBuilder();
builder.UseHttpClientFactory(httpClientFactory);
configure?.Invoke(serviceProvider, builder);
return builder.Build();
});
return serviceCollection;
}

View File

@ -39,9 +39,8 @@ public sealed class DaprPublishSubscribeClientBuilder : DaprGenericClientBuilder
/// </summary>
public override DaprPublishSubscribeClient Build()
{
var daprClientDependencies = BuildDaprClientDependencies();
var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprPublishSubscribeClient).Assembly);
var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel);
return new DaprPublishSubscribeGrpcClient(client);
return new DaprPublishSubscribeGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
}
}

View File

@ -20,14 +20,36 @@ namespace Dapr.Messaging.PublishSubscribe;
/// </summary>
internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient
{
private readonly P.DaprClient daprClient;
/// <summary>
/// The HTTP client used by the client for calling the Dapr runtime.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly HttpClient HttpClient;
/// <summary>
/// The Dapr API token value.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal readonly string? DaprApiToken;
/// <summary>
/// The autogenerated Dapr client.
/// </summary>
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
private readonly P.DaprClient Client;
/// <summary>
/// Creates a new instance of a <see cref="DaprPublishSubscribeGrpcClient"/>
/// </summary>
public DaprPublishSubscribeGrpcClient(P.DaprClient client)
public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken)
{
daprClient = client;
Client = client;
this.HttpClient = httpClient;
this.DaprApiToken = daprApiToken;
}
/// <summary>
@ -41,7 +63,7 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien
/// <returns></returns>
public override async Task<IAsyncDisposable> SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default)
{
var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient);
var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, Client);
await receiver.SubscribeAsync(cancellationToken);
return receiver;
}

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Dapr.Messaging.PublishSubscribe.Extensions;
@ -25,8 +26,9 @@ public static class PublishSubscribeServiceCollectionExtensions
var registration = new Func<IServiceProvider, DaprPublishSubscribeClient>(serviceProvider =>
{
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var configuration = serviceProvider.GetService<IConfiguration>();
var builder = new DaprPublishSubscribeClientBuilder();
var builder = new DaprPublishSubscribeClientBuilder(configuration);
builder.UseHttpClientFactory(httpClientFactory);
configure?.Invoke(serviceProvider, builder);

View File

@ -157,4 +157,4 @@ message ConfigurationItem {
// the metadata which will be passed to/from configuration store component.
map<string,string> metadata = 3;
}
}

View File

@ -340,4 +340,4 @@ message ListInputBindingsResponse {
// HealthCheckResponse is the message with the response to the health check.
// This message is currently empty as used as placeholder.
message HealthCheckResponse {}
message HealthCheckResponse {}

View File

@ -202,6 +202,9 @@ service Dapr {
// Delete a job
rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {}
// Converse with a LLM service
rpc ConverseAlpha1(ConversationRequest) returns (ConversationResponse) {}
}
// InvokeServiceRequest represents the request message for Service invocation.
@ -1206,7 +1209,7 @@ message Job {
//
// Systemd timer style cron accepts 6 fields:
// seconds | minutes | hours | day of month | month | day of week
// 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat
// 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat
//
// "0 30 * * * *" - every hour on the half hour
// "0 15 3 * * *" - every day at 03:15
@ -1274,4 +1277,56 @@ message DeleteJobRequest {
// DeleteJobResponse is the message response to delete the job by name.
message DeleteJobResponse {
// Empty
}
// ConversationRequest is the request object for Conversation.
message ConversationRequest {
// The name of Conversation component
string name = 1;
// The ID of an existing chat (like in ChatGPT)
optional string contextID = 2;
// Inputs for the conversation, support multiple input in one time.
repeated ConversationInput inputs = 3;
// Parameters for all custom fields.
map<string, google.protobuf.Any> parameters = 4;
// The metadata passing to conversation components.
map<string, string> metadata = 5;
// Scrub PII data that comes back from the LLM
optional bool scrubPII = 6;
// Temperature for the LLM to optimize for creativity or predictability
optional double temperature = 7;
}
message ConversationInput {
// The message to send to the llm
string message = 1;
// The role to set for the message
optional string role = 2;
// Scrub PII data that goes into the LLM
optional bool scrubPII = 3;
}
// ConversationResult is the result for one input.
message ConversationResult {
// Result for the one conversation input.
string result = 1;
// Parameters for all custom fields.
map<string, google.protobuf.Any> parameters = 2;
}
// ConversationResponse is the response for Conversation.
message ConversationResponse {
// The ID of an existing chat (like in ChatGPT)
optional string contextID = 1;
// An array of results.
repeated ConversationResult outputs = 2;
}

View File

@ -0,0 +1,33 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using Dapr.AI.Conversation;
namespace Dapr.AI.Test.Conversation;
public class DaprConversationClientBuilderTest
{
[Fact]
public void Build_WithDefaultConfiguration_ShouldReturnNewInstanceOfDaprConversationClient()
{
// Arrange
var conversationClientBuilder = new DaprConversationClientBuilder();
// Act
var client = conversationClientBuilder.Build();
// Assert
Assert.NotNull(client);
Assert.IsType<DaprConversationClient>(client);
}
}

View File

@ -0,0 +1,75 @@
// ------------------------------------------------------------------------
// Copyright 2024 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Net.Http;
using Dapr.AI.Conversation;
using Dapr.AI.Conversation.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Dapr.AI.Test.Conversation.Extensions;
public class DaprAiConversationBuilderExtensionsTest
{
[Fact]
public void AddDaprConversationClient_FromIConfiguration()
{
const string apiToken = "abc123";
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "DAPR_API_TOKEN", apiToken } })
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddDaprAiConversation();
var app = services.BuildServiceProvider();
var conversationClient = app.GetRequiredService<DaprConversationClient>() as DaprConversationClient;
Assert.NotNull(conversationClient!.DaprApiToken);
Assert.Equal(apiToken, conversationClient.DaprApiToken);
}
[Fact]
public void AddDaprAiConversation_WithoutConfigure_ShouldAddServices()
{
var services = new ServiceCollection();
var builder = services.AddDaprAiConversation();
Assert.NotNull(builder);
}
[Fact]
public void AddDaprAiConversation_RegistersIHttpClientFactory()
{
var services = new ServiceCollection();
services.AddDaprAiConversation();
var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
Assert.NotNull(httpClientFactory);
var daprConversationClient = serviceProvider.GetService<DaprConversationClient>();
Assert.NotNull(daprConversationClient);
}
[Fact]
public void AddDaprAiConversation_NullServices_ShouldThrowException()
{
IServiceCollection services = null;
Assert.Throws<ArgumentNullException>(() => services.AddDaprAiConversation());
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Moq" />
<PackageReference Include="coverlet.msbuild">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions"/>
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Dapr.AI\Dapr.AI.csproj" />
</ItemGroup>
</Project>

View File

@ -1,38 +0,0 @@
using System.Runtime.Serialization;
using Xunit;
namespace Dapr.Client.Test.Extensions
{
public class EnumExtensionTest
{
[Fact]
public void GetValueFromEnumMember_RedResolvesAsExpected()
{
var value = TestEnum.Red.GetValueFromEnumMember();
Assert.Equal("red", value);
}
[Fact]
public void GetValueFromEnumMember_YellowResolvesAsExpected()
{
var value = TestEnum.Yellow.GetValueFromEnumMember();
Assert.Equal("YELLOW", value);
}
[Fact]
public void GetValueFromEnumMember_BlueResolvesAsExpected()
{
var value = TestEnum.Blue.GetValueFromEnumMember();
Assert.Equal("Blue", value);
}
}
public enum TestEnum
{
[EnumMember(Value = "red")]
Red,
[EnumMember(Value = "YELLOW")]
Yellow,
Blue
}
}

View File

@ -12,14 +12,14 @@ public class EnumExtensionTest
var value = TestEnum.Red.GetValueFromEnumMember();
Assert.Equal("red", value);
}
[Fact]
public void GetValueFromEnumMember_YellowResolvesAsExpected()
{
var value = TestEnum.Yellow.GetValueFromEnumMember();
Assert.Equal("YELLOW", value);
}
[Fact]
public void GetValueFromEnumMember_BlueResolvesAsExpected()
{
@ -27,6 +27,7 @@ public class EnumExtensionTest
Assert.Equal("Blue", value);
}
}
public enum TestEnum
{
[EnumMember(Value = "red")]
@ -35,4 +36,3 @@ public enum TestEnum
Yellow,
Blue
}

View File

@ -0,0 +1,52 @@
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dapr.Common.JsonConverters;
using Xunit;
namespace Dapr.Common.Test.JsonConverters;
public class GenericEnumJsonConverterTest
{
[Fact]
public void ShouldSerializeWithEnumMemberAttribute()
{
var testValue = new TestType("ColorTest", Color.Red);
var serializedValue = JsonSerializer.Serialize(testValue);
Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}", serializedValue);
}
[Fact]
public void ShouldSerializeWithoutEnumMemberAttribute()
{
var testValue = new TestType("ColorTest", Color.Green);
var serializedValue = JsonSerializer.Serialize(testValue);
Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"Green\"}", serializedValue);
}
[Fact]
public void ShouldDeserializeWithEnumMemberAttribute()
{
const string json = "{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}";
var deserializedValue = JsonSerializer.Deserialize<TestType>(json);
Assert.Equal("ColorTest", deserializedValue.Name);
Assert.Equal(Color.Red, deserializedValue.Color);
}
[Fact]
public void ShouldDeserializeWithoutEnumMemberAttribute()
{
const string json = "{\"Name\":\"ColorTest\",\"Color\":\"Green\"}";
var deserializedValue = JsonSerializer.Deserialize<TestType>(json);
Assert.Equal("ColorTest", deserializedValue.Name);
Assert.Equal(Color.Green, deserializedValue.Color);
}
private record TestType(string Name, Color Color);
[JsonConverter(typeof(GenericEnumJsonConverter<Color>))]
private enum Color {
[EnumMember(Value="definitely-not-red")]
Red,
Green };
}

View File

@ -12,31 +12,57 @@
// ------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Dapr.Jobs.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Dapr.Jobs.Test.Extensions;
public class DaprJobsServiceCollectionExtensionsTest
{
[Fact]
public void AddDaprJobsClient_FromIConfiguration()
{
const string apiToken = "abc123";
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "DAPR_API_TOKEN", apiToken } })
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddDaprJobsClient();
var app = services.BuildServiceProvider();
var jobsClient = app.GetRequiredService<DaprJobsClient>() as DaprJobsGrpcClient;
Assert.NotNull(jobsClient!.DaprApiToken);
Assert.Equal(apiToken, jobsClient.DaprApiToken);
}
[Fact]
public void AddDaprJobsClient_RegistersDaprClientOnlyOnce()
{
var services = new ServiceCollection();
var clientBuilder = new Action<DaprJobsClientBuilder>(builder =>
builder.UseDaprApiToken("abc"));
var clientBuilder = new Action<IServiceProvider, DaprJobsClientBuilder>((sp, builder) =>
{
builder.UseDaprApiToken("abc");
});
services.AddDaprJobsClient(); //Sets a default API token value of an empty string
services.AddDaprJobsClient(clientBuilder); //Sets the API token value
var serviceProvider = services.BuildServiceProvider();
var daprJobClient = serviceProvider.GetService<DaprJobsClient>() as DaprJobsGrpcClient;
Assert.Null(daprJobClient!.apiTokenHeader);
Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _));
Assert.NotNull(daprJobClient!.HttpClient);
Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _));
}
[Fact]
@ -63,8 +89,8 @@ public class DaprJobsServiceCollectionExtensionsTest
services.AddDaprJobsClient((provider, builder) =>
{
var configProvider = provider.GetRequiredService<TestSecretRetriever>();
var daprApiToken = configProvider.GetApiTokenValue();
builder.UseDaprApiToken(daprApiToken);
var apiToken = TestSecretRetriever.GetApiTokenValue();
builder.UseDaprApiToken(apiToken);
});
var serviceProvider = services.BuildServiceProvider();
@ -72,10 +98,15 @@ public class DaprJobsServiceCollectionExtensionsTest
//Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient
Assert.NotNull(client);
Assert.NotNull(client.apiTokenHeader);
Assert.True(client.apiTokenHeader.HasValue);
Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key);
Assert.Equal("abcdef", client.apiTokenHeader.Value.Value);
Assert.NotNull(client.DaprApiToken);
Assert.Equal("abcdef", client.DaprApiToken);
Assert.NotNull(client.HttpClient);
if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken))
{
Assert.Fail();
}
Assert.Equal("abcdef", daprApiToken.FirstOrDefault());
}
[Fact]
@ -83,7 +114,7 @@ public class DaprJobsServiceCollectionExtensionsTest
{
var services = new ServiceCollection();
services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton);
services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Singleton);
var serviceProvider = services.BuildServiceProvider();
var daprJobsClient1 = serviceProvider.GetService<DaprJobsClient>();
@ -100,7 +131,7 @@ public class DaprJobsServiceCollectionExtensionsTest
{
var services = new ServiceCollection();
services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped);
services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Scoped);
var serviceProvider = services.BuildServiceProvider();
await using var scope1 = serviceProvider.CreateAsyncScope();
@ -119,7 +150,7 @@ public class DaprJobsServiceCollectionExtensionsTest
{
var services = new ServiceCollection();
services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient);
services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Transient);
var serviceProvider = services.BuildServiceProvider();
var daprJobsClient1 = serviceProvider.GetService<DaprJobsClient>();
@ -132,6 +163,6 @@ public class DaprJobsServiceCollectionExtensionsTest
private class TestSecretRetriever
{
public string GetApiTokenValue() => "abcdef";
public static string GetApiTokenValue() => "abcdef";
}
}