✨ Multi model support
This commit is contained in:
@@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -19,12 +20,45 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
public class StreamThinkingRequest
|
public class StreamThinkingRequest
|
||||||
{
|
{
|
||||||
[Required] public string UserMessage { get; set; } = null!;
|
[Required] public string UserMessage { get; set; } = null!;
|
||||||
|
public string? ServiceId { get; set; }
|
||||||
public Guid? SequenceId { get; set; }
|
public Guid? SequenceId { get; set; }
|
||||||
public List<string>? AttachedPosts { get; set; }
|
public List<string>? AttachedPosts { get; set; }
|
||||||
public List<Dictionary<string, dynamic>>? AttachedMessages { get; set; }
|
public List<Dictionary<string, dynamic>>? AttachedMessages { get; set; }
|
||||||
public List<string> AcceptProposals { get; set; } = [];
|
public List<string> AcceptProposals { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ThoughtServiceInfo
|
||||||
|
{
|
||||||
|
public string ServiceId { get; set; } = null!;
|
||||||
|
public double BillingMultiplier { get; set; }
|
||||||
|
public int PerkLevel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThoughtServicesResponse
|
||||||
|
{
|
||||||
|
public string DefaultService { get; set; } = null!;
|
||||||
|
public IEnumerable<ThoughtServiceInfo> Services { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("services")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<ThoughtServicesResponse> GetAvailableServices()
|
||||||
|
{
|
||||||
|
var services = provider.GetAvailableServicesInfo()
|
||||||
|
.Select(s => new ThoughtServiceInfo
|
||||||
|
{
|
||||||
|
ServiceId = s.ServiceId,
|
||||||
|
BillingMultiplier = s.BillingMultiplier,
|
||||||
|
PerkLevel = s.PerkLevel
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new ThoughtServicesResponse
|
||||||
|
{
|
||||||
|
DefaultService = provider.GetDefaultServiceId(),
|
||||||
|
Services = services
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Experimental("SKEXP0110")]
|
[Experimental("SKEXP0110")]
|
||||||
public async Task<ActionResult> Think([FromBody] StreamThinkingRequest request)
|
public async Task<ActionResult> Think([FromBody] StreamThinkingRequest request)
|
||||||
@@ -35,6 +69,26 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
if (request.AcceptProposals.Any(e => !AvailableProposals.Contains(e)))
|
if (request.AcceptProposals.Any(e => !AvailableProposals.Contains(e)))
|
||||||
return BadRequest("Request contains unavailable proposal");
|
return BadRequest("Request contains unavailable proposal");
|
||||||
|
|
||||||
|
var serviceId = provider.GetServiceId(request.ServiceId);
|
||||||
|
var serviceInfo = provider.GetServiceInfo(serviceId);
|
||||||
|
if (serviceInfo is null)
|
||||||
|
{
|
||||||
|
return BadRequest("Service not found or configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check perk level from `currentUser`
|
||||||
|
if (serviceInfo.PerkLevel > 0 && !currentUser.IsSuperuser)
|
||||||
|
if (currentUser.PerkSubscription is null ||
|
||||||
|
PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier) <
|
||||||
|
serviceInfo.PerkLevel)
|
||||||
|
return StatusCode(403, "Not enough perk level");
|
||||||
|
|
||||||
|
var kernel = provider.GetKernel(request.ServiceId);
|
||||||
|
if (kernel is null)
|
||||||
|
{
|
||||||
|
return BadRequest("Service not found or configured.");
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a topic if creating a new sequence
|
// Generate a topic if creating a new sequence
|
||||||
string? topic = null;
|
string? topic = null;
|
||||||
if (!request.SequenceId.HasValue)
|
if (!request.SequenceId.HasValue)
|
||||||
@@ -46,7 +100,13 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
);
|
);
|
||||||
summaryHistory.AddUserMessage(request.UserMessage);
|
summaryHistory.AddUserMessage(request.UserMessage);
|
||||||
|
|
||||||
var summaryResult = await provider.Kernel
|
var summaryKernel = provider.GetKernel(); // Get default kernel
|
||||||
|
if (summaryKernel is null)
|
||||||
|
{
|
||||||
|
return BadRequest("Default service not found or configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var summaryResult = await summaryKernel
|
||||||
.GetRequiredService<IChatCompletionService>()
|
.GetRequiredService<IChatCompletionService>()
|
||||||
.GetChatMessageContentAsync(summaryHistory);
|
.GetChatMessageContentAsync(summaryHistory);
|
||||||
|
|
||||||
@@ -58,14 +118,13 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
if (sequence == null) return Forbid(); // or NotFound
|
if (sequence == null) return Forbid(); // or NotFound
|
||||||
|
|
||||||
// Save user thought
|
// Save user thought
|
||||||
await service.SaveThoughtAsync(sequence, new List<SnThinkingMessagePart>
|
await service.SaveThoughtAsync(sequence, [
|
||||||
{
|
new SnThinkingMessagePart
|
||||||
new()
|
|
||||||
{
|
{
|
||||||
Type = ThinkingMessagePartType.Text,
|
Type = ThinkingMessagePartType.Text,
|
||||||
Text = request.UserMessage
|
Text = request.UserMessage
|
||||||
}
|
}
|
||||||
}, ThinkingThoughtRole.User);
|
], ThinkingThoughtRole.User);
|
||||||
|
|
||||||
// Build chat history
|
// Build chat history
|
||||||
var chatHistory = new ChatHistory(
|
var chatHistory = new ChatHistory(
|
||||||
@@ -172,15 +231,13 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
|
|
||||||
chatHistory.Add(assistantMessage);
|
chatHistory.Add(assistantMessage);
|
||||||
|
|
||||||
if (functionResults.Count > 0)
|
if (functionResults.Count <= 0) continue;
|
||||||
{
|
|
||||||
foreach (var fr in functionResults)
|
foreach (var fr in functionResults)
|
||||||
{
|
{
|
||||||
chatHistory.Add(fr.ToChatMessage());
|
chatHistory.Add(fr.ToChatMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
chatHistory.AddUserMessage(request.UserMessage);
|
chatHistory.AddUserMessage(request.UserMessage);
|
||||||
|
|
||||||
@@ -188,9 +245,8 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
Response.Headers.Append("Content-Type", "text/event-stream");
|
Response.Headers.Append("Content-Type", "text/event-stream");
|
||||||
Response.StatusCode = 200;
|
Response.StatusCode = 200;
|
||||||
|
|
||||||
var kernel = provider.Kernel;
|
|
||||||
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
|
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
|
||||||
var executionSettings = provider.CreatePromptExecutionSettings();
|
var executionSettings = provider.CreatePromptExecutionSettings(request.ServiceId);
|
||||||
|
|
||||||
var assistantParts = new List<SnThinkingMessagePart>();
|
var assistantParts = new List<SnThinkingMessagePart>();
|
||||||
|
|
||||||
@@ -300,7 +356,7 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
|
|||||||
sequence,
|
sequence,
|
||||||
assistantParts,
|
assistantParts,
|
||||||
ThinkingThoughtRole.Assistant,
|
ThinkingThoughtRole.Assistant,
|
||||||
provider.ModelDefault
|
serviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -13,6 +13,15 @@ using Microsoft.SemanticKernel.Plugins.Web.Google;
|
|||||||
|
|
||||||
namespace DysonNetwork.Insight.Thought;
|
namespace DysonNetwork.Insight.Thought;
|
||||||
|
|
||||||
|
public class ThoughtServiceModel
|
||||||
|
{
|
||||||
|
public string ServiceId { get; set; } = null!;
|
||||||
|
public string? Provider { get; set; }
|
||||||
|
public string? Model { get; set; }
|
||||||
|
public double BillingMultiplier { get; set; }
|
||||||
|
public int PerkLevel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class ThoughtProvider
|
public class ThoughtProvider
|
||||||
{
|
{
|
||||||
private readonly PostService.PostServiceClient _postClient;
|
private readonly PostService.PostServiceClient _postClient;
|
||||||
@@ -20,11 +29,10 @@ public class ThoughtProvider
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<ThoughtProvider> _logger;
|
private readonly ILogger<ThoughtProvider> _logger;
|
||||||
|
|
||||||
public Kernel Kernel { get; }
|
private readonly Dictionary<string, Kernel> _kernels = new();
|
||||||
|
private readonly Dictionary<string, string> _serviceProviders = new();
|
||||||
private string? ModelProviderType { get; set; }
|
private readonly Dictionary<string, ThoughtServiceModel> _serviceModels = new();
|
||||||
public string? ModelDefault { get; set; }
|
private readonly string _defaultServiceId;
|
||||||
public List<string> ModelAvailable { get; set; } = [];
|
|
||||||
|
|
||||||
[Experimental("SKEXP0050")]
|
[Experimental("SKEXP0050")]
|
||||||
public ThoughtProvider(
|
public ThoughtProvider(
|
||||||
@@ -39,29 +47,48 @@ public class ThoughtProvider
|
|||||||
_accountClient = accountServiceClient;
|
_accountClient = accountServiceClient;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
|
||||||
Kernel = InitializeThinkingProvider(configuration);
|
var cfg = configuration.GetSection("Thinking");
|
||||||
InitializeHelperFunctions();
|
_defaultServiceId = cfg.GetValue<string>("DefaultService")!;
|
||||||
|
var services = cfg.GetSection("Services").GetChildren();
|
||||||
|
|
||||||
|
foreach (var service in services)
|
||||||
|
{
|
||||||
|
var serviceId = service.Key;
|
||||||
|
var serviceModel = new ThoughtServiceModel
|
||||||
|
{
|
||||||
|
ServiceId = serviceId,
|
||||||
|
Provider = service.GetValue<string>("Provider"),
|
||||||
|
Model = service.GetValue<string>("Model"),
|
||||||
|
BillingMultiplier = service.GetValue<double>("BillingMultiplier", 1.0),
|
||||||
|
PerkLevel = service.GetValue<int>("PerkLevel", 0)
|
||||||
|
};
|
||||||
|
_serviceModels[serviceId] = serviceModel;
|
||||||
|
|
||||||
|
var providerType = service.GetValue<string>("Provider")?.ToLower();
|
||||||
|
if (providerType is null) continue;
|
||||||
|
|
||||||
|
var kernel = InitializeThinkingService(service);
|
||||||
|
InitializeHelperFunctions(kernel);
|
||||||
|
_kernels[serviceId] = kernel;
|
||||||
|
_serviceProviders[serviceId] = providerType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Kernel InitializeThinkingProvider(IConfiguration configuration)
|
private Kernel InitializeThinkingService(IConfigurationSection serviceConfig)
|
||||||
{
|
{
|
||||||
var cfg = configuration.GetSection("Thinking");
|
var providerType = serviceConfig.GetValue<string>("Provider")?.ToLower();
|
||||||
ModelProviderType = cfg.GetValue<string>("Provider")?.ToLower();
|
var model = serviceConfig.GetValue<string>("Model");
|
||||||
ModelDefault = cfg.GetValue<string>("Model");
|
var endpoint = serviceConfig.GetValue<string>("Endpoint");
|
||||||
ModelAvailable = cfg.GetValue<List<string>>("ModelAvailable") ?? [];
|
var apiKey = serviceConfig.GetValue<string>("ApiKey");
|
||||||
var endpoint = cfg.GetValue<string>("Endpoint");
|
|
||||||
var apiKey = cfg.GetValue<string>("ApiKey");
|
|
||||||
|
|
||||||
var builder = Kernel.CreateBuilder();
|
var builder = Kernel.CreateBuilder();
|
||||||
|
|
||||||
switch (ModelProviderType)
|
switch (providerType)
|
||||||
{
|
{
|
||||||
case "ollama":
|
case "ollama":
|
||||||
foreach (var model in ModelAvailable)
|
|
||||||
builder.AddOllamaChatCompletion(
|
builder.AddOllamaChatCompletion(
|
||||||
ModelDefault!,
|
model!,
|
||||||
new Uri(endpoint ?? "http://localhost:11434/api"),
|
new Uri(endpoint ?? "http://localhost:11434/api")
|
||||||
model
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
@@ -69,11 +96,10 @@ public class ThoughtProvider
|
|||||||
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") }
|
||||||
);
|
);
|
||||||
foreach (var model in ModelAvailable)
|
builder.AddOpenAIChatCompletion(model!, client);
|
||||||
builder.AddOpenAIChatCompletion(ModelDefault!, client, model);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType);
|
throw new IndexOutOfRangeException("Unknown thinking provider: " + providerType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add gRPC clients for Thought Plugins
|
// Add gRPC clients for Thought Plugins
|
||||||
@@ -89,7 +115,7 @@ public class ThoughtProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Experimental("SKEXP0050")]
|
[Experimental("SKEXP0050")]
|
||||||
private void InitializeHelperFunctions()
|
private void InitializeHelperFunctions(Kernel kernel)
|
||||||
{
|
{
|
||||||
// Add web search plugins if configured
|
// Add web search plugins if configured
|
||||||
var bingApiKey = _configuration.GetValue<string>("Thinking:BingApiKey");
|
var bingApiKey = _configuration.GetValue<string>("Thinking:BingApiKey");
|
||||||
@@ -97,7 +123,7 @@ public class ThoughtProvider
|
|||||||
{
|
{
|
||||||
var bingConnector = new BingConnector(bingApiKey);
|
var bingConnector = new BingConnector(bingApiKey);
|
||||||
var bing = new WebSearchEnginePlugin(bingConnector);
|
var bing = new WebSearchEnginePlugin(bingConnector);
|
||||||
Kernel.ImportPluginFromObject(bing, "bing");
|
kernel.ImportPluginFromObject(bing, "bing");
|
||||||
}
|
}
|
||||||
|
|
||||||
var googleApiKey = _configuration.GetValue<string>("Thinking:GoogleApiKey");
|
var googleApiKey = _configuration.GetValue<string>("Thinking:GoogleApiKey");
|
||||||
@@ -108,26 +134,58 @@ public class ThoughtProvider
|
|||||||
apiKey: googleApiKey,
|
apiKey: googleApiKey,
|
||||||
searchEngineId: googleCx);
|
searchEngineId: googleCx);
|
||||||
var google = new WebSearchEnginePlugin(googleConnector);
|
var google = new WebSearchEnginePlugin(googleConnector);
|
||||||
Kernel.ImportPluginFromObject(google, "google");
|
kernel.ImportPluginFromObject(google, "google");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PromptExecutionSettings CreatePromptExecutionSettings()
|
public Kernel? GetKernel(string? serviceId = null)
|
||||||
{
|
{
|
||||||
switch (ModelProviderType)
|
serviceId ??= _defaultServiceId;
|
||||||
{
|
return _kernels.GetValueOrDefault(serviceId);
|
||||||
case "ollama":
|
|
||||||
return new OllamaPromptExecutionSettings
|
|
||||||
{
|
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
|
|
||||||
};
|
|
||||||
case "deepseek":
|
|
||||||
return new OpenAIPromptExecutionSettings
|
|
||||||
{
|
|
||||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException("Unknown provider: " + ModelProviderType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetServiceId(string? serviceId = null)
|
||||||
|
{
|
||||||
|
return serviceId ?? _defaultServiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAvailableServices()
|
||||||
|
{
|
||||||
|
return _kernels.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ThoughtServiceModel> GetAvailableServicesInfo()
|
||||||
|
{
|
||||||
|
return _serviceModels.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThoughtServiceModel? GetServiceInfo(string? serviceId)
|
||||||
|
{
|
||||||
|
serviceId ??= _defaultServiceId;
|
||||||
|
return _serviceModels.GetValueOrDefault(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDefaultServiceId()
|
||||||
|
{
|
||||||
|
return _defaultServiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PromptExecutionSettings CreatePromptExecutionSettings(string? serviceId = null)
|
||||||
|
{
|
||||||
|
serviceId ??= _defaultServiceId;
|
||||||
|
var providerType = _serviceProviders.GetValueOrDefault(serviceId);
|
||||||
|
|
||||||
|
return providerType switch
|
||||||
|
{
|
||||||
|
"ollama" => new OllamaPromptExecutionSettings
|
||||||
|
{
|
||||||
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
|
||||||
|
},
|
||||||
|
"deepseek" => new OpenAIPromptExecutionSettings
|
||||||
|
{
|
||||||
|
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false), ModelId = serviceId
|
||||||
|
},
|
||||||
|
_ => throw new InvalidOperationException("Unknown provider for service: " + serviceId)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,22 @@
|
|||||||
"Insecure": true
|
"Insecure": true
|
||||||
},
|
},
|
||||||
"Thinking": {
|
"Thinking": {
|
||||||
|
"DefaultService": "deepseek-chat",
|
||||||
|
"Services": {
|
||||||
|
"deepseek-chat": {
|
||||||
"Provider": "deepseek",
|
"Provider": "deepseek",
|
||||||
"Model": "deepseek-chat",
|
"Model": "deepseek-chat",
|
||||||
"ModelAvailable": ["deepseek-chat", "deepseek-reasoner"],
|
"ApiKey": "sk-",
|
||||||
"ApiKey": "sk-bd20f6a2e9fa40b98c46899baa0e9f09"
|
"BillingMultiplier": 1.0,
|
||||||
|
"PerkLevel": 0
|
||||||
|
},
|
||||||
|
"deepseek-reasoner": {
|
||||||
|
"Provider": "deepseek",
|
||||||
|
"Model": "deepseek-reasoner",
|
||||||
|
"ApiKey": "sk-",
|
||||||
|
"BillingMultiplier": 1.5,
|
||||||
|
"PerkLevel": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user