diff --git a/DysonNetwork.Insight/DysonNetwork.Insight.csproj b/DysonNetwork.Insight/DysonNetwork.Insight.csproj
index 19a39e6..86dc20e 100644
--- a/DysonNetwork.Insight/DysonNetwork.Insight.csproj
+++ b/DysonNetwork.Insight/DysonNetwork.Insight.csproj
@@ -17,6 +17,7 @@
+
diff --git a/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs b/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
new file mode 100644
index 0000000..6419732
--- /dev/null
+++ b/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
@@ -0,0 +1,129 @@
+//
+using System;
+using System.Collections.Generic;
+using DysonNetwork.Insight;
+using DysonNetwork.Shared.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NodaTime;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace DysonNetwork.Insight.Migrations
+{
+ [DbContext(typeof(AppDatabase))]
+ [Migration("20251026045505_AddThinkingChunk")]
+ partial class AddThinkingChunk
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("Topic")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("topic");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("pk_thinking_sequences");
+
+ b.ToTable("thinking_sequences", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property>("Chunks")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("chunks");
+
+ b.Property("Content")
+ .HasColumnType("text")
+ .HasColumnName("content");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property>("Files")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("files");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property("SequenceId")
+ .HasColumnType("uuid")
+ .HasColumnName("sequence_id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("pk_thinking_thoughts");
+
+ b.HasIndex("SequenceId")
+ .HasDatabaseName("ix_thinking_thoughts_sequence_id");
+
+ b.ToTable("thinking_thoughts", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
+ {
+ b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
+ .WithMany()
+ .HasForeignKey("SequenceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id");
+
+ b.Navigation("Sequence");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.cs b/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.cs
new file mode 100644
index 0000000..01ee269
--- /dev/null
+++ b/DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using DysonNetwork.Shared.Models;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DysonNetwork.Insight.Migrations
+{
+ ///
+ public partial class AddThinkingChunk : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn>(
+ name: "chunks",
+ table: "thinking_thoughts",
+ type: "jsonb",
+ nullable: false,
+ defaultValue: new List()
+ );
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "chunks",
+ table: "thinking_thoughts");
+ }
+ }
+}
diff --git a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
index e3f9875..44a0533 100644
--- a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
+++ b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
@@ -66,6 +66,11 @@ namespace DysonNetwork.Insight.Migrations
.HasColumnType("uuid")
.HasColumnName("id");
+ b.Property>("Chunks")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("chunks");
+
b.Property("Content")
.HasColumnType("text")
.HasColumnName("content");
diff --git a/DysonNetwork.Insight/Thought/ThoughtController.cs b/DysonNetwork.Insight/Thought/ThoughtController.cs
index 51d0f79..f0cb644 100644
--- a/DysonNetwork.Insight/Thought/ThoughtController.cs
+++ b/DysonNetwork.Insight/Thought/ThoughtController.cs
@@ -1,4 +1,7 @@
+using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
using System.Text;
using System.Text.Json;
using DysonNetwork.Shared.Models;
@@ -21,6 +24,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
}
[HttpPost]
+ [Experimental("SKEXP0110")]
public async Task Think([FromBody] StreamThinkingRequest request)
{
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
@@ -72,7 +76,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
// Add previous thoughts (excluding the current user thought, which is the first one since descending)
var previousThoughts = await service.GetPreviousThoughtsAsync(sequence);
var count = previousThoughts.Count;
- for (var i = 1; i < count; i++) // skip first (newest, current user)
+ for (var i = 1; i < count; i++) // skip first (the newest, current user)
{
var thought = previousThoughts[i];
switch (thought.Role)
@@ -99,6 +103,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
// Kick off streaming generation
var accumulatedContent = new StringBuilder();
+ var thinkingChunks = new List();
await foreach (var chunk in chatCompletionService.GetStreamingChatMessageContentsAsync(
chatHistory,
provider.CreatePromptExecutionSettings(),
@@ -108,10 +113,33 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
// Process each item in the chunk for detailed streaming
foreach (var item in chunk.Items)
{
+ var streamingChunk = item switch
+ {
+ StreamingTextContent textContent => new SnThinkingChunk
+ { Type = StreamingContentType.Text, Data = new() { ["text"] = textContent.Text ?? "" } },
+ StreamingReasoningContent reasoningContent => new SnThinkingChunk
+ {
+ Type = StreamingContentType.Reasoning, Data = new() { ["text"] = reasoningContent.Text ?? "" }
+ },
+ StreamingFunctionCallUpdateContent functionCall => new SnThinkingChunk
+ {
+ Type = StreamingContentType.FunctionCall,
+ Data = JsonSerializer.Deserialize>(
+ JsonSerializer.Serialize(functionCall)) ?? new()
+ },
+ _ => new SnThinkingChunk
+ {
+ Type = StreamingContentType.Unknown, Data = new() { ["data"] = JsonSerializer.Serialize(item) }
+ }
+ };
+ thinkingChunks.Add(streamingChunk);
+
var messageJson = item switch
{
StreamingTextContent textContent =>
JsonSerializer.Serialize(new { type = "text", data = textContent.Text ?? "" }),
+ StreamingReasoningContent reasoningContent =>
+ JsonSerializer.Serialize(new { type = "reasoning", data = reasoningContent.Text ?? "" }),
StreamingFunctionCallUpdateContent functionCall =>
JsonSerializer.Serialize(new { type = "function_call", data = functionCall }),
_ =>
@@ -120,7 +148,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
// Write a structured JSON message to the HTTP response as SSE
var messageBytes = Encoding.UTF8.GetBytes($"data: {messageJson}\n\n");
- await Response.Body.WriteAsync(messageBytes, 0, messageBytes.Length);
+ await Response.Body.WriteAsync(messageBytes);
await Response.Body.FlushAsync();
}
@@ -129,8 +157,12 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
}
// Save assistant thought
- var savedThought =
- await service.SaveThoughtAsync(sequence, accumulatedContent.ToString(), ThinkingThoughtRole.Assistant);
+ var savedThought = await service.SaveThoughtAsync(
+ sequence,
+ accumulatedContent.ToString(),
+ ThinkingThoughtRole.Assistant,
+ thinkingChunks
+ );
// Write the topic if it was newly set, then the thought object as JSON to the stream
using (var streamBuilder = new MemoryStream())
diff --git a/DysonNetwork.Insight/Thought/ThoughtProvider.cs b/DysonNetwork.Insight/Thought/ThoughtProvider.cs
index bdef72b..7b5a938 100644
--- a/DysonNetwork.Insight/Thought/ThoughtProvider.cs
+++ b/DysonNetwork.Insight/Thought/ThoughtProvider.cs
@@ -1,4 +1,5 @@
using System.ClientModel;
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
@@ -6,8 +7,12 @@ using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Ollama;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using OpenAI;
-using PostPinMode = DysonNetwork.Shared.Proto.PostPinMode;
using PostType = DysonNetwork.Shared.Proto.PostType;
+using Microsoft.SemanticKernel.Plugins.Web;
+using Microsoft.SemanticKernel.Plugins.Web.Bing;
+using Microsoft.SemanticKernel.Plugins.Web.Google;
+using NodaTime.Serialization.Protobuf;
+using NodaTime.Text;
namespace DysonNetwork.Insight.Thought;
@@ -15,20 +20,26 @@ public class ThoughtProvider
{
private readonly PostService.PostServiceClient _postClient;
private readonly AccountService.AccountServiceClient _accountClient;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
public Kernel Kernel { get; }
private string? ModelProviderType { get; set; }
private string? ModelDefault { get; set; }
+ [Experimental("SKEXP0050")]
public ThoughtProvider(
IConfiguration configuration,
- PostService.PostServiceClient postClient,
- AccountService.AccountServiceClient accountClient
+ PostService.PostServiceClient postServiceClient,
+ AccountService.AccountServiceClient accountServiceClient,
+ ILogger logger
)
{
- _postClient = postClient;
- _accountClient = accountClient;
+ _logger = logger;
+ _postClient = postServiceClient;
+ _accountClient = accountServiceClient;
+ _configuration = configuration;
Kernel = InitializeThinkingProvider(configuration);
InitializeHelperFunctions();
@@ -63,10 +74,11 @@ public class ThoughtProvider
return builder.Build();
}
+ [Experimental("SKEXP0050")]
private void InitializeHelperFunctions()
{
// Add Solar Network tools plugin
- Kernel.ImportPluginFromFunctions("helper_functions", [
+ Kernel.ImportPluginFromFunctions("solar_network", [
KernelFunctionFactory.CreateFromMethod(async (string userId) =>
{
var request = new GetAccountRequest { Id = userId };
@@ -80,83 +92,79 @@ public class ThoughtProvider
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
}, "get_post", "Get a single post by ID from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async (string query) =>
- {
- var request = new SearchPostsRequest { Query = query, PageSize = 10 };
- var response = await _postClient.SearchPostsAsync(request);
- return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
- }, "search_posts", "Search posts by query from the Solar Network."),
+ {
+ var request = new SearchPostsRequest { Query = query, PageSize = 10 };
+ var response = await _postClient.SearchPostsAsync(request);
+ return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
+ }, "search_posts",
+ "Search posts by query from the Solar Network. The input query is will be used to search with title, description and body content"),
KernelFunctionFactory.CreateFromMethod(async (
- string? publisherId = null,
- string? realmId = null,
- int pageSize = 10,
- string? pageToken = null,
string? orderBy = null,
- List? categories = null,
- List? tags = null,
- string? query = null,
- List? types = null,
string? afterIso = null,
- string? beforeIso = null,
- bool includeReplies = false,
- string? pinned = null,
- bool onlyMedia = false,
- bool shuffle = false
+ string? beforeIso = null
) =>
{
+ _logger.LogInformation("Begin building request to list post from sphere...");
+
var request = new ListPostsRequest
{
- PublisherId = publisherId,
- RealmId = realmId,
- PageSize = pageSize,
- PageToken = pageToken,
+ PageSize = 20,
OrderBy = orderBy,
- Query = query,
- IncludeReplies = includeReplies,
- Pinned =
- !string.IsNullOrEmpty(pinned) && int.TryParse(pinned, out int p) ? (PostPinMode)p : default,
- OnlyMedia = onlyMedia,
- Shuffle = shuffle
};
if (!string.IsNullOrEmpty(afterIso))
- {
- request.After =
- Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.Parse(afterIso)
- .ToUniversalTime());
- }
-
+ try
+ {
+ request.After = InstantPattern.General.Parse(afterIso).Value.ToTimestamp();
+ }
+ catch (Exception)
+ {
+ _logger.LogWarning("Invalid afterIso format: {AfterIso}", afterIso);
+ }
if (!string.IsNullOrEmpty(beforeIso))
- {
- request.Before =
- Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(DateTimeOffset.Parse(beforeIso)
- .ToUniversalTime());
- }
+ try
+ {
+ request.Before = InstantPattern.General.Parse(beforeIso).Value.ToTimestamp();
+ }
+ catch (Exception)
+ {
+ _logger.LogWarning("Invalid beforeIso format: {BeforeIso}", beforeIso);
+ }
+
+ _logger.LogInformation("Request built, {Request}", request);
- if (categories != null) request.Categories.AddRange(categories);
- if (tags != null) request.Tags.AddRange(tags);
- if (types != null) request.Types_.AddRange(types.Select(t => (PostType)t));
var response = await _postClient.ListPostsAsync(request);
- return JsonSerializer.Serialize(response.Posts.Select(SnPost.FromProtoValue),
- GrpcTypeHelper.SerializerOptions);
+
+ var data = response.Posts.Select(SnPost.FromProtoValue);
+ _logger.LogInformation("Sphere service returned posts: {Posts}", data);
+ return JsonSerializer.Serialize(data, GrpcTypeHelper.SerializerOptions);
}, "list_posts",
- "Get posts from the Solar Network with customizable filters.\n" +
+ "Get posts from the Solar Network.\n" +
"Parameters:\n" +
- "publisherId (optional, string: publisher ID to filter by)\n" +
- "realmId (optional, string: realm ID to filter by)\n" +
- "pageSize (optional, integer: posts per page, default 20)\n" +
- "pageToken (optional, string: pagination token)\n" +
- "orderBy (optional, string: field to order by)\n" +
- "categories (optional, array of strings: category slugs)\n" +
- "tags (optional, array of strings: tag slugs)\n" +
- "query (optional, string: search query, will search in title, description and body)\n" +
- "types (optional, array of integers: post types, use 0 for Moment, 1 for Article)\n" +
+ "orderBy (optional, string: order by published date, accept asc or desc)\n" +
"afterIso (optional, string: ISO date for posts after this date)\n" +
- "beforeIso (optional, string: ISO date for posts before this date)\n" +
- "includeReplies (optional, boolean: include replies, default false)\n" +
- "pinned (optional, string: pin mode as integer string, '0' for PublisherPage, '1' for RealmPage, '2' for ReplyPage)\n" +
- "onlyMedia (optional, boolean: only posts with media, default false)\n" +
- "shuffle (optional, boolean: shuffle results, default false)"
+ "beforeIso (optional, string: ISO date for posts before this date)"
)
]);
+
+ // Add web search plugins if configured
+ var bingApiKey = _configuration.GetValue("Thinking:BingApiKey");
+ if (!string.IsNullOrEmpty(bingApiKey))
+ {
+ var bingConnector = new BingConnector(bingApiKey);
+ var bing = new WebSearchEnginePlugin(bingConnector);
+ Kernel.ImportPluginFromObject(bing, "bing");
+ }
+
+ var googleApiKey = _configuration.GetValue("Thinking:GoogleApiKey");
+ var googleCx = _configuration.GetValue("Thinking:GoogleCx");
+ if (!string.IsNullOrEmpty(googleApiKey) && !string.IsNullOrEmpty(googleCx))
+ {
+ var googleConnector = new GoogleConnector(
+ apiKey: googleApiKey,
+ searchEngineId: googleCx);
+ var google = new WebSearchEnginePlugin(googleConnector);
+ Kernel.ImportPluginFromObject(google, "google");
+ }
}
public PromptExecutionSettings CreatePromptExecutionSettings()
diff --git a/DysonNetwork.Insight/Thought/ThoughtService.cs b/DysonNetwork.Insight/Thought/ThoughtService.cs
index b9f5371..bcea8ff 100644
--- a/DysonNetwork.Insight/Thought/ThoughtService.cs
+++ b/DysonNetwork.Insight/Thought/ThoughtService.cs
@@ -6,7 +6,8 @@ namespace DysonNetwork.Insight.Thought;
public class ThoughtService(AppDatabase db, ICacheService cache)
{
- public async Task GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId, string? topic = null)
+ public async Task GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId,
+ string? topic = null)
{
if (sequenceId.HasValue)
{
@@ -23,13 +24,19 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
}
}
- public async Task SaveThoughtAsync(SnThinkingSequence sequence, string content, ThinkingThoughtRole role)
+ public async Task SaveThoughtAsync(
+ SnThinkingSequence sequence,
+ string content,
+ ThinkingThoughtRole role,
+ List? chunks = null
+ )
{
var thought = new SnThinkingThought
{
SequenceId = sequence.Id,
Content = content,
- Role = role
+ Role = role,
+ Chunks = chunks ?? []
};
db.ThinkingThoughts.Add(thought);
await db.SaveChangesAsync();
@@ -60,7 +67,8 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
return thoughts;
}
- public async Task<(int total, List sequences)> ListSequencesAsync(Guid accountId, int offset, int take)
+ public async Task<(int total, List sequences)> ListSequencesAsync(Guid accountId, int offset,
+ int take)
{
var query = db.ThinkingSequences.Where(s => s.AccountId == accountId);
var totalCount = await query.CountAsync();
@@ -72,4 +80,4 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
return (totalCount, sequences);
}
-}
+}
\ No newline at end of file
diff --git a/DysonNetwork.Insight/appsettings.json b/DysonNetwork.Insight/appsettings.json
index 6e2f978..a24fb23 100644
--- a/DysonNetwork.Insight/appsettings.json
+++ b/DysonNetwork.Insight/appsettings.json
@@ -20,8 +20,8 @@
"Insecure": true
},
"Thinking": {
- "Provider": "ollama",
- "Model": "qwen3:8b",
- "Endpoint": "http://localhost:11434/api"
+ "Provider": "deepseek",
+ "Model": "deepseek-chat",
+ "ApiKey": "sk-cd709f9f1b96432e99d2d992392b4220"
}
}
\ No newline at end of file
diff --git a/DysonNetwork.Shared/Models/ThinkingSequence.cs b/DysonNetwork.Shared/Models/ThinkingSequence.cs
index 9289b9e..71a8a94 100644
--- a/DysonNetwork.Shared/Models/ThinkingSequence.cs
+++ b/DysonNetwork.Shared/Models/ThinkingSequence.cs
@@ -18,6 +18,20 @@ public enum ThinkingThoughtRole
User
}
+public enum StreamingContentType
+{
+ Text,
+ Reasoning,
+ FunctionCall,
+ Unknown
+}
+
+public class SnThinkingChunk
+{
+ public StreamingContentType Type { get; set; }
+ public Dictionary? Data { get; set; } = new();
+}
+
public class SnThinkingThought : ModelBase
{
public Guid Id { get; set; } = Guid.NewGuid();
@@ -25,8 +39,10 @@ public class SnThinkingThought : ModelBase
[Column(TypeName = "jsonb")] public List Files { get; set; } = [];
+ [Column(TypeName = "jsonb")] public List Chunks { get; set; } = [];
+
public ThinkingThoughtRole Role { get; set; }
public Guid SequenceId { get; set; }
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
-}
\ No newline at end of file
+}
diff --git a/DysonNetwork.Shared/Proto/GrpcTypeHelper.cs b/DysonNetwork.Shared/Proto/GrpcTypeHelper.cs
index 9be9a3f..984019f 100644
--- a/DysonNetwork.Shared/Proto/GrpcTypeHelper.cs
+++ b/DysonNetwork.Shared/Proto/GrpcTypeHelper.cs
@@ -21,7 +21,7 @@ public abstract class GrpcTypeHelper
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
PropertyNameCaseInsensitive = true,
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
-
+
public static readonly JsonSerializerOptions? SerializerOptions = new JsonSerializerOptions()
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
@@ -51,6 +51,7 @@ public abstract class GrpcTypeHelper
_ => Value.ForString(JsonSerializer.Serialize(kvp.Value, SerializerOptions)) // fallback to JSON string
};
}
+
return result;
}
@@ -66,13 +67,15 @@ public abstract class GrpcTypeHelper
try
{
// Try to parse as JSON object or primitive
- result[kvp.Key] = JsonNode.Parse(value.StringValue)?.AsObject() ?? JsonObject.Create(new JsonElement());
+ result[kvp.Key] = JsonNode.Parse(value.StringValue)?.AsObject() ??
+ JsonObject.Create(new JsonElement());
}
catch
{
// Fallback to raw string
result[kvp.Key] = value.StringValue;
}
+
break;
case Value.KindOneofCase.NumberValue:
result[kvp.Key] = value.NumberValue;
@@ -106,6 +109,7 @@ public abstract class GrpcTypeHelper
break;
}
}
+
return result;
}
@@ -117,7 +121,8 @@ public abstract class GrpcTypeHelper
Value.KindOneofCase.NumberValue => value.NumberValue,
Value.KindOneofCase.BoolValue => value.BoolValue,
Value.KindOneofCase.NullValue => null,
- _ => JsonSerializer.Deserialize(JsonSerializer.Serialize(value, SerializerOptions), SerializerOptions)
+ _ => JsonSerializer.Deserialize(JsonSerializer.Serialize(value, SerializerOptions),
+ SerializerOptions)
};
}
@@ -162,9 +167,9 @@ public abstract class GrpcTypeHelper
JsonSerializer.Serialize(obj, SerializerOptions)
);
}
-
+
public static T? ConvertByteStringToObject(ByteString bytes)
{
- return JsonSerializer.Deserialize(bytes.ToStringUtf8(), SerializerOptions);
+ return bytes.IsEmpty ? default : JsonSerializer.Deserialize(bytes.ToStringUtf8(), SerializerOptions);
}
-}
+}
\ No newline at end of file
diff --git a/DysonNetwork.Sphere/Post/PostServiceGrpc.cs b/DysonNetwork.Sphere/Post/PostServiceGrpc.cs
index fd75daf..1fc302e 100644
--- a/DysonNetwork.Sphere/Post/PostServiceGrpc.cs
+++ b/DysonNetwork.Sphere/Post/PostServiceGrpc.cs
@@ -124,10 +124,14 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
.Include(p => p.Awards)
.Include(p => p.FeaturedRecords)
.AsQueryable();
-
+
query = request.Shuffle
? query.OrderBy(e => EF.Functions.Random())
- : query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt);
+ : request.OrderBy switch
+ {
+ "asc" => query.OrderBy(e => e.PublishedAt ?? e.CreatedAt),
+ _ => query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt),
+ };
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
query = query.Where(p => p.PublisherId == pid);