Skip to content

Commit 9bb313b

Browse files
committed
chore: add tool call sample
1 parent fa25c2b commit 9bb313b

File tree

11 files changed

+180
-82
lines changed

11 files changed

+180
-82
lines changed

sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@
1919
</None>
2020
</ItemGroup>
2121

22+
<ItemGroup>
23+
<PackageReference Include="Microsoft.Extensions.AI" Version="9.0.1-preview.1.24570.5" />
24+
</ItemGroup>
25+
2226
</Project>

sample/Cnblogs.DashScope.Sample/Program.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
using Json.Schema.Generation;
99
using Microsoft.Extensions.AI;
1010

11-
const string apiKey = "sk-***";
12-
var dashScopeClient = new DashScopeClient(apiKey);
11+
Console.WriteLine("Reading key from environment variable DASHSCOPE_KEY");
12+
var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY");
13+
if (string.IsNullOrEmpty(apiKey))
14+
{
15+
Console.Write("ApiKey > ");
16+
apiKey = Console.ReadLine();
17+
}
18+
19+
var dashScopeClient = new DashScopeClient(apiKey!);
1320

1421
Console.WriteLine("Choose the sample you want to run:");
1522
foreach (var sampleType in Enum.GetValues<SampleType>())
@@ -46,6 +53,9 @@
4653
case SampleType.MicrosoftExtensionsAi:
4754
await ChatWithMicrosoftExtensions();
4855
break;
56+
case SampleType.MicrosoftExtensionsAiToolCall:
57+
await dashScopeClient.ToolCallWithExtensionAsync();
58+
break;
4959
}
5060

5161
return;
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
1-
using System.ComponentModel;
2-
3-
namespace Cnblogs.DashScope.Sample;
1+
namespace Cnblogs.DashScope.Sample;
42

53
public enum SampleType
64
{
7-
[Description("Simple prompt completion")]
85
TextCompletion,
96

10-
[Description("Simple prompt completion with incremental output")]
117
TextCompletionSse,
128

13-
[Description("Conversation between user and assistant")]
149
ChatCompletion,
1510

16-
[Description("Conversation with tools")]
1711
ChatCompletionWithTool,
1812

19-
[Description("Conversation with files")]
2013
ChatCompletionWithFiles,
2114

22-
[Description("Completion with Microsoft.Extensions.AI")]
23-
MicrosoftExtensionsAi
15+
MicrosoftExtensionsAi,
16+
17+
MicrosoftExtensionsAiToolCall
2418
}

sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public static string GetDescription(this SampleType sampleType)
1212
SampleType.ChatCompletionWithTool => "Function call sample",
1313
SampleType.ChatCompletionWithFiles => "File upload sample using qwen-long",
1414
SampleType.MicrosoftExtensionsAi => "Use with Microsoft.Extensions.AI",
15+
SampleType.MicrosoftExtensionsAiToolCall => "Use tool call with Microsoft.Extensions.AI interfaces",
1516
_ => throw new ArgumentOutOfRangeException(nameof(sampleType), sampleType, "Unsupported sample option")
1617
};
1718
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.ComponentModel;
2+
using System.Text.Json;
3+
using Cnblogs.DashScope.Core;
4+
using Microsoft.Extensions.AI;
5+
6+
namespace Cnblogs.DashScope.Sample;
7+
8+
public static class ToolCallWithExtensions
9+
{
10+
public static async Task ToolCallWithExtensionAsync(this IDashScopeClient dashScopeClient)
11+
{
12+
[Description("Gets the weather")]
13+
string GetWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining";
14+
15+
var chatOptions = new ChatOptions { Tools = [AIFunctionFactory.Create(GetWeather)] };
16+
17+
var client = dashScopeClient.AsChatClient("qwen-max").AsBuilder().UseFunctionInvocation().Build();
18+
await foreach (var message in client.CompleteStreamingAsync("What is weather today?", chatOptions))
19+
{
20+
Console.WriteLine(JsonSerializer.Serialize(message));
21+
}
22+
23+
Console.WriteLine();
24+
}
25+
}

src/Cnblogs.Extensions.AI.DashScope/DashScopeChatClient.cs

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public async Task<ChatCompletion> CompleteAsync(
113113
CompletionId = response.RequestId,
114114
CreatedAt = DateTimeOffset.Now,
115115
ModelId = modelId,
116-
FinishReason = ToFinishReason(response.Output.FinishReason),
116+
FinishReason = ToFinishReason(response.Output.Choices[0].FinishReason),
117117
};
118118

