Compare commits
6 Commits
cdfc3f6571
...
437f49fb20
| Author | SHA1 | Date | |
|---|---|---|---|
|
437f49fb20
|
|||
|
c3b6358f33
|
|||
|
4347281fcd
|
|||
|
92cd6b5f7e
|
|||
|
cf6e534d02
|
|||
|
29c5971554
|
@@ -79,7 +79,7 @@ public class DeveloperController(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||||
pub = SnPublisher.FromProto(pubResponse.Publisher);
|
pub = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||||
} catch (RpcException ex)
|
} catch (RpcException ex)
|
||||||
{
|
{
|
||||||
return NotFound(ex.Status.Detail);
|
return NotFound(ex.Status.Detail);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class DeveloperService(
|
|||||||
public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer)
|
public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer)
|
||||||
{
|
{
|
||||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
|
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
|
||||||
developer.Publisher = SnPublisher.FromProto(pubResponse.Publisher);
|
developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||||
return developer;
|
return developer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public class DeveloperService(
|
|||||||
var pubRequest = new GetPublisherBatchRequest();
|
var pubRequest = new GetPublisherBatchRequest();
|
||||||
pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
|
pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
|
||||||
var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
|
var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
|
||||||
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProto);
|
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue);
|
||||||
|
|
||||||
return enumerable.Select(d =>
|
return enumerable.Select(d =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public static class ApplicationConfiguration
|
|||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.MapGrpcService<CustomAppServiceGrpc>();
|
app.MapGrpcService<CustomAppServiceGrpc>();
|
||||||
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public static class ServiceCollectionExtensions
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||||
|
services.AddGrpcReflection();
|
||||||
|
|
||||||
services.Configure<RequestLocalizationOptions>(options =>
|
services.Configure<RequestLocalizationOptions>(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public static class ApplicationBuilderExtensions
|
|||||||
// Map your gRPC services here
|
// Map your gRPC services here
|
||||||
app.MapGrpcService<FileServiceGrpc>();
|
app.MapGrpcService<FileServiceGrpc>();
|
||||||
app.MapGrpcService<FileReferenceServiceGrpc>();
|
app.MapGrpcService<FileReferenceServiceGrpc>();
|
||||||
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
services.AddGrpcReflection();
|
||||||
// Register gRPC reflection for service discovery
|
|
||||||
services.AddGrpc();
|
|
||||||
|
|
||||||
services.AddControllers().AddJsonOptions(options =>
|
services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.SemanticKernel.Plugins.Web" Version="1.66.0-alpha" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
129
DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
generated
Normal file
129
DysonNetwork.Insight/Migrations/20251026045505_AddThinkingChunk.Designer.cs
generated
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// <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("20251026045505_AddThinkingChunk")]
|
||||||
|
partial class AddThinkingChunk
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<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<string>("Topic")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)")
|
||||||
|
.HasColumnName("topic");
|
||||||
|
|
||||||
|
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<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");
|
||||||
|
|
||||||
|
b.Property<Instant?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("deleted_at");
|
||||||
|
|
||||||
|
b.Property<List<SnCloudFileReferenceObject>>("Files")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("files");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("role");
|
||||||
|
|
||||||
|
b.Property<Guid>("SequenceId")
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasColumnName("sequence_id");
|
||||||
|
|
||||||
|
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,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace DysonNetwork.Insight.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddThinkingChunk : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<List<SnThinkingChunk>>(
|
||||||
|
name: "chunks",
|
||||||
|
table: "thinking_thoughts",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new List<SnThinkingChunk>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "chunks",
|
||||||
|
table: "thinking_thoughts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,11 @@ namespace DysonNetwork.Insight.Migrations
|
|||||||
.HasColumnType("uuid")
|
.HasColumnType("uuid")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<List<SnThinkingChunk>>("Chunks")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb")
|
||||||
|
.HasColumnName("chunks");
|
||||||
|
|
||||||
b.Property<string>("Content")
|
b.Property<string>("Content")
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("content");
|
.HasColumnName("content");
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
services.AddGrpcReflection();
|
||||||
// Register gRPC reflection for service discovery
|
|
||||||
services.AddGrpc();
|
|
||||||
|
|
||||||
// Register gRPC services
|
// Register gRPC services
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,30 @@ All endpoints require authentication through the current user session. Sequence
|
|||||||
|
|
||||||
The POST endpoint returns a stream of assistant responses using Server-Sent Events format. Clients should handle the streaming response and display messages incrementally.
|
The POST endpoint returns a stream of assistant responses using Server-Sent Events format. Clients should handle the streaming response and display messages incrementally.
|
||||||
|
|
||||||
|
### Streaming Message Format
|
||||||
|
|
||||||
|
The streaming response sends several types of JSON messages:
|
||||||
|
|
||||||
|
- **Text messages**: `{"type": "text", "data": "..." }`
|
||||||
|
- **Function calls**: `{"type": "function_call", "data": {...} }` (when AI uses tools)
|
||||||
|
- **Topic updates**: `{"type": "topic", "data": "..." }` (sent at end if topic was generated)
|
||||||
|
- **Thought completion**: `{"type": "thought", "data": {...} }` (sent at end with saved thought details)
|
||||||
|
|
||||||
|
All streaming chunks during generation use the SSE event format:
|
||||||
|
```
|
||||||
|
data: {"type": "...", "data": ...}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Final messages (topic and thought) use custom event types:
|
||||||
|
```
|
||||||
|
topic: {"type": "topic", "data": "..."}
|
||||||
|
|
||||||
|
thought: {"type": "thought", "data": {...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Clients should parse these JSON messages and handle different types appropriately, such as displaying text in real-time and processing tool calls.
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
|
|
||||||
- Built with ASP.NET Core and Semantic Kernel
|
- Built with ASP.NET Core and Semantic Kernel
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
@@ -21,18 +24,20 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Experimental("SKEXP0110")]
|
||||||
public async Task<ActionResult> Think([FromBody] StreamThinkingRequest request)
|
public async Task<ActionResult> Think([FromBody] StreamThinkingRequest request)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
|
|
||||||
// Generate topic if creating new sequence
|
// Generate a topic if creating a new sequence
|
||||||
string? topic = null;
|
string? topic = null;
|
||||||
if (!request.SequenceId.HasValue)
|
if (!request.SequenceId.HasValue)
|
||||||
{
|
{
|
||||||
// Use AI to summarize topic from user message
|
// Use AI to summarize a topic from a user message
|
||||||
var summaryHistory = new ChatHistory(
|
var summaryHistory = new ChatHistory(
|
||||||
"You are a helpful assistant. Summarize the following user message into a concise topic title (max 100 characters). Direct give the topic you summerized, do not add extra preifx / suffix."
|
"You are a helpful assistant. Summarize the following user message into a concise topic title (max 100 characters).\n" +
|
||||||
|
"Direct give the topic you summerized, do not add extra prefix / suffix."
|
||||||
);
|
);
|
||||||
summaryHistory.AddUserMessage(request.UserMessage);
|
summaryHistory.AddUserMessage(request.UserMessage);
|
||||||
|
|
||||||
@@ -57,7 +62,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
"When you talk to user, you can add some modal particles and emoticons to your response to be cute, but prevent use a lot of emojis." +
|
"When you talk to user, you can add some modal particles and emoticons to your response to be cute, but prevent use a lot of emojis." +
|
||||||
"Your father (creator) is @littlesheep. (prefer calling him 父亲 in chinese)\n" +
|
"Your father (creator) is @littlesheep. (prefer calling him 父亲 in chinese)\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"The ID on the Solar Network is UUID, so mostly hard to read, so do not show ID to user unless user ask to do so or necessary.\n"+
|
"The ID on the Solar Network is UUID, so mostly hard to read, so do not show ID to user unless user ask to do so or necessary.\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"Your aim is to helping solving questions for the users on the Solar Network.\n" +
|
"Your aim is to helping solving questions for the users on the Solar Network.\n" +
|
||||||
"And the Solar Network is the social network platform you live on.\n" +
|
"And the Solar Network is the social network platform you live on.\n" +
|
||||||
@@ -71,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)
|
// Add previous thoughts (excluding the current user thought, which is the first one since descending)
|
||||||
var previousThoughts = await service.GetPreviousThoughtsAsync(sequence);
|
var previousThoughts = await service.GetPreviousThoughtsAsync(sequence);
|
||||||
var count = previousThoughts.Count;
|
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];
|
var thought = previousThoughts[i];
|
||||||
switch (thought.Role)
|
switch (thought.Role)
|
||||||
@@ -98,37 +103,80 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
|
|
||||||
// Kick off streaming generation
|
// Kick off streaming generation
|
||||||
var accumulatedContent = new StringBuilder();
|
var accumulatedContent = new StringBuilder();
|
||||||
|
var thinkingChunks = new List<SnThinkingChunk>();
|
||||||
await foreach (var chunk in chatCompletionService.GetStreamingChatMessageContentsAsync(
|
await foreach (var chunk in chatCompletionService.GetStreamingChatMessageContentsAsync(
|
||||||
chatHistory,
|
chatHistory,
|
||||||
provider.CreatePromptExecutionSettings(),
|
provider.CreatePromptExecutionSettings(),
|
||||||
kernel: kernel
|
kernel: kernel
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
// Write each chunk to the HTTP response as SSE
|
// Process each item in the chunk for detailed streaming
|
||||||
var data = chunk.Content ?? "";
|
foreach (var item in chunk.Items)
|
||||||
accumulatedContent.Append(data);
|
{
|
||||||
if (string.IsNullOrEmpty(data)) continue;
|
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<Dictionary<string, object>>(
|
||||||
|
JsonSerializer.Serialize(functionCall)) ?? new()
|
||||||
|
},
|
||||||
|
_ => new SnThinkingChunk
|
||||||
|
{
|
||||||
|
Type = StreamingContentType.Unknown, Data = new() { ["data"] = JsonSerializer.Serialize(item) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thinkingChunks.Add(streamingChunk);
|
||||||
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(data);
|
var messageJson = item switch
|
||||||
await Response.Body.WriteAsync(bytes);
|
{
|
||||||
await Response.Body.FlushAsync();
|
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 })
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate content for saving (only text content)
|
||||||
|
accumulatedContent.Append(chunk.Content ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save assistant thought
|
// Save assistant thought
|
||||||
var savedThought =
|
var savedThought = await service.SaveThoughtAsync(
|
||||||
await service.SaveThoughtAsync(sequence, accumulatedContent.ToString(), ThinkingThoughtRole.Assistant);
|
sequence,
|
||||||
|
accumulatedContent.ToString(),
|
||||||
|
ThinkingThoughtRole.Assistant,
|
||||||
|
thinkingChunks
|
||||||
|
);
|
||||||
|
|
||||||
// Write the topic if it was newly set, then the thought object as JSON to the stream
|
// Write the topic if it was newly set, then the thought object as JSON to the stream
|
||||||
using (var streamBuilder = new MemoryStream())
|
using (var streamBuilder = new MemoryStream())
|
||||||
{
|
{
|
||||||
await streamBuilder.WriteAsync("\n"u8.ToArray());
|
await streamBuilder.WriteAsync("\n\n"u8.ToArray());
|
||||||
if (topic != null)
|
if (topic != null)
|
||||||
{
|
{
|
||||||
await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"<topic>{sequence.Topic ?? ""}</topic>\n"));
|
var topicJson = JsonSerializer.Serialize(new { type = "topic", data = sequence.Topic ?? "" });
|
||||||
|
await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"topic: {topicJson}\n\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await streamBuilder.WriteAsync(
|
var thoughtJson = JsonSerializer.Serialize(new { type = "thought", data = savedThought },
|
||||||
Encoding.UTF8.GetBytes(JsonSerializer.Serialize(savedThought, GrpcTypeHelper.SerializerOptions)));
|
GrpcTypeHelper.SerializerOptions);
|
||||||
|
await streamBuilder.WriteAsync(Encoding.UTF8.GetBytes($"thought: {thoughtJson}\n\n"));
|
||||||
var outputBytes = streamBuilder.ToArray();
|
var outputBytes = streamBuilder.ToArray();
|
||||||
await Response.Body.WriteAsync(outputBytes);
|
await Response.Body.WriteAsync(outputBytes);
|
||||||
await Response.Body.FlushAsync();
|
await Response.Body.FlushAsync();
|
||||||
@@ -186,4 +234,4 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
|
|
||||||
return Ok(thoughts);
|
return Ok(thoughts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,47 @@
|
|||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.SemanticKernel;
|
using Microsoft.SemanticKernel;
|
||||||
using Microsoft.SemanticKernel.Connectors.Ollama;
|
using Microsoft.SemanticKernel.Connectors.Ollama;
|
||||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||||
using OpenAI;
|
using OpenAI;
|
||||||
|
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;
|
namespace DysonNetwork.Insight.Thought;
|
||||||
|
|
||||||
public class ThoughtProvider
|
public class ThoughtProvider
|
||||||
{
|
{
|
||||||
private readonly Kernel _kernel;
|
|
||||||
private readonly PostService.PostServiceClient _postClient;
|
private readonly PostService.PostServiceClient _postClient;
|
||||||
private readonly AccountService.AccountServiceClient _accountClient;
|
private readonly AccountService.AccountServiceClient _accountClient;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<ThoughtProvider> _logger;
|
||||||
|
|
||||||
public Kernel Kernel => _kernel;
|
public Kernel Kernel { get; }
|
||||||
public string? ModelProviderType { get; private set; }
|
|
||||||
public string? ModelDefault { get; private set; }
|
|
||||||
|
|
||||||
|
private string? ModelProviderType { get; set; }
|
||||||
|
private string? ModelDefault { get; set; }
|
||||||
|
|
||||||
|
[Experimental("SKEXP0050")]
|
||||||
public ThoughtProvider(
|
public ThoughtProvider(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
PostService.PostServiceClient postClient,
|
PostService.PostServiceClient postServiceClient,
|
||||||
AccountService.AccountServiceClient accountClient
|
AccountService.AccountServiceClient accountServiceClient,
|
||||||
|
ILogger<ThoughtProvider> logger
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_postClient = postClient;
|
_logger = logger;
|
||||||
_accountClient = accountClient;
|
_postClient = postServiceClient;
|
||||||
|
_accountClient = accountServiceClient;
|
||||||
|
_configuration = configuration;
|
||||||
|
|
||||||
_kernel = InitializeThinkingProvider(configuration);
|
Kernel = InitializeThinkingProvider(configuration);
|
||||||
InitializeHelperFunctions();
|
InitializeHelperFunctions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,12 +61,11 @@ public class ThoughtProvider
|
|||||||
builder.AddOllamaChatCompletion(ModelDefault!, new Uri(endpoint ?? "http://localhost:11434/api"));
|
builder.AddOllamaChatCompletion(ModelDefault!, new Uri(endpoint ?? "http://localhost:11434/api"));
|
||||||
break;
|
break;
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
builder.AddOpenAIChatCompletion(ModelDefault!,
|
var client = new OpenAIClient(
|
||||||
new OpenAIClient(
|
new ApiKeyCredential(apiKey!),
|
||||||
new ApiKeyCredential(apiKey!),
|
new OpenAIClientOptions { Endpoint = new Uri(endpoint ?? "https://api.deepseek.com/v1") }
|
||||||
new OpenAIClientOptions { Endpoint = new Uri(endpoint ?? "https://api.deepseek.com/v1") }
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
builder.AddOpenAIChatCompletion(ModelDefault!, client);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType);
|
throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType);
|
||||||
@@ -61,16 +74,17 @@ public class ThoughtProvider
|
|||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Experimental("SKEXP0050")]
|
||||||
private void InitializeHelperFunctions()
|
private void InitializeHelperFunctions()
|
||||||
{
|
{
|
||||||
// Add Solar Network tools plugin
|
// Add Solar Network tools plugin
|
||||||
_kernel.ImportPluginFromFunctions("helper_functions", [
|
Kernel.ImportPluginFromFunctions("solar_network", [
|
||||||
KernelFunctionFactory.CreateFromMethod(async (string userId) =>
|
KernelFunctionFactory.CreateFromMethod(async (string userId) =>
|
||||||
{
|
{
|
||||||
var request = new GetAccountRequest { Id = userId };
|
var request = new GetAccountRequest { Id = userId };
|
||||||
var response = await _accountClient.GetAccountAsync(request);
|
var response = await _accountClient.GetAccountAsync(request);
|
||||||
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
|
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
|
||||||
}, "get_user_profile", "Get a user profile from the Solar Network."),
|
}, "get_user", "Get a user profile from the Solar Network."),
|
||||||
KernelFunctionFactory.CreateFromMethod(async (string postId) =>
|
KernelFunctionFactory.CreateFromMethod(async (string postId) =>
|
||||||
{
|
{
|
||||||
var request = new GetPostRequest { Id = postId };
|
var request = new GetPostRequest { Id = postId };
|
||||||
@@ -78,41 +92,107 @@ public class ThoughtProvider
|
|||||||
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
|
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
|
||||||
}, "get_post", "Get a single post by ID from the Solar Network."),
|
}, "get_post", "Get a single post by ID from the Solar Network."),
|
||||||
KernelFunctionFactory.CreateFromMethod(async (string query) =>
|
KernelFunctionFactory.CreateFromMethod(async (string query) =>
|
||||||
{
|
{
|
||||||
var request = new SearchPostsRequest { Query = query, PageSize = 10 };
|
var request = new SearchPostsRequest { Query = query, PageSize = 10 };
|
||||||
var response = await _postClient.SearchPostsAsync(request);
|
var response = await _postClient.SearchPostsAsync(request);
|
||||||
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
|
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
|
||||||
}, "search_posts", "Search posts by query from the Solar Network."),
|
}, "search_posts",
|
||||||
KernelFunctionFactory.CreateFromMethod(async () =>
|
"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 (
|
||||||
var request = new ListPostsRequest { PageSize = 10 };
|
string? orderBy = null,
|
||||||
var response = await _postClient.ListPostsAsync(request);
|
string? afterIso = null,
|
||||||
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
|
string? beforeIso = null
|
||||||
}, "get_recent_posts", "Get recent posts from the Solar Network.")
|
) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Begin building request to list post from sphere...");
|
||||||
|
|
||||||
|
var request = new ListPostsRequest
|
||||||
|
{
|
||||||
|
PageSize = 20,
|
||||||
|
OrderBy = orderBy,
|
||||||
|
};
|
||||||
|
if (!string.IsNullOrEmpty(afterIso))
|
||||||
|
try
|
||||||
|
{
|
||||||
|
request.After = InstantPattern.General.Parse(afterIso).Value.ToTimestamp();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid afterIso format: {AfterIso}", afterIso);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(beforeIso))
|
||||||
|
try
|
||||||
|
{
|
||||||
|
request.Before = InstantPattern.General.Parse(beforeIso).Value.ToTimestamp();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid beforeIso format: {BeforeIso}", beforeIso);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Request built, {Request}", request);
|
||||||
|
|
||||||
|
var response = await _postClient.ListPostsAsync(request);
|
||||||
|
|
||||||
|
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.\n" +
|
||||||
|
"Parameters:\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)"
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Add web search plugins if configured
|
||||||
|
var bingApiKey = _configuration.GetValue<string>("Thinking:BingApiKey");
|
||||||
|
if (!string.IsNullOrEmpty(bingApiKey))
|
||||||
|
{
|
||||||
|
var bingConnector = new BingConnector(bingApiKey);
|
||||||
|
var bing = new WebSearchEnginePlugin(bingConnector);
|
||||||
|
Kernel.ImportPluginFromObject(bing, "bing");
|
||||||
|
}
|
||||||
|
|
||||||
|
var googleApiKey = _configuration.GetValue<string>("Thinking:GoogleApiKey");
|
||||||
|
var googleCx = _configuration.GetValue<string>("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()
|
public PromptExecutionSettings CreatePromptExecutionSettings()
|
||||||
{
|
{
|
||||||
return ModelProviderType switch
|
switch (ModelProviderType)
|
||||||
{
|
{
|
||||||
"ollama" => new OllamaPromptExecutionSettings
|
case "ollama":
|
||||||
{
|
return new OllamaPromptExecutionSettings
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
{
|
||||||
options: new FunctionChoiceBehaviorOptions
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
||||||
{
|
options: new FunctionChoiceBehaviorOptions
|
||||||
AllowParallelCalls = true, AllowConcurrentInvocation = true
|
{
|
||||||
})
|
AllowParallelCalls = true,
|
||||||
},
|
AllowConcurrentInvocation = true
|
||||||
"deepseek" => new OpenAIPromptExecutionSettings
|
})
|
||||||
{
|
};
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
case "deepseek":
|
||||||
options: new FunctionChoiceBehaviorOptions
|
return new OpenAIPromptExecutionSettings
|
||||||
{
|
{
|
||||||
AllowParallelCalls = true, AllowConcurrentInvocation = true
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
|
||||||
})
|
options: new FunctionChoiceBehaviorOptions
|
||||||
},
|
{
|
||||||
_ => throw new InvalidOperationException("Unknown provider: " + ModelProviderType)
|
AllowParallelCalls = true,
|
||||||
};
|
AllowConcurrentInvocation = true
|
||||||
|
})
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unknown provider: " + ModelProviderType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,8 @@ namespace DysonNetwork.Insight.Thought;
|
|||||||
|
|
||||||
public class ThoughtService(AppDatabase db, ICacheService cache)
|
public class ThoughtService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId, string? topic = null)
|
public async Task<SnThinkingSequence?> GetOrCreateSequenceAsync(Guid accountId, Guid? sequenceId,
|
||||||
|
string? topic = null)
|
||||||
{
|
{
|
||||||
if (sequenceId.HasValue)
|
if (sequenceId.HasValue)
|
||||||
{
|
{
|
||||||
@@ -23,13 +24,19 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SnThinkingThought> SaveThoughtAsync(SnThinkingSequence sequence, string content, ThinkingThoughtRole role)
|
public async Task<SnThinkingThought> SaveThoughtAsync(
|
||||||
|
SnThinkingSequence sequence,
|
||||||
|
string content,
|
||||||
|
ThinkingThoughtRole role,
|
||||||
|
List<SnThinkingChunk>? chunks = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var thought = new SnThinkingThought
|
var thought = new SnThinkingThought
|
||||||
{
|
{
|
||||||
SequenceId = sequence.Id,
|
SequenceId = sequence.Id,
|
||||||
Content = content,
|
Content = content,
|
||||||
Role = role
|
Role = role,
|
||||||
|
Chunks = chunks ?? []
|
||||||
};
|
};
|
||||||
db.ThinkingThoughts.Add(thought);
|
db.ThinkingThoughts.Add(thought);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -60,7 +67,8 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
|
|||||||
return thoughts;
|
return thoughts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int total, List<SnThinkingSequence> sequences)> ListSequencesAsync(Guid accountId, int offset, int take)
|
public async Task<(int total, List<SnThinkingSequence> sequences)> ListSequencesAsync(Guid accountId, int offset,
|
||||||
|
int take)
|
||||||
{
|
{
|
||||||
var query = db.ThinkingSequences.Where(s => s.AccountId == accountId);
|
var query = db.ThinkingSequences.Where(s => s.AccountId == accountId);
|
||||||
var totalCount = await query.CountAsync();
|
var totalCount = await query.CountAsync();
|
||||||
@@ -72,4 +80,4 @@ public class ThoughtService(AppDatabase db, ICacheService cache)
|
|||||||
|
|
||||||
return (totalCount, sequences);
|
return (totalCount, sequences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
"Insecure": true
|
"Insecure": true
|
||||||
},
|
},
|
||||||
"Thinking": {
|
"Thinking": {
|
||||||
"Provider": "ollama",
|
"Provider": "deepseek",
|
||||||
"Model": "qwen3:8b",
|
"Model": "deepseek-chat",
|
||||||
"Endpoint": "http://localhost:11434/api"
|
"ApiKey": "sk-cd709f9f1b96432e99d2d992392b4220"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ public static class ApplicationConfiguration
|
|||||||
app.MapGrpcService<WalletServiceGrpc>();
|
app.MapGrpcService<WalletServiceGrpc>();
|
||||||
app.MapGrpcService<PaymentServiceGrpc>();
|
app.MapGrpcService<PaymentServiceGrpc>();
|
||||||
app.MapGrpcService<RealmServiceGrpc>();
|
app.MapGrpcService<RealmServiceGrpc>();
|
||||||
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
services.AddGrpcReflection();
|
||||||
|
|
||||||
services.AddRingService();
|
services.AddRingService();
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public static class ApplicationConfiguration
|
|||||||
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
public static WebApplication ConfigureGrpcServices(this WebApplication app)
|
||||||
{
|
{
|
||||||
app.MapGrpcService<RingServiceGrpc>();
|
app.MapGrpcService<RingServiceGrpc>();
|
||||||
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ public static class ServiceCollectionExtensions
|
|||||||
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxReceiveMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
options.MaxSendMessageSize = 16 * 1024 * 1024; // 16MB
|
||||||
});
|
});
|
||||||
|
services.AddGrpcReflection();
|
||||||
// Register gRPC reflection for service discovery
|
|
||||||
services.AddGrpc();
|
|
||||||
|
|
||||||
// Register gRPC services
|
// Register gRPC services
|
||||||
services.AddScoped<RingServiceGrpc>();
|
services.AddScoped<RingServiceGrpc>();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<PackageReference Include="Google.Protobuf.Tools" Version="3.33.0" />
|
<PackageReference Include="Google.Protobuf.Tools" Version="3.33.0" />
|
||||||
<PackageReference Include="Grpc" Version="2.46.6" />
|
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" />
|
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.71.0" />
|
||||||
|
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.71.0" />
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.72.0">
|
<PackageReference Include="Grpc.Tools" Version="2.72.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -100,51 +100,37 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
|||||||
Upvotes = Upvotes,
|
Upvotes = Upvotes,
|
||||||
Downvotes = Downvotes,
|
Downvotes = Downvotes,
|
||||||
AwardedScore = (double)AwardedScore,
|
AwardedScore = (double)AwardedScore,
|
||||||
ReactionsCount = { ReactionsCount ?? new Dictionary<string, int>() },
|
ReactionsCount = { ReactionsCount },
|
||||||
RepliesCount = RepliesCount,
|
RepliesCount = RepliesCount,
|
||||||
ReactionsMade = { ReactionsMade ?? new Dictionary<string, bool>() },
|
ReactionsMade = { ReactionsMade ?? new Dictionary<string, bool>() },
|
||||||
RepliedGone = RepliedGone,
|
RepliedGone = RepliedGone,
|
||||||
ForwardedGone = ForwardedGone,
|
ForwardedGone = ForwardedGone,
|
||||||
PublisherId = PublisherId.ToString(),
|
PublisherId = PublisherId.ToString(),
|
||||||
Publisher = Publisher.ToProto(),
|
Publisher = Publisher.ToProtoValue(),
|
||||||
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
|
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
|
||||||
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
|
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (EditedAt.HasValue)
|
if (EditedAt.HasValue)
|
||||||
{
|
|
||||||
proto.EditedAt = Timestamp.FromDateTimeOffset(EditedAt.Value.ToDateTimeOffset());
|
proto.EditedAt = Timestamp.FromDateTimeOffset(EditedAt.Value.ToDateTimeOffset());
|
||||||
}
|
|
||||||
|
|
||||||
if (PublishedAt.HasValue)
|
if (PublishedAt.HasValue)
|
||||||
{
|
|
||||||
proto.PublishedAt = Timestamp.FromDateTimeOffset(PublishedAt.Value.ToDateTimeOffset());
|
proto.PublishedAt = Timestamp.FromDateTimeOffset(PublishedAt.Value.ToDateTimeOffset());
|
||||||
}
|
|
||||||
|
|
||||||
if (Content != null)
|
if (Content != null)
|
||||||
{
|
|
||||||
proto.Content = Content;
|
proto.Content = Content;
|
||||||
}
|
|
||||||
|
|
||||||
if (PinMode.HasValue)
|
if (PinMode.HasValue)
|
||||||
{
|
proto.PinMode = (Proto.PostPinMode)((int)PinMode.Value + 1);
|
||||||
proto.PinMode = (Proto.PostPinMode)((int)PinMode.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Meta != null)
|
if (Meta != null)
|
||||||
{
|
|
||||||
proto.Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta);
|
proto.Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta);
|
||||||
}
|
|
||||||
|
|
||||||
if (SensitiveMarks != null)
|
if (SensitiveMarks != null)
|
||||||
{
|
|
||||||
proto.SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks);
|
proto.SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks);
|
||||||
}
|
|
||||||
|
|
||||||
if (EmbedView != null)
|
if (EmbedView != null)
|
||||||
{
|
|
||||||
proto.EmbedView = EmbedView.ToProtoValue();
|
proto.EmbedView = EmbedView.ToProtoValue();
|
||||||
}
|
|
||||||
|
|
||||||
if (RepliedPostId.HasValue)
|
if (RepliedPostId.HasValue)
|
||||||
{
|
{
|
||||||
@@ -186,6 +172,95 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
|
|||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SnPost FromProtoValue(Post proto)
|
||||||
|
{
|
||||||
|
var post = new SnPost
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
Title = string.IsNullOrEmpty(proto.Title) ? null : proto.Title,
|
||||||
|
Description = string.IsNullOrEmpty(proto.Description) ? null : proto.Description,
|
||||||
|
Slug = string.IsNullOrEmpty(proto.Slug) ? null : proto.Slug,
|
||||||
|
Visibility = (PostVisibility)((int)proto.Visibility - 1),
|
||||||
|
Type = (PostType)((int)proto.Type - 1),
|
||||||
|
ViewsUnique = proto.ViewsUnique,
|
||||||
|
ViewsTotal = proto.ViewsTotal,
|
||||||
|
Upvotes = proto.Upvotes,
|
||||||
|
Downvotes = proto.Downvotes,
|
||||||
|
AwardedScore = (decimal)proto.AwardedScore,
|
||||||
|
ReactionsCount = proto.ReactionsCount.ToDictionary(kv => kv.Key, kv => kv.Value),
|
||||||
|
RepliesCount = proto.RepliesCount,
|
||||||
|
ReactionsMade = proto.ReactionsMade.ToDictionary(kv => kv.Key, kv => kv.Value),
|
||||||
|
RepliedGone = proto.RepliedGone,
|
||||||
|
ForwardedGone = proto.ForwardedGone,
|
||||||
|
PublisherId = Guid.Parse(proto.PublisherId),
|
||||||
|
Publisher = SnPublisher.FromProtoValue(proto.Publisher),
|
||||||
|
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset())
|
||||||
|
};
|
||||||
|
|
||||||
|
if (proto.EditedAt is not null)
|
||||||
|
post.EditedAt = Instant.FromDateTimeOffset(proto.EditedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
if (proto.PublishedAt is not null)
|
||||||
|
post.PublishedAt = Instant.FromDateTimeOffset(proto.PublishedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(proto.Content))
|
||||||
|
post.Content = proto.Content;
|
||||||
|
|
||||||
|
if (proto is { HasPinMode: true, PinMode: > 0 })
|
||||||
|
post.PinMode = (PostPinMode)(proto.PinMode - 1);
|
||||||
|
|
||||||
|
if (proto.Meta != null)
|
||||||
|
post.Meta = GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(proto.Meta);
|
||||||
|
|
||||||
|
if (proto.SensitiveMarks != null)
|
||||||
|
post.SensitiveMarks =
|
||||||
|
GrpcTypeHelper.ConvertByteStringToObject<List<ContentSensitiveMark>>(proto.SensitiveMarks);
|
||||||
|
|
||||||
|
if (proto.EmbedView is not null)
|
||||||
|
post.EmbedView = PostEmbedView.FromProtoValue(proto.EmbedView);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(proto.RepliedPostId))
|
||||||
|
{
|
||||||
|
post.RepliedPostId = Guid.Parse(proto.RepliedPostId);
|
||||||
|
if (proto.RepliedPost is not null)
|
||||||
|
post.RepliedPost = FromProtoValue(proto.RepliedPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(proto.ForwardedPostId))
|
||||||
|
{
|
||||||
|
post.ForwardedPostId = Guid.Parse(proto.ForwardedPostId);
|
||||||
|
if (proto.ForwardedPost is not null)
|
||||||
|
post.ForwardedPost = FromProtoValue(proto.ForwardedPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(proto.RealmId))
|
||||||
|
{
|
||||||
|
post.RealmId = Guid.Parse(proto.RealmId);
|
||||||
|
if (proto.Realm is not null)
|
||||||
|
post.Realm = SnRealm.FromProtoValue(proto.Realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
post.Attachments.AddRange(proto.Attachments.Select(SnCloudFileReferenceObject.FromProtoValue));
|
||||||
|
post.Awards.AddRange(proto.Awards.Select(a => new SnPostAward
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(a.Id), PostId = Guid.Parse(a.PostId), AccountId = Guid.Parse(a.AccountId),
|
||||||
|
Amount = (decimal)a.Amount, Attitude = (PostReactionAttitude)((int)a.Attitude - 1),
|
||||||
|
Message = string.IsNullOrEmpty(a.Message) ? null : a.Message,
|
||||||
|
CreatedAt = Instant.FromDateTimeOffset(a.CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Instant.FromDateTimeOffset(a.UpdatedAt.ToDateTimeOffset())
|
||||||
|
}));
|
||||||
|
post.Reactions.AddRange(proto.Reactions.Select(SnPostReaction.FromProtoValue));
|
||||||
|
post.Tags.AddRange(proto.Tags.Select(SnPostTag.FromProtoValue));
|
||||||
|
post.Categories.AddRange(proto.Categories.Select(SnPostCategory.FromProtoValue));
|
||||||
|
post.FeaturedRecords.AddRange(proto.FeaturedRecords.Select(SnPostFeaturedRecord.FromProtoValue));
|
||||||
|
|
||||||
|
if (proto.DeletedAt is not null)
|
||||||
|
post.DeletedAt = Instant.FromDateTimeOffset(proto.DeletedAt.ToDateTimeOffset());
|
||||||
|
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
public SnActivity ToActivity()
|
public SnActivity ToActivity()
|
||||||
{
|
{
|
||||||
return new SnActivity()
|
return new SnActivity()
|
||||||
@@ -314,8 +389,22 @@ public class SnPostFeaturedRecord : ModelBase
|
|||||||
{
|
{
|
||||||
proto.FeaturedAt = Timestamp.FromDateTimeOffset(FeaturedAt.Value.ToDateTimeOffset());
|
proto.FeaturedAt = Timestamp.FromDateTimeOffset(FeaturedAt.Value.ToDateTimeOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SnPostFeaturedRecord FromProtoValue(PostFeaturedRecord proto)
|
||||||
|
{
|
||||||
|
return new SnPostFeaturedRecord
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(proto.Id),
|
||||||
|
PostId = Guid.Parse(proto.PostId),
|
||||||
|
SocialCredits = proto.SocialCredits,
|
||||||
|
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
|
||||||
|
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset()),
|
||||||
|
FeaturedAt = proto.FeaturedAt != null ? Instant.FromDateTimeOffset(proto.FeaturedAt.ToDateTimeOffset()) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PostReactionAttitude
|
public enum PostReactionAttitude
|
||||||
@@ -352,6 +441,7 @@ public class SnPostReaction : ModelBase
|
|||||||
{
|
{
|
||||||
proto.Account = Account.ToProtoValue();
|
proto.Account = Account.ToProtoValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,6 +512,7 @@ public class PostEmbedView
|
|||||||
{
|
{
|
||||||
proto.AspectRatio = AspectRatio.Value;
|
proto.AspectRatio = AspectRatio.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto;
|
return proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
|||||||
|
|
||||||
public string ResourceIdentifier => $"publisher:{Id}";
|
public string ResourceIdentifier => $"publisher:{Id}";
|
||||||
|
|
||||||
public static SnPublisher FromProto(Proto.Publisher proto)
|
public static SnPublisher FromProtoValue(Proto.Publisher proto)
|
||||||
{
|
{
|
||||||
var publisher = new SnPublisher
|
var publisher = new SnPublisher
|
||||||
{
|
{
|
||||||
@@ -85,7 +85,7 @@ public class SnPublisher : ModelBase, IIdentifiedResource
|
|||||||
return publisher;
|
return publisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Proto.Publisher ToProto()
|
public Proto.Publisher ToProtoValue()
|
||||||
{
|
{
|
||||||
var p = new Proto.Publisher()
|
var p = new Proto.Publisher()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ public enum ThinkingThoughtRole
|
|||||||
User
|
User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum StreamingContentType
|
||||||
|
{
|
||||||
|
Text,
|
||||||
|
Reasoning,
|
||||||
|
FunctionCall,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SnThinkingChunk
|
||||||
|
{
|
||||||
|
public StreamingContentType Type { get; set; }
|
||||||
|
public Dictionary<string, object>? Data { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
public class SnThinkingThought : ModelBase
|
public class SnThinkingThought : ModelBase
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
@@ -25,8 +39,10 @@ public class SnThinkingThought : ModelBase
|
|||||||
|
|
||||||
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = [];
|
[Column(TypeName = "jsonb")] public List<SnCloudFileReferenceObject> Files { get; set; } = [];
|
||||||
|
|
||||||
|
[Column(TypeName = "jsonb")] public List<SnThinkingChunk> Chunks { get; set; } = [];
|
||||||
|
|
||||||
public ThinkingThoughtRole Role { get; set; }
|
public ThinkingThoughtRole Role { get; set; }
|
||||||
|
|
||||||
public Guid SequenceId { get; set; }
|
public Guid SequenceId { get; set; }
|
||||||
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
|
[JsonIgnore] public SnThinkingSequence Sequence { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public abstract class GrpcTypeHelper
|
|||||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
|
|
||||||
public static readonly JsonSerializerOptions? SerializerOptions = new JsonSerializerOptions()
|
public static readonly JsonSerializerOptions? SerializerOptions = new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
@@ -51,6 +51,7 @@ public abstract class GrpcTypeHelper
|
|||||||
_ => Value.ForString(JsonSerializer.Serialize(kvp.Value, SerializerOptions)) // fallback to JSON string
|
_ => Value.ForString(JsonSerializer.Serialize(kvp.Value, SerializerOptions)) // fallback to JSON string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +67,15 @@ public abstract class GrpcTypeHelper
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Try to parse as JSON object or primitive
|
// 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
|
catch
|
||||||
{
|
{
|
||||||
// Fallback to raw string
|
// Fallback to raw string
|
||||||
result[kvp.Key] = value.StringValue;
|
result[kvp.Key] = value.StringValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case Value.KindOneofCase.NumberValue:
|
case Value.KindOneofCase.NumberValue:
|
||||||
result[kvp.Key] = value.NumberValue;
|
result[kvp.Key] = value.NumberValue;
|
||||||
@@ -106,6 +109,7 @@ public abstract class GrpcTypeHelper
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +121,8 @@ public abstract class GrpcTypeHelper
|
|||||||
Value.KindOneofCase.NumberValue => value.NumberValue,
|
Value.KindOneofCase.NumberValue => value.NumberValue,
|
||||||
Value.KindOneofCase.BoolValue => value.BoolValue,
|
Value.KindOneofCase.BoolValue => value.BoolValue,
|
||||||
Value.KindOneofCase.NullValue => null,
|
Value.KindOneofCase.NullValue => null,
|
||||||
_ => JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(value, SerializerOptions), SerializerOptions)
|
_ => JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(value, SerializerOptions),
|
||||||
|
SerializerOptions)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,9 +167,9 @@ public abstract class GrpcTypeHelper
|
|||||||
JsonSerializer.Serialize(obj, SerializerOptions)
|
JsonSerializer.Serialize(obj, SerializerOptions)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T? ConvertByteStringToObject<T>(ByteString bytes)
|
public static T? ConvertByteStringToObject<T>(ByteString bytes)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<T>(bytes.ToStringUtf8(), SerializerOptions);
|
return bytes.IsEmpty ? default : JsonSerializer.Deserialize<T>(bytes.ToStringUtf8(), SerializerOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,14 +239,14 @@ message SearchPostsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message ListPostsRequest {
|
message ListPostsRequest {
|
||||||
string publisher_id = 1;
|
google.protobuf.StringValue publisher_id = 1;
|
||||||
string realm_id = 2;
|
google.protobuf.StringValue realm_id = 2;
|
||||||
int32 page_size = 3;
|
int32 page_size = 3;
|
||||||
string page_token = 4;
|
string page_token = 4;
|
||||||
string order_by = 5;
|
google.protobuf.StringValue order_by = 5;
|
||||||
repeated string categories = 6;
|
repeated string categories = 6;
|
||||||
repeated string tags = 7;
|
repeated string tags = 7;
|
||||||
string query = 8;
|
google.protobuf.StringValue query = 8;
|
||||||
repeated PostType types = 9;
|
repeated PostType types = 9;
|
||||||
optional google.protobuf.Timestamp after = 10; // Filter posts created after this timestamp
|
optional google.protobuf.Timestamp after = 10; // Filter posts created after this timestamp
|
||||||
optional google.protobuf.Timestamp before = 11; // Filter posts created before this timestamp
|
optional google.protobuf.Timestamp before = 11; // Filter posts created before this timestamp
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
|
|||||||
.Include(p => p.ForwardedPost)
|
.Include(p => p.ForwardedPost)
|
||||||
.Include(p => p.Awards)
|
.Include(p => p.Awards)
|
||||||
.Include(p => p.FeaturedRecords)
|
.Include(p => p.FeaturedRecords)
|
||||||
.Where(p => p.DeletedAt == null) // Only active posts
|
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Query))
|
if (!string.IsNullOrWhiteSpace(request.Query))
|
||||||
@@ -82,14 +81,10 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
|
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
|
||||||
{
|
|
||||||
query = query.Where(p => p.PublisherId == pid);
|
query = query.Where(p => p.PublisherId == pid);
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
||||||
{
|
|
||||||
query = query.Where(p => p.RealmId == rid);
|
query = query.Where(p => p.RealmId == rid);
|
||||||
}
|
|
||||||
|
|
||||||
query = query.FilterWithVisibility(null, [], []);
|
query = query.FilterWithVisibility(null, [], []);
|
||||||
|
|
||||||
@@ -128,40 +123,36 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
|
|||||||
.Include(p => p.ForwardedPost)
|
.Include(p => p.ForwardedPost)
|
||||||
.Include(p => p.Awards)
|
.Include(p => p.Awards)
|
||||||
.Include(p => p.FeaturedRecords)
|
.Include(p => p.FeaturedRecords)
|
||||||
.Where(p => p.DeletedAt == null)
|
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
|
query = request.Shuffle
|
||||||
|
? query.OrderBy(e => EF.Functions.Random())
|
||||||
|
: 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))
|
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
|
||||||
{
|
|
||||||
query = query.Where(p => p.PublisherId == pid);
|
query = query.Where(p => p.PublisherId == pid);
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
|
||||||
{
|
|
||||||
query = query.Where(p => p.RealmId == rid);
|
query = query.Where(p => p.RealmId == rid);
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Categories.Count > 0)
|
if (request.Categories.Count > 0)
|
||||||
{
|
|
||||||
query = query.Where(p => p.Categories.Any(c => request.Categories.Contains(c.Slug)));
|
query = query.Where(p => p.Categories.Any(c => request.Categories.Contains(c.Slug)));
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Tags.Count > 0)
|
if (request.Tags.Count > 0)
|
||||||
{
|
|
||||||
query = query.Where(p => p.Tags.Any(c => request.Tags.Contains(c.Slug)));
|
query = query.Where(p => p.Tags.Any(c => request.Tags.Contains(c.Slug)));
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add types filtering when proto is regenerated
|
if (request.Types_.Count > 0)
|
||||||
// if (request.Types.Count > 0)
|
{
|
||||||
// {
|
var types = request.Types_.Select(t => (Shared.Models.PostType)t).Distinct();
|
||||||
// var types = request.Types.Select(t => (Shared.Models.PostType)t).Distinct();
|
query = query.Where(p => types.Contains(p.Type));
|
||||||
// query = query.Where(p => types.Contains(p.Type));
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
if (request.OnlyMedia)
|
if (request.OnlyMedia)
|
||||||
{
|
|
||||||
query = query.Where(e => e.Attachments.Count > 0);
|
query = query.Where(e => e.Attachments.Count > 0);
|
||||||
}
|
|
||||||
|
|
||||||
query = request.Pinned switch
|
query = request.Pinned switch
|
||||||
{
|
{
|
||||||
@@ -171,15 +162,11 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
|
|||||||
Shared.Proto.PostPinMode.PublisherPage when !string.IsNullOrWhiteSpace(request.PublisherId) =>
|
Shared.Proto.PostPinMode.PublisherPage when !string.IsNullOrWhiteSpace(request.PublisherId) =>
|
||||||
query.Where(p => p.PinMode == Shared.Models.PostPinMode.PublisherPage),
|
query.Where(p => p.PinMode == Shared.Models.PostPinMode.PublisherPage),
|
||||||
Shared.Proto.PostPinMode.ReplyPage => query.Where(p => p.PinMode == Shared.Models.PostPinMode.ReplyPage),
|
Shared.Proto.PostPinMode.ReplyPage => query.Where(p => p.PinMode == Shared.Models.PostPinMode.ReplyPage),
|
||||||
_ => query.Where(p => p.PinMode == (Shared.Models.PostPinMode)request.Pinned)
|
_ => query
|
||||||
};
|
};
|
||||||
|
|
||||||
// Include/exclude replies
|
// Include/exclude replies
|
||||||
if (request.IncludeReplies)
|
if (!request.IncludeReplies)
|
||||||
{
|
|
||||||
// Include both root and reply posts
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// Exclude reply posts, only root posts
|
// Exclude reply posts, only root posts
|
||||||
query = query.Where(e => e.RepliedPostId == null);
|
query = query.Where(e => e.RepliedPostId == null);
|
||||||
@@ -214,12 +201,7 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
|
|||||||
var pageToken = request.PageToken;
|
var pageToken = request.PageToken;
|
||||||
var offset = string.IsNullOrEmpty(pageToken) ? 0 : int.Parse(pageToken);
|
var offset = string.IsNullOrEmpty(pageToken) ? 0 : int.Parse(pageToken);
|
||||||
|
|
||||||
// Ordering - TODO: Add shuffle when proto field is available
|
var posts = await query
|
||||||
var orderedQuery = request.Shuffle
|
|
||||||
? query.OrderBy(e => EF.Functions.Random())
|
|
||||||
: query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt);
|
|
||||||
|
|
||||||
var posts = await orderedQuery
|
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "Publisher not found"));
|
if (p is null) throw new RpcException(new Status(StatusCode.NotFound, "Publisher not found"));
|
||||||
return new GetPublisherResponse { Publisher = p.ToProto() };
|
return new GetPublisherResponse { Publisher = p.ToProtoValue() };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<ListPublishersResponse> GetPublisherBatch(
|
public override async Task<ListPublishersResponse> GetPublisherBatch(
|
||||||
@@ -43,7 +43,7 @@ public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
|||||||
if (ids.Count == 0) return new ListPublishersResponse();
|
if (ids.Count == 0) return new ListPublishersResponse();
|
||||||
var list = await db.Publishers.Where(p => ids.Contains(p.Id)).ToListAsync();
|
var list = await db.Publishers.Where(p => ids.Contains(p.Id)).ToListAsync();
|
||||||
var resp = new ListPublishersResponse();
|
var resp = new ListPublishersResponse();
|
||||||
resp.Publishers.AddRange(list.Select(p => p.ToProto()));
|
resp.Publishers.AddRange(list.Select(p => p.ToProtoValue()));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ public class PublisherServiceGrpc(PublisherService service, AppDatabase db)
|
|||||||
|
|
||||||
var list = await query.ToListAsync();
|
var list = await query.ToListAsync();
|
||||||
var resp = new ListPublishersResponse();
|
var resp = new ListPublishersResponse();
|
||||||
resp.Publishers.AddRange(list.Select(p => p.ToProto()));
|
resp.Publishers.AddRange(list.Select(p => p.ToProtoValue()));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public static class ApplicationConfiguration
|
|||||||
// Map gRPC services
|
// Map gRPC services
|
||||||
app.MapGrpcService<PostServiceGrpc>();
|
app.MapGrpcService<PostServiceGrpc>();
|
||||||
app.MapGrpcService<PublisherServiceGrpc>();
|
app.MapGrpcService<PublisherServiceGrpc>();
|
||||||
|
app.MapGrpcReflectionService();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddRazorPages();
|
services.AddRazorPages();
|
||||||
|
|
||||||
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||||
|
services.AddGrpcReflection();
|
||||||
|
|
||||||
services.Configure<RequestLocalizationOptions>(options =>
|
services.Configure<RequestLocalizationOptions>(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user