Skip to content
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
2 changes: 1 addition & 1 deletion src/Cnblogs.DashScope.AI/DashScopeChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
Role = streamedRole
};

if (response.Output.Choices?.FirstOrDefault()?.Message.Content is { Length: > 0 })
if (response.Output.Choices?.FirstOrDefault()?.Message.Content.ToString() is { Length: > 0 })
{
update.Contents.Add(new TextContent(response.Output.Choices[0].Message.Content));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Cnblogs.DashScope.Core.Internals;

internal class TextChatMessageContentConvertor : JsonConverter<TextChatMessageContent>
{
/// <inheritdoc />
public override TextChatMessageContent? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var s = reader.GetString();
return s == null ? null : new TextChatMessageContent(s);
}

if (reader.TokenType == JsonTokenType.StartArray)
{
var contents = JsonSerializer.Deserialize<List<TextChatMessageContentInternal>>(ref reader, options);
if (contents == null)
{
// impossible
return null;
}

var text = contents.FirstOrDefault(x => 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);
}

throw new JsonException("Unknown type for TextChatMessageContent");
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TextChatMessageContent value, JsonSerializerOptions options)
{
if (value.DocUrls != null)
{
JsonSerializer.Serialize(
writer,
new List<TextChatMessageContentInternal>()
{
new() { Type = "text", Text = value.Text },
new()
{
Type = "doc_url",
DocUrl = value.DocUrls.ToList(),
FileParsingStrategy = "auto"
}
},
options);
}
else
{
JsonSerializer.Serialize(writer, value.Text, options);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Cnblogs.DashScope.Core.Internals;

internal class TextChatMessageContentInternal
{
public string Type { get; set; } = "text";
public string? Text { get; set; }
public List<string>? DocUrl { get; set; }
public string? FileParsingStrategy { get; set; }
}
17 changes: 14 additions & 3 deletions src/Cnblogs.DashScope.Core/TextChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Cnblogs.DashScope.Core;
/// <summary>
/// Represents a chat message between the user and the model.
/// </summary>
public record TextChatMessage : IMessage<string>
public record TextChatMessage : IMessage<TextChatMessageContent>
{
/// <summary>
/// Create chat message from an uploaded DashScope file.
Expand Down Expand Up @@ -38,7 +38,7 @@ public TextChatMessage(IEnumerable<DashScopeFileId> fileIds)
[JsonConstructor]
public TextChatMessage(
string role,
string content,
TextChatMessageContent content,
string? toolCallId = null,
bool? partial = null,
string? reasoningContent = null,
Expand All @@ -56,7 +56,7 @@ public TextChatMessage(
public string Role { get; init; }

/// <summary>The content of this message.</summary>
public string Content { get; init; }
public TextChatMessageContent Content { get; init; }

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

/// <summary>
/// Create a docUrls message.
/// </summary>
/// <param name="prompt">Text input.</param>
/// <param name="docUrls">The doc urls.</param>
/// <returns></returns>
public static TextChatMessage DocUrl(string prompt, IEnumerable<string> docUrls)
{
return new TextChatMessage(DashScopeRoleNames.User, new TextChatMessageContent(prompt, docUrls));
}

/// <summary>
/// Create a user message.
/// </summary>
Expand Down
62 changes: 62 additions & 0 deletions src/Cnblogs.DashScope.Core/TextChatMessageContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Text.Json.Serialization;
using Cnblogs.DashScope.Core.Internals;

namespace Cnblogs.DashScope.Core;

/// <summary>
/// Content of the <see cref="TextChatMessage"/>.
/// </summary>
[JsonConverter(typeof(TextChatMessageContentConvertor))]
public class TextChatMessageContent
{
/// <summary>
/// The text part of the content.
/// </summary>
public string Text { get; }

/// <summary>
/// Optional doc urls.
/// </summary>
public IEnumerable<string>? DocUrls { get; }

/// <summary>
/// Creates a <see cref="TextChatMessageContent"/> with text content.
/// </summary>
/// <param name="text">The text content.</param>
public TextChatMessageContent(string text)
{
Text = text;
DocUrls = null;
}

/// <summary>
/// Creates a <see cref="TextChatMessageContent"/> with text content and doc urls.
/// </summary>
/// <param name="text">The text content.</param>
/// <param name="docUrls">The doc urls.</param>
public TextChatMessageContent(string text, IEnumerable<string>? docUrls)
{
Text = text;
DocUrls = docUrls;
}

/// <summary>
/// Convert string to TextChatMessageContent implicitly.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <returns></returns>
public static implicit operator TextChatMessageContent(string value) => new(value);

/// <summary>
/// Convert to string implicitly.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns></returns>
public static implicit operator string(TextChatMessageContent value) => value.Text;

/// <inheritdoc />
public override string ToString()
{
return Text;
}
}
5 changes: 5 additions & 0 deletions src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public class TextGenerationTokenUsage
/// The total number of token.
/// </summary>
public int TotalTokens { get; set; }

/// <summary>
/// Cached token count.
/// </summary>
public int? CachedTokens { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Core.Internals;
using Cnblogs.DashScope.Tests.Shared.Utils;
using NSubstitute;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text.Json;
using Cnblogs.DashScope.Core;
using Cnblogs.DashScope.Core.Internals;

namespace Cnblogs.DashScope.Sdk.UnitTests;

public class TextChatMessageContentJsonConvertorTests
{
private const string TextJson = "\"some text\"";

private const string DocUrlJson =
"[{\"type\":\"text\",\"text\":\"some content\"},{\"type\":\"doc_url\",\"doc_url\":[\"url1\"],\"file_parsing_strategy\":\"auto\"}]";

[Fact]
public void Serialize_Text_StringAsync()
{
// Arrange
var content = new TextChatMessageContent("some text");

// Act
var json = JsonSerializer.Serialize(content, DashScopeDefaults.SerializationOptions);

// Assert
Assert.Equal(TextJson, json);
}

[Fact]
public void Serialize_DocUrl_ObjectAsync()
{
// Arrange
var content = new TextChatMessageContent("some content", new[] { "url1" });

// Act
var json = JsonSerializer.Serialize(content, DashScopeDefaults.SerializationOptions);

// Assert
Assert.Equal(DocUrlJson, json);
}

[Fact]
public void Deserialize_InvalidType_ThrowAsync()
{
// Arrange
const string errJson = "{}";

// Act
var act = () => JsonSerializer.Deserialize<TextChatMessageContent>(
errJson,
DashScopeDefaults.SerializationOptions);

// Assert
Assert.Throws<JsonException>(act);
}

[Fact]
public void Deserialize_InvalidArray_ThrowAsync()
{
// Arrange
const string errJson = "[{\"type\":\"doc_url\", \"doc_url\":[]}]";

// Act
var act = () => JsonSerializer.Deserialize<TextChatMessageContent>(
errJson,
DashScopeDefaults.SerializationOptions);

// Assert
Assert.Throws<JsonException>(act);
}

[Fact]
public void Deserialize_Text_SetTextOnlyAsync()
{
// Act
var content = JsonSerializer.Deserialize<TextChatMessageContent>(
TextJson,
DashScopeDefaults.SerializationOptions);

// Assert
Assert.NotNull(content);
Assert.Equal("some text", content.Text);
Assert.Null(content.DocUrls);
}

[Fact]
public void Deserialize_DocUrl_SetUrlsAsync()
{
// Act
var content = JsonSerializer.Deserialize<TextChatMessageContent>(
DocUrlJson,
DashScopeDefaults.SerializationOptions);

// Assert
Assert.NotNull(content);
Assert.Equal("some content", content.Text);
Assert.Equivalent(new[] { "url1" }, content.DocUrls);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ public async Task ConversationCompletion_DeepResearchSse_SuccessAsync(
public static readonly TheoryData<RequestSnapshot<ModelRequest<TextGenerationInput, ITextGenerationParameters>,
ModelResponse<TextGenerationOutput, TextGenerationTokenUsage>>> ConversationMessageFormatSseData = new(
Snapshots.TextGeneration.MessageFormat.ConversationMessageIncremental,
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithFilesIncremental);
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithFilesIncremental,
Snapshots.TextGeneration.MessageFormat.ConversationMessageWithDocUrlsIncremental);

public static readonly TheoryData<RequestSnapshot<ModelRequest<TextGenerationInput, ITextGenerationParameters>,
ModelResponse<TextGenerationOutput, TextGenerationTokenUsage>>> ConversationMessageFormatNoSseData = new(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"model": "qwen-doc-turbo",
"input": {
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "从这两份产品手册中,提取所有产品信息,并整理成一个标准的JSON数组。每个对象需要包含:model(产品的型号)、name(产品的名称)、price(价格(去除货币符号和逗号))"
},
{
"type": "doc_url",
"doc_url": [
"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"
],
"file_parsing_strategy": "auto"
}
]
}
]
},
"parameters": {
"result_format": "message",
"incremental_output": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
POST /api/v1/services/aigc/text-generation/generation HTTP/1.1
Accept: text/event-stream
Content-Type: application/json
Cache-Control: no-cache
Host: dashscope.aliyuncs.com
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 1341
Loading