diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0af6bf5..681349e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,13 +4,12 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] + branches: [ "main", "staging" ] jobs: test-net6: runs-on: ubuntu-latest container: mcr.microsoft.com/dotnet/sdk:6.0 - steps: - name: Checkout uses: actions/checkout@v4 diff --git a/README.md b/README.md index 9ecbb6f..76f2676 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,6 @@ A non-official DashScope (Bailian) service SDK maintained by Cnblogs. ## Quick Start -### Using `Microsoft.Extensions.AI` Interface - -Install NuGet package `Cnblogs.DashScope.AI` -```csharp -var client = new DashScopeClient("your-api-key").AsChatClient("qwen-max"); -var completion = await client.CompleteAsync("hello"); -Console.WriteLine(completion) -``` - ### Console Application Install NuGet package `Cnblogs.DashScope.Sdk` @@ -90,7 +81,93 @@ public class YourService(IDashScopeClient client) } } ``` +### Using `Microsoft.Extensions.AI` Interface + +Install NuGet package `Cnblogs.DashScope.AI` + +```csharp +var client = new DashScopeClient("your-api-key").AsChatClient("qwen-max"); +var completion = await client.GetResponseAsync("hello"); +Console.WriteLine(completion.Text); +``` + +#### Fallback to raw messages + +If you need to use input data or parameters not supported by `Microsoft.Extensions.AI`, you can directly invoke the underlying SDK by passing a raw `TextChatMessage` or `MultimodalMessage` via `RawPresentation`. + +Similarly, to pass unsupported parameters, you can also do so directly by setting the `raw` property within `AdditionalProperties`. + +Example(Using `qwen-doc-turbo`) + +```csharp +var messages = new List() +{ + TextChatMessage.DocUrl( + "从这两份产品手册中,提取所有产品信息,并整理成一个标准的JSON数组。每个对象需要包含:model(产品的型号)、name(产品的名称)、price(价格(去除货币符号和逗号))", + [ + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/jockge/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CA.docx", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/ztwxzr/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CB.docx" + ]) +}; +var parameters = new TextGenerationParameters() +{ + ResultFormat = "message", IncrementalOutput = true, +}; + +var response = client + .AsChatClient("qwen-doc-turbo") + .GetStreamingResponseAsync( + messages.Select(x => new ChatMessage() { RawRepresentation = x }), + new ChatOptions() + { + AdditionalProperties = new AdditionalPropertiesDictionary() { { "raw", parameters } } + }); +await foreach (var chunk in response) +{ + Console.Write(chunk.Text); +} +``` + +Similarly, you can also retrieve the raw message from the `RawPresentation` in the message returned by the model. + +Example (Getting the token count for an image when calling `qwen3-vl-plus`): + +```csharp +var response = client + .AsChatClient("qwen3-vl-plus") + .GetStreamingResponseAsync( + new List() + { + new( + ChatRole.User, + new List() + { + new UriContent( + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", + MediaTypeNames.Image.Jpeg), + new UriContent( + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/tiger.png", + MediaTypeNames.Image.Jpeg), + new TextContent("这些图展现了什么内容?") + }) + }, + new ChatOptions()); +var lastChunk = (ChatResponseUpdate?)null; +await foreach (var chunk in response) +{ + Console.Write(chunk.Text); + lastChunk = chunk; +} + +Console.WriteLine(); + +// Access underlying raw response +var raw = lastChunk?.RawRepresentation as ModelResponse; +Console.WriteLine($"Image token usage: {raw?.Usage?.ImageTokens}"); +``` + ## Supported APIs + - [Text Generation](#text-generation) - QWen3, DeepSeek, etc. Supports reasoning/tool calling/web search/translation scenarios - [Conversation](#conversation) - [Thinking Models](#thinking-models) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index f101473..b1c2b2e 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -12,16 +12,6 @@ ## 快速开始 -### 使用 `Microsoft.Extensions.AI` 接口 - -安装 NuGet 包 `Cnblogs.DashScope.AI` - -```csharp -var client = new DashScopeClient("your-api-key").AsChatClient("qwen-max"); -var completion = await client.CompleteAsync("hello"); -Console.WriteLine(completion) -``` - ### 控制台应用 安装 NuGet 包 `Cnblogs.DashScope.Sdk`。 @@ -88,6 +78,93 @@ public class YourService(IDashScopeClient client) } ``` +### 使用 `Microsoft.Extensions.AI` 接口 + +安装 NuGet 包 `Cnblogs.DashScope.AI` + +```csharp +var client = new DashScopeClient("your-api-key").AsChatClient("qwen-max"); +var completion = await client.GetResponseAsync("hello"); +Console.WriteLine(completion.Text); +``` + +#### 调用原始 SDK + +如果需要使用 `Microsoft.Extensions.AI` 不支持的输入数据或参数,可以通过 `RawPresentation` 直接传入原始的 `TextChatMessage` 或者 `MultimodalMessage` 来直接调用底层 SDK。 + +类似地,当需要传入不支持的参数时,也可以通过设置 `AdditionalProperties` 里的 `raw` 直接传入原始参数。 + +示例(调用 `qwen-doc-turbo`) + +```csharp +var messages = new List() +{ + TextChatMessage.DocUrl( + "从这两份产品手册中,提取所有产品信息,并整理成一个标准的JSON数组。每个对象需要包含:model(产品的型号)、name(产品的名称)、price(价格(去除货币符号和逗号))", + [ + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/jockge/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CA.docx", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/ztwxzr/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CB.docx" + ]) +}; +var parameters = new TextGenerationParameters() +{ + ResultFormat = "message", IncrementalOutput = true, +}; + +var response = client + .AsChatClient("qwen-doc-turbo") + .GetStreamingResponseAsync( + messages.Select(x => new ChatMessage() { RawRepresentation = x }), + new ChatOptions() + { + AdditionalProperties = new AdditionalPropertiesDictionary() { { "raw", parameters } } + }); +await foreach (var chunk in response) +{ + Console.Write(chunk.Text); +} +``` + +类似地,也可以通过模型返回消息里的 `RawPresentation` 获取原始消息。 + +示例(调用 `qwen3-vl-plus` 时获取图像消耗的 Token 数): + +```csharp +var response = client + .AsChatClient("qwen3-vl-plus") + .GetStreamingResponseAsync( + new List() + { + new( + ChatRole.User, + new List() + { + new UriContent( + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", + MediaTypeNames.Image.Jpeg), + new UriContent( + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/tiger.png", + MediaTypeNames.Image.Jpeg), + new TextContent("这些图展现了什么内容?") + }) + }, + new ChatOptions()); +var lastChunk = (ChatResponseUpdate?)null; +await foreach (var chunk in response) +{ + Console.Write(chunk.Text); + lastChunk = chunk; +} + +Console.WriteLine(); + +// 访问原始消息 +var raw = lastChunk?.RawRepresentation as ModelResponse; +Console.WriteLine($"Image token usage: {raw?.Usage?.ImageTokens}"); +``` + + + ## 支持的 API - [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 @@ -97,6 +174,10 @@ public class YourService(IDashScopeClient client) - [工具调用](#工具调用) - [前缀续写](#前缀续写) - [长上下文(Qwen-Long)](#长上下文(Qwen-Long)) + - [翻译能力(Qwen-MT)](#翻译能力(Qwen-MT)) + - [角色扮演(Qwen-Character)](#角色扮演(Qwen-Character)) + - [数据挖掘(Qwen-doc-turbo)](#数据挖掘(Qwen-doc-turbo)) + - [深入研究(Qwen-Deep-Research)](#深入研究(Qwen-Deep-Research)) - [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景 - [视觉理解/推理](#视觉理解/推理) - 图像/视频输入与理解,支持推理模式 - [文字提取](#文字提取) - OCR 任务,读取表格/文档/公式等 @@ -2670,7 +2751,7 @@ var completion = client.GetMultimodalGenerationAsync( 示例: -![倾斜的图像](sample/Cnblogs.DashScope.Sample/tilted.png) +![网页](sample/Cnblogs.DashScope.Sample/webpage.jpg) ```csharp Console.WriteLine("Text:"); diff --git a/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawInputExample.cs b/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawInputExample.cs new file mode 100644 index 0000000..67ee947 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawInputExample.cs @@ -0,0 +1,98 @@ +using Cnblogs.DashScope.Core; +using Microsoft.Extensions.AI; + +namespace Cnblogs.DashScope.Sample.MsExtensionsAI; + +public class RawInputExample : MsExtensionsAiSample +{ + /// + public override string Description => "Chat with raw message and parameter input"; + + /// + public override async Task RunAsync(IDashScopeClient client) + { + var messages = new List() + { + TextChatMessage.DocUrl( + "从这两份产品手册中,提取所有产品信息,并整理成一个标准的JSON数组。每个对象需要包含:model(产品的型号)、name(产品的名称)、price(价格(去除货币符号和逗号))", + [ + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/jockge/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CA.docx", + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251107/ztwxzr/%E7%A4%BA%E4%BE%8B%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8CB.docx" + ]) + }; + var parameters = new TextGenerationParameters() + { + ResultFormat = "message", IncrementalOutput = true, + }; + + var response = client + .AsChatClient("qwen-doc-turbo") + .GetStreamingResponseAsync( + messages.Select(x => new ChatMessage() { RawRepresentation = x }), + new ChatOptions() + { + AdditionalProperties = new AdditionalPropertiesDictionary() { { "raw", parameters } } + }); + await foreach (var chunk in response) + { + Console.Write(chunk.Text); + } + } +} + +/* +```json +[ + { + "model": "PRO-100", + "name": "智能打印机", + "price": "8999" + }, + { + "model": "PRO-200", + "name": "智能扫描仪", + "price": "12999" + }, + { + "model": "PRO-300", + "name": "智能会议系统", + "price": "25999" + }, + { + "model": "PRO-400", + "name": "智能考勤机", + "price": "6999" + }, + { + "model": "PRO-500", + "name": "智能文件柜", + "price": "15999" + }, + { + "model": "SEC-100", + "name": "智能监控摄像头", + "price": "3999" + }, + { + "model": "SEC-200", + "name": "智能门禁系统", + "price": "15999" + }, + { + "model": "SEC-300", + "name": "智能报警系统", + "price": "28999" + }, + { + "model": "SEC-400", + "name": "智能访客系统", + "price": "9999" + }, + { + "model": "SEC-500", + "name": "智能停车管理", + "price": "22999" + } +] +``` + */ diff --git a/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawOutputExample.cs b/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawOutputExample.cs new file mode 100644 index 0000000..56eb013 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/MsExtensionsAI/RawOutputExample.cs @@ -0,0 +1,60 @@ +using System.Net.Mime; +using Cnblogs.DashScope.Core; +using Microsoft.Extensions.AI; + +namespace Cnblogs.DashScope.Sample.MsExtensionsAI; + +public class RawOutputExample : MsExtensionsAiSample +{ + /// + public override string Description => "Chat with extra data from raw output"; + + /// + public override async Task RunAsync(IDashScopeClient client) + { + var response = client + .AsChatClient("qwen3-vl-plus") + .GetStreamingResponseAsync( + new List() + { + new( + ChatRole.User, + new List() + { + new UriContent( + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", + MediaTypeNames.Image.Jpeg), + new UriContent( + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/tiger.png", + MediaTypeNames.Image.Png), + new TextContent("这些图展现了什么内容?") + }) + }, + new ChatOptions()); + var lastChunk = (ChatResponseUpdate?)null; + await foreach (var chunk in response) + { + Console.Write(chunk.Text); + lastChunk = chunk; + } + + Console.WriteLine(); + var raw = lastChunk?.RawRepresentation as ModelResponse; + Console.WriteLine($"Image token usage: {raw?.Usage?.ImageTokens}"); + } +} + +/* +这两张图片展现了截然不同的主题和氛围: + +- **第一张图片**:描绘了一幅温馨、宁静的场景。一位年轻女子坐在沙滩上,与一只金毛犬互动。狗狗抬起前爪,似乎在与女子“击掌”,女子面带微笑,享受着与宠物共度的美好时光。背景是波光粼粼的大海和柔和的日落光线,整个画面充满了温暖、快乐和人与动物之间亲密无间的情感。 + +- **第二张图片**:展现了一只威严的老虎在森林中行走的画面。老虎正视镜头,眼神锐利,姿态充满力量感,仿佛正在巡视自己的领地。周围是茂密的树林、覆盖苔藓的树根和散落的落叶,营造出一种原始、神秘且略带危险的自然野性氛围。 + +总的来说,这两张图分别代表了: +- **人与宠物的温情陪伴**(第一张) +- **野生动物的原始力量与自然之美**(第二张) + +它们形成了鲜明对比:一张是温柔的人类情感,另一张是野性的自然力量。 +Image token usage: 3529 + */ diff --git a/sample/Cnblogs.DashScope.Sample/MsExtensionsAiSample.cs b/sample/Cnblogs.DashScope.Sample/MsExtensionsAiSample.cs new file mode 100644 index 0000000..536b26d --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/MsExtensionsAiSample.cs @@ -0,0 +1,15 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample; + +public abstract class MsExtensionsAiSample : ISample +{ + /// + public string Group => "Microsoft.Extensions.AI"; + + /// + public abstract string Description { get; } + + /// + public abstract Task RunAsync(IDashScopeClient client); +} diff --git a/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj b/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj index 4685740..9aa095e 100644 --- a/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj +++ b/src/Cnblogs.DashScope.AI/Cnblogs.DashScope.AI.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs index 9cef10f..89d0047 100644 --- a/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs +++ b/src/Cnblogs.DashScope.AI/DashScopeChatClient.cs @@ -15,6 +15,7 @@ public sealed class DashScopeChatClient : IChatClient { private readonly IDashScopeClient _dashScopeClient; private readonly string _modelId; + private readonly bool _useVl; private static readonly JsonSchema EmptyObjectSchema = JsonSchema.FromText("{\"type\":\"object\",\"required\":[],\"properties\":{}}"); @@ -34,6 +35,10 @@ public DashScopeChatClient(IDashScopeClient dashScopeClient, string modelId) _dashScopeClient = dashScopeClient; _modelId = modelId; + _useVl = modelId.StartsWith("qwen-vl") + || modelId.StartsWith("qwen3-vl") + || modelId.StartsWith("qwen3-omni") + || modelId.StartsWith("gui-plus"); } /// @@ -50,7 +55,7 @@ public async Task GetResponseAsync( var modelId = options?.ModelId ?? _modelId; var useVlRaw = options?.AdditionalProperties?.GetValueOrDefault("useVl")?.ToString(); var useVl = string.IsNullOrEmpty(useVlRaw) - ? modelId.Contains("qwen-vl", StringComparison.OrdinalIgnoreCase) + ? _useVl : string.Equals(useVlRaw, "true", StringComparison.OrdinalIgnoreCase); if (useVl) { @@ -96,8 +101,9 @@ public async Task GetResponseAsync( { Input = new TextGenerationInput { - Messages = chatMessages.SelectMany( - c => ToTextChatMessages(c, parameters.Tools?.ToList())), + Messages = chatMessages.SelectMany(c => ToTextChatMessages( + c, + parameters.Tools?.ToList())), Tools = ToToolDefinitions(options?.Tools) }, Model = modelId, @@ -135,8 +141,9 @@ public async IAsyncEnumerable GetStreamingResponseAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { var useVlRaw = options?.AdditionalProperties?.GetValueOrDefault("useVl")?.ToString(); - var useVl = string.Equals(useVlRaw, "true", StringComparison.OrdinalIgnoreCase) - || (options?.ModelId?.Contains("qwen-vl") ?? false); + var useVl = string.IsNullOrEmpty(useVlRaw) + ? _useVl + : string.Equals(useVlRaw, "true", StringComparison.OrdinalIgnoreCase); var modelId = options?.ModelId ?? _modelId; ChatRole? streamedRole = null; @@ -220,8 +227,9 @@ public async IAsyncEnumerable GetStreamingResponseAsync( { Input = new TextGenerationInput { - Messages = chatMessages.SelectMany( - c => ToTextChatMessages(c, parameters.Tools?.ToList())), + Messages = chatMessages.SelectMany(c => ToTextChatMessages( + c, + parameters.Tools?.ToList())), Tools = ToToolDefinitions(options?.Tools) }, Model = modelId, @@ -261,6 +269,7 @@ public async IAsyncEnumerable GetStreamingResponseAsync( { InputTokenCount = response.Usage.InputTokens, OutputTokenCount = response.Usage.OutputTokens, + TotalTokenCount = response.Usage.TotalTokens, })); } @@ -292,8 +301,9 @@ public void Dispose() : finishReason switch { "stop" => ChatFinishReason.Stop, - "length" => ChatFinishReason.ContentFilter, + "length" => ChatFinishReason.Length, "tool_calls" => ChatFinishReason.ToolCalls, + "null" => null, _ => new ChatFinishReason(finishReason), }; @@ -311,18 +321,17 @@ private static ChatMessage ToChatMessage(TextChatMessage message) if (message.ToolCalls is { Count: > 0 }) { - message.ToolCalls.ForEach( - call => - { - var arguments = string.IsNullOrEmpty(call.Function.Arguments) - ? null - : JsonSerializer.Deserialize>(call.Function.Arguments); - returnMessage.Contents.Add( - new FunctionCallContent( - call.Id ?? string.Empty, - call.Function.Name, - arguments) { RawRepresentation = call }); - }); + message.ToolCalls.ForEach(call => + { + var arguments = string.IsNullOrEmpty(call.Function.Arguments) + ? null + : JsonSerializer.Deserialize>(call.Function.Arguments); + returnMessage.Contents.Add( + new FunctionCallContent( + call.Id ?? string.Empty, + call.Function.Name, + arguments) { RawRepresentation = call }); + }); } return returnMessage; @@ -340,19 +349,26 @@ private static ChatRole ToChatRole(string role) private MultimodalParameters ToMultimodalParameters(ChatOptions? options) { - var parameters = new MultimodalParameters(); if (options is null) { - return parameters; + return new MultimodalParameters(); } - parameters.Temperature = options.Temperature; - parameters.MaxTokens = options.MaxOutputTokens; - parameters.TopP = options.TopP; - parameters.TopK = options.TopK; - parameters.RepetitionPenalty = options.FrequencyPenalty; - parameters.PresencePenalty = options.PresencePenalty; - parameters.Seed = (ulong?)options.Seed; + if (options.AdditionalProperties?.GetValueOrDefault("raw") is MultimodalParameters raw) + { + return raw; + } + + var parameters = new MultimodalParameters + { + Temperature = options.Temperature, + MaxTokens = options.MaxOutputTokens, + TopP = options.TopP, + TopK = options.TopK, + RepetitionPenalty = options.FrequencyPenalty, + PresencePenalty = options.PresencePenalty, + Seed = (ulong?)options.Seed + }; if (options.StopSequences is { Count: > 0 }) { parameters.Stop = new TextGenerationStop(options.StopSequences); @@ -391,6 +407,7 @@ private List ToMultimodalMessageContents(IList raw, TextContent text => MultimodalMessageContent.TextContent(text.Text), DataContent { Data.Length: > 0 } data when data.HasTopLevelMediaType("image") => MultimodalMessageContent.ImageContent( @@ -398,6 +415,10 @@ private List ToMultimodalMessageContents(IList MultimodalMessageContent.ImageContent(uri), + DataContent { Uri: { } uri } data when data.HasTopLevelMediaType("video") => MultimodalMessageContent + .VideoContent(uri), + UriContent uri when uri.HasTopLevelMediaType("image") => MultimodalMessageContent.ImageContent( + uri.Uri.AbsoluteUri), _ => null }; if (content is not null) @@ -420,6 +441,11 @@ private IEnumerable ToTextChatMessages( { if (from.Role == ChatRole.System || from.Role == ChatRole.User) { + if (from.RawRepresentation is TextChatMessage text) + { + yield return text; + } + yield return new TextChatMessage( from.Role.Value, from.Text, @@ -452,12 +478,11 @@ private IEnumerable ToTextChatMessages( { var functionCall = from.Contents .OfType() - .Select( - c => new ToolCall( - c.CallId, - "function", - tools?.FindIndex(f => f.Function?.Name == c.Name) ?? -1, - new FunctionCall(c.Name, JsonSerializer.Serialize(c.Arguments, ToolCallJsonSerializerOptions)))) + .Select(c => new ToolCall( + c.CallId, + "function", + tools?.FindIndex(f => f.Function?.Name == c.Name) ?? -1, + new FunctionCall(c.Name, JsonSerializer.Serialize(c.Arguments, ToolCallJsonSerializerOptions)))) .ToList(); // function all array must be null when empty @@ -479,6 +504,11 @@ private IEnumerable ToTextChatMessages( return null; } + if (options.AdditionalProperties?.GetValueOrDefault("raw") is TextGenerationParameters parameters) + { + return parameters; + } + var format = "message"; if (options.ResponseFormat is ChatResponseFormatJson) { @@ -510,13 +540,12 @@ RequiredChatToolMode required when string.IsNullOrEmpty(required.RequiredFunctio private static IEnumerable? ToToolDefinitions(IList? tools) { - return tools?.OfType().Select( - f => new ToolDefinition( - "function", - new FunctionDefinition( - f.Name, - f.Description, - GetParameterSchema(f.JsonSchema)))); + return tools?.OfType().Select(f => new ToolDefinition( + "function", + new FunctionDefinition( + f.Name, + f.Description, + GetParameterSchema(f.JsonSchema)))); } private static JsonSchema GetParameterSchema(JsonElement metadata) diff --git a/src/Cnblogs.DashScope.Core/Internals/TextChatMessageContentConvertor.cs b/src/Cnblogs.DashScope.Core/Internals/TextChatMessageContentConvertor.cs index c0fa679..11ad871 100644 --- a/src/Cnblogs.DashScope.Core/Internals/TextChatMessageContentConvertor.cs +++ b/src/Cnblogs.DashScope.Core/Internals/TextChatMessageContentConvertor.cs @@ -28,8 +28,11 @@ internal class TextChatMessageContentConvertor : JsonConverter string.IsNullOrEmpty(x.Text) == false)?.Text ?? throw new JsonException("No text found in content array"); - var docUrls = contents.FirstOrDefault(x => x.DocUrl != null)?.DocUrl; - return new TextChatMessageContent(text, docUrls); + var docUrlContent = contents.FirstOrDefault(x => x is { DocUrl: not null, FileParsingStrategy: not null }) + ?? throw new JsonException("No doc_url and file_parsing_strategy were found"); + var docUrls = docUrlContent?.DocUrl; + var strategy = docUrlContent?.FileParsingStrategy; + return new TextChatMessageContent(text, docUrls, strategy); } throw new JsonException("Unknown type for TextChatMessageContent"); @@ -49,7 +52,7 @@ public override void Write(Utf8JsonWriter writer, TextChatMessageContent value, { Type = "doc_url", DocUrl = value.DocUrls.ToList(), - FileParsingStrategy = "auto" + FileParsingStrategy = value.FileParsingStrategy } }, options); diff --git a/src/Cnblogs.DashScope.Core/TextChatMessage.cs b/src/Cnblogs.DashScope.Core/TextChatMessage.cs index 1ed4d25..9ef595d 100644 --- a/src/Cnblogs.DashScope.Core/TextChatMessage.cs +++ b/src/Cnblogs.DashScope.Core/TextChatMessage.cs @@ -110,10 +110,16 @@ public static TextChatMessage File(IEnumerable fileIds) /// /// Text input. /// The doc urls. + /// Can be one of ['auto', 'text_only', 'text_and_images']. /// - public static TextChatMessage DocUrl(string prompt, IEnumerable docUrls) + public static TextChatMessage DocUrl( + string prompt, + IEnumerable docUrls, + string fileParsingStrategy = "auto") { - return new TextChatMessage(DashScopeRoleNames.User, new TextChatMessageContent(prompt, docUrls)); + return new TextChatMessage( + DashScopeRoleNames.User, + new TextChatMessageContent(prompt, docUrls, fileParsingStrategy)); } /// diff --git a/src/Cnblogs.DashScope.Core/TextChatMessageContent.cs b/src/Cnblogs.DashScope.Core/TextChatMessageContent.cs index 0121861..9d00c48 100644 --- a/src/Cnblogs.DashScope.Core/TextChatMessageContent.cs +++ b/src/Cnblogs.DashScope.Core/TextChatMessageContent.cs @@ -19,6 +19,11 @@ public class TextChatMessageContent /// public IEnumerable? DocUrls { get; } + /// + /// Control the content that model can read. Can be one of ['auto', 'text_only', 'text_and_images']. + /// + public string? FileParsingStrategy { get; } + /// /// Creates a with text content. /// @@ -27,6 +32,7 @@ public TextChatMessageContent(string text) { Text = text; DocUrls = null; + FileParsingStrategy = null; } /// @@ -34,10 +40,12 @@ public TextChatMessageContent(string text) /// /// The text content. /// The doc urls. - public TextChatMessageContent(string text, IEnumerable? docUrls) + /// Can be one of ['auto', 'text_only', 'text_and_images']. + public TextChatMessageContent(string text, IEnumerable? docUrls, string? fileParsingStrategy) { Text = text; DocUrls = docUrls; + FileParsingStrategy = fileParsingStrategy; } /// diff --git a/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs index 8bd80fa..7d8307a 100644 --- a/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs +++ b/test/Cnblogs.DashScope.AI.UnitTests/ChatClientTests.cs @@ -1,7 +1,6 @@ using System.Text; using Cnblogs.DashScope.Core; using Cnblogs.DashScope.Tests.Shared.Utils; - using Microsoft.Extensions.AI; using NSubstitute; using NSubstitute.Extensions; @@ -10,6 +9,43 @@ namespace Cnblogs.DashScope.AI.UnitTests; public class ChatClientTests { + [Fact] + public async Task ChatClient_TextCompletionRaw_SuccessAsync() + { + // Arrange + var testCase = Snapshots.TextGeneration.MessageFormat.ConversationMessageWithDocUrlsIncremental; + var dashScopeClient = Substitute.For(); + dashScopeClient.Configure() + .GetTextCompletionStreamAsync( + Arg.Any>(), + Arg.Any()) + .Returns(AsyncEnumerable.Repeat(testCase.ResponseModel, 1)); + var client = dashScopeClient.AsChatClient(testCase.RequestModel.Model); + + // Act + var response = client.GetStreamingResponseAsync( + testCase.RequestModel.Input.Messages!.Select(m => new ChatMessage { RawRepresentation = m }), + new ChatOptions + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + { "raw", testCase.RequestModel.Parameters } + } + }); + var responseText = new StringBuilder(); + await foreach (var chatResponseUpdate in response) + { + responseText.Append(chatResponseUpdate.Text); + } + + // Assert + _ = dashScopeClient.Received().GetTextCompletionStreamAsync( + Arg.Is>(m + => m.IsEquivalent(testCase.RequestModel)), + Arg.Any()); + Assert.Equal(testCase.ResponseModel.Output.Choices![0].Message.Content, responseText.ToString()); + } + [Fact] public async Task ChatClient_TextCompletion_SuccessAsync() { @@ -43,8 +79,8 @@ public async Task ChatClient_TextCompletion_SuccessAsync() // Assert _ = dashScopeClient.Received().GetTextCompletionAsync( - Arg.Is>( - m => m.IsEquivalent(testCase.RequestModel)), + Arg.Is>(m + => m.IsEquivalent(testCase.RequestModel)), Arg.Any()); Assert.Equal(testCase.ResponseModel.Output.Choices![0].Message.Content, response.Messages[0].Text); } @@ -90,8 +126,8 @@ public async Task ChatClient_TextCompletionStream_SuccessAsync() // Assert _ = dashScopeClient.Received().GetTextCompletionStreamAsync( - Arg.Is>( - m => m.IsEquivalent(testCase.RequestModel)), + Arg.Is>(m + => m.IsEquivalent(testCase.RequestModel)), Arg.Any()); Assert.Equal(testCase.ResponseModel.Output.Choices![0].Message.Content, text.ToString()); } diff --git a/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj b/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj index 514c2c0..82cd87a 100644 --- a/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj +++ b/test/Cnblogs.DashScope.AI.UnitTests/Cnblogs.DashScope.AI.UnitTests.csproj @@ -1,31 +1,32 @@  - - net8.0 - enable - enable - false - + + net8.0 + enable + enable + false + true + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + + + - - - - + + + + diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextChatMessageContentJsonConvertorTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextChatMessageContentJsonConvertorTests.cs index 782bba6..fa4a99f 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextChatMessageContentJsonConvertorTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextChatMessageContentJsonConvertorTests.cs @@ -28,7 +28,7 @@ public void Serialize_Text_StringAsync() public void Serialize_DocUrl_ObjectAsync() { // Arrange - var content = new TextChatMessageContent("some content", new[] { "url1" }); + var content = new TextChatMessageContent("some content", new[] { "url1" }, "auto"); // Act var json = JsonSerializer.Serialize(content, DashScopeDefaults.SerializationOptions);