diff --git a/all.sln b/all.sln index 28a168df6..8f5cd97d0 100644 --- a/all.sln +++ b/all.sln @@ -231,6 +231,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Abstractions. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Testcontainers.Xunit", "src\Dapr.Testcontainers.Xunit\Dapr.Testcontainers.Xunit.csproj", "{2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.AI", "test\Dapr.IntegrationTest.AI\Dapr.IntegrationTest.AI.csproj", "{AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -609,6 +611,10 @@ Global {2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5}.Release|Any CPU.Build.0 = Release|Any CPU + {AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -721,6 +727,7 @@ Global {E958E875-8DDE-4B25-BE3A-C0760EC89376} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} {38AAD849-B59C-4011-B309-3E9F291E9B9F} = {0AF0FE8D-C234-4F04-8514-32206ACE01BD} {2D6EB9E0-C5BF-4BA4-B69F-0D2B5A0E36D5} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {AD9F25C7-7BBD-459A-B3EF-1BE75C25E80A} = {8462B106-175A-423A-BA94-BE0D39D0BD8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.AI/Conversation/CalledToolFunction.cs b/src/Dapr.AI/Conversation/CalledToolFunction.cs index fd7d1ff21..b1feea922 100644 --- a/src/Dapr.AI/Conversation/CalledToolFunction.cs +++ b/src/Dapr.AI/Conversation/CalledToolFunction.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation; +// ------------------------------------------------------------------------ +// Copyright 2026 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; /// /// Documents a tool call by a function within the context of a conversation message. diff --git a/src/Dapr.AI/Conversation/CompletionUsage.cs b/src/Dapr.AI/Conversation/CompletionUsage.cs new file mode 100644 index 000000000..bca1e1267 --- /dev/null +++ b/src/Dapr.AI/Conversation/CompletionUsage.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 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.Client.Autogen.Grpc.v1; + +namespace Dapr.AI.Conversation; + +/// +/// The breakdown of tokens used in a conversation. +/// +/// Number of tokens in the generated completion. +/// The number of tokes in the prompt. +/// The total number of tokens used in the request (prompt and completion). +public sealed record CompletionUsage(ulong CompletionTokens, ulong PromptTokens, ulong TotalTokens) +{ + /// + /// Breakdown of tokens used in completion. + /// + public UsageCompletionTokensDetails? CompletionTokensDetails { get; init; } + + /// + /// Breakdown of tokens used in the prompt. + /// + public UsagePromptTokensDetails? PromptTokensDetails { get; init; } + + /// + /// Creates an instance of from the prototype value. + /// + /// The gRPC response from the runtime to parse. + /// A new instance of . + internal static CompletionUsage? FromProto(ConversationResultAlpha2CompletionUsage? proto) => + proto is null + ? null + : new(proto.CompletionTokens, proto.PromptTokens, proto.TotalTokens) + { + CompletionTokensDetails = UsageCompletionTokensDetails.FromProto(proto.CompletionTokensDetails), + PromptTokensDetails = UsagePromptTokensDetails.FromProto(proto.PromptTokensDetails) + }; +} + + diff --git a/src/Dapr.AI/Conversation/ConversationOptions.cs b/src/Dapr.AI/Conversation/ConversationOptions.cs index 4952765de..501e70182 100644 --- a/src/Dapr.AI/Conversation/ConversationOptions.cs +++ b/src/Dapr.AI/Conversation/ConversationOptions.cs @@ -68,4 +68,14 @@ public sealed record ConversationOptions(string ConversationComponentId) /// - 'auto' is the default if tools are present. /// public ToolChoice? ToolChoice { get; init; } + + /// + /// Retention policy for the prompt cache. + /// + public TimeSpan? PromptCacheRetention { get; init; } + + /// + /// The JSON schema used to coerce the response into a specific format. + /// + public Struct? ResponseFormat { get; init; } } diff --git a/src/Dapr.AI/Conversation/ConversationProtoUtilities.cs b/src/Dapr.AI/Conversation/ConversationProtoUtilities.cs index 89a6b02db..6218fb28f 100644 --- a/src/Dapr.AI/Conversation/ConversationProtoUtilities.cs +++ b/src/Dapr.AI/Conversation/ConversationProtoUtilities.cs @@ -101,6 +101,16 @@ public static Autogenerated.ConversationRequestAlpha2 CreateConversationInputReq request.Temperature = options.Temperature.Value; } + if (options.PromptCacheRetention is not null) + { + request.PromptCacheRetention = options.PromptCacheRetention.Value.ToDuration(); + } + + if (options.ResponseFormat is not null) + { + request.ResponseFormat = options.ResponseFormat; + } + if (options.Tools.Count > 0) { var tools = options.Tools.Select(tool => @@ -163,7 +173,12 @@ public static ConversationResponse ToDomain(this Autogenerated.ConversationRespo return new ConversationResultChoice(didParseReason ? parsedFinishReason : null, c.Index, resultMessage); }).ToList(); - return new ConversationResponseResult(choices); + + return new ConversationResponseResult(choices) + { + Model = !string.IsNullOrWhiteSpace(convoResult.Model) ? convoResult.Model : null, + Usage = convoResult.Usage is not null ? CompletionUsage.FromProto(convoResult.Usage) : null + }; }).ToList(); return new ConversationResponse(conversationResults, contextId); diff --git a/src/Dapr.AI/Conversation/ConversationResponseResult.cs b/src/Dapr.AI/Conversation/ConversationResponseResult.cs index 9cf57fe4b..8bebc8b35 100644 --- a/src/Dapr.AI/Conversation/ConversationResponseResult.cs +++ b/src/Dapr.AI/Conversation/ConversationResponseResult.cs @@ -1,7 +1,31 @@ -namespace Dapr.AI.Conversation; +// ------------------------------------------------------------------------ +// Copyright 2026 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; /// /// The result in a conversation for a given input. /// /// The resulting choices from the conversation. -public sealed record ConversationResponseResult(IReadOnlyList Choices); +public sealed record ConversationResponseResult(IReadOnlyList Choices) +{ + /// + /// The model used for the conversation. + /// + public string? Model { get; init; } + + /// + /// Usage statistics for the completion request. + /// + public CompletionUsage? Usage { get; init; } +} diff --git a/src/Dapr.AI/Conversation/ConversationResultChoice.cs b/src/Dapr.AI/Conversation/ConversationResultChoice.cs index 2419b5e4d..b35827d2a 100644 --- a/src/Dapr.AI/Conversation/ConversationResultChoice.cs +++ b/src/Dapr.AI/Conversation/ConversationResultChoice.cs @@ -1,4 +1,17 @@ -using Dapr.AI.Conversation.Tools; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Tools; namespace Dapr.AI.Conversation; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/AssistantMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/AssistantMessage.cs index b41bdb40c..c22cadf9f 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/AssistantMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/AssistantMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; using Dapr.Common.JsonConverters; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/DeveloperMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/DeveloperMessage.cs index 7c2cb595d..608264513 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/DeveloperMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/DeveloperMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; using Dapr.Common.JsonConverters; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/IConversationMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/IConversationMessage.cs index 06b2f30cc..3de2c3e10 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/IConversationMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/IConversationMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/MessageContent.cs b/src/Dapr.AI/Conversation/ConversationRoles/MessageContent.cs index dfc15f970..e1de0b9da 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/MessageContent.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/MessageContent.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation.ConversationRoles; +// ------------------------------------------------------------------------ +// Copyright 2026 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.ConversationRoles; /// /// The content of a message. diff --git a/src/Dapr.AI/Conversation/ConversationRoles/MessageRole.cs b/src/Dapr.AI/Conversation/ConversationRoles/MessageRole.cs index dc506e425..9341d5cc2 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/MessageRole.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/MessageRole.cs @@ -1,4 +1,17 @@ -using System.Runtime.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Runtime.Serialization; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/SystemMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/SystemMessage.cs index 324a9b3b8..7e8046bc3 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/SystemMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/SystemMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; using Dapr.Common.JsonConverters; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/ToolMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/ToolMessage.cs index ba2dcd5d9..1dc78eec6 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/ToolMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/ToolMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; using Dapr.Common.JsonConverters; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/ConversationRoles/UserMessage.cs b/src/Dapr.AI/Conversation/ConversationRoles/UserMessage.cs index bc966fb01..001984b1d 100644 --- a/src/Dapr.AI/Conversation/ConversationRoles/UserMessage.cs +++ b/src/Dapr.AI/Conversation/ConversationRoles/UserMessage.cs @@ -1,4 +1,17 @@ -using System.Text.Json.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Serialization; using Dapr.Common.JsonConverters; namespace Dapr.AI.Conversation.ConversationRoles; diff --git a/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs index 466aa1c89..7a9c2c37b 100644 --- a/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs +++ b/src/Dapr.AI/Conversation/DaprConversationGrpcClient.cs @@ -23,9 +23,13 @@ namespace Dapr.AI.Conversation; /// The Dapr client. /// The HTTP client used by the client for calling the Dapr runtime. /// An optional token required to send requests to the Dapr sidecar. -[Experimental("DAPR_CONVERSATION", UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] -internal sealed class DaprConversationGrpcClient(Autogenerated.Dapr.DaprClient client, HttpClient httpClient, string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken) -{ +[Experimental("DAPR_CONVERSATION", + UrlFormat = "https://docs.dapr.io/developing-applications/building-blocks/conversation/conversation-overview/")] +internal sealed class DaprConversationGrpcClient( + Autogenerated.Dapr.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) : DaprConversationClient(client, httpClient, daprApiToken: daprApiToken) +{ /// public override async Task ConverseAsync(IReadOnlyList inputs, ConversationOptions options, CancellationToken cancellationToken = default) @@ -42,7 +46,7 @@ public override async Task ConverseAsync(IReadOnlyList protected override void Dispose(bool disposing) { diff --git a/src/Dapr.AI/Conversation/Extensions/ProtobufHelpers.cs b/src/Dapr.AI/Conversation/Extensions/ProtobufHelpers.cs index ed7cdf928..bf9f6b7ee 100644 --- a/src/Dapr.AI/Conversation/Extensions/ProtobufHelpers.cs +++ b/src/Dapr.AI/Conversation/Extensions/ProtobufHelpers.cs @@ -1,4 +1,17 @@ -using System.Collections; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Collections; using System.Text.Json; using Google.Protobuf.Collections; using Google.Protobuf.WellKnownTypes; diff --git a/src/Dapr.AI/Conversation/ResultMessage.cs b/src/Dapr.AI/Conversation/ResultMessage.cs index 0ce25bcd1..f747014b5 100644 --- a/src/Dapr.AI/Conversation/ResultMessage.cs +++ b/src/Dapr.AI/Conversation/ResultMessage.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation; +// ------------------------------------------------------------------------ +// Copyright 2026 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; /// /// The resulting message in a conversation. diff --git a/src/Dapr.AI/Conversation/ToolCallBase.cs b/src/Dapr.AI/Conversation/ToolCallBase.cs index a92af4baf..0abb9a21a 100644 --- a/src/Dapr.AI/Conversation/ToolCallBase.cs +++ b/src/Dapr.AI/Conversation/ToolCallBase.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation; +// ------------------------------------------------------------------------ +// Copyright 2026 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; /// /// Represents a call to a tool within the context of a conversation. diff --git a/src/Dapr.AI/Conversation/Tools/FinishReason.cs b/src/Dapr.AI/Conversation/Tools/FinishReason.cs index f05084295..3a47a5471 100644 --- a/src/Dapr.AI/Conversation/Tools/FinishReason.cs +++ b/src/Dapr.AI/Conversation/Tools/FinishReason.cs @@ -1,4 +1,17 @@ -using System.Runtime.Serialization; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Runtime.Serialization; namespace Dapr.AI.Conversation.Tools; diff --git a/src/Dapr.AI/Conversation/Tools/ITool.cs b/src/Dapr.AI/Conversation/Tools/ITool.cs index c645d384f..1b6469680 100644 --- a/src/Dapr.AI/Conversation/Tools/ITool.cs +++ b/src/Dapr.AI/Conversation/Tools/ITool.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation.Tools; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Tools; /// /// The types of tools available to be called. diff --git a/src/Dapr.AI/Conversation/Tools/ToolChoice.cs b/src/Dapr.AI/Conversation/Tools/ToolChoice.cs index e97bba47e..80025dec4 100644 --- a/src/Dapr.AI/Conversation/Tools/ToolChoice.cs +++ b/src/Dapr.AI/Conversation/Tools/ToolChoice.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation.Tools; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Tools; /// /// Strongly-typed representation of a tool choice that supports known values ('none', 'auto') as well diff --git a/src/Dapr.AI/Conversation/Tools/ToolFunction.cs b/src/Dapr.AI/Conversation/Tools/ToolFunction.cs index 4ab072042..d38a77d19 100644 --- a/src/Dapr.AI/Conversation/Tools/ToolFunction.cs +++ b/src/Dapr.AI/Conversation/Tools/ToolFunction.cs @@ -1,4 +1,17 @@ -namespace Dapr.AI.Conversation.Tools; +// ------------------------------------------------------------------------ +// Copyright 2026 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.Tools; /// /// The main tool type to be used in a conversation. diff --git a/src/Dapr.AI/Conversation/UsageCompletionTokensDetails.cs b/src/Dapr.AI/Conversation/UsageCompletionTokensDetails.cs new file mode 100644 index 000000000..f2a554069 --- /dev/null +++ b/src/Dapr.AI/Conversation/UsageCompletionTokensDetails.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 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.Client.Autogen.Grpc.v1; + +namespace Dapr.AI.Conversation; + +/// +/// Represents the breakdown of usage completion tokens used in a conversation. +/// +/// When using predicted outputs, the number of tokens in the +/// prediction that appeared in the completion. +/// The audio input tokens generated by the model. +/// Tokens generated by the model for reasoning. +/// When using predicted outputs, the number of tokens in the prediction that did not +/// appear in the completion. However, like reasoning tokens, these tokens are still counted in the total completion +/// tokens for purposes for billing, output, and context window limits. +public sealed record UsageCompletionTokensDetails( + ulong AcceptedPredictionTokens, + ulong AudioTokens, + ulong ReasoningTokens, + ulong RejectedPredictionTokens) + +{ + /// + /// Creates an instance of from the prototype value. + /// + /// The gRPC response from the runtime to parse. + /// A new instance of . + internal static UsageCompletionTokensDetails? + FromProto(ConversationResultAlpha2CompletionUsageCompletionTokensDetails? proto) => + proto is null + ? null + : new( + proto.AcceptedPredictionTokens, proto.AudioTokens, proto.ReasoningTokens, + proto.RejectedPredictionTokens); +} + diff --git a/src/Dapr.AI/Conversation/UsagePromptTokensDetails.cs b/src/Dapr.AI/Conversation/UsagePromptTokensDetails.cs new file mode 100644 index 000000000..3381e922d --- /dev/null +++ b/src/Dapr.AI/Conversation/UsagePromptTokensDetails.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 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.Client.Autogen.Grpc.v1; + +namespace Dapr.AI.Conversation; + +/// +/// Represents the breakdown of prompt tokens used in a conversation. +/// +/// Audio input tokens present in the prompt. +/// Cached tokens present in the prompt. +public sealed record UsagePromptTokensDetails(ulong AudioTokens, ulong CachedTokens) +{ + /// + /// Creates an instance of from the prototype value. + /// + /// The gRPC response from the runtime to parse. + /// A new instance of . + internal static UsagePromptTokensDetails? + FromProto(ConversationResultAlpha2CompletionUsagePromptTokensDetails? proto) => + proto is null ? null : new(proto.AudioTokens, proto.CachedTokens); +} diff --git a/src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs b/src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs index d1d7e3f31..fdff714bd 100644 --- a/src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs +++ b/src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs @@ -92,12 +92,10 @@ public DaprHarnessBuilder WithEnvironment(DaprTestEnvironment environment) /// Builds a distributed lock harness. /// public DistributedLockHarness BuildDistributedLock() => new(_componentsDirectory, _startApp, _options, _environment); - // - // /// - // /// Builds a conversation harness. - // /// - // public ConversationHarness BuildConversation() => new(_componentsDirectory, _startApp, _options, _environment); - // + /// + /// Builds a conversation harness. + /// + public ConversationHarness BuildConversation() => new(_componentsDirectory, _startApp, _options, _environment); // /// // /// Builds a cryptography harness. // /// diff --git a/src/Dapr.Testcontainers/Containers/OllamaContainer.cs b/src/Dapr.Testcontainers/Containers/OllamaContainer.cs index d581c7edd..7e5b08e02 100644 --- a/src/Dapr.Testcontainers/Containers/OllamaContainer.cs +++ b/src/Dapr.Testcontainers/Containers/OllamaContainer.cs @@ -13,6 +13,9 @@ using System; using System.IO; +using System.Net.Http; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Testcontainers.Common; @@ -41,6 +44,7 @@ public OllamaContainer(INetwork network) .WithImage("ollama/ollama") .WithName(_containerName) .WithNetwork(network) + .WithEnvironment("CUDA_VISIBLE_DEVICES", "-1") .WithPortBinding(InternalPort, assignRandomHostPort: true) .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort)) .Build(); @@ -63,6 +67,32 @@ public OllamaContainer(INetwork network) /// public int Port { get; private set; } + /// + /// Ensures the indicated model is available in the running Ollama instance. + /// + /// The model name to pull if missing. + /// Cancellation token. + public async Task EnsureModelAsync(string model, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(model); + + if (Port <= 0) + throw new InvalidOperationException("Ollama container must be started before pulling models."); + + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri($"http://127.0.0.1:{Port}"); + + if (await IsModelAvailableAsync(httpClient, model, cancellationToken)) + return; + + var payload = JsonSerializer.Serialize(new { name = model }); + using var content = new StringContent(payload, Encoding.UTF8, "application/json"); + using var response = await httpClient.PostAsync("/api/pull", content, cancellationToken); + response.EnsureSuccessStatusCode(); + + await DrainResponseAsync(response.Content, cancellationToken); + } + /// public async Task StartAsync(CancellationToken cancellationToken = default) { @@ -75,6 +105,53 @@ public async Task StartAsync(CancellationToken cancellationToken = default) /// public ValueTask DisposeAsync() => _container.DisposeAsync(); + private static async Task IsModelAvailableAsync( + HttpClient httpClient, + string model, + CancellationToken cancellationToken) + { + try + { + using var response = await httpClient.GetAsync("/api/tags", cancellationToken); + if (!response.IsSuccessStatusCode) + return false; + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + + if (!document.RootElement.TryGetProperty("models", out var models)) + return false; + + foreach (var entry in models.EnumerateArray()) + { + if (!entry.TryGetProperty("name", out var nameElement)) + continue; + + if (string.Equals(nameElement.GetString(), model, StringComparison.OrdinalIgnoreCase)) + return true; + } + } + catch (Exception) + { + return false; + } + + return false; + } + + private static async Task DrainResponseAsync(HttpContent content, CancellationToken cancellationToken) + { + await using var stream = await content.ReadAsStreamAsync(cancellationToken); + var buffer = new byte[8192]; + + while (true) + { + var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken); + if (read == 0) + break; + } + } + /// /// Builds out the YAML components for Ollama. /// @@ -83,7 +160,7 @@ public static class Yaml /// /// Writes the component YAML. /// - public static void WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1") + public static void WriteConversationYamlToFolder(string folderPath, string model, string fileName = "ollama-conversation.yaml", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1") { var yaml = GetConversationYaml(model, cacheTtl, endpoint); WriteToFolder(folderPath, fileName, yaml); @@ -97,18 +174,21 @@ private static void WriteToFolder(string folderPath, string fileName, string yam } private static string GetConversationYaml(string model, string cacheTtl, string endpoint) => - $@"apiVersion: dapr.io/v1alpha -kind: Component -metadata: - name: {Constants.DaprComponentNames.ConversationComponentName} -spec: - type: conversation.ollama - metadata: - - name: model - value: {model} - - name: cacheTTL - value: {cacheTtl} - - name: endpoint - value: {endpoint}"; + $""" + apiVersion: dapr.io/v1alpha1 + kind: Component + metadata: + name: {Constants.DaprComponentNames.ConversationComponentName} + spec: + type: conversation.ollama + version: v1 + metadata: + - name: model + value: {model} + - name: cacheTTL + value: {cacheTtl} + - name: endpoint + value: {endpoint} + """; } } diff --git a/src/Dapr.Testcontainers/Harnesses/ConversationHarness.cs b/src/Dapr.Testcontainers/Harnesses/ConversationHarness.cs index 2d33ade86..879fa7e15 100644 --- a/src/Dapr.Testcontainers/Harnesses/ConversationHarness.cs +++ b/src/Dapr.Testcontainers/Harnesses/ConversationHarness.cs @@ -24,6 +24,10 @@ namespace Dapr.Testcontainers.Harnesses; /// public sealed class ConversationHarness : BaseHarness { + /// + /// The name of the model to run for the test. + /// + private string ModelName = "smollm2:135m"; private readonly OllamaContainer _ollama; private readonly string componentsDir; @@ -34,20 +38,32 @@ public sealed class ConversationHarness : BaseHarness /// The test app to validate in the harness. /// The Dapr runtime options. /// The isolated environment instance. - public ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options, DaprTestEnvironment? environment = null) : base(componentsDir, startApp, options, environment) + public ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options, + DaprTestEnvironment? environment = null) : base(componentsDir, startApp, options, environment) { this.componentsDir = componentsDir; _ollama = new(Network); } + /// + /// Sets the model to run for the test. + /// + /// The name of the model to pull and use from Ollama. + public void UseModel(string modelName) + { + ModelName = modelName; + } + /// protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // Start infrastructure await _ollama.StartAsync(cancellationToken); + await _ollama.EnsureModelAsync(ModelName, cancellationToken); // Emit component YAMLs for Ollama (use the default tiny model) OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir, + model: ModelName, endpoint: $"http://{_ollama.NetworkAlias}:{OllamaContainer.ContainerPort}/v1"); } diff --git a/test/Dapr.IntegrationTest.AI/Conversation/ConversationTests.cs b/test/Dapr.IntegrationTest.AI/Conversation/ConversationTests.cs new file mode 100644 index 000000000..1bdee9002 --- /dev/null +++ b/test/Dapr.IntegrationTest.AI/Conversation/ConversationTests.cs @@ -0,0 +1,241 @@ +// ------------------------------------------------------------------------ +// 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.AI.Conversation; +using Dapr.AI.Conversation.ConversationRoles; +using Dapr.AI.Conversation.Extensions; +using Dapr.AI.Conversation.Tools; +using Dapr.Testcontainers; +using Dapr.Testcontainers.Common; +using Dapr.Testcontainers.Harnesses; +using Dapr.Testcontainers.Xunit.Attributes; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.IntegrationTest.AI.Conversation; + +public sealed class ConversationTests +{ + [MinimumDaprRuntimeFact("1.17")] + public async Task ShouldProcessConversation() + { + var componentsDir = TestDirectoryManager.CreateTestDirectory("conversation-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildConversation(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprConversationClient((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + var httpEndpoint = config["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + if (!string.IsNullOrEmpty(httpEndpoint)) + clientBuilder.UseHttpEndpoint(httpEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var inputs = new[] + { + new ConversationInput( + [ + new SystemMessage + { + Content = [new MessageContent("You are a concise assistant.")] + }, + new UserMessage + { + Content = [new MessageContent("Respond with a short greeting.")] + } + ]) + }; + + var options = new ConversationOptions(Constants.DaprComponentNames.ConversationComponentName) + { + Temperature = 0 + }; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var response = await client.ConverseAsync(inputs, options, cts.Token); + + Assert.NotNull(response); + Assert.Single(response.Outputs); + Assert.NotEmpty(response.Outputs[0].Choices); + Assert.False(string.IsNullOrWhiteSpace(response.Outputs[0].Choices[0].Message.Content)); + } + + [MinimumDaprRuntimeFact("1.17")] + public async Task ShouldProcessMultipleInputs() + { + var componentsDir = TestDirectoryManager.CreateTestDirectory("conversation-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildConversation(); + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprConversationClient((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + var httpEndpoint = config["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + if (!string.IsNullOrEmpty(httpEndpoint)) + clientBuilder.UseHttpEndpoint(httpEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var inputs = new[] + { + new ConversationInput( + [ + new UserMessage + { + Content = [new MessageContent("Give a one-word greeting.")] + } + ]), + new ConversationInput( + [ + new UserMessage + { + Content = [new MessageContent("Reply with a short farewell.")] + } + ]) + }; + + var options = new ConversationOptions(Constants.DaprComponentNames.ConversationComponentName) + { + Temperature = 0 + }; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var response = await client.ConverseAsync(inputs, options, cts.Token); + + Assert.NotNull(response); + Assert.NotEmpty(response.Outputs); + Assert.True(response.Outputs.Count <= inputs.Length, "Received more outputs than inputs."); + + foreach (var output in response.Outputs) + { + Assert.NotEmpty(output.Choices); + Assert.False(string.IsNullOrWhiteSpace(output.Choices[0].Message.Content)); + } + } + + [MinimumDaprRuntimeFact("1.17")] + public async Task ShouldAcceptToolsAndMetadata() + { + var componentsDir = TestDirectoryManager.CreateTestDirectory("conversation-components"); + + await using var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(); + await environment.StartAsync(); + + var harness = new DaprHarnessBuilder(componentsDir) + .WithEnvironment(environment) + .BuildConversation(); + harness.UseModel("qwen2.5:0.5b"); // SmolLM2 doesn't actually support tools + await using var testApp = await DaprHarnessBuilder.ForHarness(harness) + .ConfigureServices(builder => + { + builder.Services.AddDaprConversationClient((sp, clientBuilder) => + { + var config = sp.GetRequiredService(); + var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"]; + var httpEndpoint = config["DAPR_HTTP_ENDPOINT"]; + + if (!string.IsNullOrEmpty(grpcEndpoint)) + clientBuilder.UseGrpcEndpoint(grpcEndpoint); + if (!string.IsNullOrEmpty(httpEndpoint)) + clientBuilder.UseHttpEndpoint(httpEndpoint); + }); + }) + .BuildAndStartAsync(); + + using var scope = testApp.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + + var inputs = new[] + { + new ConversationInput( + [ + new UserMessage + { + Content = [new MessageContent("Say hello and keep it short.")] + } + ]) + }; + + var toolSchema = new Dictionary + { + ["type"] = "object", + ["properties"] = new Dictionary + { + ["location"] = new Dictionary { ["type"] = "string" }, + ["units"] = new Dictionary + { + ["type"] = "string", + ["enum"] = new[] { "c", "f" } + } + }, + ["required"] = new[] { "location" } + }; + + var options = new ConversationOptions(Constants.DaprComponentNames.ConversationComponentName) + { + Temperature = 0, + ToolChoice = ToolChoice.Auto, + Metadata = new Dictionary + { + ["test-run"] = "true" + }, + Tools = + [ + new ToolFunction("get_weather") + { + Description = "Returns the current weather for a location.", + Parameters = toolSchema + } + ] + }; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var response = await client.ConverseAsync(inputs, options, cts.Token); + + Assert.NotNull(response); + Assert.NotEmpty(response.Outputs); + Assert.NotEmpty(response.Outputs[0].Choices); + Assert.False(string.IsNullOrWhiteSpace(response.Outputs[0].Choices[0].Message.Content)); + } +} diff --git a/test/Dapr.IntegrationTest.AI/Dapr.IntegrationTest.AI.csproj b/test/Dapr.IntegrationTest.AI/Dapr.IntegrationTest.AI.csproj new file mode 100644 index 000000000..1c452d45f --- /dev/null +++ b/test/Dapr.IntegrationTest.AI/Dapr.IntegrationTest.AI.csproj @@ -0,0 +1,27 @@ + + + + enable + enable + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs b/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs index 00adb5ddd..80da40be1 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobFailurePolicyTests.cs @@ -16,7 +16,6 @@ using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Options; using Dapr.Testcontainers.Harnesses; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs b/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs index b39073328..6b1a07fef 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobManagementTests.cs @@ -16,7 +16,6 @@ using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Options; using Dapr.Testcontainers.Harnesses; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs b/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs index 5de5e862b..25644d4fe 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobPayloadTests.cs @@ -17,7 +17,6 @@ using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Options; using Dapr.Testcontainers.Harnesses; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs b/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs index 168b2396f..a4ddfc9e3 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobSchedulingTests.cs @@ -15,7 +15,6 @@ using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Options; using Dapr.Testcontainers.Harnesses; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/test/Dapr.IntegrationTest.Jobs/JobsTests.cs b/test/Dapr.IntegrationTest.Jobs/JobsTests.cs index b51a8afa5..645ad6f51 100644 --- a/test/Dapr.IntegrationTest.Jobs/JobsTests.cs +++ b/test/Dapr.IntegrationTest.Jobs/JobsTests.cs @@ -16,7 +16,6 @@ using Dapr.Jobs.Extensions; using Dapr.Jobs.Models; using Dapr.Testcontainers.Common; -using Dapr.Testcontainers.Common.Options; using Dapr.Testcontainers.Harnesses; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;