119119
if (response.Usage != null)
@@ -214,59 +214,61 @@ public async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAs
214214
ModelId = completion.ModelId,
215215
};
216216
}
217-
218-
var parameters = ToTextGenerationParameters(options) ?? DefaultTextGenerationParameter;
219-
parameters.IncrementalOutput = true;
220-
var stream = _dashScopeClient.GetTextCompletionStreamAsync(
221-
new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
222-
{
223-
Input = new TextGenerationInput
217+
else
218+
{
219+
var parameters = ToTextGenerationParameters(options) ?? DefaultTextGenerationParameter;
220+
parameters.IncrementalOutput = true;
221+
var stream = _dashScopeClient.GetTextCompletionStreamAsync(
222+
new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
224223
{
225-
Messages = chatMessages.SelectMany(
226-
c => ToTextChatMessages(c, parameters.Tools?.ToList())),
227-
Tools = ToToolDefinitions(options?.Tools)
224+
Input = new TextGenerationInput
225+
{
226+
Messages = chatMessages.SelectMany(
227+
c => ToTextChatMessages(c, parameters.Tools?.ToList())),
228+
Tools = ToToolDefinitions(options?.Tools)
229+
},
230+
Model = modelId,
231+
Parameters = parameters
228232
},
229-
Model = modelId,
230-
Parameters = parameters
231-
},
232-
cancellationToken);
233-
await foreach (var response in stream)
234-
{
235-
streamedRole ??= string.IsNullOrEmpty(response.Output.Choices?.FirstOrDefault()?.Message.Role)
236-
? null
237-
: ToChatRole(response.Output.Choices[0].Message.Role);
238-
finishReason ??= string.IsNullOrEmpty(response.Output.Choices?.FirstOrDefault()?.FinishReason)
239-
? null
240-
: ToFinishReason(response.Output.Choices[0].FinishReason);
241-
completionId ??= response.RequestId;
242-
243-
var update = new StreamingChatCompletionUpdate()
233+
cancellationToken);
234+
await foreach (var response in stream)
244235
{
245-
CompletionId = completionId,
246-
CreatedAt = DateTimeOffset.Now,
247-
FinishReason = finishReason,
248-
ModelId = modelId,
249-
RawRepresentation = response,
250-
Role = streamedRole
251-
};
236+
streamedRole ??= string.IsNullOrEmpty(response.Output.Choices?.FirstOrDefault()?.Message.Role)
237+
? null
238+
: ToChatRole(response.Output.Choices[0].Message.Role);
239+
finishReason ??= string.IsNullOrEmpty(response.Output.Choices?.FirstOrDefault()?.FinishReason)
240+
? null
241+
: ToFinishReason(response.Output.Choices[0].FinishReason);
242+
completionId ??= response.RequestId;
252243

253-
if (response.Output.Choices?.FirstOrDefault()?.Message.Content is { Length: > 0 })
254-
{
255-
update.Contents.Add(new TextContent(response.Output.Choices[0].Message.Content));
256-
}
244+
var update = new StreamingChatCompletionUpdate()
245+
{
246+
CompletionId = completionId,
247+
CreatedAt = DateTimeOffset.Now,
248+
FinishReason = finishReason,
249+
ModelId = modelId,
250+
RawRepresentation = response,
251+
Role = streamedRole
252+
};
253+
254+
if (response.Output.Choices?.FirstOrDefault()?.Message.Content is { Length: > 0 })
255+
{
256+
update.Contents.Add(new TextContent(response.Output.Choices[0].Message.Content));
257+
}
257258

258-
if (response.Usage != null)
259-
{
260-
update.Contents.Add(
261-
new UsageContent(
262-
new UsageDetails()
263-
{
264-
InputTokenCount = response.Usage.InputTokens,
265-
OutputTokenCount = response.Usage.OutputTokens,
266-
}));
267-
}
259+
if (response.Usage != null)
260+
{
261+
update.Contents.Add(
262+
new UsageContent(
263+
new UsageDetails()
264+
{
265+
InputTokenCount = response.Usage.InputTokens,
266+
OutputTokenCount = response.Usage.OutputTokens,
267+
}));
268+
}
268269

269-
yield return update;
270+
yield return update;
271+
}
270272
}
271273
}
272274
}

