Skip to content

Commit dbbca66

Browse files
authored
Merge pull request #141 from cnblogs/support-doc-url
feat: support doc url
2 parents fc2425f + 78d8307 commit dbbca66

15 files changed

+839
-8
lines changed

src/Cnblogs.DashScope.AI/DashScopeChatClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
248248
Role = streamedRole
249249
};
250250

251-
if (response.Output.Choices?.FirstOrDefault()?.Message.Content is { Length: > 0 })
251+
if (response.Output.Choices?.FirstOrDefault()?.Message.Content.ToString() is { Length: > 0 })
252252
{
253253
update.Contents.Add(new TextContent(response.Output.Choices[0].Message.Content));
254254
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Cnblogs.DashScope.Core.Internals;
5+
6+
internal class TextChatMessageContentConvertor : JsonConverter<TextChatMessageContent>
7+
{
8+
/// <inheritdoc />
9+
public override TextChatMessageContent? Read(
10+
ref Utf8JsonReader reader,
11+
Type typeToConvert,
12+
JsonSerializerOptions options)
13+
{
14+
if (reader.TokenType == JsonTokenType.String)
15+
{
16+
var s = reader.GetString();
17+
return s == null ? null : new TextChatMessageContent(s);
18+
}
19+
20+
if (reader.TokenType == JsonTokenType.StartArray)
21+
{
22+
var contents = JsonSerializer.Deserialize<List<TextChatMessageContentInternal>>(ref reader, options);
23+
if (contents == null)
24+
{
25+
// impossible
26+
return null;
27+
}
28+
29+
var text = contents.FirstOrDefault(x => string.IsNullOrEmpty(x.Text) == false)?.Text
30+
?? throw new JsonException("No text found in content array");
31+
var docUrls = contents.FirstOrDefault(x => x.DocUrl != null)?.DocUrl;
32+
return new TextChatMessageContent(text, docUrls);
33+
}
34+
35+
throw new JsonException("Unknown type for TextChatMessageContent");
36+
}
37+
38+
/// <inheritdoc />
39+
public override void Write(Utf8JsonWriter writer, TextChatMessageContent value, JsonSerializerOptions options)
40+
{
41+
if (value.DocUrls != null)
42+
{
43+
JsonSerializer.Serialize(
44+
writer,
45+
new List<TextChatMessageContentInternal>()
46+
{
47+
new() { Type = "text", Text = value.Text },
48+
new()
49+
{
50+
Type = "doc_url",
51+
DocUrl = value.DocUrls.ToList(),
52+
FileParsingStrategy = "auto"
53+
}
54+
},
55+
options);
56+
}
57+
else
58+
{
59+
JsonSerializer.Serialize(writer, value.Text, options);
60+
}
61+
}
62+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Cnblogs.DashScope.Core.Internals;
2+
3+
internal class TextChatMessageContentInternal
4+
{
5+
public string Type { get; set; } = "text";
6+
public string? Text { get; set; }
7+
public List<string>? DocUrl { get; set; }
8+
public string? FileParsingStrategy { get; set; }
9+
}

src/Cnblogs.DashScope.Core/TextChatMessage.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Cnblogs.DashScope.Core;
66
/// <summary>
77
/// Represents a chat message between the user and the model.
88
/// </summary>
9-
public record TextChatMessage : IMessage<string>
9+
public record TextChatMessage : IMessage<TextChatMessageContent>
1010
{
1111
/// <summary>
1212
/// Create chat message from an uploaded DashScope file.
@@ -38,7 +38,7 @@ public TextChatMessage(IEnumerable<DashScopeFileId> fileIds)
3838
[JsonConstructor]
3939
public TextChatMessage(
4040
string role,
41-
string content,
41+
TextChatMessageContent content,
4242
string? toolCallId = null,
4343
bool? partial = null,
4444
string? reasoningContent = null,
@@ -56,7 +56,7 @@ public TextChatMessage(
5656
public string Role { get; init; }
5757

5858
/// <summary>The content of this message.</summary>
59-
public string Content { get; init; }
59+
public TextChatMessageContent Content { get; init; }
6060

6161
/// <summary>Used when role is tool, represents the function name of this message generated by.</summary>
6262
public string? ToolCallId { get; init; }
@@ -105,6 +105,17 @@ public static TextChatMessage File(IEnumerable<DashScopeFileId> fileIds)
105105
return new TextChatMessage(fileIds);
106106
}
107107

108+
/// <summary>
109+
/// Create a docUrls message.
110+
/// </summary>
111+
/// <param name="prompt">Text input.</param>
112+
/// <param name="docUrls">The doc urls.</param>
113+
/// <returns></returns>
114+
public static TextChatMessage DocUrl(string prompt, IEnumerable<string> docUrls)
115+
{
116+
return new TextChatMessage(DashScopeRoleNames.User, new TextChatMessageContent(prompt, docUrls));
117+
}
118+
108119
/// <summary>
109120
/// Create a user message.
110121
/// </summary>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.Text.Json.Serialization;
2+
using Cnblogs.DashScope.Core.Internals;
3+
4+
namespace Cnblogs.DashScope.Core;
5+
6+
/// <summary>
7+
/// Content of the <see cref="TextChatMessage"/>.
8+
/// </summary>
9+
[JsonConverter(typeof(TextChatMessageContentConvertor))]
10+
public class TextChatMessageContent
11+
{
12+
/// <summary>
13+
/// The text part of the content.
14+
/// </summary>
15+
public string Text { get; }
16+
17+
/// <summary>
18+
/// Optional doc urls.
19+
/// </summary>
20+
public IEnumerable<string>? DocUrls { get; }
21+
22+
/// <summary>
23+
/// Creates a <see cref="TextChatMessageContent"/> with text content.
24+
/// </summary>
25+
/// <param name="text">The text content.</param>
26+
public TextChatMessageContent(string text)
27+
{
28+
Text = text;
29+
DocUrls = null;
30+
}
31+
32+
/// <summary>
33+
/// Creates a <see cref="TextChatMessageContent"/> with text content and doc urls.
34+
/// </summary>
35+
/// <param name="text">The text content.</param>
36+
/// <param name="docUrls">The doc urls.</param>
37+
public TextChatMessageContent(string text, IEnumerable<string>? docUrls)
38+
{
39+
Text = text;
40+
DocUrls = docUrls;
41+
}
42+
43+
/// <summary>
44+
/// Convert string to TextChatMessageContent implicitly.
45+
/// </summary>
46+
/// <param name="value">The string value to convert.</param>
47+
/// <returns></returns>
48+
public static implicit operator TextChatMessageContent(string value) => new(value);
49+
50+
/// <summary>
51+
/// Convert to string implicitly.
52+
/// </summary>
53+
/// <param name="value">The value to convert.</param>
54+
/// <returns></returns>
55+
public static implicit operator string(TextChatMessageContent value) => value.Text;
56+
57+
/// <inheritdoc />
58+
public override string ToString()
59+
{
60+
return Text;
61+
}
62+
}

src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ public class TextGenerationTokenUsage
3535
/// The total number of token.
3636
/// </summary>
3737
public int TotalTokens { get; set; }
38+
39+
/// <summary>
40+
/// Cached token count.
41+
/// </summary>
42+
public int? CachedTokens { get; set; }
3843
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Cnblogs.DashScope.Core;
2-
using Cnblogs.DashScope.Core.Internals;
32
using Cnblogs.DashScope.Tests.Shared.Utils;
43
using NSubstitute;
54

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Text.Json;
2+
using Cnblogs.DashScope.Core;
3+
using Cnblogs.DashScope.Core.Internals;
4+
5+
namespace Cnblogs.DashScope.Sdk.UnitTests;
6+
7+
public class TextChatMessageContentJsonConvertorTests
8+
{
9+
private const string TextJson = "\"some text\"";
10+
11+
private const string DocUrlJson =
12+
"[{\"type\":\"text\",\"text\":\"some content\"},{\"type\":\"doc_url\",\"doc_url\":[\"url1\"],\"file_parsing_strategy\":\"auto\"}]";
13+
14+
[Fact]
15+
public void Serialize_Text_StringAsync()
16+
{
17+
// Arrange
18+
var content = new TextChatMessageContent("some text");
19+
20+
// Act
21+
var json = JsonSerializer.Serialize(content, DashScopeDefaults.SerializationOptions);
22+
23+
// Assert
24+
Assert.Equal(TextJson, json);
25+
}
26+
27+
[Fact]
28+
public void Serialize_DocUrl_ObjectAsync()
29+
{
30+
// Arrange
31+
var content = new TextChatMessageContent("some content", new[] { "url1" });
32+
33+
// Act
34+
var json = JsonSerializer.Serialize(content, DashScopeDefaults.SerializationOptions);
35+
36+
// Assert
37+
Assert.Equal(DocUrlJson, json);
38+
}
39+
40+
[Fact]
41+
public void Deserialize_InvalidType_ThrowAsync()
42+
{
43+
// Arrange
44+
const string errJson = "{}";
45+
46+
// Act
47+
var act = () => JsonSerializer.Deserialize<TextChatMessageContent>(
48+
errJson,
49+
DashScopeDefaults.SerializationOptions);
50+
51+
// Assert
52+
Assert.Throws<JsonException>(act);
53+
}
54+
55+
[Fact]
56+
public void Deserialize_InvalidArray_ThrowAsync()
57+
{
58+
// Arrange
59+
const string errJson = "[{\"type\":\"doc_url\", \"doc_url\":[]}]";
60+
61+
// Act
62+
var act = () => JsonSerializer.Deserialize<TextChatMessageContent>(
63+
errJson,
64+
DashScopeDefaults.SerializationOptions);
65+
66+
// Assert
67+
Assert.Throws<JsonException>(act);
68+
}
69+
70+
[Fact]
71+
public void Deserialize_Text_SetTextOnlyAsync()
72+
{
73+
// Act
74+
var content = JsonSerializer.Deserialize<TextChatMessageContent>(
75+
TextJson,
76+
DashScopeDefaults.SerializationOptions);
77+
78+
// Assert
79+
Assert.NotNull(content);
80+
Assert.Equal("some text", content.Text);
81+
Assert.Null(content.DocUrls);
82+
}
83+
84+
[Fact]
85+
public void Deserialize_DocUrl_SetUrlsAsync()
86+
{
87+
// Act
88+
var content = JsonSerializer.Deserialize<TextChatMessageContent>(
89+
DocUrlJson,
90+
DashScopeDefaults.SerializationOptions);
91+
92+
// Assert
93+
Assert.NotNull(content);
94+
Assert.Equal("some content", content.Text);
95+
Assert.Equivalent(new[] { "url1" }, content.DocUrls);
96+
}
97+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ public async Task ConversationCompletion_DeepResearchSse_SuccessAsync(
225225
public static readonly TheoryData<RequestSnapshot<ModelRequest<TextGenerationInput, ITextGenerationParameters>,
226226
ModelResponse<TextGenerationOutput, TextGenerationTokenUsage>>> ConversationMessageFormatSseData = new(
227227
Snapshots.TextGeneration.MessageFormat.ConversationMessageIncremental,
228-
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithFilesIncremental);
228+
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithFilesIncremental,
229+
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithDocUrlsIncremental);
229230

230231
public static readonly TheoryData<RequestSnapshot<ModelRequest<TextGenerationInput, ITextGenerationParameters>,
231232
ModelResponse<TextGenerationOutput, TextGenerationTokenUsage>>> ConversationMessageFormatNoSseData = new(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"model": "qwen-doc-turbo",
3+
"input": {
4+
"messages": [
5+
{
6+
"role": "system",
7+
"content": "You are a helpful assistant."
8+
},
9+
{
10+
"role": "user",
11+
"content": [
12+
{
13+
"type": "text",
14+
"text": "从这两份产品手册中,提取所有产品信息,并整理成一个标准的JSON数组。每个对象需要包含:model(产品的型号)、name(产品的名称)、price(价格(去除货币符号和逗号))"
15+
},
16+
{
17+
"type": "doc_url",
18+
"doc_url": [
19+
"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",
20+
"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"
21+
],
22+
"file_parsing_strategy": "auto"
23+
}
24+
]
25+
}
26+
]
27+
},
28+
"parameters": {
29+
"result_format": "message",
30+
"incremental_output": true
31+
}
32+
}

0 commit comments

Comments
 (0)