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 };
}
}
}
|