using System.Data; using System.Text.Json; using Tango.Portal.Chat.Web.Models; using Tango.Portal.Chat.Web.Services; using Kusto.Data.Data; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; using System.Text.Json.Nodes; using System.Collections; using Tango.Portal.Chat.Web.Utils; namespace Tango.Portal.Chat.Web.Controllers { [ApiController] [Route("api/[controller]")] public sealed class ChatController : ControllerBase { private readonly SchemaRegistry _schema; private readonly KqlGuard _guard; private readonly KustoQueryService _adx; private readonly LlmClient _llm; public ChatController(SchemaRegistry schema, KqlGuard guard, KustoQueryService adx, LlmClient llm) { _schema = schema; _guard = guard; _adx = adx; _llm = llm; } [HttpPost("ask")] public async Task> Ask([FromBody] ChatRequest req, CancellationToken ct, LlmProvider? provider = null) { try { var schemaJson = _schema.GetSchemaJson(); var plannerPrompt = _schema.GetPlannerPrompt(); var plotySample = _schema.GetPlotySample(); // 1) Ask the model for KQL ProposeKqlResult? plan = null; if (provider == LlmProvider.Claude) { plan = await _llm.ProposeKqlWithClaudeAsync(plannerPrompt, plotySample, req.Question, schemaJson, req.History, ct); } else { plan = await _llm.ProposeKqlAsync(plannerPrompt, plotySample, req.Question, schemaJson, req.History, ct); } if (plan.Assistant == "data" || plan.Assistant == "ploty") { return await AnswerWithDataAssistant(req, plan, ct); } else if (plan.Assistant == "docs") { return await AnswerWithDocsAssistant(req, plan, ct); } else { return AnswerWithPlannerConversation(req, plan); } } catch (Exception ex) { return new ChatResponse { Answer = $"Ooops something went wrong...\n{ex.Message}", ThreadId = req.ThreadId }; } } private static ActionResult AnswerWithPlannerConversation(ChatRequest req, ProposeKqlResult plan) { return new ChatResponse { Answer = plan.ConversationAnswer, ThreadId = req.ThreadId }; } private async Task> AnswerWithDocsAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct) { // AFTER var run = await _llm.AnswerWithAssistantAsync( LlmClient.AssistantType.Docs, req.Question, string.Empty, plan.Kql, req.ThreadId, // <-- reuse if provided ct); return new ChatResponse { Answer = run.Answer, ThreadId = run.ThreadId }; } private async Task> AnswerWithDataAssistant(ChatRequest req, ProposeKqlResult plan, CancellationToken ct) { // 2) Guardrail validation var val = _guard.Validate(plan.Kql); if (!val.IsOk) { // Return error to the client so they can iterate return new ChatResponse { Answer = $"The generated kusto query contains invalid tokens..\n{val.Error}", ThreadId = req.ThreadId, UsedKql = plan.Kql }; } // 4) Execute in ADX DataTable table; try { table = await _adx.QueryAsync(plan.Kql, plan.Parameters, ct); } catch (Exception ex) { if (plan.Provider == LlmProvider.OpenAI) { return await Ask(req, ct, LlmProvider.Claude); } else { // Return error to the client so they can iterate return new ChatResponse { Answer = $"Seems like my kusto query ran into some issue..\n{ex.Message}", ThreadId = req.ThreadId, UsedKql = plan.Kql }; } } if (table.Columns.Contains("ploty") && table.Rows.Count > 0) { return AnswerWithPloty(req, plan, table); } // 5) Build compact facts (limit rows/cols) if (table.Rows.Count <= 200) { return await AnswerWithDataAssistantInternal(req, plan, table, ct); } else { return AnswerWithMarkdownTable(req, plan, table); } } private async Task> AnswerWithDataAssistantInternal(ChatRequest req, ProposeKqlResult plan, DataTable table, CancellationToken ct) { var preview = DataHelper.ToPreview(table, 200); var facts = JsonSerializer.Serialize(preview); // 6) Ask model for final answer //var answer = await _llm.AnswerFromFactsAsync(req.Question, facts, plan.Kql, ct); var run = await _llm.AnswerWithAssistantAsync( LlmClient.AssistantType.Data, req.Question, facts, plan.Kql, req.ThreadId, // <-- reuse if provided ct); return new ChatResponse { Answer = run.Answer, UsedKql = plan.Kql, Preview = preview, ThreadId = run.ThreadId // <-- echo back the thread id used/created }; } private static ActionResult AnswerWithMarkdownTable(ChatRequest req, ProposeKqlResult plan, DataTable table) { var markdown = DataHelper.ToMarkdownTable(table); return new ChatResponse { Answer = $"The result set was too big for me to analyze so I'm just going to drop the entire result set.\n{markdown}", UsedKql = plan.Kql, Preview = markdown, ThreadId = req.ThreadId // <-- echo back the thread id used/created }; } private static ActionResult AnswerWithPloty(ChatRequest req, ProposeKqlResult plan, DataTable table) { String? ploty = table.Rows[0]["ploty"]?.ToString(); return new ChatResponse { Answer = plan.ConversationAnswer, ThreadId = req.ThreadId, UsedKql = plan.Kql, Ploty = ploty ?? String.Empty }; } } }