Skip to content

feat: support qwen long #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class YourService(IDashScopeClient client)
- Image Synthesis - `CreateWanxImageSynthesisTaskAsync()` and `GetWanxImageSynthesisTaskAsync()`
- Image Generation - `CreateWanxImageGenerationTaskAsync()` and `GetWanxImageGenerationTaskAsync()`
- Background Image Generation - `CreateWanxBackgroundGenerationTaskAsync()` and `GetWanxBackgroundGenerationTaskAsync()`

- File API that used by Qwen-Long - `dashScopeClient.UploadFileAsync()` and `dashScopeClient.DeleteFileAsync`

# Examples

Expand Down Expand Up @@ -163,3 +163,35 @@ Console.WriteLine(completion.Output.Choice[0].Message.Content);
```

Append the tool calling result with `tool` role, then model will generate answers based on tool calling result.


## QWen-Long with files

Upload file first.

```csharp
var file = new FileInfo("test.txt");
var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name);
```

Using uploaded file id in messages.

```csharp
var history = new List<ChatMessage>
{
new(uploadedFile.Id), // use array for multiple files, e.g. [file1.Id, file2.Id]
new("user", "Summarize the content of file.")
}
var parameters = new TextGenerationParameters()
{
ResultFormat = ResultFormats.Message
};
var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history, parameters);
Console.WriteLine(completion.Output.Choices[0].Message.Content);
```

Delete file if needed

```csharp
var deletionResult = await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
```
32 changes: 32 additions & 0 deletions README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class YourService(IDashScopeClient client)
- 文生图 - `CreateWanxImageSynthesisTaskAsync()` and `GetWanxImageSynthesisTaskAsync()`
- 人像风格重绘 - `CreateWanxImageGenerationTaskAsync()` and `GetWanxImageGenerationTaskAsync()`
- 图像背景生成 - `CreateWanxBackgroundGenerationTaskAsync()` and `GetWanxBackgroundGenerationTaskAsync()`
- 适用于 QWen-Long 的文件 API `dashScopeClient.UploadFileAsync()` and `dashScopeClient.DeleteFileAsync`


# 示例
Expand Down Expand Up @@ -159,3 +160,34 @@ Console.WriteLine(completion.Output.Choice[0].Message.Content) // 现在浙江
```

当模型认为应当调用工具时,返回消息中 `ToolCalls` 会提供调用的详情,本地在调用完成后可以把结果以 `tool` 角色返回。

## 上传文件(QWen-Long)

需要先提前将文件上传到 DashScope 来获得 Id。

```csharp
var file = new FileInfo("test.txt");
var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name);
```

使用文件 Id 初始化一个消息,内部会转换成 system 角色的一个文件引用。

```csharp
var history = new List<ChatMessage>
{
new(uploadedFile.Id), // 多文件情况下可以直接传入文件 Id 数组, 例如:[file1.Id, file2.Id]
new("user", "总结一下文件的内容。")
}
var parameters = new TextGenerationParameters()
{
ResultFormat = ResultFormats.Message
};
var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history, parameters);
Console.WriteLine(completion.Output.Choices[0].Message.Content);
```

如果需要,完成对话后可以使用 API 删除之前上传的文件。

```csharp
var deletionResult = await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
```
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@
<ProjectReference Include="..\..\src\Cnblogs.DashScope.Sdk\Cnblogs.DashScope.Sdk.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="test.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
46 changes: 46 additions & 0 deletions sample/Cnblogs.DashScope.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
case SampleType.ChatCompletionWithTool:
await ChatWithToolsAsync();
break;
case SampleType.ChatCompletionWithFiles:
await ChatWithFilesAsync();
break;
}

return;
Expand Down Expand Up @@ -97,6 +100,49 @@ async Task ChatStreamAsync()
// ReSharper disable once FunctionNeverReturns
}

async Task ChatWithFilesAsync()
{
var history = new List<ChatMessage>();
Console.WriteLine("uploading file \"test.txt\" ");
var file = new FileInfo("test.txt");
var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name);
Console.WriteLine("file uploaded, id: " + uploadedFile.Id);
Console.WriteLine();

var fileMessage = new ChatMessage(uploadedFile.Id);
history.Add(fileMessage);
Console.WriteLine("system > " + fileMessage.Content);
var userPrompt = new ChatMessage("user", "该文件的内容是什么");
history.Add(userPrompt);
Console.WriteLine("user > " + userPrompt.Content);
var stream = dashScopeClient.GetQWenChatStreamAsync(
QWenLlm.QWenLong,
history,
new TextGenerationParameters { IncrementalOutput = true, ResultFormat = ResultFormats.Message });
var role = string.Empty;
var message = new StringBuilder();
await foreach (var modelResponse in stream)
{
var chunk = modelResponse.Output.Choices![0];
if (string.IsNullOrEmpty(role) && string.IsNullOrEmpty(chunk.Message.Role) == false)
{
role = chunk.Message.Role;
Console.Write(chunk.Message.Role + " > ");
}

message.Append(chunk.Message.Content);
Console.Write(chunk.Message.Content);
}

