791 lines
27 KiB
C#
791 lines
27 KiB
C#
using Ease.Core.DataAccess;
|
||
using Ease.Core.Model;
|
||
using Ease.Core.Utility;
|
||
using HRM.BO;
|
||
using ICSharpCode.SharpZipLib.Zip;
|
||
using iTextSharp.text;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.SemanticKernel;
|
||
using Microsoft.VisualBasic;
|
||
using NPOI.SS.Formula.Functions;
|
||
using NPOI.SS.UserModel;
|
||
using OpenAI;
|
||
using OpenAI.Chat;
|
||
using OpenAI.Models;
|
||
using Org.BouncyCastle.Asn1.Crmf;
|
||
using System;
|
||
using System.ClientModel;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Net.Http.Headers;
|
||
using System.Security.Cryptography.Xml;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
|
||
|
||
namespace HRM.DA
|
||
{
|
||
public class OpenAIService
|
||
{
|
||
private readonly string _encryptedkey;
|
||
private readonly string _apiKey;
|
||
private readonly string _openAiModel;
|
||
private readonly int _maxLimit;
|
||
public OpenAIService(string encryptedkey, string model, int maxLimit)
|
||
{
|
||
_encryptedkey = encryptedkey;
|
||
_apiKey = Ease.Core.Utility.Global.CipherFunctions.Decrypt("CeLiMiTeD.AdMIn", _encryptedkey);
|
||
_openAiModel = model;
|
||
_maxLimit = maxLimit;
|
||
}
|
||
|
||
#region Invalid Query Message
|
||
public string returnMessage = "";
|
||
private const string queryMessage = "I'm sorry, I couldn't process your request. Please try rephrasing it. I'm here to help with anything related to payroll or HR tasks.";
|
||
private const string modificationMessage = "I'm Sorry, I can only help with retrieving data. I am not allowed to update or delete any records.";
|
||
#endregion
|
||
|
||
#region Schema Generation (Old Version)
|
||
// Aliases
|
||
//sb.AppendLine("\nAlias Mappings:");
|
||
//foreach (var row in columns.AsEnumerable())
|
||
//{
|
||
// string caption = row["CAPTION"]?.ToString();
|
||
// string columnName = row["COLUMNNAME"].ToString();
|
||
// string tableName = row["TABLENAME"].ToString();
|
||
// if (!string.IsNullOrWhiteSpace(caption) && caption != columnName)
|
||
// {
|
||
// sb.AppendLine($"- {tableName}.{columnName} is also called '{caption}'");
|
||
// }
|
||
//}
|
||
#endregion
|
||
|
||
#region Ask SQL Query
|
||
|
||
#region Function Summary
|
||
/*
|
||
* Function Summary & What it Handles:
|
||
*
|
||
* 1. Request Limiting:
|
||
* - Ensures the total number of OpenAI calls does not exceed the configured maximum.
|
||
* - Throws a polite error message if the request limit is reached.
|
||
*
|
||
* 2. Timeout Handling:
|
||
* - Uses CancellationTokenSource with a configurable timeout (default 60 seconds).
|
||
* - Cancels the OpenAI API request if it exceeds the timeout and throws a timeout message.
|
||
*
|
||
* 3. Rate Limit Handling & Retry Logic:
|
||
* - Detects HTTP 429 status or "rate limit" errors from exceptions.
|
||
* - Retries the API call up to 5 times with exponential backoff.
|
||
* - Returns a polite "service busy" message if retries are exhausted.
|
||
*
|
||
* 4. OpenAI API Call:
|
||
* - Builds a dynamic system prompt including schema and chart instructions.
|
||
* - Sends the user’s prompt to OpenAI for SQL and chart generation.
|
||
* - Extracts and cleans the AI response.
|
||
*
|
||
* 5. Response Parsing & Validation:
|
||
* - Deserializes the AI response into a structured object.
|
||
* - Ensures only SELECT queries are allowed.
|
||
* - Rejects invalid or unsafe queries with descriptive errors.
|
||
*
|
||
* 6. Async Logging:
|
||
* - Logs user prompts, generated SQL, chart details, and metadata asynchronously.
|
||
* - Logging errors do not interrupt the main flow.
|
||
*
|
||
* 7. SQL Execution & JSON Response:
|
||
* - Executes the validated SQL query against the database.
|
||
* - Wraps the dataset with chart metadata and returns JSON.
|
||
*
|
||
* 8. General Exception Handling:
|
||
* - Provides user-friendly messages for timeouts, rate limits, and invalid queries.
|
||
* - Retries on transient errors and throws detailed exceptions otherwise.
|
||
*/
|
||
#endregion
|
||
|
||
public async Task<string> AskOpenAISqlQueryWithChartAsync(string prompt, int userID, EnumReportGroupType reportGroupType, DateTime nextPayprocessDate = new DateTime(), int timeoutSeconds = 60)
|
||
{
|
||
#region Initialize OpenAI Call Count For Limitation
|
||
int totalOpenAICall = TotalOpenAICall();
|
||
if (totalOpenAICall >= _maxLimit)
|
||
{
|
||
throw new Exception("You have reached the maximum limit of requests. Please try again later.");
|
||
}
|
||
#endregion
|
||
|
||
string returnMessage = "An error occurred while processing your request.";
|
||
int maxRetries = 5;
|
||
int delaySeconds = 1;
|
||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||
{
|
||
try
|
||
{
|
||
#region Initialize OpenAI Client and Cancellation Token
|
||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
|
||
var client = new ChatClient(model: _openAiModel, apiKey: _apiKey);
|
||
#endregion
|
||
|
||
#region Build System Prompt
|
||
string systemPrompt = BuildInitialDescriptionWithChart(reportGroupType) + BuildDynamicSchemaPrompt(reportGroupType, nextPayprocessDate) + BuildInstructionWithChart(reportGroupType);
|
||
#endregion
|
||
|
||
//throw new Exception("You have reached the maximum limit of requests. Please try again later.");
|
||
|
||
#region Call OpenAI API
|
||
var completion = await client.CompleteChatAsync(
|
||
new ChatMessage[]
|
||
{
|
||
ChatMessage.CreateSystemMessage(systemPrompt),
|
||
ChatMessage.CreateUserMessage(prompt)
|
||
},
|
||
cancellationToken: cts.Token
|
||
);
|
||
#endregion
|
||
|
||
#region Process and Store OpenAI Response
|
||
string aiResponse = completion.Value.Content[0].Text.Trim();
|
||
if (string.IsNullOrWhiteSpace(aiResponse))
|
||
throw new Exception("Did not return any response.");
|
||
|
||
aiResponse = aiResponse.Replace("```", "").Trim();
|
||
aiResponse = aiResponse.Replace("\n", "\\n").Replace("\r", "\\r");
|
||
OpenAIResponse aiResponseObject = Newtonsoft.Json.JsonConvert.DeserializeObject<OpenAIResponse>(aiResponse);
|
||
#endregion
|
||
|
||
#region Validate AI response
|
||
string sql = aiResponseObject.Sql;
|
||
bool isChart = aiResponseObject.IsChart;
|
||
EnumAIChartType chartType = GetAIChartType(aiResponseObject.ChartType);
|
||
|
||
if (!string.IsNullOrWhiteSpace(aiResponseObject.Message))
|
||
{
|
||
throw new Exception(aiResponseObject.Message);
|
||
}
|
||
if (!sql.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
|
||
throw new Exception("Only SELECT queries are allowed.");
|
||
#endregion
|
||
|
||
#region Save OpenAI Query Log
|
||
_ = Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
var logService = new OpenAIQueryLogService();
|
||
logService.Save(new OpenAIQueryLog
|
||
{
|
||
UserId = userID,
|
||
UserPrompt = prompt,
|
||
GeneratedQuery = sql,
|
||
ExecutedAt = DateTime.Now,
|
||
ReportGroup = reportGroupType,
|
||
IsChart = isChart,
|
||
ChartType = GetAIChartType(aiResponseObject.ChartType),
|
||
Title = aiResponseObject.Title,
|
||
IsCommon = false
|
||
});
|
||
}
|
||
catch (Exception logEx)
|
||
{
|
||
Console.WriteLine($"[LogError] OpenAI log failed: {logEx.Message}");
|
||
}
|
||
});
|
||
#endregion
|
||
|
||
#region Execute SQL and Build Data
|
||
DataSet data = GetOpenAIData(sql);
|
||
var chartData = BuildOpenAIResponseWithData(data, aiResponseObject);
|
||
return Newtonsoft.Json.JsonConvert.SerializeObject(chartData);
|
||
#endregion
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw new Exception("AI request timed out. Please try again.");
|
||
}
|
||
catch (HttpRequestException httpEx) when (httpEx.Message.Contains("429") || httpEx.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (attempt == maxRetries)
|
||
throw new Exception("Sorry, our AI service is busy now. Please try again after some time.");
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
|
||
delaySeconds *= 2;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (ex.Message.Contains("You've reached our limits of messages", StringComparison.OrdinalIgnoreCase) ||
|
||
ex.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (attempt == maxRetries)
|
||
throw new Exception("Sorry, our AI service is busy now. Please try again after some time.");
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
|
||
delaySeconds *= 2;
|
||
continue;
|
||
}
|
||
throw new Exception(returnMessage + ex.Message, ex);
|
||
}
|
||
}
|
||
throw new Exception("Failed to get a response from AI after multiple attempts. Please try again later.");
|
||
}
|
||
#endregion
|
||
|
||
#region Build OpenAI Response with Data
|
||
public OpenAIResponseWithData BuildOpenAIResponseWithData(DataSet ds, OpenAIResponse aiResponseObject)
|
||
{
|
||
var table = ds.Tables[0];
|
||
var labels = new List<string>();
|
||
var values = new List<double>();
|
||
if (aiResponseObject.IsChart)
|
||
{
|
||
labels = table.AsEnumerable().Select(r => r[0].ToString()).ToList();
|
||
values = table.AsEnumerable().Select(r => Convert.ToDouble(r[1])).ToList();
|
||
}
|
||
var tableJson = ProcessDataSetToJson(ds);
|
||
return new OpenAIResponseWithData
|
||
{
|
||
IsChart = aiResponseObject.IsChart,
|
||
ChartType = aiResponseObject.ChartType,
|
||
Title = aiResponseObject.Title,
|
||
Labels = labels,
|
||
Values = values,
|
||
TableData = tableJson
|
||
};
|
||
}
|
||
#endregion
|
||
|
||
#region Build Initial Description (System Prompt)
|
||
public string BuildInitialDescriptionWithChart(EnumReportGroupType reportGroupType)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("You are an expert SQL assistant.");
|
||
sb.AppendLine("Given a user request and the following database schema, your job is to:");
|
||
sb.AppendLine("1.Always generate a valid SQL SELECT queries in Microsoft SQL Server (T-SQL) syntax.");
|
||
sb.AppendLine("2.Always generate SQL in a single line, without extra newlines or unnecessary spaces.");
|
||
sb.AppendLine("3.Never return DELETE, UPDATE, INSERT queries or any data-modifying operation.You are read-only.");
|
||
sb.AppendLine("4.Detect if the user wants a chart/graph.");
|
||
sb.AppendLine("5.Always return only valid JSON following structure:" +
|
||
"{\"isChart\":true|false,\"chartType\":\"pie|bar|line|scatter|area|histogram|waterfall|gantt|radar|activity\",\"title\":\"<title>\",\"sql\":\"<SQL>\",\"message\":\"\"}");
|
||
sb.AppendLine("6.Title must be short, human-readable, based on the user request.");
|
||
sb.AppendLine("7.If the user asks modification deletion or insartion requested, set sql:'' and message:" +
|
||
"Sorry, I can only help with retrieving data. I am not allowed to update or delete any records.");
|
||
|
||
#region Check
|
||
if (reportGroupType == EnumReportGroupType.Employee)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Information\"");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Performance)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Performance\"");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Salary)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Salary\"");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Training)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Training\"");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Attendance)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Attendance\"");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Leave)
|
||
{
|
||
sb.AppendLine("8.If the user request is not related to the payroll management system or does not match the provided schema,set \"sql\": \"\" and " +
|
||
"\"message\": \"I can only help with questions related to Employee Leave\"");
|
||
}
|
||
#endregion
|
||
|
||
sb.AppendLine("9.Do not return actual data values or any explanations.");
|
||
return sb.ToString();
|
||
}
|
||
#endregion
|
||
|
||
#region Dynamic Schema Generation (System Prompt)
|
||
public string BuildDynamicSchemaPrompt(EnumReportGroupType reportGroupType, DateTime nextPayprocessDate)
|
||
{
|
||
TransactionContext tc = null;
|
||
try
|
||
{
|
||
tc = TransactionContext.Begin();
|
||
DataTable columns = GetColumnDefinitions(tc, reportGroupType);
|
||
DataTable relations = GetColumnRelations(tc,reportGroupType);
|
||
DataTable allData = null;
|
||
if (reportGroupType == EnumReportGroupType.Salary || reportGroupType == EnumReportGroupType.Leave)
|
||
{
|
||
allData = GetAllType(tc, reportGroupType);
|
||
}
|
||
tc.End();
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Database Schema:");
|
||
var pkColumns = new Dictionary<string, List<string>>();
|
||
var grouped = columns.AsEnumerable().GroupBy(row => row["TABLENAME"].ToString()).OrderBy(g => g.Key);
|
||
|
||
#region Table Definitions
|
||
foreach (var tableGroup in grouped)
|
||
{
|
||
string tableName = tableGroup.Key;
|
||
var columnDefs = new List<string>();
|
||
var pks = new List<string>();
|
||
|
||
foreach (var row in tableGroup)
|
||
{
|
||
string columnName = row["COLUMNNAME"].ToString();
|
||
string caption = row["CAPTION"]?.ToString();
|
||
string dataType = ((EnumColumnDataType)Convert.ToInt32(row["DATATYPE"])).ToString();
|
||
bool isPK = row.Table.Columns.Contains("IsPrimaryKey") && Convert.ToBoolean(row["IsPrimaryKey"]);
|
||
bool isFK = row.Table.Columns.Contains("IsForeignKey") && Convert.ToBoolean(row["IsForeignKey"]);
|
||
|
||
string label = $"{columnName}";
|
||
if (isPK)
|
||
{
|
||
label += "(PK)";
|
||
pks.Add(columnName);
|
||
}
|
||
else if (isFK)
|
||
{
|
||
label += "(FK)";
|
||
}
|
||
label += $" ({dataType})";
|
||
if (!string.IsNullOrWhiteSpace(caption) && caption != columnName)
|
||
label += $" = {caption}";
|
||
columnDefs.Add(label);
|
||
}
|
||
|
||
sb.AppendLine($"-{tableName}({string.Join(", ", columnDefs)})");
|
||
if (pks.Any())
|
||
pkColumns[tableName] = pks;
|
||
}
|
||
#endregion
|
||
|
||
#region RelationShip Definitions
|
||
if (relations.Rows.Count > 0)
|
||
{
|
||
sb.AppendLine("Relationships:");
|
||
foreach (DataRow rel in relations.Rows)
|
||
{
|
||
string master = rel["MASTERTABLENAME"].ToString();
|
||
string masterCol = rel["MASTERCOLUMN1NAME"].ToString();
|
||
string link = rel["LINKTABLENAME"].ToString();
|
||
string linkCol = rel["LINKCOLUMN1NAME"].ToString();
|
||
sb.AppendLine($"-{link}.{linkCol} → {master}.{masterCol}");
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
sb.AppendLine(BuildInitialNoted(reportGroupType, nextPayprocessDate, allData));
|
||
return sb.ToString();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
throw new ServiceException(e.Message, e);
|
||
#endregion
|
||
}
|
||
}
|
||
public string BuildInitialNoted(EnumReportGroupType reportGroupType, DateTime nextPayprocessDate, DataTable allData = null)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Note:");
|
||
sb.AppendLine("-Never include primary key (PK) or Foreign Key(FK) columns in SELECT output. For reference only.");
|
||
if (reportGroupType == EnumReportGroupType.Employee)
|
||
{
|
||
sb.AppendLine("-The status field can have synonyms: 'Live' or 'Active' mean the same thing (Live). 'Discontinue' or 'Inactive' mean the same thing (Discontinue).");
|
||
sb.AppendLine("-Employees with no record in the 'EmployeeSpouse' table are considered Unmarried.");
|
||
}
|
||
if (allData != null && reportGroupType == EnumReportGroupType.Salary)
|
||
{
|
||
if (allData.Columns.Contains("Head"))
|
||
{
|
||
var heads = allData.AsEnumerable()
|
||
.Select(r => r["Head"]?.ToString())
|
||
.Where(h => !string.IsNullOrEmpty(h))
|
||
.Distinct()
|
||
.ToList();
|
||
if (heads.Any())
|
||
{
|
||
sb.AppendLine($"-The 'Head' column represents different salary components such as: {string.Join(",", heads)}");
|
||
}
|
||
}
|
||
sb.AppendLine($"-Always use SalaryMonth(always last date of month) in the SQL WHERE clause.");
|
||
sb.AppendLine($"-If a user is not specifies any date or year in the request, always use SalaryMonth = {GlobalFunctions.LastDateOfMonth(nextPayprocessDate).ToString("dd MMM yyyy")} in the SQL WHERE clause.");
|
||
sb.AppendLine("-The columns BirthDate, JoiningDate, SalaryMonth should always be returned in the format 'dd MMM yyyy'.");
|
||
}
|
||
if (allData != null && reportGroupType == EnumReportGroupType.Leave)
|
||
{
|
||
if (allData.Columns.Contains("LeaveType"))
|
||
{
|
||
var heads = allData.AsEnumerable()
|
||
.Select(r => r["LeaveType"]?.ToString())
|
||
.Where(h => !string.IsNullOrEmpty(h))
|
||
.Distinct()
|
||
.ToList();
|
||
if (heads.Any())
|
||
{
|
||
sb.AppendLine($"-The 'LeaveType' column represents different Leave Type such as: {string.Join(",", heads)}");
|
||
}
|
||
}
|
||
sb.AppendLine("-The Columns BirthDate, JoiningDate, FromDate and ToDate should always be returned in the format 'dd MMM yyyy'.");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Attendance)
|
||
{
|
||
sb.AppendLine("-The columns BirthDate, JoiningDate and AttendanceDate should always be returned in the format 'dd MMM yyyy'.");
|
||
sb.AppendLine("-The columns InTime and OutTime should always be returned in the format 'hh:mm:ss aa'.");
|
||
sb.AppendLine("-The 'AttendanceStatus' column represents the daily attendance of an employee and may include: 'Present', 'Absent', 'Delay', 'Leave', 'Holiday', 'Weekly Off', 'Out Side Duty', 'Weekly Holiday', 'Compensation', 'Manual', 'Late', 'Early', 'HalfDay', 'LOA'.");
|
||
sb.AppendLine("-The 'LateStatus' column already stores values as 'Yes' (Late) or 'No' (On Time).");
|
||
sb.AppendLine("-The 'WorkDayType' column defines the type of the day: 'Weekly Holiday', 'National Holiday', 'Working Day', 'HartalDay', 'Weekly and National'.");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Employee || reportGroupType == EnumReportGroupType.Training)
|
||
{
|
||
sb.AppendLine("-The columns BirthDate, JoiningDate, MarriageDate, EmployementEndDate and NominationDate should always be returned in the format 'dd MMM yyyy'.");
|
||
sb.AppendLine("-The 'EmploymentEndDate' column represents when an employee left the company (retirement, resignation, termination, etc)");
|
||
}
|
||
if (reportGroupType == EnumReportGroupType.Training)
|
||
{
|
||
sb.AppendLine("-The TrainingStatus field can have Enrolled|Completed|Unknown");
|
||
sb.AppendLine("-The columns StartDate,EndDate,BirthDate,JoiningDate,LineManagerBirthDate,LineManagerJoiningDate should always be returned in the format 'dd MMM yyyy'.");
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
#endregion
|
||
|
||
#region Build Instruction (System Prompt)
|
||
public string BuildInstructionWithChart(EnumReportGroupType reportGroupType)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Instructions:");
|
||
sb.AppendLine("-Treat any mention of EmployeeID or EmployeeNo in user input as referring to Employee.EmployeeNo.");
|
||
sb.AppendLine("-Use appropriate JOINs based on foreign keys.");
|
||
sb.AppendLine("-If only certain columns are requested, include only those.");
|
||
sb.AppendLine("-Always map user-friendly aliases (e.g., 'Employee No', 'Department Name') to the correct table and column.");
|
||
sb.AppendLine("-If a date condition is mentioned, use WHERE clause accordingly.");
|
||
sb.AppendLine("-Support ordering, limits (TOP/OFFSET), and filtering.");
|
||
return sb.ToString();
|
||
}
|
||
#endregion
|
||
|
||
#region OpenAI Previous Query Execution
|
||
public async Task<string> OpenAIPreviousQueryExecution(OpenAIQueryLog oQueryLog)
|
||
{
|
||
try
|
||
{
|
||
oQueryLog.TotalExecution++;
|
||
|
||
#region Update OpenAI Query Log Total Execution
|
||
Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
var logService = new OpenAIQueryLogService();
|
||
logService.UpdateQueryLogTotalExecution(oQueryLog);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[LogError] OpenAI log failed: {ex.Message}");
|
||
}
|
||
});
|
||
#endregion
|
||
|
||
string sql = oQueryLog.GeneratedQuery;
|
||
DataSet data = GetOpenAIData(sql);
|
||
|
||
OpenAIResponse aiResponseObject = new OpenAIResponse
|
||
{
|
||
IsChart = oQueryLog.IsChart,
|
||
ChartType = GetAIChartType(oQueryLog.ChartType),
|
||
Sql = sql,
|
||
Message = string.Empty,
|
||
Title = oQueryLog.Title
|
||
};
|
||
var chartData = BuildOpenAIResponseWithData(data, aiResponseObject);
|
||
return Newtonsoft.Json.JsonConvert.SerializeObject(chartData);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception(returnMessage);
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Process DataSet to JSON
|
||
public string ProcessDataSetToJson(DataSet OpenAiData)
|
||
{
|
||
string output;
|
||
DataSet information = new DataSet();
|
||
information = OpenAiData;
|
||
var value = (object)information;
|
||
switch (value)
|
||
{
|
||
case null:
|
||
output = "";
|
||
break;
|
||
case DataSet ds:
|
||
output = JsonSerializer.Serialize(ds.Tables.Cast<DataTable>()
|
||
.Select(tbl => new {
|
||
TableName = tbl.TableName,
|
||
Rows = tbl.Rows.Cast<DataRow>()
|
||
.Select(row => tbl.Columns.Cast<DataColumn>()
|
||
.ToDictionary(col => col.ColumnName, col => row[col]))
|
||
}),
|
||
new JsonSerializerOptions { WriteIndented = true }
|
||
);
|
||
break;
|
||
case DataTable dt:
|
||
output = JsonSerializer.Serialize(
|
||
dt.Rows.Cast<DataRow>()
|
||
.Select(row => dt.Columns.Cast<DataColumn>()
|
||
.ToDictionary(col => col.ColumnName, col => row[col])),
|
||
new JsonSerializerOptions { WriteIndented = true }
|
||
);
|
||
break;
|
||
case IEnumerable<object> list:
|
||
output = JsonSerializer.Serialize(list, new JsonSerializerOptions { WriteIndented = true });
|
||
break;
|
||
case string str:
|
||
output = str;
|
||
break;
|
||
default:
|
||
output = JsonSerializer.Serialize(value, new JsonSerializerOptions
|
||
{
|
||
WriteIndented = true,
|
||
IgnoreReadOnlyProperties = true,
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||
});
|
||
break;
|
||
}
|
||
return output;
|
||
}
|
||
#endregion
|
||
|
||
#region Get OpenAI Data (Generate OpenAI's Provided SQL Query)
|
||
public DataSet GetOpenAIData(string sql)
|
||
{
|
||
TransactionContext tc = null;
|
||
|
||
DataSet information = new DataSet();
|
||
try
|
||
{
|
||
tc = TransactionContext.Begin();
|
||
information = OpenAIDA.GetOpenAIData(tc, sql);
|
||
tc.End();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
|
||
throw new ServiceException(e.Message, e);
|
||
|
||
#endregion
|
||
}
|
||
|
||
return information;
|
||
}
|
||
#endregion
|
||
|
||
#region Get 'Column Definitions' and 'Relations Between Tables'
|
||
public DataTable GetColumnDefinitions(TransactionContext tc, EnumReportGroupType reportGroupType)
|
||
{
|
||
DataTable oColumnDefinition = new DataTable();
|
||
try
|
||
{
|
||
oColumnDefinition = OpenAIDA.GetColumnDefinitions(tc, reportGroupType);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
|
||
throw new ServiceException(e.Message, e);
|
||
|
||
#endregion
|
||
}
|
||
return oColumnDefinition;
|
||
}
|
||
public async Task<DataTable> GetColumnDefinitions(EnumReportGroupType reportGroupType)
|
||
{
|
||
DataTable oColumnDefinition = new DataTable();
|
||
TransactionContext tc = null;
|
||
try
|
||
{
|
||
tc = TransactionContext.Begin();
|
||
oColumnDefinition = OpenAIDA.GetColumnDefinitions(tc, reportGroupType);
|
||
tc.End();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
|
||
throw new ServiceException(e.Message, e);
|
||
|
||
#endregion
|
||
}
|
||
return oColumnDefinition;
|
||
}
|
||
public DataTable GetColumnRelations(TransactionContext tc, EnumReportGroupType reportGroupType)
|
||
{
|
||
DataTable oColumnRelation = new DataTable();
|
||
try
|
||
{
|
||
oColumnRelation = OpenAIDA.GetColumnRelations(tc, reportGroupType);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
|
||
throw new ServiceException(e.Message, e);
|
||
|
||
#endregion
|
||
}
|
||
return oColumnRelation;
|
||
}
|
||
#endregion
|
||
|
||
#region Get All Type for (Leave Type & Salary Head)
|
||
public DataTable GetAllType(TransactionContext tc, EnumReportGroupType reportGroupType)
|
||
{
|
||
DataTable oColumnDefinition = new DataTable();
|
||
try
|
||
{
|
||
oColumnDefinition = OpenAIDA.GetAllType(tc, reportGroupType);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
|
||
throw new ServiceException(e.Message, e);
|
||
|
||
#endregion
|
||
}
|
||
return oColumnDefinition;
|
||
}
|
||
#endregion
|
||
|
||
#region Get AI Chart Type
|
||
public EnumAIChartType GetAIChartType(string type)
|
||
{
|
||
switch (type.ToLower())
|
||
{
|
||
case "pie":
|
||
return EnumAIChartType.PieChart;
|
||
case "bar":
|
||
return EnumAIChartType.BarChart;
|
||
case "line":
|
||
return EnumAIChartType.LineGraph;
|
||
case "scatter":
|
||
return EnumAIChartType.ScatterPlot;
|
||
case "area":
|
||
return EnumAIChartType.AreaChart;
|
||
case "histogram":
|
||
return EnumAIChartType.Histogram;
|
||
case "waterfall":
|
||
return EnumAIChartType.WaterfallChart;
|
||
case "gantt":
|
||
return EnumAIChartType.GantChart;
|
||
case "radar":
|
||
return EnumAIChartType.RadarChart;
|
||
case "activity":
|
||
return EnumAIChartType.ActivityDiagram;
|
||
default:
|
||
return EnumAIChartType.None;
|
||
}
|
||
}
|
||
public string GetAIChartType(EnumAIChartType type)
|
||
{
|
||
switch (type)
|
||
{
|
||
case EnumAIChartType.PieChart:
|
||
return "pie";
|
||
case EnumAIChartType.BarChart:
|
||
return "bar";
|
||
case EnumAIChartType.LineGraph:
|
||
return "line";
|
||
case EnumAIChartType.ScatterPlot:
|
||
return "scatter";
|
||
case EnumAIChartType.AreaChart:
|
||
return "area";
|
||
case EnumAIChartType.Histogram:
|
||
return "histogram";
|
||
case EnumAIChartType.WaterfallChart:
|
||
return "waterfall";
|
||
case EnumAIChartType.GantChart:
|
||
return "gantt";
|
||
case EnumAIChartType.RadarChart:
|
||
return "radar";
|
||
case EnumAIChartType.ActivityDiagram:
|
||
return "activity";
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Total OpenAI Call
|
||
public int TotalOpenAICall()
|
||
{
|
||
TransactionContext tc = null;
|
||
int? totalCall = 0;
|
||
try
|
||
{
|
||
tc = TransactionContext.Begin();
|
||
DataReader dr = new DataReader(OpenAIDA.TotalOpenAICall(tc));
|
||
if (dr.Read())
|
||
{
|
||
totalCall = dr.GetInt32(0);
|
||
}
|
||
dr.Close();
|
||
tc.End();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
#region Handle Exception
|
||
if (tc != null)
|
||
tc.HandleError();
|
||
ExceptionLog.Write(e);
|
||
throw new ServiceException(e.Message, e);
|
||
#endregion
|
||
}
|
||
return totalCall ?? 0;
|
||
}
|
||
#endregion
|
||
}
|
||
public class AIServiceUsageLimitException : Exception
|
||
{
|
||
public AIServiceUsageLimitException(string message) : base(message) { }
|
||
}
|
||
}
|