src/Cnblogs.Extensions.AI.DashScope/DashScopeClientExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ public static IChatClient AsChatClient(this IDashScopeClient dashScopeClient, st
2323
public static IEmbeddingGenerator<string, Embedding<float>> AsEmbeddingGenerator(
2424
this IDashScopeClient dashScopeClient,
2525
string modelId,
26-
int? dimensions)
26+
int? dimensions = null)
2727
=> new DashScopeTextEmbeddingGenerator(dashScopeClient, modelId, dimensions);
2828
}

test/Cnblogs.DashScope.Sdk.UnitTests/ChatClientTests.cs

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public async Task ChatClient_TextCompletion_SuccessAsync()
4444
// Assert
4545
_ = dashScopeClient.Received().GetTextCompletionAsync(
4646
Arg.Is<ModelRequest<TextGenerationInput, ITextGenerationParameters>>(
47-
m => IsEquivalent(m, testCase.RequestModel)),
47+
m => m.IsEquivalent(testCase.RequestModel)),
4848
Arg.Any<CancellationToken>());
4949
response.Message.Text.Should().Be(testCase.ResponseModel.Output.Choices?.First().Message.Content);
5050
}
@@ -53,7 +53,6 @@ public async Task ChatClient_TextCompletion_SuccessAsync()
5353
public async Task ChatClient_TextCompletionStream_SuccessAsync()
5454
{
5555
// Arrange
56-
const bool sse = true;
5756
var testCase = Snapshots.TextGeneration.MessageFormat.SingleMessageChatClientIncremental;
5857
var dashScopeClient = Substitute.For<IDashScopeClient>();
5958
var returnThis = new[] { testCase.ResponseModel }.ToAsyncEnumerable();
@@ -92,7 +91,7 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync()
9291
// Assert
9392
_ = dashScopeClient.Received().GetTextCompletionStreamAsync(
9493
Arg.Is<ModelRequest<TextGenerationInput, ITextGenerationParameters>>(
95-
m => IsEquivalent(m, testCase.RequestModel)),
94+
m => m.IsEquivalent(testCase.RequestModel)),
9695
Arg.Any<CancellationToken>());
9796
text.ToString().Should().Be(testCase.ResponseModel.Output.Choices?.First().Message.Content);
9897
}
@@ -101,7 +100,6 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync()
101100
public async Task ChatClient_ImageRecognition_SuccessAsync()
102101
{
103102
// Arrange
104-
const bool sse = false;
105103
var testCase = Snapshots.MultimodalGeneration.VlChatClientNoSse;
106104
var dashScopeClient = Substitute.For<IDashScopeClient>();
107105
dashScopeClient.Configure()
@@ -136,7 +134,7 @@ public async Task ChatClient_ImageRecognition_SuccessAsync()
136134

137135
// Assert
138136
await dashScopeClient.Received().GetMultimodalGenerationAsync(
139-
Arg.Is<ModelRequest<MultimodalInput, IMultimodalParameters>>(m => IsEquivalent(m, testCase.RequestModel)),
137+
Arg.Is<ModelRequest<MultimodalInput, IMultimodalParameters>>(m => m.IsEquivalent(testCase.RequestModel)),
140138
Arg.Any<CancellationToken>());
141139
response.Choices[0].Text.Should()
142140
.BeEquivalentTo(testCase.ResponseModel.Output.Choices[0].Message.Content[0].Text);
@@ -146,7 +144,6 @@ await dashScopeClient.Received().GetMultimodalGenerationAsync(
146144
public async Task ChatClient_ImageRecognitionStream_SuccessAsync()
147145
{
148146
// Arrange
149-
const bool sse = true;
150147
var testCase = Snapshots.MultimodalGeneration.VlChatClientSse;
151148
var dashScopeClient = Substitute.For<IDashScopeClient>();
152149
dashScopeClient.Configure()
@@ -186,22 +183,8 @@ public async Task ChatClient_ImageRecognitionStream_SuccessAsync()
186183

187184
// Assert
188185
_ = dashScopeClient.Received().GetMultimodalGenerationStreamAsync(
189-
Arg.Is<ModelRequest<MultimodalInput, IMultimodalParameters>>(m => IsEquivalent(m, testCase.RequestModel)),
186+
Arg.Is<ModelRequest<MultimodalInput, IMultimodalParameters>>(m => m.IsEquivalent(testCase.RequestModel)),
190187
Arg.Any<CancellationToken>());
191188
text.ToString().Should().Be(testCase.ResponseModel.Output.Choices.First().Message.Content[0].Text);
192189
}
193-
194-
private bool IsEquivalent<T>(T left, T right)
195-
{
196-
try
197-
{
198-
left.Should().BeEquivalentTo(right);
199-
}
200-
catch (Exception e)
201-
{
202-
return false;
203-
}
204-
205-
return true;
206-
}
207190
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Cnblogs.DashScope.Core;
2+
using Cnblogs.DashScope.Sdk.UnitTests.Utils;
3+
using FluentAssertions;
4+
using Microsoft.Extensions.AI;
5+
using NSubstitute;
6+
using NSubstitute.Extensions;
7+
8+
namespace Cnblogs.DashScope.Sdk.UnitTests;
9+
10+
public class EmbeddingClientTests
11+
{
12+
[Fact]
13+
public async Task EmbeddingClient_Text_SuccessAsync()
14+
{
15+
// Arrange
16+
var testCase = Snapshots.TextEmbedding.EmbeddingClientNoSse;
17+
var dashScopeClient = Substitute.For<IDashScopeClient>();
18+
dashScopeClient.Configure()
19+
.GetEmbeddingsAsync(
20+
Arg.Any<ModelRequest<TextEmbeddingInput, ITextEmbeddingParameters>>(),
21+
Arg.Any<CancellationToken>())
22+
.Returns(Task.FromResult(testCase.ResponseModel));
23+
var client = dashScopeClient.AsEmbeddingGenerator(testCase.RequestModel.Model, 1024);
24+
var content = testCase.RequestModel.Input.Texts.ToList();
25+
var parameter = testCase.RequestModel.Parameters;
26+
27+
// Act
28+
var response = await client.GenerateAsync(
29+
content,
30+
new EmbeddingGenerationOptions()
31+
{
32+
ModelId = testCase.RequestModel.Model, Dimensions = parameter?.Dimension
33+
});
34+
35+
// Assert
36+
_ = dashScopeClient.Received().GetEmbeddingsAsync(
37+
Arg.Is<ModelRequest<TextEmbeddingInput, ITextEmbeddingParameters>>(
38+
m => m.IsEquivalent(testCase.RequestModel)),
39+
Arg.Any<CancellationToken>());
40+
response.Select(x => x.Vector.ToArray()).Should()
41+
.BeEquivalentTo(testCase.ResponseModel.Output.Embeddings.Select(x => x.Embedding));
42+
}
43+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using FluentAssertions;
2+
3+
namespace Cnblogs.DashScope.Sdk.UnitTests.Utils;
4+
5+
public static class EquivalentUtils
6+
{
7+
internal static bool IsEquivalent<T>(this T left, T right)
8+
{
9+
try
10+
{
11+
left.Should().BeEquivalentTo(right);
12+
}
13+
catch (Exception)
14+
{
15+
return false;
16+
}
17+
18+
return true;
19+
}
20+
}

test/Cnblogs.DashScope.Sdk.UnitTests/Utils/Snapshots.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,22 @@ public static class TextEmbedding
12401240
Usage = new TextEmbeddingTokenUsage(3)
12411241
});
12421242

1243+
public static readonly RequestSnapshot<ModelRequest<TextEmbeddingInput, ITextEmbeddingParameters>,
1244+
ModelResponse<TextEmbeddingOutput, TextEmbeddingTokenUsage>> EmbeddingClientNoSse = new(
1245+
"text-embedding",
1246+
new ModelRequest<TextEmbeddingInput, ITextEmbeddingParameters>
1247+
{
1248+
Input = new TextEmbeddingInput { Texts = ["代码改变世界"] },
1249+
Model = "text-embedding-v3",
1250+
Parameters = new TextEmbeddingParameters { Dimension = 1024 }
1251+
},
1252+
new ModelResponse<TextEmbeddingOutput, TextEmbeddingTokenUsage>
1253+
{
1254+
Output = new TextEmbeddingOutput([new TextEmbeddingItem(0, [])]),
1255+
RequestId = "1773f7b2-2148-9f74-b335-b413e398a116",
1256+
Usage = new TextEmbeddingTokenUsage(3)
1257+
});
1258+
12431259
public static readonly
12441260
RequestSnapshot<ModelRequest<BatchGetEmbeddingsInput, IBatchGetEmbeddingsParameters>,
12451261
ModelResponse<BatchGetEmbeddingsOutput, TextEmbeddingTokenUsage>> BatchNoSse = new(

0 commit comments

Comments
 (0)