♻️ Refactored the thought Solar Network related plugins

This commit is contained in:
2025-11-15 13:05:58 +08:00
parent 05985e0852
commit b5f9faa724
8 changed files with 153 additions and 84 deletions

View File

@@ -1,3 +1,4 @@
using DysonNetwork.Insight.Thought;
using Quartz;
namespace DysonNetwork.Insight.Startup;

View File

@@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using DysonNetwork.Insight.Thought;
using DysonNetwork.Shared.Cache;
using DysonNetwork.Shared.Registry;
using Microsoft.SemanticKernel;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
@@ -63,17 +64,13 @@ 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>();
// Add gRPC clients for ThoughtService
services.AddGrpcClient<Shared.Proto.PaymentService.PaymentServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
services.AddGrpcClient<Shared.Proto.WalletService.WalletServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true });
return services;
}
}

View File

@@ -0,0 +1,29 @@
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using Microsoft.IdentityModel.Tokens;
using Microsoft.SemanticKernel;
namespace DysonNetwork.Insight.Thought.Plugins;
public class SnAccountKernelPlugin(
AccountService.AccountServiceClient accountClient
)
{
[KernelFunction("get_account")]
public async Task<SnAccount?> GetAccount(string userId)
{
var request = new GetAccountRequest { Id = userId };
var response = await accountClient.GetAccountAsync(request);
if (response is null) return null;
return SnAccount.FromProtoValue(response);
}
[KernelFunction("get_account_by_name")]
public async Task<SnAccount?> GetAccountByName(string username)
{
var request = new LookupAccountBatchRequest();
request.Names.Add(username);
var response = await accountClient.LookupAccountBatchAsync(request);
return response.Accounts.IsNullOrEmpty() ? null : SnAccount.FromProtoValue(response.Accounts[0]);
}
}

View File

@@ -0,0 +1,98 @@
using System.ComponentModel;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using Microsoft.SemanticKernel;
using NodaTime;
using NodaTime.Serialization.Protobuf;
using NodaTime.Text;
namespace DysonNetwork.Insight.Thought.Plugins;
public class SnPostKernelPlugin(
PostService.PostServiceClient postClient
)
{
[KernelFunction("get_post")]
public async Task<SnPost?> GetPost(string postId)
{
var request = new GetPostRequest { Id = postId };
var response = await postClient.GetPostAsync(request);
return response is null ? null : SnPost.FromProtoValue(response);
}
[KernelFunction("search_posts")]
[Description("Perform a full-text search in all Solar Network posts.")]
public async Task<List<SnPost>> SearchPostsContent(string contentQuery, int pageSize = 10, int page = 1)
{
var request = new SearchPostsRequest
{
Query = contentQuery,
PageSize = pageSize,
PageToken = ((page - 1) * pageSize).ToString()
};
var response = await postClient.SearchPostsAsync(request);
return response.Posts.Select(SnPost.FromProtoValue).ToList();
}
public class KernelPostListResult
{
public List<SnPost> Posts { get; set; } = [];
public int TotalCount { get; set; }
}
[KernelFunction("list_posts")]
[Description("List all posts on the Solar Network without filters, orderBy can be date or popularity")]
public async Task<KernelPostListResult> ListPosts(
string orderBy = "date",
bool orderDesc = true,
int pageSize = 10,
int page = 1
)
{
var request = new ListPostsRequest
{
OrderBy = orderBy,
OrderDesc = orderDesc,
PageSize = pageSize,
PageToken = ((page - 1) * pageSize).ToString()
};
var response = await postClient.ListPostsAsync(request);
return new KernelPostListResult
{
Posts = response.Posts.Select(SnPost.FromProtoValue).ToList(),
TotalCount = response.TotalSize,
};
}
[KernelFunction("list_posts_within_time")]
[Description(
"List posts in a period of time, the time requires ISO-8601 format, one of the start and end must be provided.")]
public async Task<KernelPostListResult> ListPostsWithinTime(
string? beforeTime,
string? afterTime,
int pageSize = 10,
int page = 1
)
{
var pattern = InstantPattern.General;
Instant? before = !string.IsNullOrWhiteSpace(beforeTime)
? pattern.Parse(beforeTime).TryGetValue(default, out var beforeValue) ? beforeValue : null
: null;
Instant? after = !string.IsNullOrWhiteSpace(afterTime)
? pattern.Parse(afterTime).TryGetValue(default, out var afterValue) ? afterValue : null
: null;
var request = new ListPostsRequest
{
After = after?.ToTimestamp(),
Before = before?.ToTimestamp(),
PageSize = pageSize,
PageToken = ((page - 1) * pageSize).ToString()
};
var response = await postClient.ListPostsAsync(request);
return new KernelPostListResult
{
Posts = response.Posts.Select(SnPost.FromProtoValue).ToList(),
TotalCount = response.TotalSize,
};
}
}

