376 lines
12 KiB
C#
376 lines
12 KiB
C#
using ExperionCrawler.Core.Application.DTOs;
|
|
using ExperionCrawler.Core.Application.Interfaces;
|
|
using ExperionCrawler.Infrastructure.Mcp;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ExperionCrawler.Web.Controllers;
|
|
|
|
/// <summary>
|
|
/// Text-to-SQL API 컨트롤러
|
|
/// 자연어 질의를 파싱하고 시계열 데이터를 조회합니다.
|
|
/// MCP (Model Context Protocol) 통합을 위한 엔드포인트를 제공합니다.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/text-to-sql")]
|
|
public class TextToSqlController : ControllerBase
|
|
{
|
|
private readonly ITextToSqlService _textToSqlService;
|
|
private readonly IExperionDbService _dbService;
|
|
private readonly IMcpService _mcpService;
|
|
private readonly ILogger<TextToSqlController> _logger;
|
|
|
|
public TextToSqlController(
|
|
ITextToSqlService textToSqlService,
|
|
IExperionDbService dbService,
|
|
IMcpService mcpService,
|
|
ILogger<TextToSqlController> logger)
|
|
{
|
|
_textToSqlService = textToSqlService;
|
|
_dbService = dbService;
|
|
_mcpService = mcpService;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 자연어 질의를 SQL로 변환
|
|
/// </summary>
|
|
[HttpPost("parse")]
|
|
public async Task<IActionResult> Parse([FromBody] NaturalLanguageQueryDto dto)
|
|
{
|
|
try
|
|
{
|
|
var sql = await _textToSqlService.ParseNaturalLanguageAsync(dto.Query);
|
|
return Ok(new { success = true, sql });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP query_with_nl 도구 호출 - 자연어 → LLM SQL 생성 → 실행
|
|
/// </summary>
|
|
[HttpPost("query-nl")]
|
|
public async Task<IActionResult> QueryWithNl([FromBody] NaturalLanguageQueryDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.Query))
|
|
return BadRequest(new { success = false, error = "질문이 비어있음" });
|
|
|
|
try
|
|
{
|
|
var result = await _mcpService.QueryWithNlAsync(dto.Query);
|
|
if (!result.Success)
|
|
return Ok(new { success = false, error = result.Error });
|
|
|
|
try
|
|
{
|
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>?>(result.Data!);
|
|
_logger.LogInformation("[TextToSql] query-nl 응답: success={Success}, data={Data}", jsonData?.GetType(), jsonData);
|
|
|
|
// data 필드가 JSON 문자열일 수 있으므로 다시 디시리얼라이즈
|
|
if (jsonData != null && jsonData.TryGetValue("data", out var dataObj) && dataObj is string dataString)
|
|
{
|
|
_logger.LogInformation("[TextToSql] data 필드가 문자열임: {DataString}", dataString);
|
|
var parsedData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>?>(dataString);
|
|
jsonData["data"] = parsedData;
|
|
}
|
|
|
|
return Ok(new { success = true, data = jsonData });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] query-nl JSON 디시리얼라이즈 실패: {Data}", result.Data);
|
|
return Ok(new { success = true, data = result.Data });
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] query-nl 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP 도구 목록 조회
|
|
/// </summary>
|
|
[HttpGet("tools")]
|
|
public async Task<IActionResult> ListTools()
|
|
{
|
|
try
|
|
{
|
|
var tools = await _mcpService.ListToolsAsync();
|
|
return Ok(new { success = true, tools });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] 도구 목록 조회 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP run_sql 도구 호출 - SQL 실행
|
|
/// Text-to-SQL 엔진으로 생성된 SQL을 안전하게 실행
|
|
/// </summary>
|
|
[HttpPost("execute-mcp")]
|
|
public async Task<IActionResult> ExecuteFromMcp([FromBody] SqlQueryDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.Sql))
|
|
{
|
|
return BadRequest(new { success = false, error = "SQL이 비어있음" });
|
|
}
|
|
|
|
try
|
|
{
|
|
// MCP run_sql 도구 호출
|
|
var result = await _mcpService.RunSqlAsync(dto.Sql);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = false,
|
|
error = result.Error
|
|
});
|
|
}
|
|
|
|
// JSON 결과 반환 (쿼리 결과)
|
|
try
|
|
{
|
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
|
return Ok(new { success = true, data = jsonData });
|
|
}
|
|
catch
|
|
{
|
|
return Ok(new { success = true, data = result.Data });
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] MCP 실행 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP query_pv_history 도구 호출 - 과거 값 히스토리 조회
|
|
/// </summary>
|
|
[HttpPost("query-history")]
|
|
public async Task<IActionResult> QueryHistory([FromBody] HistoryQueryRequestDto dto)
|
|
{
|
|
try
|
|
{
|
|
var tagNames = dto.TagNames ?? [];
|
|
var timeFrom = dto.From ?? DateTime.UtcNow.AddDays(-1).ToString("o");
|
|
var timeTo = dto.To ?? DateTime.UtcNow.ToString("o");
|
|
var limit = dto.Limit ?? 100;
|
|
|
|
var result = await _mcpService.QueryPvHistoryAsync(
|
|
tagNames,
|
|
timeFrom,
|
|
timeTo,
|
|
limit
|
|
);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = false,
|
|
error = result.Error
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = jsonData
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = result.Data
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] History 쿼리 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP get_tag_metadata 도구 호출 - 태그 메타데이터 검색
|
|
/// </summary>
|
|
[HttpGet("tags/search")]
|
|
public async Task<IActionResult> SearchTags([FromQuery] string query, [FromQuery] int? limit)
|
|
{
|
|
try
|
|
{
|
|
var tagLimit = limit ?? 10;
|
|
var result = await _mcpService.GetTagMetadataAsync(query, tagLimit);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = false,
|
|
error = result.Error
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = jsonData
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = result.Data
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] 태그 검색 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MCP list_drawings 도구 호출 - 도면 목록 조회
|
|
/// </summary>
|
|
[HttpGet("drawings")]
|
|
public async Task<IActionResult> ListDrawings([FromQuery] string? unitNo)
|
|
{
|
|
try
|
|
{
|
|
var result = await _mcpService.ListDrawingsAsync(unitNo);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = false,
|
|
error = result.Error
|
|
});
|
|
}
|
|
|
|
try
|
|
{
|
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = jsonData
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
return Ok(new
|
|
{
|
|
success = true,
|
|
data = result.Data
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] 도면 목록 조회 실패");
|
|
return Ok(new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 쿼리 제안 (자동 완성)
|
|
/// </summary>
|
|
[HttpGet("suggest")]
|
|
public async Task<IActionResult> Suggest([FromQuery] string input = "")
|
|
{
|
|
var suggestions = await _textToSqlService.SuggestQueriesAsync(input);
|
|
return Ok(new { success = true, suggestions });
|
|
}
|
|
|
|
/// <summary>
|
|
/// 시계열 분석 (평균, 최대, 최소, 추세)
|
|
/// </summary>
|
|
[HttpPost("analyze")]
|
|
public async Task<IActionResult> Analyze([FromBody] AnalyzeRequestDto dto)
|
|
{
|
|
var result = await _textToSqlService.AnalyzeAsync(dto);
|
|
return Ok(new {
|
|
success = result.Success,
|
|
error = result.Error,
|
|
tags = result.Tags?.Select(t => new {
|
|
tagName = t.TagName,
|
|
avg = t.Avg,
|
|
mean = t.Mean,
|
|
min = t.Min,
|
|
max = t.Max,
|
|
first = t.First,
|
|
last = t.Last,
|
|
pointCount = t.PointCount,
|
|
stddev = t.StdDev,
|
|
from = t.From,
|
|
to = t.To
|
|
}).ToList()
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사용자 지정 간격으로 history 이력 조회
|
|
/// history_table의 기본 저장 간격(60초)을 기반으로 사용자가 요청한 간격으로 데이터 집계
|
|
/// </summary>
|
|
[HttpPost("query-history-interval")]
|
|
public async Task<IActionResult> QueryHistoryInterval([FromBody] HistoryIntervalQueryRequestDto dto)
|
|
{
|
|
try
|
|
{
|
|
var request = new HistoryIntervalQueryRequest(
|
|
dto.TagNames,
|
|
dto.From,
|
|
dto.To,
|
|
dto.Interval,
|
|
dto.Limit);
|
|
|
|
var result = await _dbService.QueryHistoryWithIntervalAsync(request);
|
|
|
|
var response = new
|
|
{
|
|
success = true,
|
|
tagNames = result.TagNames.ToList(),
|
|
rows = result.Rows.Select(r => new
|
|
{
|
|
timeBucket = r.TimeBucket,
|
|
values = r.Values
|
|
}).ToList(),
|
|
baseIntervalSeconds = result.BaseIntervalSeconds,
|
|
queryInterval = result.QueryInterval
|
|
};
|
|
|
|
return Ok(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "[TextToSql] QueryHistoryInterval 실패");
|
|
return StatusCode(StatusCodes.Status500InternalServerError, new { success = false, error = ex.Message });
|
|
}
|
|
}
|
|
}
|