aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio_22/Tango.Portal.Chat.Web
diff options
context:
space:
mode:
authorRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-07 06:47:24 +0300
committerRoy Ben Shabat <roy.mail.net@gmail.com>2025-09-07 06:47:24 +0300
commit9ba1902ee9dbac3b3c48d735b3bbcc3ba867dd2e (patch)
treeea0617d8dc1a4e83439a65b32e8720ab78a5c973 /Software/Visual_Studio_22/Tango.Portal.Chat.Web
parent2ff424e4b00ed154ae5febd1827d0d31d69c34ad (diff)
downloadTango-9ba1902ee9dbac3b3c48d735b3bbcc3ba867dd2e.tar.gz
Tango-9ba1902ee9dbac3b3c48d735b3bbcc3ba867dd2e.zip
Added seesion, chat history on ADX...
Diffstat (limited to 'Software/Visual_Studio_22/Tango.Portal.Chat.Web')
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs108
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs18
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/ChatConversationMessage.cs17
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/SessionUser.cs13
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Program.cs3
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/ChatMessageLogger.cs116
-rw-r--r--Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SessionUtils.cs36
7 files changed, 291 insertions, 20 deletions
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
index a13b6a47a..396651e3f 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
@@ -19,13 +19,15 @@ namespace Tango.Portal.Chat.Web.Controllers
private readonly KqlGuard _guard;
private readonly KustoQueryService _adx;
private readonly LlmClient _llm;
+ private readonly ChatMessageLogger _logger;
- public ChatController(SchemaRegistry schema, KqlGuard guard, KustoQueryService adx, LlmClient llm)
+ public ChatController(SchemaRegistry schema, KqlGuard guard, KustoQueryService adx, LlmClient llm, ChatMessageLogger logger)
{
_schema = schema;
_guard = guard;
_adx = adx;
_llm = llm;
+ _logger = logger;
}
[HttpPost("ask")]
@@ -33,6 +35,36 @@ namespace Tango.Portal.Chat.Web.Controllers
{
try
{
+ if (!SessionUtils.IsUserAuthenticated(HttpContext))
+ {
+ return new ChatResponse
+ {
+ Answer = "User is not authenticated or session expired",
+ ThreadId = req.ThreadId
+ };
+ }
+
+ var sessionUser = SessionUtils.GetSessionUser(HttpContext);
+ var sessionId = HttpContext.Session.Id;
+
+ // Log the question
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await _logger.LogQuestionAsync(
+ sessionId,
+ sessionUser?.Email ?? "unknown",
+ sessionUser?.FullName ?? "unknown",
+ req.Question,
+ ct);
+ }
+ catch
+ {
+ // Ignore logging failures
+ }
+ }, ct);
+
var schemaJson = _schema.GetSchemaJson();
var plannerPrompt = _schema.GetPlannerPrompt();
var plotySample = _schema.GetPlotySample();
@@ -50,30 +82,77 @@ namespace Tango.Portal.Chat.Web.Controllers
plan = await _llm.ProposeKqlAsync(plannerPrompt, plotySample, req.Question, schemaJson, req.History, ct);
}
+ ChatResponse response;
if (plan.Assistant == "data" || plan.Assistant == "ploty")
{
- return await AnswerWithDataAssistant(req, plan, ct);
+ response = await AnswerWithDataAssistant(req, plan, ct);
}
else if (plan.Assistant == "docs")
{
- return await AnswerWithDocsAssistant(req, plan, ct);
+ response = await AnswerWithDocsAssistant(req, plan, ct);
}
else
{
- return AnswerWithPlannerConversation(req, plan);
+ response = AnswerWithPlannerConversation(req, plan);
}
+
+ // Log the answer
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await _logger.LogAnswerAsync(
+ sessionId,
+ sessionUser?.Email ?? "unknown",
+ sessionUser?.FullName ?? "unknown",
+ response,
+ plan.Assistant,
+ plan.Provider.ToString(),
+ ct);
+ }
+ catch
+ {
+ // Ignore logging failures
+ }
+ }, ct);
+
+ return response;
}
catch (Exception ex)
{
- return new ChatResponse
+ var errorResponse = new ChatResponse
{
Answer = $"Ooops something went wrong...\n{ex.Message}",
ThreadId = req.ThreadId
};
+
+ // Log the error response
+ var sessionUser = SessionUtils.GetSessionUser(HttpContext);
+ var sessionId = req.ThreadId ?? Guid.NewGuid().ToString();
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await _logger.LogAnswerAsync(
+ sessionId,
+ sessionUser?.Email ?? "unknown",
+ sessionUser?.FullName ?? "unknown",
+ errorResponse,
+ "error",
+ "Unknown",
+ ct);
+ }
+ catch
+ {
+ // Ignore logging failures
+ }
+ }, ct);
+
+ return errorResponse;
}
}
- private static ActionResult<ChatResponse> AnswerWithPlannerConversation(ChatRequest req, ProposeKqlResult plan)
+ private ChatResponse AnswerWithPlannerConversation(ChatRequest req, ProposeKqlResult plan)
{
return new ChatResponse
{
@@ -82,7 +161,7 @@ namespace Tango.Portal.Chat.Web.Controllers
};
}
- private async Task<ActionResult<ChatResponse>> AnswerWithDocsAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
+ private async Task<ChatResponse> AnswerWithDocsAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
{
// AFTER
var run = await _llm.AnswerWithAssistantAsync(
@@ -100,7 +179,7 @@ namespace Tango.Portal.Chat.Web.Controllers
};
}
- private async Task<ActionResult<ChatResponse>> AnswerWithDataAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
+ private async Task<ChatResponse> AnswerWithDataAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct)
{
// 2) Guardrail validation
var val = _guard.Validate(plan.Kql);
@@ -125,7 +204,12 @@ namespace Tango.Portal.Chat.Web.Controllers
{
if (plan.Provider == LlmProvider.OpenAI)
{
- return await Ask(req, ct, LlmProvider.Claude);
+ var fallbackResult = await Ask(req, ct, LlmProvider.Claude);
+ return fallbackResult.Value ?? new ChatResponse
+ {
+ Answer = "Fallback to Claude failed",
+ ThreadId = req.ThreadId
+ };
}
else
{
@@ -156,7 +240,7 @@ namespace Tango.Portal.Chat.Web.Controllers
}
}
- private async Task<ActionResult<ChatResponse>> AnswerWithDataAssistantInternal(ChatRequest req, ProposeKqlResult plan, DataTable table, CancellationToken ct)
+ private async Task<ChatResponse> AnswerWithDataAssistantInternal(ChatRequest req, ProposeKqlResult plan, DataTable table, CancellationToken ct)
{
var preview = DataHelper.ToPreview(table, 200);
var facts = JsonSerializer.Serialize(preview);
@@ -181,7 +265,7 @@ namespace Tango.Portal.Chat.Web.Controllers
};
}
- private static ActionResult<ChatResponse> AnswerWithMarkdownTable(ChatRequest req, ProposeKqlResult plan, DataTable table)
+ private ChatResponse AnswerWithMarkdownTable(ChatRequest req, ProposeKqlResult plan, DataTable table)
{
var markdown = DataHelper.ToMarkdownTable(table);
@@ -194,7 +278,7 @@ namespace Tango.Portal.Chat.Web.Controllers
};
}
- private static ActionResult<ChatResponse> AnswerWithPloty(ChatRequest req, ProposeKqlResult plan, DataTable table)
+ private ChatResponse AnswerWithPloty(ChatRequest req, ProposeKqlResult plan, DataTable table)
{
String? ploty = table.Rows[0]["ploty"]?.ToString();
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs
index 6f1aa5dfa..8ba9fec32 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/HomeController.cs
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Text.Json;
+using Tango.Portal.Chat.Web.Models;
+using Tango.Portal.Chat.Web.Utils;
using Tango.Portal.Chat.Web.ViewModels;
namespace Tango.Portal.Chat.Web.Controllers
@@ -11,8 +13,9 @@ namespace Tango.Portal.Chat.Web.Controllers
{
if (Debugger.IsAttached)
{
+ SessionUtils.SetSessionUser(HttpContext, new Models.SessionUser() { Name = "Debug User", Email = "roy@twine-s.com", FullName = "Debug User" });
HomeViewVM v = new HomeViewVM();
- v.UserName = "debug-user";
+ v.UserName = "Debug User";
return View(v);
}
@@ -20,27 +23,26 @@ namespace Tango.Portal.Chat.Web.Controllers
if (String.IsNullOrWhiteSpace(session)) return new RedirectResult(loginUrl);
- String decryptedSession = String.Empty;
+ String json = String.Empty;
try
{
- decryptedSession = SimpleCryptoHelper.Decrypt(session);
+ json = SimpleCryptoHelper.Decrypt(session);
}
catch
{
return new RedirectResult(loginUrl);
}
- var template = new { UserName = "", Expires = DateTime.MinValue };
-
- var sessionUser = JsonSerializer.Deserialize(decryptedSession, template.GetType());
- if (sessionUser == null || (DateTime)(sessionUser as dynamic).Expires < DateTime.UtcNow)
+ var sessionUser = JsonSerializer.Deserialize<SessionUser>(json);
+ if (sessionUser == null || sessionUser.Expires < DateTime.UtcNow)
{
return new RedirectResult(loginUrl);
}
HomeViewVM vm = new HomeViewVM();
- vm.UserName = (sessionUser as dynamic).UserName;
+ vm.UserName = sessionUser.Name;
+ SessionUtils.SetSessionUser(HttpContext, sessionUser);
return View(vm);
}
}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/ChatConversationMessage.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/ChatConversationMessage.cs
new file mode 100644
index 000000000..3c056a922
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/ChatConversationMessage.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace Tango.Portal.Chat.Web.Models
+{
+ public class ChatConversationMessage
+ {
+ public string ID { get; set; } = Guid.NewGuid().ToString();
+ public string SessionID { get; set; } = string.Empty;
+ public DateTime Time { get; set; }
+ public string Email { get; set; } = string.Empty;
+ public string User { get; set; } = string.Empty;
+ public string Role { get; set; } = string.Empty;
+ public string Classification { get; set; } = string.Empty;
+ public string Message { get; set; } = string.Empty;
+ public string Provider { get; set; } = string.Empty;
+ }
+}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/SessionUser.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/SessionUser.cs
new file mode 100644
index 000000000..a9ce649f0
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Models/SessionUser.cs
@@ -0,0 +1,13 @@
+namespace Tango.Portal.Chat.Web.Models
+{
+ public class SessionUser
+ {
+ public bool IsAuthenticated { get; set; }
+ public String Name { get; set; } = String.Empty;
+ public bool IsTwineUser { get; set; }
+ public string Email { get; set; } = string.Empty;
+ public string Organization { get; set; } = string.Empty;
+ public string FullName { get; set; } = string.Empty;
+ public DateTime Expires { get; set; }
+ }
+}
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Program.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Program.cs
index da82f82f5..41402c527 100644
--- a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Program.cs
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Program.cs
@@ -12,8 +12,10 @@ builder.Services.Configure<LlmOptions>(builder.Configuration.GetSection("OpenAI"
// ADX config
builder.Services.Configure<AdxOptions>(builder.Configuration.GetSection("ADX"));
builder.Services.AddSingleton<KustoQueryService>();
+builder.Services.AddSingleton<ChatMessageLogger>();
builder.Services.AddSingleton<SchemaRegistry>();
builder.Services.AddSingleton<KqlGuard>();
+builder.Services.AddSession();
// Simple HTTP client for LLM
builder.Services.AddHttpClient<LlmClient>();
@@ -22,6 +24,7 @@ var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
+app.UseSession();
app.MapControllerRoute(
name: "default",
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/ChatMessageLogger.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/ChatMessageLogger.cs
new file mode 100644
index 000000000..4eed2182d
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Services/ChatMessageLogger.cs
@@ -0,0 +1,116 @@
+using Azure.Core;
+using Azure.Identity;
+using Kusto.Data;
+using Kusto.Data.Common;
+using Kusto.Data.Net.Client;
+using Microsoft.Extensions.Options;
+using System.Text.Json;
+using Tango.Portal.Chat.Web.Models;
+
+namespace Tango.Portal.Chat.Web.Services
+{
+ public sealed class ChatMessageLogger
+ {
+ private readonly ICslAdminProvider _admin;
+ private readonly string _database;
+ private readonly ILogger<ChatMessageLogger> _logger;
+
+ public ChatMessageLogger(IOptions<AdxOptions> opts, ILogger<ChatMessageLogger> logger)
+ {
+ var options = opts.Value;
+ _database = options.Database;
+ _logger = logger;
+
+ var cred = new ClientSecretCredential(
+ options.TenantId,
+ options.ClientId,
+ options.ClientSecret);
+
+ var kcsb = new KustoConnectionStringBuilder(options.ClusterUri)
+ .WithAadAzureTokenCredentialsAuthentication(cred);
+
+ _admin = KustoClientFactory.CreateCslAdminProvider(kcsb);
+ }
+
+ public async Task LogMessageAsync(ChatConversationMessage message, CancellationToken ct = default)
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(message);
+ var command = $".ingest inline into table ChatConversationsTable with (jsonMappingReference = 'ChatConversationsTableMapping') <| {json}";
+
+ var props = new ClientRequestProperties
+ {
+ ClientRequestId = $"chat_log_{Guid.NewGuid()}"
+ };
+
+ using var result = await _admin.ExecuteControlCommandAsync(_database, command, props);
+ _logger.LogDebug("Successfully logged chat message {MessageId} for session {SessionId}",
+ message.ID, message.SessionID);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to log chat message {MessageId} for session {SessionId}",
+ message.ID, message.SessionID);
+ }
+ }
+
+ public async Task LogQuestionAsync(string sessionId, string userEmail, string userName, string question, CancellationToken ct = default)
+ {
+ var message = new ChatConversationMessage
+ {
+ SessionID = sessionId,
+ Time = DateTime.UtcNow,
+ Email = userEmail,
+ User = userName,
+ Role = "user",
+ Classification = "question",
+ Message = question,
+ Provider = string.Empty
+ };
+
+ await LogMessageAsync(message, ct);
+ }
+
+ public async Task LogAnswerAsync(string sessionId, string userEmail, string userName, ChatResponse response, string assistantType, string provider, CancellationToken ct = default)
+ {
+ var answerMessage = BuildAnswerMessage(response);
+
+ var message = new ChatConversationMessage
+ {
+ SessionID = sessionId,
+ Time = DateTime.UtcNow,
+ Email = userEmail,
+ User = userName,
+ Role = "assistant",
+ Classification = assistantType,
+ Message = answerMessage,
+ Provider = provider
+ };
+
+ await LogMessageAsync(message, ct);
+ }
+
+ private static string BuildAnswerMessage(ChatResponse response)
+ {
+ var parts = new List<string>();
+
+ if (!string.IsNullOrEmpty(response.Answer))
+ {
+ parts.Add($"Answer: {response.Answer}");
+ }
+
+ if (!string.IsNullOrEmpty(response.UsedKql))
+ {
+ parts.Add($"Used KQL: {response.UsedKql}");
+ }
+
+ if (!string.IsNullOrEmpty(response.Ploty))
+ {
+ parts.Add($"Visualization: {response.Ploty}");
+ }
+
+ return parts.Count > 0 ? string.Join("\n\n", parts) : response.Answer ?? string.Empty;
+ }
+ }
+} \ No newline at end of file
diff --git a/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SessionUtils.cs b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SessionUtils.cs
new file mode 100644
index 000000000..91f118e0d
--- /dev/null
+++ b/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Utils/SessionUtils.cs
@@ -0,0 +1,36 @@
+using Tango.Portal.Chat.Web.Models;
+
+namespace Tango.Portal.Chat.Web.Utils
+{
+ public class SessionUtils
+ {
+ public static void SetSessionUser(HttpContext context, SessionUser user)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ if (user == null) throw new ArgumentNullException(nameof(user));
+
+ var json = System.Text.Json.JsonSerializer.Serialize(user);
+ context.Session.SetString("SessionUser", json);
+ }
+
+ public static SessionUser? GetSessionUser(HttpContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ var json = context.Session.GetString("SessionUser");
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return null;
+ }
+
+ return System.Text.Json.JsonSerializer.Deserialize<SessionUser>(json);
+ }
+
+ public static bool IsUserAuthenticated(HttpContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ var user = GetSessionUser(context);
+ return user != null;
+ }
+ }
+}