Console.WriteLine();
history.Add(new ChatMessage(role, message.ToString()));

Console.WriteLine();
Console.WriteLine("Deleting file by id: " + uploadedFile.Id);
var result = await dashScopeClient.DeleteFileAsync(uploadedFile.Id);
Console.WriteLine("Deletion result: " + result.Deleted);
}

async Task ChatWithToolsAsync()
{
var history = new List<ChatMessage>();
Expand Down
5 changes: 4 additions & 1 deletion sample/Cnblogs.DashScope.Sample/SampleType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ public enum SampleType
ChatCompletion,

[Description("Conversation with tools")]
ChatCompletionWithTool
ChatCompletionWithTool,

[Description("Conversation with files")]
ChatCompletionWithFiles
}
1 change: 1 addition & 0 deletions sample/Cnblogs.DashScope.Sample/SampleTypeDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static string GetDescription(this SampleType sampleType)
SampleType.TextCompletionSse => "Simple prompt completion with incremental output",
SampleType.ChatCompletion => "Conversation between user and assistant",
SampleType.ChatCompletionWithTool => "Function call sample",
SampleType.ChatCompletionWithFiles => "File upload sample using qwen-long",
_ => throw new ArgumentOutOfRangeException(nameof(sampleType), sampleType, "Unsupported sample option")
};
}
Expand Down
1 change: 1 addition & 0 deletions sample/Cnblogs.DashScope.Sample/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
测试内容。
29 changes: 27 additions & 2 deletions src/Cnblogs.DashScope.Core/ChatMessage.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Cnblogs.DashScope.Core.Internals;
using System.Text.Json.Serialization;
using Cnblogs.DashScope.Core.Internals;

namespace Cnblogs.DashScope.Core;

Expand All @@ -9,4 +10,28 @@ namespace Cnblogs.DashScope.Core;
/// <param name="Content">The content of this message.</param>
/// <param name="Name">Used when role is tool, represents the function name of this message generated by.</param>
/// <param name="ToolCalls">Calls to the function.</param>
public record ChatMessage(string Role, string Content, string? Name = null, List<ToolCall>? ToolCalls = null) : IMessage<string>;
[method: JsonConstructor]
public record ChatMessage(
string Role,
string Content,
string? Name = null,
List<ToolCall>? ToolCalls = null) : IMessage<string>
{
/// <summary>
/// Create chat message from an uploaded DashScope file.
/// </summary>
/// <param name="fileId">The id of the file.</param>
public ChatMessage(DashScopeFileId fileId)
: this("system", fileId.ToUrl())
{
}

/// <summary>
/// Create chat message from multiple DashScope file.
/// </summary>
/// <param name="fileIds">Ids of the files.</param>
public ChatMessage(IEnumerable<DashScopeFileId> fileIds)
: this("system", string.Join(',', fileIds.Select(f => f.ToUrl())))
{
}
}
98 changes: 89 additions & 9 deletions src/Cnblogs.DashScope.Core/DashScopeClientCore.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http.Headers;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -130,32 +131,32 @@ public async Task<DashScopeTaskList> ListTasksAsync(

if (startTime.HasValue)
{
queryString.Append($"start_time={startTime:YYYYMMDDhhmmss}");
queryString.Append($"&start_time={startTime:YYYYMMDDhhmmss}");
}

if (endTime.HasValue)
{
queryString.Append($"end_time={endTime:YYYYMMDDhhmmss}");
queryString.Append($"&end_time={endTime:YYYYMMDDhhmmss}");
}

if (string.IsNullOrEmpty(modelName) == false)
{
queryString.Append($"model_name={modelName}");
queryString.Append($"&model_name={modelName}");
}

if (status.HasValue)
{
queryString.Append($"status={status}");
queryString.Append($"&status={status}");
}

if (pageNo.HasValue)
{
queryString.Append($"page_no={pageNo}");
queryString.Append($"&page_no={pageNo}");
}

if (pageSize.HasValue)
{
queryString.Append($"page_size={pageSize}");
queryString.Append($"&page_size={pageSize}");
}

var request = BuildRequest(HttpMethod.Get, $"{ApiLinks.Tasks}?{queryString}");
Expand Down Expand Up @@ -202,6 +203,41 @@ public async Task<ModelResponse<BackgroundGenerationOutput, BackgroundGeneration
cancellationToken))!;
}

