Skip to content

Commit

Permalink
Merge pull request #28 from cnblogs/support-qwen-long
Browse files Browse the repository at this point in the history
feat: support qwen long
  • Loading branch information
ikesnowy authored Jul 10, 2024
2 parents 2388620 + b691e35 commit d8d3119
Show file tree
Hide file tree
Showing 40 changed files with 824 additions and 21 deletions.
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

0 comments on commit d8d3119

Please sign in to comment.