Compare commits
2 Commits
b5f9faa724
...
c69256bda6
| Author | SHA1 | Date | |
|---|---|---|---|
|
c69256bda6
|
|||
|
80ea44f2cc
|
@@ -9,6 +9,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeKit" Version="4.14.0" />
|
||||
<PackageReference Include="MimeTypes" Version="2.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SemanticKernel" Version="1.67.1" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" />
|
||||
|
||||
@@ -69,11 +69,6 @@ namespace DysonNetwork.Insight.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<List<SnThinkingChunk>>("Chunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("chunks");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("content");
|
||||
|
||||
@@ -12,21 +12,13 @@ namespace DysonNetwork.Insight.Migrations
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<List<SnThinkingChunk>>(
|
||||
name: "chunks",
|
||||
table: "thinking_thoughts",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValue: new List<SnThinkingChunk>()
|
||||
);
|
||||
// The chunk type has been removed, so this did nothing
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "chunks",
|
||||
table: "thinking_thoughts");
|
||||
// The chunk type has been removed, so this did nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,6 @@ namespace DysonNetwork.Insight.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<List<SnThinkingChunk>>("Chunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("chunks");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("content");
|
||||
|
||||
142
DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs
generated
Normal file
142
DysonNetwork.Insight/Migrations/20251115084746_RefactorThoughtMessage.Designer.cs
generated
Normal file
@@ -0,0 +1,142 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long>("PaidToken")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("paid_token");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("topic");
|
||||
|
||||
b.Property<long>("TotalToken")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("total_token");
|
||||
|
||||
b.Property<Instant>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<List<SnCloudFileReferenceObject>>("Files")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("files");
|
||||
|
||||
b.Property<string>("ModelName")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("model_name");
|
||||
|
||||
b.Property<List<SnThinkingMessagePart>>("Parts")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parts");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<Guid>("SequenceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("sequence_id");
|
||||
|
||||
b.Property<long>("TokenCount")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("token_count");
|
||||
|
||||
b.Property<Instant>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Insight.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RefactorThoughtMessage : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<List<SnThinkingMessagePart>>(
|
||||
name: "parts",
|
||||
table: "thinking_thoughts",
|
||||
type: "jsonb",
|
||||
nullable: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "parts",
|
||||
table: "thinking_thoughts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<SnThinkingChunk>>("Chunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("chunks");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("content");
|
||||
|
||||
b.Property<Instant>("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<List<SnThinkingMessagePart>>("Parts")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parts");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
@@ -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<ThoughtProvider>();
|
||||
services.AddScoped<ThoughtService>();
|
||||
|
||||
|
||||
155
DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md
Normal file
155
DysonNetwork.Insight/Thought/CLIENT_UPDATE_GUIDE.md
Normal file
@@ -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 = `<i>Using tool: <strong>${functionCall.name}</strong>...</i>`;
|
||||
// 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.
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -9,7 +7,6 @@ using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||
|
||||
namespace DysonNetwork.Insight.Thought;
|
||||
|
||||
@@ -61,7 +58,14 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
||||
if (sequence == null) return Forbid(); // or NotFound
|
||||
|
||||
// Save user thought
|
||||
await service.SaveThoughtAsync(sequence, request.UserMessage, ThinkingThoughtRole.User);
|
||||
await service.SaveThoughtAsync(sequence, new List<SnThinkingMessagePart>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = ThinkingMessagePartType.Text,
|
||||
Text = request.UserMessage
|
||||
}
|
||||
}, ThinkingThoughtRole.User);
|
||||
|
||||
// Build chat history
|
||||
var chatHistory = new ChatHistory(
|
||||
@@ -111,16 +115,48 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
||||
for (var i = 1; i < count; i++) // skip first (the newest, current user)
|
||||
{
|
||||
var thought = previousThoughts[i];
|
||||
switch (thought.Role)
|
||||
var textContent = new StringBuilder();
|
||||
var functionCalls = new List<FunctionCallContent>();
|
||||
var hasFunctionCalls = false;
|
||||
|
||||
foreach (var part in thought.Parts)
|
||||
{
|
||||
case ThinkingThoughtRole.User:
|
||||
chatHistory.AddUserMessage(thought.Content ?? "");
|
||||
break;
|
||||
case ThinkingThoughtRole.Assistant:
|
||||
chatHistory.AddAssistantMessage(thought.Content ?? "");
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
switch (part.Type)
|
||||
{
|
||||
case ThinkingMessagePartType.Text:
|
||||
textContent.Append(part.Text);
|
||||
break;
|
||||
case ThinkingMessagePartType.FunctionCall:
|
||||
hasFunctionCalls = true;
|
||||
functionCalls.Add(new FunctionCallContent(part.FunctionCall!.Name, part.FunctionCall.Arguments,
|
||||
part.FunctionCall.Id));
|
||||
break;
|
||||
case ThinkingMessagePartType.FunctionResult:
|
||||
var resultObject = part.FunctionResult!.Result;
|
||||
var resultString = resultObject is string s ? s : JsonSerializer.Serialize(resultObject);
|
||||
var result = new FunctionResultContent(part.FunctionResult!.CallId, resultString);
|
||||
chatHistory.Add(result.ToChatMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (thought.Role == ThinkingThoughtRole.User)
|
||||
{
|
||||
chatHistory.AddUserMessage(textContent.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, textContent.ToString());
|
||||
if (hasFunctionCalls)
|
||||
{
|
||||
assistantMessage.Items = new ChatMessageContentItemCollection();
|
||||
foreach (var fc in functionCalls)
|
||||
{
|
||||
assistantMessage.Items.Add(fc);
|
||||
}
|
||||
}
|
||||
|
||||
chatHistory.Add(assistantMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,72 +168,120 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
||||
|
||||
var kernel = provider.Kernel;
|
||||
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
var executionSettings = provider.CreatePromptExecutionSettings();
|
||||
|
||||
// Kick off streaming generation
|
||||
var accumulatedContent = new StringBuilder();
|
||||
var thinkingChunks = new List<SnThinkingChunk>();
|
||||
await foreach (var chunk in chatCompletionService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory,
|
||||
provider.CreatePromptExecutionSettings(),
|
||||
kernel: kernel
|
||||
))
|
||||
var assistantParts = new List<SnThinkingMessagePart>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Process each item in the chunk for detailed streaming
|
||||
foreach (var item in chunk.Items)
|
||||
var textContentBuilder = new StringBuilder();
|
||||
AuthorRole? authorRole = null;
|
||||
var functionCallBuilder = new FunctionCallContentBuilder();
|
||||
|
||||
await foreach (var streamingContent in chatCompletionService.GetStreamingChatMessageContentsAsync(
|
||||
chatHistory, executionSettings, kernel))
|
||||
{
|
||||
var streamingChunk = item switch
|
||||
authorRole ??= streamingContent.Role;
|
||||
|
||||
if (streamingContent.Content is not null)
|
||||
{
|
||||
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 => string.IsNullOrEmpty(functionCall.CallId)
|
||||
? null
|
||||
: new SnThinkingChunk
|
||||
{
|
||||
Type = StreamingContentType.FunctionCall,
|
||||
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(
|
||||
JsonSerializer.Serialize(functionCall)) ?? new Dictionary<string, object>()
|
||||
},
|
||||
_ => new SnThinkingChunk
|
||||
{
|
||||
Type = StreamingContentType.Unknown, Data = new() { ["data"] = JsonSerializer.Serialize(item) }
|
||||
}
|
||||
};
|
||||
if (streamingChunk == null) continue;
|
||||
textContentBuilder.Append(streamingContent.Content);
|
||||
var messageJson = JsonSerializer.Serialize(new
|
||||
{ type = "text", data = streamingContent.Content });
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"data: {messageJson}\n\n"));
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
|
||||
thinkingChunks.Add(streamingChunk);
|
||||
|
||||
var messageJson = item switch
|
||||
if (streamingContent.Items.Count > 0)
|
||||
{
|
||||
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 }),
|
||||
_ =>
|
||||
JsonSerializer.Serialize(new { type = "unknown", data = item })
|
||||
};
|
||||
functionCallBuilder.Append(streamingContent);
|
||||
}
|
||||
|
||||
// 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);
|
||||
await Response.Body.FlushAsync();
|
||||
foreach (var functionCallUpdate in streamingContent.Items.OfType<StreamingFunctionCallUpdateContent>())
|
||||
{
|
||||
var messageJson = JsonSerializer.Serialize(new
|
||||
{ type = "function_call_update", data = functionCallUpdate });
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"data: {messageJson}\n\n"));
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate content for saving (only text content)
|
||||
accumulatedContent.Append(chunk.Content ?? "");
|
||||
var finalMessageText = textContentBuilder.ToString();
|
||||
if (!string.IsNullOrEmpty(finalMessageText))
|
||||
{
|
||||
assistantParts.Add(new SnThinkingMessagePart
|
||||
{ Type = ThinkingMessagePartType.Text, Text = finalMessageText });
|
||||
}
|
||||
|
||||
var functionCalls = functionCallBuilder.Build();
|
||||
|
||||
if (functionCalls.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var assistantMessage = new ChatMessageContent(authorRole ?? AuthorRole.Assistant,
|
||||
string.IsNullOrEmpty(finalMessageText) ? null : finalMessageText);
|
||||
foreach (var functionCall in functionCalls)
|
||||
{
|
||||
assistantMessage.Items.Add(functionCall);
|
||||
}
|
||||
chatHistory.Add(assistantMessage);
|
||||
|
||||
foreach (var functionCall in functionCalls)
|
||||
{
|
||||
var part = new SnThinkingMessagePart
|
||||
{
|
||||
Type = ThinkingMessagePartType.FunctionCall,
|
||||
FunctionCall = new SnFunctionCall
|
||||
{
|
||||
Id = functionCall.Id!,
|
||||
Name = functionCall.FunctionName!,
|
||||
Arguments = JsonSerializer.Serialize(functionCall.Arguments)
|
||||
}
|
||||
};
|
||||
assistantParts.Add(part);
|
||||
|
||||
var messageJson = JsonSerializer.Serialize(new { type = "function_call", data = part.FunctionCall });
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"data: {messageJson}\n\n"));
|
||||
await Response.Body.FlushAsync();
|
||||
|
||||
FunctionResultContent resultContent;
|
||||
try
|
||||
{
|
||||
resultContent = await functionCall.InvokeAsync(kernel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
resultContent = new FunctionResultContent(functionCall.Id!, ex.Message);
|
||||
}
|
||||
|
||||
chatHistory.Add(resultContent.ToChatMessage());
|
||||
|
||||
var resultPart = new SnThinkingMessagePart
|
||||
{
|
||||
Type = ThinkingMessagePartType.FunctionResult,
|
||||
FunctionResult = new SnFunctionResult
|
||||
{
|
||||
CallId = resultContent.CallId,
|
||||
Result = resultContent.Result!,
|
||||
IsError = resultContent.Result is Exception
|
||||
}
|
||||
};
|
||||
assistantParts.Add(resultPart);
|
||||
|
||||
var resultMessageJson =
|
||||
JsonSerializer.Serialize(new { type = "function_result", data = resultPart.FunctionResult });
|
||||
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes($"data: {resultMessageJson}\n\n"));
|
||||
await Response.Body.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Save assistant thought
|
||||
var savedThought = await service.SaveThoughtAsync(
|
||||
sequence,
|
||||
accumulatedContent.ToString(),
|
||||
assistantParts,
|
||||
ThinkingThoughtRole.Assistant,
|
||||
thinkingChunks,
|
||||
provider.ModelDefault
|
||||
);
|
||||
|
||||
@@ -209,7 +293,6 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
||||
{
|
||||
var topicJson = JsonSerializer.Serialize(new { type = "topic", data = sequence.Topic ?? "" });
|
||||
await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"topic: {topicJson}\n\n"));
|
||||
savedThought.Sequence.Topic = topic;
|
||||
}
|
||||
|
||||
var thoughtJson = JsonSerializer.Serialize(new { type = "thought", data = savedThought },
|
||||
|
||||
@@ -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<SnAccountKernelPlugin>();
|
||||
builder.Plugins.AddFromType<SnPostKernelPlugin>();
|
||||
|
||||
@@ -109,22 +116,12 @@ public class ThoughtProvider
|
||||
case "ollama":
|
||||
return new OllamaPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
||||
options: new FunctionChoiceBehaviorOptions
|
||||
{
|
||||
AllowParallelCalls = true,
|
||||
AllowConcurrentInvocation = true
|
||||
})
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
case "deepseek":
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
||||
options: new FunctionChoiceBehaviorOptions
|
||||
{
|
||||
AllowParallelCalls = true,
|
||||
AllowConcurrentInvocation = true
|
||||
})
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
|
||||
};
|
||||
default:
|
||||
throw new InvalidOperationException("Unknown provider: " + ModelProviderType);
|
||||
|
||||
@@ -11,8 +11,7 @@ namespace DysonNetwork.Insight.Thought;
|
||||
public class ThoughtService(
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
PaymentService.PaymentServiceClient paymentService,
|
||||
WalletService.WalletServiceClient walletService
|
||||
PaymentService.PaymentServiceClient paymentService
|
||||
)
|
||||
{
|
||||
public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(
|
||||
@@ -39,38 +38,39 @@ public class ThoughtService(
|
||||
|
||||
public async Task<SnThinkingThought> SaveThoughtAsync(
|
||||
SnThinkingSequence sequence,
|
||||
string content,
|
||||
List<SnThinkingMessagePart> parts,
|
||||
ThinkingThoughtRole role,
|
||||
List<SnThinkingChunk>? chunks = null,
|
||||
string? model = null
|
||||
)
|
||||
{
|
||||
// Approximate token count (1 token ≈ 4 characters for GPT-like models)
|
||||
var tokenCount = content?.Length / 4 ?? 0;
|
||||
|
||||
var totalChars = parts.Sum(part =>
|
||||
(part.Type == ThinkingMessagePartType.Text ? part.Text?.Length : 0) ?? 0 +
|
||||
(part.Type == ThinkingMessagePartType.FunctionCall ? part.FunctionCall?.Arguments.Length : 0) ?? 0
|
||||
);
|
||||
var tokenCount = totalChars / 4;
|
||||
|
||||
var thought = new SnThinkingThought
|
||||
{
|
||||
SequenceId = sequence.Id,
|
||||
Content = content,
|
||||
Parts = parts,
|
||||
Role = role,
|
||||
TokenCount = tokenCount,
|
||||
ModelName = model,
|
||||
Chunks = chunks ?? new List<SnThinkingChunk>(),
|
||||
};
|
||||
db.ThinkingThoughts.Add(thought);
|
||||
|
||||
|
||||
// Update sequence total tokens only for assistant responses
|
||||
if (role == ThinkingThoughtRole.Assistant)
|
||||
sequence.TotalToken += tokenCount;
|
||||
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
||||
// Invalidate cache for this sequence's thoughts
|
||||
await cache.RemoveGroupAsync($"sequence:{sequence.Id}");
|
||||
|
||||
|
||||
return thought;
|
||||
}
|
||||
|
||||
public async Task<List<SnThinkingThought>> GetPreviousThoughtsAsync(SnThinkingSequence sequence)
|
||||
{
|
||||
var cacheKey = $"thoughts:{sequence.Id}";
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Nager.Holiday" Version="1.0.1" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="MailKit" Version="4.14.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
|
||||
@@ -8,7 +8,7 @@ public class SnThinkingSequence : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(4096)] public string? Topic { get; set; }
|
||||
|
||||
|
||||
public long TotalToken { get; set; }
|
||||
public long PaidToken { get; set; }
|
||||
|
||||
@@ -21,33 +21,48 @@ public enum ThinkingThoughtRole
|
||||
User
|
||||
}
|
||||
|
||||
public enum StreamingContentType
|
||||
public enum ThinkingMessagePartType
|
||||
{
|
||||
Text,
|
||||
Reasoning,
|
||||
FunctionCall,
|
||||
Unknown
|
||||
FunctionResult
|
||||
}
|
||||
|
||||
public class SnThinkingChunk
|
||||
public class SnThinkingMessagePart
|
||||
{
|
||||
public StreamingContentType Type { get; set; }
|
||||
public Dictionary<string, object>? Data { get; set; } = new();
|
||||
public ThinkingMessagePartType Type { get; set; }
|
||||
public string? Text { get; set; }
|
||||
public SnFunctionCall? FunctionCall { get; set; }
|
||||
public SnFunctionResult? FunctionResult { get; set; }
|
||||
}
|
||||
|
||||
public class SnFunctionCall
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public string Arguments { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class SnFunctionResult
|
||||
{
|
||||
public string CallId { get; set; } = null!;
|
||||
public object Result { get; set; } = null!;
|
||||
public bool IsError { get; set; }
|
||||
}
|
||||
|
||||
public class SnThinkingThought : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string? Content { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = [];
|
||||
[Column(TypeName = "jsonb")] public List<SnThinkingChunk> Chunks { get; set; } = [];
|
||||
|
||||
[Column(TypeName = "jsonb")] public List<SnThinkingMessagePart> Parts { get; set; } = [];
|
||||
|
||||
public ThinkingThoughtRole Role { get; set; }
|
||||
|
||||
public long TokenCount { get; set; }
|
||||
[MaxLength(4096)] public string? ModelName { get; set; }
|
||||
|
||||
|
||||
public Guid SequenceId { get; set; }
|
||||
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<PackageReference Include="Markdig" Version="0.43.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeTypes" Version="2.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user