From 72d2d9316e27f623456574dd854da064611254a1 Mon Sep 17 00:00:00 2001 From: Roy Ben Shabat Date: Thu, 4 Sep 2025 13:42:02 +0300 Subject: OpenAI & Claude Providers. --- .../Tango.Portal.Chat.Web/Models/Options.cs | 22 +++++-- .../Tango.Portal.Chat.Web/Services/LlmClient.cs | 73 ++++++++++++++++++++-- .../appsettings.Development.json | 8 ++- .../Tango.Portal.Chat.Web/appsettings.json | 8 ++- 4 files changed, 98 insertions(+), 13 deletions(-) (limited to 'Software/Visual_Studio_22/Tango.Portal.Chat.Web') diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Options.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Options.cs index bfd8fdaf5..6c6a537d6 100644 --- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Options.cs +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/Options.cs @@ -1,16 +1,30 @@ namespace Tango.Portal.Chat.Web.Services { + public enum LlmProvider + { + OpenAI, + Claude + } + public sealed class LlmOptions { - // If using Azure OpenAI, set IsAzure = true and Endpoint = full chat completions URL (with api-version query). - // If using OpenAI, set IsAzure = false and Endpoint = https://api.openai.com/v1/chat/completions - public bool IsAzure { get; set; } = false; + // Provider selection: OpenAI, AzureOpenAI, or Claude + public LlmProvider Provider { get; set; } = LlmProvider.OpenAI; + + // If using OpenAI, set Provider = OpenAI and Endpoint = https://api.openai.com/v1/chat/completions + // If using Claude, set Provider = Claude and Endpoint = https://api.anthropic.com/v1/messages public string Endpoint { get; set; } = string.Empty; public string ApiKey { get; set; } = string.Empty; - public string Model { get; set; } = "gpt-4o-mini"; // or your Azure deployment name + public string Model { get; set; } = "gpt-4o-mini"; // or your Azure deployment name, or claude-3-5-sonnet-20241022 public double Temperature { get; set; } = 0.2; public string AnswererAssistantId { get; set; } = string.Empty; // NEW public string DocsAssistantId { get; set; } = string.Empty; // NEW + + // Claude-specific settings + public string ClaudeApiKey { get; set; } = string.Empty; + public string ClaudeModel { get; set; } = "claude-3-5-sonnet-20241022"; + public string ClaudeEndpoint { get; set; } = "https://api.anthropic.com/v1/messages"; + public int MaxTokens { get; set; } = 4000; } public sealed class AdxOptions diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs index fc970d52e..7b2d8c3ef 100644 --- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/LlmClient.cs @@ -33,6 +33,17 @@ namespace Tango.Portal.Chat.Web.Services public async Task ProposeKqlAsync(String plannerPrompt, String plotySample, string question, string schemaJson, IEnumerable? history, CancellationToken ct = default) + { + return _opt.Provider switch + { + LlmProvider.Claude => await ProposeKqlWithClaudeAsync(plannerPrompt, plotySample, question, schemaJson, history, ct), + LlmProvider.OpenAI => await ProposeKqlWithOpenAIAsync(plannerPrompt, plotySample, question, schemaJson, history, ct), + _ => await ProposeKqlWithOpenAIAsync(plannerPrompt, plotySample, question, schemaJson, history, ct) // Default to OpenAI + }; + } + + private async Task ProposeKqlWithOpenAIAsync(String plannerPrompt, String plotySample, + string question, string schemaJson, IEnumerable? history, CancellationToken ct) { var messages = new List { new { role = "system", content = plannerPrompt } }; @@ -55,8 +66,8 @@ namespace Tango.Portal.Chat.Web.Services using var req = new HttpRequestMessage(HttpMethod.Post, _opt.Endpoint); req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); - if (_opt.IsAzure) req.Headers.Add("api-key", _opt.ApiKey); - else req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _opt.ApiKey); + + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _opt.ApiKey); using var resp = await _http.SendAsync(req, ct); resp.EnsureSuccessStatusCode(); @@ -64,7 +75,60 @@ namespace Tango.Portal.Chat.Web.Services var root = JsonNode.Parse(body)!.AsObject(); var content = root["choices"]![0]!["message"]!["content"]?.ToString() ?? "{}"; - content = StripCodeFences(content); // your existing helper + content = StripCodeFences(content); + + var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + opts.Converters.Add(new FlexibleStringListConverter()); + var result = JsonSerializer.Deserialize(content, opts) + ?? new ProposeKqlResult { Kql = "", Parameters = new() }; + return result; + } + + private async Task ProposeKqlWithClaudeAsync(String plannerPrompt, String plotySample, + string question, string schemaJson, IEnumerable? history, CancellationToken ct) + { + var messages = new List(); + + if (history != null) + { + foreach (var m in history.TakeLast(6)) + { + messages.Add(new { + role = m.Role == "assistant" ? "assistant" : "user", + content = CapString(m.Content, 1000) + }); + } + } + + var schemaBlock = $"SCHEMA:\n{schemaJson}"; + var userMessage = $"Question: {question}\n\n{schemaBlock}\n\n{plotySample}\n\nPlease respond with valid JSON only."; + messages.Add(new { role = "user", content = userMessage }); + + var payload = new + { + model = !string.IsNullOrEmpty(_opt.ClaudeModel) ? _opt.ClaudeModel : "claude-3-5-sonnet-20241022", + max_tokens = _opt.MaxTokens, + temperature = _opt.Temperature, + system = plannerPrompt, + messages = messages + }; + + var endpoint = !string.IsNullOrEmpty(_opt.ClaudeEndpoint) ? _opt.ClaudeEndpoint : "https://api.anthropic.com/v1/messages"; + var apiKey = !string.IsNullOrEmpty(_opt.ClaudeApiKey) ? _opt.ClaudeApiKey : _opt.ApiKey; + + using var req = new HttpRequestMessage(HttpMethod.Post, endpoint); + req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + req.Headers.Add("x-api-key", apiKey); + req.Headers.Add("anthropic-version", "2023-06-01"); + + using var resp = await _http.SendAsync(req, ct); + resp.EnsureSuccessStatusCode(); + var body = await resp.Content.ReadAsStringAsync(ct); + + var root = JsonNode.Parse(body)!.AsObject(); + var contentArray = root["content"]?.AsArray(); + var content = contentArray?[0]?["text"]?.ToString() ?? "{}"; + content = StripCodeFences(content); var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; opts.Converters.Add(new FlexibleStringListConverter()); @@ -123,8 +187,7 @@ namespace Tango.Portal.Chat.Web.Services var json = JsonSerializer.Serialize(payload); req.Content = new StringContent(json, Encoding.UTF8, "application/json"); - if (_opt.IsAzure) req.Headers.Add("api-key", _opt.ApiKey); - else req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _opt.ApiKey); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _opt.ApiKey); using var resp = await _http.SendAsync(req, ct); resp.EnsureSuccessStatusCode(); diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json index 8a35484d1..62f61e974 100644 --- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.Development.json @@ -1,12 +1,16 @@ { "OpenAI": { - "IsAzure": false, + "Provider": "Claude", "Endpoint": "https://api.openai.com/v1/chat/completions", "ApiKey": "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", "Model": "gpt-4o", "Temperature": 0.2, "AnswererAssistantId": "asst_JRKGFqWUYG2rP6CptUgyVcJk", - "DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz" + "DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz", + "ClaudeApiKey": "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", + "ClaudeModel": "claude-sonnet-4-20250514", + "ClaudeEndpoint": "https://api.anthropic.com/v1/messages", + "MaxTokens": 10000 }, "ADX": { "ClusterUri": "https://adx-twine.westeurope.kusto.windows.net/", diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json index e126e16bb..4ea92d050 100644 --- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json +++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/appsettings.json @@ -6,13 +6,17 @@ } }, "OpenAI": { - "IsAzure": false, + "Provider": "OpenAI", "Endpoint": "https://api.openai.com/v1/chat/completions", "ApiKey": "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", "Model": "gpt-4o", "Temperature": 0.2, "AnswererAssistantId": "asst_JRKGFqWUYG2rP6CptUgyVcJk", - "DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz" + "DocsAssistantId": "asst_HQ0C8tsdtzjENITM4qq6kFpz", + "ClaudeApiKey": "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", + "ClaudeModel": "claude-3-5-sonnet-4", + "ClaudeEndpoint": "https://api.anthropic.com/v1/messages", + "MaxTokens": 4000 }, "ADX": { "ClusterUri": "https://adx-twine.westeurope.kusto.windows.net/", -- cgit v1.3.1