View File

@@ -1,6 +1,7 @@
using System.ClientModel;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using DysonNetwork.Insight.Thought.Plugins;
using DysonNetwork.Shared.Models;
using DysonNetwork.Shared.Proto;
using Microsoft.SemanticKernel;
@@ -71,81 +72,15 @@ public class ThoughtProvider
throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType);
}
builder.Plugins.AddFromType<SnAccountKernelPlugin>();
builder.Plugins.AddFromType<SnPostKernelPlugin>();
return builder.Build();
}
[Experimental("SKEXP0050")]
private void InitializeHelperFunctions()
{
// Add Solar Network tools plugin
Kernel.ImportPluginFromFunctions("solar_network", [
KernelFunctionFactory.CreateFromMethod(async (string userId) =>
{
var request = new GetAccountRequest { Id = userId };
var response = await _accountClient.GetAccountAsync(request);
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
}, "get_user", "Get a user profile from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async (string postId) =>
{
var request = new GetPostRequest { Id = postId };
var response = await _postClient.GetPostAsync(request);
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
}, "get_post", "Get a single post by ID from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async (string query) =>
{
var request = new SearchPostsRequest { Query = query, PageSize = 10 };
var response = await _postClient.SearchPostsAsync(request);
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
}, "search_posts",
"Search posts by query from the Solar Network. The input query is will be used to search with title, description and body content"),
KernelFunctionFactory.CreateFromMethod(async (
string? orderBy = null,
string? afterIso = null,
string? beforeIso = null
) =>
{
_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))

View File

@@ -1,7 +1,6 @@
using DysonNetwork.Insight.Thought;
using Quartz;
namespace DysonNetwork.Insight.Startup;
namespace DysonNetwork.Insight.Thought;
public class TokenBillingJob(ThoughtService thoughtService, ILogger<TokenBillingJob> logger) : IJob
{

View File

@@ -243,6 +243,7 @@ message ListPostsRequest {
int32 page_size = 3;
string page_token = 4;
google.protobuf.StringValue order_by = 5;
bool order_desc = 16;
repeated string categories = 6;
repeated string tags = 7;
google.protobuf.StringValue query = 8;

View File

@@ -125,13 +125,22 @@ public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.Post
.Include(p => p.FeaturedRecords)
.AsQueryable();
query = request.Shuffle
? query.OrderBy(e => EF.Functions.Random())
: request.OrderBy switch
if (request.Shuffle)
{
query = query.OrderBy(e => EF.Functions.Random());
}
else
{
query = request.OrderBy switch
{
"asc" => query.OrderBy(e => e.PublishedAt ?? e.CreatedAt),
_ => query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt),
"popularity" => request.OrderDesc
? query.OrderByDescending(e => e.Upvotes * 10 - e.Downvotes * 10 + e.AwardedScore)
: query.OrderBy(e => e.Upvotes * 10 - e.Downvotes * 10 + e.AwardedScore),
_ => request.OrderDesc
? query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
: query.OrderBy(e => e.PublishedAt ?? e.CreatedAt)
};
}
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
query = query.Where(p => p.PublisherId == pid);