refs #735: add few-shot NL2SQL MCP tool
This commit is contained in:
37
docs/design/735-sgmp-fewshot-nl2sql-mcp/README.md
Normal file
37
docs/design/735-sgmp-fewshot-nl2sql-mcp/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# SGMP Few-shot NL2SQL MCP (#735)
|
||||||
|
|
||||||
|
> 상태: Approved
|
||||||
|
> 구현: `McpSseService`, `SelectAiService`, `McpProperties`
|
||||||
|
|
||||||
|
## 목적
|
||||||
|
|
||||||
|
기존 Select AI Text2SQL 경로와 분리된 검증용 MCP tool을 제공한다. 질문과 유사한 검토 완료 예제를 벡터 검색하고, 그 SQL 패턴을 prompt에 참고자료로 넣은 뒤 `SHOWSQL`로 생성한 SQL을 읽기 전용으로 실행한다.
|
||||||
|
|
||||||
|
## MCP 계약
|
||||||
|
|
||||||
|
- **tool name**: `oracle.select_ai.smilegate_fewshot_nl2sql` (환경변수로 재정의 가능)
|
||||||
|
- **입력**: `prompt` (최대 4,000자)
|
||||||
|
- **출력**: 벡터 Few-shot 예제(질문, SQL, 모델, cosine distance), 생성 SQL, 실행 상태, 행 수, 최대 100건 결과
|
||||||
|
|
||||||
|
Few-shot SQL은 실행하지 않는다. 현재 메타데이터·alias 정책을 우선하고, Select AI가 새로 생성한 SQL만 read-only 검증 후 실행한다.
|
||||||
|
|
||||||
|
## 안전 규칙
|
||||||
|
|
||||||
|
1. 벡터 검색 실패는 `UNAVAILABLE` 상태로 남기고 기존 정책 prompt로 폴백한다.
|
||||||
|
2. 생성 결과는 단일 `SELECT` 또는 `WITH`만 허용한다.
|
||||||
|
3. DDL, DML, PL/SQL, 시스템 객체, 잠금 구문, 다중 문장은 차단한다.
|
||||||
|
4. JDBC read-only 트랜잭션과 30초 query timeout, 최대 100행 제한을 적용한다.
|
||||||
|
5. 기존 `data_text2sql`, `data_showprompt`, `qa_vector_search`, `qa_vector_store` tool은 변경하지 않는다.
|
||||||
|
|
||||||
|
## 설정
|
||||||
|
|
||||||
|
```text
|
||||||
|
BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_NAME
|
||||||
|
BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_LABEL
|
||||||
|
BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION
|
||||||
|
```
|
||||||
|
|
||||||
|
## 추적성
|
||||||
|
|
||||||
|
- Redmine: #735
|
||||||
|
- 테스트: `McpSseServiceTest`
|
||||||
@@ -17,7 +17,10 @@ public record McpProperties(
|
|||||||
String qaVectorSearchToolDescription,
|
String qaVectorSearchToolDescription,
|
||||||
String qaVectorStoreToolName,
|
String qaVectorStoreToolName,
|
||||||
String qaVectorStoreToolLabel,
|
String qaVectorStoreToolLabel,
|
||||||
String qaVectorStoreToolDescription
|
String qaVectorStoreToolDescription,
|
||||||
|
String fewShotNl2SqlToolName,
|
||||||
|
String fewShotNl2SqlToolLabel,
|
||||||
|
String fewShotNl2SqlToolDescription
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
|
private static final String DEFAULT_TOOL_NAME = "oracle.select_ai.data_text2sql";
|
||||||
@@ -46,6 +49,13 @@ public record McpProperties(
|
|||||||
private static final String DEFAULT_QA_VECTOR_STORE_TOOL_DESCRIPTION =
|
private static final String DEFAULT_QA_VECTOR_STORE_TOOL_DESCRIPTION =
|
||||||
"검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다. "
|
"검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다. "
|
||||||
+ "질문과 읽기 전용 답 SQL이 필요합니다.";
|
+ "질문과 읽기 전용 답 SQL이 필요합니다.";
|
||||||
|
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_NAME =
|
||||||
|
"oracle.select_ai.smilegate_fewshot_nl2sql";
|
||||||
|
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_LABEL =
|
||||||
|
"Few-shot NL2SQL 실행";
|
||||||
|
private static final String DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION =
|
||||||
|
"벡터 Few-shot 예제를 찾아 prompt에 반영하고, SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. "
|
||||||
|
+ "Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.";
|
||||||
|
|
||||||
public String resolvedToolName() {
|
public String resolvedToolName() {
|
||||||
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
|
return requiredOrDefault(toolName, DEFAULT_TOOL_NAME);
|
||||||
@@ -99,6 +109,18 @@ public record McpProperties(
|
|||||||
return requiredOrDefault(qaVectorStoreToolDescription, DEFAULT_QA_VECTOR_STORE_TOOL_DESCRIPTION);
|
return requiredOrDefault(qaVectorStoreToolDescription, DEFAULT_QA_VECTOR_STORE_TOOL_DESCRIPTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String resolvedFewShotNl2SqlToolName() {
|
||||||
|
return requiredOrDefault(fewShotNl2SqlToolName, DEFAULT_FEW_SHOT_NL2SQL_TOOL_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolvedFewShotNl2SqlToolLabel() {
|
||||||
|
return requiredOrDefault(fewShotNl2SqlToolLabel, DEFAULT_FEW_SHOT_NL2SQL_TOOL_LABEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolvedFewShotNl2SqlToolDescription() {
|
||||||
|
return requiredOrDefault(fewShotNl2SqlToolDescription, DEFAULT_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION);
|
||||||
|
}
|
||||||
|
|
||||||
private String requiredOrDefault(String value, String fallback) {
|
private String requiredOrDefault(String value, String fallback) {
|
||||||
return value == null || value.isBlank() ? fallback : value.trim();
|
return value == null || value.isBlank() ? fallback : value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ public class McpSseService {
|
|||||||
/** Tools registered by this MCP server. */
|
/** Tools registered by this MCP server. */
|
||||||
public List<McpToolView> registeredTools() {
|
public List<McpToolView> registeredTools() {
|
||||||
return List.of(
|
return List.of(
|
||||||
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(), qaVectorStoreView());
|
selectAiQueryView(), selectAiShowpromptView(), qaVectorSearchView(),
|
||||||
|
qaVectorStoreView(), fewShotNl2SqlView());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObjectNode initializeResult(String contextPath) {
|
private ObjectNode initializeResult(String contextPath) {
|
||||||
@@ -96,6 +97,7 @@ public class McpSseService {
|
|||||||
tools.add(toolDefinition(selectAiShowpromptView()));
|
tools.add(toolDefinition(selectAiShowpromptView()));
|
||||||
tools.add(toolDefinition(qaVectorSearchView()));
|
tools.add(toolDefinition(qaVectorSearchView()));
|
||||||
tools.add(toolDefinition(qaVectorStoreView()));
|
tools.add(toolDefinition(qaVectorStoreView()));
|
||||||
|
tools.add(toolDefinition(fewShotNl2SqlView()));
|
||||||
result.set("tools", tools);
|
result.set("tools", tools);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,8 @@ public class McpSseService {
|
|||||||
boolean showpromptTool = showpromptToolName().equals(calledToolName);
|
boolean showpromptTool = showpromptToolName().equals(calledToolName);
|
||||||
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
|
boolean qaVectorSearchTool = qaVectorSearchToolName().equals(calledToolName);
|
||||||
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
|
boolean qaVectorStoreTool = qaVectorStoreToolName().equals(calledToolName);
|
||||||
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool) {
|
boolean fewShotNl2SqlTool = fewShotNl2SqlToolName().equals(calledToolName);
|
||||||
|
if (!queryTool && !showpromptTool && !qaVectorSearchTool && !qaVectorStoreTool && !fewShotNl2SqlTool) {
|
||||||
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
|
throw new AppException("등록되지 않은 MCP tool입니다: " + calledToolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +175,7 @@ public class McpSseService {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
String prompt = arguments.path("prompt").asText("");
|
String prompt = arguments.path("prompt").asText("");
|
||||||
response = queryTool
|
response = queryTool || fewShotNl2SqlTool
|
||||||
? selectAiService.generateAndExecute(token, prompt)
|
? selectAiService.generateAndExecute(token, prompt)
|
||||||
: selectAiService.generatePrompt(token, prompt);
|
: selectAiService.generatePrompt(token, prompt);
|
||||||
}
|
}
|
||||||
@@ -278,6 +281,12 @@ public class McpSseService {
|
|||||||
qaVectorStoreToolLabel(), SELECT_AI_TOOL_PATH);
|
qaVectorStoreToolLabel(), SELECT_AI_TOOL_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private McpToolView fewShotNl2SqlView() {
|
||||||
|
return new McpToolView(
|
||||||
|
fewShotNl2SqlToolName(), fewShotNl2SqlToolDescription(), -1L,
|
||||||
|
fewShotNl2SqlToolLabel(), SELECT_AI_TOOL_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
private String selectAiProfile() {
|
private String selectAiProfile() {
|
||||||
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
|
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
|
||||||
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
|
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
|
||||||
@@ -354,6 +363,22 @@ public class McpSseService {
|
|||||||
: mcpProperties.resolvedQaVectorStoreToolDescription();
|
: mcpProperties.resolvedQaVectorStoreToolDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String fewShotNl2SqlToolName() {
|
||||||
|
return mcpProperties == null ? "oracle.select_ai.smilegate_fewshot_nl2sql"
|
||||||
|
: mcpProperties.resolvedFewShotNl2SqlToolName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fewShotNl2SqlToolLabel() {
|
||||||
|
return mcpProperties == null ? "Few-shot NL2SQL 실행"
|
||||||
|
: mcpProperties.resolvedFewShotNl2SqlToolLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fewShotNl2SqlToolDescription() {
|
||||||
|
return mcpProperties == null
|
||||||
|
? "벡터 Few-shot 예제를 찾아 prompt에 반영하고 SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다."
|
||||||
|
: mcpProperties.resolvedFewShotNl2SqlToolDescription();
|
||||||
|
}
|
||||||
|
|
||||||
private String pretty(Object value) {
|
private String pretty(Object value) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public class SelectAiService {
|
|||||||
response.put("originalPrompt", normalizedPrompt);
|
response.put("originalPrompt", normalizedPrompt);
|
||||||
response.put("fewShotStatus", enrichedPrompt.status());
|
response.put("fewShotStatus", enrichedPrompt.status());
|
||||||
response.put("fewShotExampleCount", enrichedPrompt.exampleCount());
|
response.put("fewShotExampleCount", enrichedPrompt.exampleCount());
|
||||||
|
addFewShotExamples(response, enrichedPrompt.examples());
|
||||||
response.put("generatedSql", normalizedSql);
|
response.put("generatedSql", normalizedSql);
|
||||||
response.put("execution", "READ_ONLY_EXECUTED");
|
response.put("execution", "READ_ONLY_EXECUTED");
|
||||||
response.put("rowCount", execution.items().size());
|
response.put("rowCount", execution.items().size());
|
||||||
@@ -89,6 +90,21 @@ public class SelectAiService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addFewShotExamples(ObjectNode response, List<QaVectorService.VectorExample> examples) {
|
||||||
|
ArrayNode items = response.putArray("fewShotExamples");
|
||||||
|
for (QaVectorService.VectorExample example : examples) {
|
||||||
|
ObjectNode item = items.addObject();
|
||||||
|
item.put("exampleId", example.exampleId());
|
||||||
|
item.put("question", example.question());
|
||||||
|
item.put("answerSql", example.answerSql());
|
||||||
|
if (example.answer() != null) {
|
||||||
|
item.put("answer", example.answer());
|
||||||
|
}
|
||||||
|
item.put("embeddingModel", example.embeddingModel());
|
||||||
|
item.put("cosineDistance", example.cosineDistance());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the prompt Select AI assembled for SQL generation without executing generated SQL. */
|
/** Returns the prompt Select AI assembled for SQL generation without executing generated SQL. */
|
||||||
public JsonNode generatePrompt(String bearerToken, String prompt) {
|
public JsonNode generatePrompt(String bearerToken, String prompt) {
|
||||||
requireActiveToken(bearerToken);
|
requireActiveToken(bearerToken);
|
||||||
@@ -171,20 +187,21 @@ public class SelectAiService {
|
|||||||
String prompt
|
String prompt
|
||||||
) {
|
) {
|
||||||
if (!fewShotEnabled(selectAi) || qaVectorService == null) {
|
if (!fewShotEnabled(selectAi) || qaVectorService == null) {
|
||||||
return new EnrichedPrompt(prompt, "DISABLED", 0);
|
return new EnrichedPrompt(prompt, "DISABLED", 0, List.of());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
List<QaVectorService.VectorExample> examples = qaVectorService
|
List<QaVectorService.VectorExample> examples = qaVectorService
|
||||||
.search(bearerToken, prompt, fewShotTopK(selectAi))
|
.search(bearerToken, prompt, fewShotTopK(selectAi))
|
||||||
.examples();
|
.examples();
|
||||||
if (examples.isEmpty()) {
|
if (examples.isEmpty()) {
|
||||||
return new EnrichedPrompt(composePolicyPrompt(prompt), "NO_MATCH", 0);
|
return new EnrichedPrompt(composePolicyPrompt(prompt), "NO_MATCH", 0, List.of());
|
||||||
}
|
}
|
||||||
return new EnrichedPrompt(
|
return new EnrichedPrompt(
|
||||||
composeFewShotPrompt(prompt, examples), "APPLIED", Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES));
|
composeFewShotPrompt(prompt, examples), "APPLIED", Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES),
|
||||||
|
examples.subList(0, Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES)));
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// Vector retrieval is an optional prompt aid; preserve the normal Text2SQL path on failure.
|
// Vector retrieval is an optional prompt aid; preserve the normal Text2SQL path on failure.
|
||||||
return new EnrichedPrompt(composePolicyPrompt(prompt), "UNAVAILABLE", 0);
|
return new EnrichedPrompt(composePolicyPrompt(prompt), "UNAVAILABLE", 0, List.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,5 +342,6 @@ public class SelectAiService {
|
|||||||
|
|
||||||
private record QueryExecution(ArrayNode items, boolean truncated) {}
|
private record QueryExecution(ArrayNode items, boolean truncated) {}
|
||||||
|
|
||||||
private record EnrichedPrompt(String prompt, String status, int exampleCount) {}
|
private record EnrichedPrompt(
|
||||||
|
String prompt, String status, int exampleCount, List<QaVectorService.VectorExample> examples) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ backoffice:
|
|||||||
qa-vector-store-tool-name: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_NAME:oracle.select_ai.qa_vector_store}
|
qa-vector-store-tool-name: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_NAME:oracle.select_ai.qa_vector_store}
|
||||||
qa-vector-store-tool-label: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_LABEL:Select AI 예제 SQL 저장}
|
qa-vector-store-tool-label: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_LABEL:Select AI 예제 SQL 저장}
|
||||||
qa-vector-store-tool-description: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_DESCRIPTION:검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다. 질문과 읽기 전용 답 SQL이 필요합니다.}
|
qa-vector-store-tool-description: ${BACKOFFICE_MCP_QA_VECTOR_STORE_TOOL_DESCRIPTION:검토된 Select AI 결과를 후속 Text2SQL 품질 향상용 예제 SQL로 저장합니다. 질문과 읽기 전용 답 SQL이 필요합니다.}
|
||||||
|
few-shot-nl2sql-tool-name: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_NAME:oracle.select_ai.smilegate_fewshot_nl2sql}
|
||||||
|
few-shot-nl2sql-tool-label: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_LABEL:Few-shot NL2SQL 실행}
|
||||||
|
few-shot-nl2sql-tool-description: ${BACKOFFICE_MCP_FEW_SHOT_NL2SQL_TOOL_DESCRIPTION:벡터 Few-shot 예제를 찾아 prompt에 반영하고 SHOWSQL로 생성한 읽기 전용 SQL을 실행합니다. Few-shot 근거, 생성 SQL, 실행 결과를 함께 반환합니다.}
|
||||||
masking:
|
masking:
|
||||||
policies: ${BACKOFFICE_MASKING_POLICIES:}
|
policies: ${BACKOFFICE_MASKING_POLICIES:}
|
||||||
security-sql-scripts:
|
security-sql-scripts:
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ class McpSseServiceTest {
|
|||||||
"테스트 질문에 사용할 예제 SQL을 조회합니다.",
|
"테스트 질문에 사용할 예제 SQL을 조회합니다.",
|
||||||
"oracle.select_ai.test_qa_vector_store",
|
"oracle.select_ai.test_qa_vector_store",
|
||||||
"테스트 예제 SQL 저장",
|
"테스트 예제 SQL 저장",
|
||||||
"테스트 예제 SQL을 저장합니다."
|
"테스트 예제 SQL을 저장합니다.",
|
||||||
|
"oracle.select_ai.test_fewshot_nl2sql",
|
||||||
|
"테스트 Few-shot NL2SQL",
|
||||||
|
"테스트 Few-shot 검색 및 실행 도구입니다."
|
||||||
),
|
),
|
||||||
qaVectorService
|
qaVectorService
|
||||||
);
|
);
|
||||||
@@ -47,7 +50,7 @@ class McpSseServiceTest {
|
|||||||
ObjectNode response = service.handle("default", request(1, "tools/list"));
|
ObjectNode response = service.handle("default", request(1, "tools/list"));
|
||||||
|
|
||||||
var tools = response.path("result").path("tools");
|
var tools = response.path("result").path("tools");
|
||||||
assertThat(tools).hasSize(4);
|
assertThat(tools).hasSize(5);
|
||||||
var selectAi = tools.get(0);
|
var selectAi = tools.get(0);
|
||||||
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
|
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
|
||||||
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
|
assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
|
||||||
@@ -82,6 +85,11 @@ class McpSseServiceTest {
|
|||||||
assertThat(vectorStore.path("inputSchema").path("required"))
|
assertThat(vectorStore.path("inputSchema").path("required"))
|
||||||
.extracting(node -> node.asText())
|
.extracting(node -> node.asText())
|
||||||
.contains("question", "answerSql");
|
.contains("question", "answerSql");
|
||||||
|
|
||||||
|
var fewShot = tools.get(4);
|
||||||
|
assertThat(fewShot.path("name").asText())
|
||||||
|
.isEqualTo("oracle.select_ai.test_fewshot_nl2sql");
|
||||||
|
assertThat(fewShot.path("description").asText()).contains("Few-shot");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -128,6 +136,24 @@ class McpSseServiceTest {
|
|||||||
.contains("RESULT");
|
.contains("RESULT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void callsDedicatedFewShotNl2SqlToolWithGeneratedSqlAndExecutionResult() {
|
||||||
|
ObjectNode request = request(7, "tools/call");
|
||||||
|
ObjectNode params = (ObjectNode) request.putObject("params");
|
||||||
|
params.put("name", "oracle.select_ai.test_fewshot_nl2sql");
|
||||||
|
params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");
|
||||||
|
|
||||||
|
ObjectNode response = service.handle("default", request, "user-bearer");
|
||||||
|
|
||||||
|
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
|
||||||
|
String payload = response.path("result").path("content").get(0).path("text").asText();
|
||||||
|
assertThat(payload)
|
||||||
|
.contains("oracle.select_ai.test_fewshot_nl2sql")
|
||||||
|
.contains("SHOWSQL_AND_EXECUTED")
|
||||||
|
.contains("generatedSql")
|
||||||
|
.contains("READ_ONLY_EXECUTED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
|
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
|
||||||
ObjectNode request = request(3, "tools/call");
|
ObjectNode request = request(3, "tools/call");
|
||||||
|
|||||||
Reference in New Issue
Block a user