using System.ClientModel; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using DysonNetwork.Shared.Models; using DysonNetwork.Shared.Proto; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.Ollama; using Microsoft.SemanticKernel.Connectors.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; public class ThoughtProvider { private readonly PostService.PostServiceClient _postClient; private readonly AccountService.AccountServiceClient _accountClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; public Kernel Kernel { get; } private string? ModelProviderType { get; set; } private string? ModelDefault { get; set; } [Experimental("SKEXP0050")] public ThoughtProvider( IConfiguration configuration, PostService.PostServiceClient postServiceClient, AccountService.AccountServiceClient accountServiceClient, ILogger logger ) { _logger = logger; _postClient = postServiceClient; _accountClient = accountServiceClient; _configuration = configuration; Kernel = InitializeThinkingProvider(configuration); InitializeHelperFunctions(); } private Kernel InitializeThinkingProvider(IConfiguration configuration) { var cfg = configuration.GetSection("Thinking"); ModelProviderType = cfg.GetValue("Provider")?.ToLower(); ModelDefault = cfg.GetValue("Model"); var endpoint = cfg.GetValue("Endpoint"); var apiKey = cfg.GetValue("ApiKey"); var builder = Kernel.CreateBuilder(); switch (ModelProviderType) { case "ollama": builder.AddOllamaChatCompletion(ModelDefault!, new Uri(endpoint ?? "http://localhost:11434/api")); break; case "deepseek": var client = new OpenAIClient( new ApiKeyCredential(apiKey!), new OpenAIClientOptions { Endpoint = new Uri(endpoint ?? "https://api.deepseek.com/v1") } ); builder.AddOpenAIChatCompletion(ModelDefault!, client); break; default: throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType); } 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)) { var bingConnector = new BingConnector(bingApiKey); var bing = new WebSearchEnginePlugin(bingConnector); Kernel.ImportPluginFromObject(bing, "bing"); } var googleApiKey = _configuration.GetValue("Thinking:GoogleApiKey"); var googleCx = _configuration.GetValue("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() { switch (ModelProviderType) { case "ollama": return new OllamaPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto( options: new FunctionChoiceBehaviorOptions { AllowParallelCalls = true, AllowConcurrentInvocation = true }) }; case "deepseek": return new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto( options: new FunctionChoiceBehaviorOptions { AllowParallelCalls = true, AllowConcurrentInvocation = true }) }; default: throw new InvalidOperationException("Unknown provider: " + ModelProviderType); } } }