/// <inheritdoc />
public async Task<DashScopeFile> UploadFileAsync(
Stream file,
string filename,
string purpose = "file-extract",
CancellationToken cancellationToken = default)
{
var form = new MultipartFormDataContent();
form.Add(new StreamContent(file), "file", filename);
form.Add(new StringContent(purpose), nameof(purpose));
var request = new HttpRequestMessage(HttpMethod.Post, ApiLinks.Files) { Content = form };
return (await SendCompatibleAsync<DashScopeFile>(request, cancellationToken))!;
}

/// <inheritdoc />
public async Task<DashScopeFile> GetFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default)
{
var request = BuildRequest(HttpMethod.Get, ApiLinks.Files + $"/{id}");
return (await SendCompatibleAsync<DashScopeFile>(request, cancellationToken))!;
}

/// <inheritdoc />
public async Task<DashScopeFileList> ListFilesAsync(CancellationToken cancellationToken = default)
{
var request = BuildRequest(HttpMethod.Get, ApiLinks.Files);
return (await SendCompatibleAsync<DashScopeFileList>(request, cancellationToken))!;
}

/// <inheritdoc />
public async Task<DashScopeDeleteFileResult> DeleteFileAsync(DashScopeFileId id, CancellationToken cancellationToken = default)
{
var request = BuildRequest(HttpMethod.Delete, ApiLinks.Files + $"/{id}");
return (await SendCompatibleAsync<DashScopeDeleteFileResult>(request, cancellationToken))!;
}

private static HttpRequestMessage BuildSseRequest<TPayload>(HttpMethod method, string url, TPayload payload)
where TPayload : class
{
Expand Down Expand Up @@ -239,6 +275,24 @@ private static HttpRequestMessage BuildRequest<TPayload>(
return message;
}

private async Task<TResponse?> SendCompatibleAsync<TResponse>(
HttpRequestMessage message,
CancellationToken cancellationToken)
where TResponse : class
{
var response = await GetSuccessResponseAsync<OpenAiErrorResponse>(
message,
r => new DashScopeError()
{
Code = r.Error.Type,
Message = r.Error.Message,
RequestId = string.Empty
},
HttpCompletionOption.ResponseContentRead,
cancellationToken);
return await response.Content.ReadFromJsonAsync<TResponse>(SerializationOptions, cancellationToken);
}

private async Task<TResponse?> SendAsync<TResponse>(HttpRequestMessage message, CancellationToken cancellationToken)
where TResponse : class
{
Expand Down Expand Up @@ -286,6 +340,15 @@ private async Task<HttpResponseMessage> GetSuccessResponseAsync(
HttpRequestMessage message,
HttpCompletionOption completeOption = HttpCompletionOption.ResponseContentRead,
CancellationToken cancellationToken = default)
{
return await GetSuccessResponseAsync<DashScopeError>(message, f => f, completeOption, cancellationToken);
}

private async Task<HttpResponseMessage> GetSuccessResponseAsync<TError>(
HttpRequestMessage message,
Func<TError, DashScopeError> errorMapper,
HttpCompletionOption completeOption = HttpCompletionOption.ResponseContentRead,
CancellationToken cancellationToken = default)
{
HttpResponseMessage response;
try
Expand All @@ -305,14 +368,31 @@ private async Task<HttpResponseMessage> GetSuccessResponseAsync(
DashScopeError? error = null;
try
{
error = await response.Content.ReadFromJsonAsync<DashScopeError>(SerializationOptions, cancellationToken);
var r = await response.Content.ReadFromJsonAsync<TError>(SerializationOptions, cancellationToken);
error = r == null ? null : errorMapper.Invoke(r);
}
catch (Exception)
{
// ignore
}

await ThrowDashScopeExceptionAsync(error, message, response, cancellationToken);
// will never reach here
return response;
}

[DoesNotReturn]
private static async Task ThrowDashScopeExceptionAsync(
DashScopeError? error,
HttpRequestMessage message,
HttpResponseMessage response,
CancellationToken cancellationToken)
{
var errorMessage = error?.Message ?? await response.Content.ReadAsStringAsync(cancellationToken);
throw new DashScopeException(message.RequestUri?.ToString(), (int)response.StatusCode, error, errorMessage);
throw new DashScopeException(
message.RequestUri?.ToString(),
(int)response.StatusCode,
error,
errorMessage);
}
}
9 changes: 9 additions & 0 deletions src/Cnblogs.DashScope.Core/DashScopeDeleteFileResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Cnblogs.DashScope.Core;

/// <summary>
/// Result of a delete file action.
/// </summary>
/// <param name="Object">Always be "file".</param>
/// <param name="Deleted">Deletion result.</param>
/// <param name="Id">Deleting file's id.</param>
public record DashScopeDeleteFileResult(string Object, bool Deleted, DashScopeFileId Id);
Loading