aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio_22/Tango.Portal.Chat.Web/Controllers/ChatController.cs
blob: 35b17034703c1402d9bbdefe6dacf75e07a2a481 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
using System.Data;
using System.Text.Json;
using ChatADX.Web.Models;
using ChatADX.Web.Services;
using Kusto.Data.Data;
using Microsoft.AspNetCore.Mvc;

namespace ChatADX.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;
        private static readonly string[] AllowTables = new[] { "JobRunsTable", "JobStatusTable", "TelemetryTable", "MachinesTable" };

        public ChatController(SchemaRegistry schema, KqlGuard guard, KustoQueryService adx, LlmClient llm)
        {
            _schema = schema;
            _guard = guard;
            _adx = adx;
            _llm = llm;
        }

        [HttpPost("ask")]
        public async Task<ActionResult<ChatResponse>> Ask([FromBody] ChatRequest req, CancellationToken ct)
        {
            try
            {
                var schemaJson = _schema.GetSchemaJson();

                // 1) Ask the model for KQL
                var plan = await _llm.ProposeKqlAsync(req.Question, schemaJson, req.History, ct);

                if (plan.Assistant == "data")
                {
                    // 2) Guardrail validation
                    var val = _guard.Validate(plan.Kql);
                    if (!val.IsOk) return BadRequest(new { error = "Invalid KQL", details = val.Error, plan });

                    // 4) Execute in ADX
                    DataTable table;
                    try
                    {
                        table = await _adx.QueryAsync(plan.Kql, plan.Parameters, ct);
                    }
                    catch (Exception ex)
                    {
                        // 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
                        };
                    }

                    // 5) Build compact facts (limit rows/cols)
                    var preview = 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
                    };
                }
                else
                {
                    // 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
                    };
                }
            }
            catch (Exception ex)
            {
                return new ChatResponse
                {
                    Answer = $"Ooops something went wrong...\n{ex.Message}",
                    ThreadId = req.ThreadId
                };
            }
        }

        private static object ToPreview(DataTable dt, int maxRows)
        {
            var cols = dt.Columns.Cast<DataColumn>().Select(c => c.ColumnName).ToArray();
            var rows = new List<Dictionary<string, object?>>();
            int count = 0;
            foreach (DataRow r in dt.Rows)
            {
                if (count++ >= maxRows) break;
                var d = new Dictionary<string, object?>();
                foreach (var c in cols) d[c] = r[c];
                rows.Add(d);
            }
            return new { columns = cols, rows };
        }
    }
}