Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 19 additions & 56 deletions src/Dapr.AI/Conversation/DaprConversationClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,26 @@
// limitations under the License.
// ------------------------------------------------------------------------

using Dapr.Common;
using Dapr.Common.Extensions;
using P = Dapr.Client.Autogen.Grpc.v1;
using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr;

namespace Dapr.AI.Conversation;

/// <summary>
/// <para>
/// Used to interact with the Dapr conversation building block.
/// Use <see cref="DaprConversationClientBuilder"/> to create a <see cref="DaprConversationClient"/> or register
/// for use with dependency injection via
/// <see><cref>DaprJobsServiceCollectionExtensions.AddDaprJobsClient</cref></see>.
/// </para>
/// <para>
/// Implementations of <see cref="DaprConversationClient"/> implement <see cref="IDisposable"/> because the
/// client accesses network resources. For best performance, create a single long-lived client instance
/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid
/// creating a disposing a client instance for each operation that the application performs - this can lead to socket
/// exhaustion and other problems.
/// </para>
/// </summary>
public sealed class DaprConversationClient : DaprAIClient
public abstract class DaprConversationClient : DaprAIClient
{
/// <summary>
/// The HTTP client used by the client for calling the Dapr runtime.
Expand All @@ -42,15 +52,15 @@ public sealed class DaprConversationClient : DaprAIClient
/// <remarks>
/// Property exposed for testing purposes.
/// </remarks>
internal P.Dapr.DaprClient Client { get; }
internal Autogenerated.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,
protected DaprConversationClient(Autogenerated.DaprClient client,
HttpClient httpClient,
string? daprApiToken = null)
{
Expand All @@ -67,54 +77,7 @@ public DaprConversationClient(P.Dapr.DaprClient client,
/// <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)
{
if (options.ConversationId 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,
Content = input.Content,
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);
}
public abstract Task<DaprConversationResponse> ConverseAsync(string daprConversationComponentName,
IReadOnlyList<DaprConversationInput> inputs, ConversationOptions? options = null,
CancellationToken cancellationToken = default);
}
6 changes: 3 additions & 3 deletions src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder<Dap
/// <summary>
/// Used to initialize a new instance of the <see cref="DaprConversationClient"/>.
/// </summary>
/// <param name="configuration"></param>
/// <param name="configuration">An optional <see cref="IConfiguration"/> to configure the client with.</param>
public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration)
{
}

/// <summary>
/// Builds the client instance from the properties of the builder.
/// </summary>
Expand All @@ -41,6 +41,6 @@ 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);
return new DaprConversationGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken);
}
}
95 changes: 95 additions & 0 deletions src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// ------------------------------------------------------------------------
// Copyright 2025 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 Autogenerated = Dapr.Client.Autogen.Grpc.v1;

namespace Dapr.AI.Conversation;

/// <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>
internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, 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 Autogenerated.ConversationRequest
{
Name = daprConversationComponentName
};

if (options is not null)
{
if (options.ConversationId 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 Autogenerated.ConversationInput
{
ScrubPII = input.ScrubPII,
Content = input.Content,
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);
}

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.HttpClient.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,10 @@ 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
public sealed class DaprAiConversationBuilder(IServiceCollection services) : 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;
}
public IServiceCollection Services { get; } = services;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
// limitations under the License.
// ------------------------------------------------------------------------

using Microsoft.Extensions.Configuration;
using Dapr.Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Dapr.AI.Conversation.Extensions;

Expand All @@ -23,42 +22,11 @@ namespace Dapr.AI.Conversation.Extensions;
public static class DaprAiConversationBuilderExtensions
{
/// <summary>
/// Registers the necessary functionality for the Dapr AI conversation functionality.
/// Registers the necessary functionality for the Dapr AI Conversation functionality.
/// </summary>
/// <returns></returns>
public static IDaprAiConversationBuilder AddDaprConversationClient(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);
}
public static IDaprAiConversationBuilder AddDaprConversationClient(
this IServiceCollection services,
Action<IServiceProvider, DaprConversationClientBuilder>? configure = null,
ServiceLifetime lifetime = ServiceLifetime.Singleton) => services
.AddDaprClient<DaprConversationClient, DaprConversationGrpcClient, DaprAiConversationBuilder, DaprConversationClientBuilder>(configure, lifetime);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,4 @@ 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
{
}
public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder;
30 changes: 19 additions & 11 deletions src/Dapr.AI/DaprAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,32 @@
// limitations under the License.
// ------------------------------------------------------------------------

using Dapr.AI.Conversation;
using Dapr.Common;

namespace Dapr.AI;

/// <summary>
/// The base implementation of a Dapr AI client.
/// </summary>
public abstract class DaprAIClient
public abstract class DaprAIClient : IDaprClient
{
private bool disposed;

/// <inheritdoc />
public void Dispose()
{
if (!this.disposed)
{
Dispose(disposing: true);
this.disposed = true;
}
}

/// <summary>
/// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar.
/// Disposes the resources associated with the object.
/// </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);
/// <param name="disposing"><c>true</c> if called by a call to the <c>Dispose</c> method; otherwise false.</param>
protected virtual void Dispose(bool disposing)
{
}
}
9 changes: 2 additions & 7 deletions src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,12 @@
// limitations under the License.
// ------------------------------------------------------------------------

using Dapr.Common;
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; }
}
public interface IDaprAiServiceBuilder : IDaprServiceBuilder;
1 change: 1 addition & 0 deletions src/Dapr.Common/Dapr.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Dapr.Protos\Dapr.Protos.csproj" />
<PackageReference Include="Google.Api.CommonProtos" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Microsoft.Extensions.Http" />
Expand Down
Loading