Skip to content

Commit 2d352b9

Browse files
committed
feat: wip
1 parent d96eadd commit 2d352b9

File tree

3 files changed

+317
-0
lines changed

3 files changed

+317
-0
lines changed

src/Cnblogs.DashScope.Sdk/Cnblogs.DashScope.Sdk.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77
<ItemGroup>
88
<PackageReference Include="JsonSchema.Net.Generation" Version="4.5.1" />
9+
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.1-preview.1.24570.5" />
910
</ItemGroup>
1011
<ItemGroup>
1112
<ProjectReference Include="..\Cnblogs.DashScope.Core\Cnblogs.DashScope.Core.csproj" />
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
using System.Text.Json;
2+
using Cnblogs.DashScope.Core;
3+
using Json.Schema;
4+
using Json.Schema.Generation;
5+
using Microsoft.Extensions.AI;
6+
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
7+
8+
namespace Cnblogs.DashScope.Sdk;
9+
10+
/// <summary>
11+
/// <see cref="IChatClient" /> implemented with DashScope.
12+
/// </summary>
13+
public sealed class DashScopeChatClient : IChatClient
14+
{
15+
private readonly IDashScopeClient _dashScopeClient;
16+
private readonly string _modelId;
17+
18+
private static readonly JsonSchema EmptyObjectSchema =
19+
JsonSchema.FromText("""{"type":"object","required":[],"properties":{}}""");
20+
21+
private static readonly TextGenerationParameters DefaultTextGenerationParameter = new() { ResultFormat = "message" };
22+
23+
/// <summary>
24+
/// Initialize a new instance of the
25+
/// </summary>
26+
/// <param name="dashScopeClient"></param>
27+
/// <param name="modelId"></param>
28+
public DashScopeChatClient(IDashScopeClient dashScopeClient, string modelId)
29+
{
30+
ArgumentNullException.ThrowIfNull(dashScopeClient, nameof(dashScopeClient));
31+
ArgumentNullException.ThrowIfNull(modelId, nameof(modelId));
32+
33+
_dashScopeClient = dashScopeClient;
34+
_modelId = modelId;
35+
Metadata = new ChatClientMetadata("dashscope", _dashScopeClient.BaseAddress, _modelId);
36+
}
37+
38+
/// <summary>
39+
/// Gets or sets <see cref="JsonSerializerOptions"/> to use for any serialization activities related to tool call arguments and results.
40+
/// </summary>
41+
public JsonSerializerOptions ToolCallJsonSerializerOptions { get; set; } = new(JsonSerializerDefaults.Web);
42+
43+
/// <inheritdoc />
44+
public async Task<ChatCompletion> CompleteAsync(
45+
IList<ChatMessage> chatMessages,
46+
ChatOptions? options = null,
47+
CancellationToken cancellationToken = default)
48+
{
49+
var useVl = options?.AdditionalProperties?.GetValueOrDefault("useVl") ?? false;
50+
if (useVl)
51+
{
52+
var response = await _dashScopeClient.GetMultimodalGenerationAsync(
53+
new ModelRequest<MultimodalInput, IMultimodalParameters>()
54+
{
55+
Input = new MultimodalInput() { Messages = }
56+
})
57+
}
58+
else
59+
{
60+
var parameters = options
61+
var response = await _dashScopeClient.GetTextCompletionAsync(
62+
new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
63+
{
64+
Input = new TextGenerationInput()
65+
{
66+
Messages = chatMessages.SelectMany(ToTextChatMessages),
67+
Tools = ToToolDefinitions(options?.Tools)
68+
},
69+
Model = _modelId,
70+
Parameters =
71+
})
72+
}
73+
}
74+
75+
/// <inheritdoc />
76+
public IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
77+
IList<ChatMessage> chatMessages,
78+
ChatOptions? options = null,
79+
CancellationToken cancellationToken = default)
80+
{
81+
throw new NotImplementedException();
82+
}
83+
84+
/// <inheritdoc />
85+
public object? GetService(Type serviceType, object? serviceKey = null)
86+
{
87+
throw new NotImplementedException();
88+
}
89+
90+
/// <inheritdoc />
91+
public void Dispose()
92+
{
93+
// nothing to dispose.
94+
}
95+
96+
/// <inheritdoc />
97+
public ChatClientMetadata Metadata { get; }
98+
99+
private IEnumerable<Cnblogs.DashScope.Core.ChatMessage> ToTextChatMessages(ChatMessage from)
100+
{
101+
if (from.Role == ChatRole.System || from.Role == ChatRole.User)
102+
{
103+
yield return new Core.ChatMessage(from.Role.Value, from.Text ?? string.Empty, from.AuthorName);
104+
}
105+
else if (from.Role == ChatRole.Tool)
106+
{
107+
foreach (var content in from.Contents)
108+
{
109+
if (content is FunctionResultContent resultContent)
110+
{
111+
var result = resultContent.Result as string;
112+
if (result is null && resultContent.Result is not null)
113+
{
114+
try
115+
{
116+
result = JsonSerializer.Serialize(resultContent.Result, ToolCallJsonSerializerOptions);
117+
}
118+
catch (NotSupportedException)
119+
{
120+
// If the type can't be serialized, skip it.
121+
}
122+
}
123+
124+
yield return new Core.ChatMessage(from.Role.Value, result ?? string.Empty);
125+
}
126+
}
127+
}
128+
else if (from.Role == ChatRole.Assistant)
129+
{
130+
var functionCall = from.Contents
131+
.OfType<FunctionCallContent>()
132+
.Select(
133+
c => new ToolCall(
134+
c.CallId,
135+
"function",
136+
new FunctionCall(c.Name, JsonSerializer.Serialize(c.Arguments, ToolCallJsonSerializerOptions))))
137+
.ToList();
138+
yield return new Core.ChatMessage(
139+
from.Role.Value,
140+
from.Text ?? string.Empty,
141+
from.AuthorName,
142+
functionCall);
143+
}
144+
}
145+
146+
private static TextGenerationParameters? ToTextGenerationParameters(ChatOptions? options)
147+
{
148+
if (options is null)
149+
{
150+
return null;
151+
}
152+
153+
var format = "message";
154+
if (options.ResponseFormat is ChatResponseFormatJson)
155+
{
156+
format = "json_object";
157+
}
158+
159+
160+
var parameter = new TextGenerationParameters()
161+
{
162+
ResultFormat = format,
163+
Temperature = options.Temperature,
164+
MaxTokens = options.MaxOutputTokens,
165+
TopP = options.TopP,
166+
TopK = options.TopK,
167+
RepetitionPenalty = options.FrequencyPenalty,
168+
Seed = options.Seed == null ? null : (ulong)options.Seed.Value,
169+
Stop = options.StopSequences == null ? null : new TextGenerationStop(options.StopSequences),
170+
Tools = options.Tools == null ? null : ToToolDefinitions(options.Tools),
171+
ToolChoice = options.ToolMode
172+
};
173+
}
174+
175+
private static IEnumerable<ToolDefinition>? ToToolDefinitions(IList<AITool>? tools)
176+
{
177+
return tools?.OfType<AIFunction>().Select(
178+
f => new ToolDefinition(
179+
"function",
180+
new FunctionDefinition(
181+
f.Metadata.Name,
182+
f.Metadata.Description,
183+
GetParameterSchema(f.Metadata.Parameters))));
184+
}
185+
186+
private static JsonSchema GetParameterSchema(IEnumerable<AIFunctionParameterMetadata> metadata)
187+
{
188+
return new JsonSchemaBuilder()
189+
.Properties(metadata.Select(c => (c.Name, Schema: c.Schema as JsonSchema ?? EmptyObjectSchema)).ToArray())
190+
.Build();
191+
}
192+
193+
private static List<ChatMessageContentPart> GetContentParts(IList<AIContent> contents)
194+
{
195+
List<ChatMessageContentPart> parts = [];
196+
foreach (var content in contents)
197+
{
198+
switch (content)
199+
{
200+
case TextContent textContent:
201+
parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text));
202+
break;
203+
204+
case ImageContent imageContent when imageContent.Data is { IsEmpty: false } data:
205+
parts.Add(
206+
ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(data), imageContent.MediaType));
207+
break;
208+
209+
case ImageContent imageContent when imageContent.Uri is string uri:
210+
parts.Add(ChatMessageContentPart.CreateImagePart(new Uri(uri)));
211+
break;
212+
}
213+
}
214+
215+
if (parts.Count == 0)
216+
{
217+
parts.Add(ChatMessageContentPart.CreateTextPart(string.Empty));
218+
}
219+
220+
return parts;
221+
}
222+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Cnblogs.DashScope.Core;
3+
using Cnblogs.DashScope.Sdk.TextEmbedding;
4+
using Microsoft.Extensions.AI;
5+
6+
namespace Cnblogs.DashScope.Sdk;
7+
8+
/// <summary>
9+
/// An <see cref="IEmbeddingGenerator{TInput,TEmbedding}"/> for a DashScope client.
10+
/// </summary>
11+
public sealed class DashScopeTextEmbeddingGenerator
12+
: IEmbeddingGenerator<string, Embedding<float>>
13+
{
14+
private readonly IDashScopeClient _dashScopeClient;
15+
private readonly string _modelId;
16+
private readonly TextEmbeddingParameters _parameters;
17+
18+
/// <summary>
19+
/// Initialize a new instance of the <see cref="DashScopeTextEmbeddingGenerator"/> class.
20+
/// </summary>
21+
/// <param name="dashScopeClient">The underlying client.</param>
22+
/// <param name="modelId">The model name used to generate embedding.</param>
23+
/// <param name="dimensions">The number of dimensions produced by the generator.</param>
24+
public DashScopeTextEmbeddingGenerator(IDashScopeClient dashScopeClient, string modelId, int? dimensions = null)
25+
{
26+
ArgumentNullException.ThrowIfNull(dashScopeClient, nameof(dashScopeClient));
27+
ArgumentNullException.ThrowIfNull(modelId, nameof(modelId));
28+
29+
_dashScopeClient = dashScopeClient;
30+
_modelId = modelId;
31+
_parameters = new TextEmbeddingParameters { Dimension = dimensions };
32+
Metadata = new EmbeddingGeneratorMetadata("dashscope", _dashScopeClient.BaseAddress, modelId, dimensions);
33+
}
34+
35+
/// <inheritdoc />
36+
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
37+
IEnumerable<string> values,
38+
EmbeddingGenerationOptions? options = null,
39+
CancellationToken cancellationToken = default)
40+
{
41+
var parameters = ToParameters(options) ?? _parameters;
42+
var rawResponse =
43+
await _dashScopeClient.GetTextEmbeddingsAsync(_modelId, values, parameters, cancellationToken);
44+
var embeddings = rawResponse.Output.Embeddings.Select(
45+
e => new Embedding<float>(e.Embedding) { ModelId = _modelId, CreatedAt = DateTimeOffset.Now });
46+
var rawUsage = rawResponse.Usage;
47+
var usage = rawUsage != null
48+
? new UsageDetails() { InputTokenCount = rawUsage.TotalTokens, TotalTokenCount = rawUsage.TotalTokens }
49+
: null;
50+
return new GeneratedEmbeddings<Embedding<float>>(embeddings)
51+
{
52+
Usage = usage,
53+
AdditionalProperties =
54+
new AdditionalPropertiesDictionary { { nameof(rawResponse.RequestId), rawResponse.RequestId } }
55+
};
56+
}
57+
58+
/// <inheritdoc />
59+
public object? GetService(Type serviceType, object? serviceKey = null)
60+
{
61+
return
62+
serviceKey is not null ? null :
63+
serviceType == typeof(IDashScopeClient) ? _dashScopeClient :
64+
serviceType.IsInstanceOfType(this) ? this :
65+
null;
66+
}
67+
68+
/// <inheritdoc />
69+
public void Dispose()
70+
{
71+
// Nothing to dispose. Implementation required for the IEmbeddingGenerator interface.
72+
}
73+
74+
[return: NotNullIfNotNull(nameof(options))]
75+
private static TextEmbeddingParameters? ToParameters(EmbeddingGenerationOptions? options)
76+
{
77+
if (options is null)
78+
{
79+
return null;
80+
}
81+
82+
return new TextEmbeddingParameters
83+
{
84+
Dimension = options.Dimensions,
85+
OutputType =
86+
options.AdditionalProperties?.GetValueOrDefault(nameof(TextEmbeddingParameters.OutputType)) as string,
87+
TextType =
88+
options.AdditionalProperties?.GetValueOrDefault(nameof(TextEmbeddingParameters.TextType)) as string,
89+
};
90+
}
91+
92+
/// <inheritdoc />
93+
public EmbeddingGeneratorMetadata Metadata { get; }
94+
}

0 commit comments

Comments
 (0)