diff --git a/DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs b/DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs index d07c5f3..e35c37a 100644 --- a/DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs +++ b/DysonNetwork.Insight/Startup/ScheduledJobsConfiguration.cs @@ -1,3 +1,4 @@ +using DysonNetwork.Insight.Thought; using Quartz; namespace DysonNetwork.Insight.Startup; diff --git a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs index 110f8ae..197e38a 100644 --- a/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Insight/Startup/ServiceCollectionExtensions.cs @@ -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(); services.AddScoped(); - // Add gRPC clients for ThoughtService - services.AddGrpcClient(o => o.Address = new Uri("https://_grpc.pass")) - .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() - { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }); - services.AddGrpcClient(o => o.Address = new Uri("https://_grpc.pass")) - .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() - { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }); - return services; } } diff --git a/DysonNetwork.Insight/Thought/Plugins/SnAccountKernelPlugin.cs b/DysonNetwork.Insight/Thought/Plugins/SnAccountKernelPlugin.cs new file mode 100644 index 0000000..e37a9b8 --- /dev/null +++ b/DysonNetwork.Insight/Thought/Plugins/SnAccountKernelPlugin.cs @@ -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 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 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]); + } +} \ No newline at end of file diff --git a/DysonNetwork.Insight/Thought/Plugins/SnPostKernelPlugin.cs b/DysonNetwork.Insight/Thought/Plugins/SnPostKernelPlugin.cs new file mode 100644 index 0000000..911582b --- /dev/null +++ b/DysonNetwork.Insight/Thought/Plugins/SnPostKernelPlugin.cs @@ -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 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> 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 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 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 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, + }; + } +} \ No newline at end of file diff --git a/DysonNetwork.Insight/Thought/ThoughtProvider.cs b/DysonNetwork.Insight/Thought/ThoughtProvider.cs index b449929..88b08f2 100644 --- a/DysonNetwork.Insight/Thought/ThoughtProvider.cs +++ b/DysonNetwork.Insight/Thought/ThoughtProvider.cs @@ -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(); + builder.Plugins.AddFromType(); + 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("Thinking:BingApiKey"); if (!string.IsNullOrEmpty(bingApiKey)) diff --git a/DysonNetwork.Insight/Startup/TokenBillingJob.cs b/DysonNetwork.Insight/Thought/TokenBillingJob.cs similarity index 76% rename from DysonNetwork.Insight/Startup/TokenBillingJob.cs rename to DysonNetwork.Insight/Thought/TokenBillingJob.cs index e83233c..9d92df4 100644 --- a/DysonNetwork.Insight/Startup/TokenBillingJob.cs +++ b/DysonNetwork.Insight/Thought/TokenBillingJob.cs @@ -1,7 +1,6 @@ -using DysonNetwork.Insight.Thought; using Quartz; -namespace DysonNetwork.Insight.Startup; +namespace DysonNetwork.Insight.Thought; public class TokenBillingJob(ThoughtService thoughtService, ILogger logger) : IJob { diff --git a/DysonNetwork.Shared/Proto/post.proto b/DysonNetwork.Shared/Proto/post.proto index c19a4a7..fa94f72 100644 --- a/DysonNetwork.Shared/Proto/post.proto +++ b/DysonNetwork.Shared/Proto/post.proto @@ -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; diff --git a/DysonNetwork.Sphere/Post/PostServiceGrpc.cs b/DysonNetwork.Sphere/Post/PostServiceGrpc.cs index 1fc302e..f7153f8 100644 --- a/DysonNetwork.Sphere/Post/PostServiceGrpc.cs +++ b/DysonNetwork.Sphere/Post/PostServiceGrpc.cs @@ -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);