From c69256bda6737d041f2f4afe2af8c4e7890cac36 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 15 Nov 2025 17:11:39 +0800 Subject: [PATCH] :bug: Fix some issues in new thought system --- .../DysonNetwork.Develop.csproj | 4 + DysonNetwork.Drive/DysonNetwork.Drive.csproj | 4 + .../DysonNetwork.Insight.csproj | 4 + ...5084746_RefactorThoughtMessage.Designer.cs | 142 ++++++++++++++++ .../20251115084746_RefactorThoughtMessage.cs | 30 ++++ .../Migrations/AppDatabaseModelSnapshot.cs | 16 +- .../Startup/ServiceCollectionExtensions.cs | 4 - .../Thought/CLIENT_UPDATE_GUIDE.md | 155 ++++++++++++++++++ .../Thought/ThoughtProvider.cs | 7 + DysonNetwork.Pass/DysonNetwork.Pass.csproj | 4 + DysonNetwork.Ring/DysonNetwork.Ring.csproj | 4 + .../DysonNetwork.Sphere.csproj | 4 + 12 files changed, 364 insertions(+), 14 deletions(-) create mode 100644 DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs create mode 100644 DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.cs create mode 100644 DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md diff --git a/DysonNetwork.Develop/DysonNetwork.Develop.csproj b/DysonNetwork.Develop/DysonNetwork.Develop.csproj index 76b7b6b..87f3906 100644 --- a/DysonNetwork.Develop/DysonNetwork.Develop.csproj +++ b/DysonNetwork.Develop/DysonNetwork.Develop.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DysonNetwork.Drive/DysonNetwork.Drive.csproj b/DysonNetwork.Drive/DysonNetwork.Drive.csproj index 6d94bbd..1193893 100644 --- a/DysonNetwork.Drive/DysonNetwork.Drive.csproj +++ b/DysonNetwork.Drive/DysonNetwork.Drive.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/DysonNetwork.Insight/DysonNetwork.Insight.csproj b/DysonNetwork.Insight/DysonNetwork.Insight.csproj index 559ab75..35b2e01 100644 --- a/DysonNetwork.Insight/DysonNetwork.Insight.csproj +++ b/DysonNetwork.Insight/DysonNetwork.Insight.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs b/DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs new file mode 100644 index 0000000..7ddb51a --- /dev/null +++ b/DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs @@ -0,0 +1,142 @@ +// +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("20251115084746_RefactorThoughtMessage")] + partial class RefactorThoughtMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .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("PaidToken") + .HasColumnType("bigint") + .HasColumnName("paid_token"); + + b.Property("Topic") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("topic"); + + b.Property("TotalToken") + .HasColumnType("bigint") + .HasColumnName("total_token"); + + 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("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("ModelName") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("model_name"); + + b.Property>("Parts") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("parts"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("SequenceId") + .HasColumnType("uuid") + .HasColumnName("sequence_id"); + + b.Property("TokenCount") + .HasColumnType("bigint") + .HasColumnName("token_count"); + + 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/20251115084746_RefactorThoughtMessage.cs b/DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.cs new file mode 100644 index 0000000..b9d3716 --- /dev/null +++ b/DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using DysonNetwork.Shared.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DysonNetwork.Insight.Migrations +{ + /// + public partial class RefactorThoughtMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn>( + name: "parts", + table: "thinking_thoughts", + type: "jsonb", + nullable: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "parts", + table: "thinking_thoughts"); + } + } +} diff --git a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs index d1b6e82..570cc74 100644 --- a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs +++ b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs @@ -20,7 +20,7 @@ namespace DysonNetwork.Insight.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.10") + .HasAnnotation("ProductVersion", "9.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -74,15 +74,6 @@ namespace DysonNetwork.Insight.Migrations .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"); @@ -101,6 +92,11 @@ namespace DysonNetwork.Insight.Migrations .HasColumnType("character varying(4096)") .HasColumnName("model_name"); + b.Property>("Parts") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("parts"); + b.Property("Role") .HasColumnType("integer") .HasColumnName("role"); diff --git a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs index 197e38a..40fca32 100644 --- a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs @@ -64,10 +64,6 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddThinkingServices(this IServiceCollection services, IConfiguration configuration) { - // Add gRPC clients for ThoughtService - services.AddSphereService(); - services.AddAccountService(); - services.AddSingleton(); services.AddScoped(); diff --git a/DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md b/DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md new file mode 100644 index 0000000..342e391 --- /dev/null +++ b/DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md @@ -0,0 +1,155 @@ +# Client-Side Guide: Handling the New Message Structure + +This document outlines how to update your client application to support the new rich message structure for the thinking/chat feature. The backend now sends structured messages that can include plain text, function calls, and function results, allowing for a more interactive and transparent user experience. + +When using with gateway, all the response type are in snake case + +## 1. Data Models + +When you receive a complete message (a "thought"), it will be in the form of an `SnThinkingThought` object. The core of this object is the `Parts` array, which contains the different components of the message. + +Here are the primary data models you will be working with, represented here in a TypeScript-like format for clarity: + +```typescript +// The main message object from the assistant or user +interface SnThinkingThought { + id: string; + parts: SnThinkingMessagePart[]; + role: 'Assistant' /*Value is (0)*/ | 'User' /*Value is (1)*/; + createdAt: string; // ISO 8601 date string + // ... other metadata +} + +// A single part of a message +interface SnThinkingMessagePart { + type: ThinkingMessagePartType; + text?: string; + functionCall?: SnFunctionCall; + functionResult?: SnFunctionResult; +} + +// Enum for the different part types +enum ThinkingMessagePartType { + Text = 0, + FunctionCall = 1, + FunctionResult = 2, +} + +// Represents a function/tool call made by the assistant +interface SnFunctionCall { + id: string; + name: string; + arguments: string; // A JSON string of the arguments +} + +// Represents the result of a function call +interface SnFunctionResult { + callId: string; // The ID of the corresponding function call + result: any; // The data returned by the function + isError: boolean; +} +``` + +## 2. Handling the SSE Stream + +The response is streamed using Server-Sent Events (SSE). Your client should listen to this stream and process events as they arrive to build the UI in real-time. + +The stream sends different types of messages, identified by a `type` field in the JSON payload. + +| Event Type | `data` Payload | Client-Side Action | +| ------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `text` | `{ "type": "text", "data": "some text" }` | Append the text content to the current message being displayed. This is the most common event. | +| `function_call_update` | `{ "type": "function_call_update", "data": { ... } }` | This provides real-time updates as the AI decides on a function call. You can use this to show an advanced "thinking" state, but it's optional. The key events to handle are `function_call` and `function_result`. | +| `function_call` | `{ "type": "function_call", "data": SnFunctionCall }` | The AI has committed to using a tool. Display a "Using tool..." indicator. You can show the `name` of the tool for more clarity. | +| `function_result` | `{ "type": "function_result", "data": SnFunctionResult }` | The tool has finished running. You can hide the "thinking" indicator for this tool and optionally display a summary of the result. | +| `topic` | `{ "type": "topic", "data": "A new topic" }` | If this is the first message in a new conversation, this event provides the auto-generated topic title. Update your UI accordingly. | +| `thought` | `{ "type": "thought", "data": SnThinkingThought }` | This is the **final event** in the stream. It contains the complete, persisted message object with all its `Parts`. You should use this final object to replace the incrementally-built message in your state to ensure consistency. | + +## 3. Rendering a Message from `SnThinkingThought` + +Once you have the final `SnThinkingThought` object (either from the `thought` event in the stream or by fetching conversation history), you can render it by iterating through the `parts` array. + +### Pseudocode for Rendering + +```javascript +function renderThought(thought: SnThinkingThought) { + const messageContainer = document.createElement('div'); + messageContainer.className = `message message-role-${thought.role}`; + + // User messages are simple and will only have one text part + if (thought.role === 'User') { + const textPart = thought.parts[0]; + messageContainer.innerText = textPart.text; + return messageContainer; + } + + // Assistant messages can have multiple parts + let textBuffer = ''; + + thought.parts.forEach(part => { + switch (part.type) { + case ThinkingMessagePartType.Text: + // Buffer text to combine consecutive text parts + textBuffer += part.text; + break; + + case ThinkingMessagePartType.FunctionCall: + // First, render any buffered text + if (textBuffer) { + messageContainer.appendChild(renderText(textBuffer)); + textBuffer = ''; + } + // Then, render the function call UI component + messageContainer.appendChild(renderFunctionCall(part.functionCall)); + break; + + case ThinkingMessagePartType.FunctionResult: + // Render buffered text + if (textBuffer) { + messageContainer.appendChild(renderText(textBuffer)); + textBuffer = ''; + } + // Then, render the function result UI component + messageContainer.appendChild(renderFunctionResult(part.functionResult)); + break; + } + }); + + // Render any remaining text at the end + if (textBuffer) { + messageContainer.appendChild(renderText(textBuffer)); + } + + return messageContainer; +} + +// Helper functions to create UI components +function renderText(text) { + const p = document.createElement('p'); + p.innerText = text; + return p; +} + +function renderFunctionCall(functionCall) { + const el = document.createElement('div'); + el.className = 'function-call-indicator'; + el.innerHTML = `Using tool: ${functionCall.name}...`; + // You could add a button to show functionCall.arguments + return el; +} + +function renderFunctionResult(functionResult) { + const el = document.createElement('div'); + el.className = 'function-result-indicator'; + if (functionResult.isError) { + el.classList.add('error'); + el.innerText = 'An error occurred while using the tool.'; + } else { + el.innerText = 'Tool finished.'; + } + // You could expand this to show a summary of functionResult.result + return el; +} +``` + +This approach ensures that text and tool-use indicators are rendered inline and in the correct order, providing a clear and accurate representation of the assistant's actions. diff --git a/DysonNetwork.Insight/Thought/ThoughtProvider.cs b/DysonNetwork.Insight/Thought/ThoughtProvider.cs index 2e324bf..d4b96c2 100644 --- a/DysonNetwork.Insight/Thought/ThoughtProvider.cs +++ b/DysonNetwork.Insight/Thought/ThoughtProvider.cs @@ -4,6 +4,7 @@ using System.Text.Json; using DysonNetwork.Insight.Thought.Plugins; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Registry; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.Ollama; using Microsoft.SemanticKernel.Connectors.OpenAI; @@ -72,6 +73,12 @@ public class ThoughtProvider throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType); } + // Add gRPC clients for Thought Plugins + builder.Services.AddServiceDiscoveryCore(); + builder.Services.AddServiceDiscovery(); + builder.Services.AddAccountService(); + builder.Services.AddSphereService(); + builder.Plugins.AddFromType(); builder.Plugins.AddFromType(); diff --git a/DysonNetwork.Pass/DysonNetwork.Pass.csproj b/DysonNetwork.Pass/DysonNetwork.Pass.csproj index 2f97692..57114d3 100644 --- a/DysonNetwork.Pass/DysonNetwork.Pass.csproj +++ b/DysonNetwork.Pass/DysonNetwork.Pass.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DysonNetwork.Ring/DysonNetwork.Ring.csproj b/DysonNetwork.Ring/DysonNetwork.Ring.csproj index 8eff1cc..d4b6818 100644 --- a/DysonNetwork.Ring/DysonNetwork.Ring.csproj +++ b/DysonNetwork.Ring/DysonNetwork.Ring.csproj @@ -14,6 +14,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj index 1afe9bb..6cc27ef 100644 --- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj +++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj @@ -19,6 +19,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive