fix: normalize scaffold pod and tool generation
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 35s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 35s
This commit is contained in:
@@ -78,7 +78,8 @@ public class ScaffoldingController {
|
||||
}
|
||||
System.setProperty("AXHUB_SOURCE_DIR", workspacePath);
|
||||
|
||||
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date, req.get("toolServiceManifest"));
|
||||
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date,
|
||||
req.get("toolServiceManifest"), targetModules(req.get("targetModules")));
|
||||
} catch (Exception e) {
|
||||
return "오류 발생: " + e.getMessage();
|
||||
}
|
||||
@@ -89,6 +90,9 @@ public class ScaffoldingController {
|
||||
String description = req.getOrDefault("description", "").trim();
|
||||
if (description.isBlank()) return ResponseEntity.badRequest().body(Map.of("error", "Pod 업무 설명을 입력해주세요."));
|
||||
try {
|
||||
String moduleName = req.getOrDefault("moduleName", "dat-was-cus").trim();
|
||||
if (!moduleName.startsWith("dat-was-")) moduleName = "dat-was-" + moduleName;
|
||||
List<String> targetModules = targetModules(req.get("targetModules"));
|
||||
String prompt = """
|
||||
Generate only YAML for an MCP tool service manifest.
|
||||
The root must be mcp.manifest.routing-functions with one routing function.
|
||||
@@ -105,8 +109,9 @@ public class ScaffoldingController {
|
||||
Use valid YAML only, without Markdown fences or explanations.
|
||||
Pod module: %s
|
||||
Business description: %s
|
||||
""".formatted(req.getOrDefault("moduleName", "dat-was-cus"), description);
|
||||
String content = stripCodeFence(generateAiContent(prompt, req.get("model")));
|
||||
""".formatted(moduleName, description);
|
||||
String content = PodScaffolder.normalizeToolServiceManifest(
|
||||
stripCodeFence(generateAiContent(prompt, req.get("model"))), moduleName, targetModules);
|
||||
return ResponseEntity.ok(Map.of("toolServiceManifest", content));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("error", "AI Manifest 초안 생성 실패: " + safeMessage(e)));
|
||||
@@ -140,7 +145,7 @@ public class ScaffoldingController {
|
||||
if (title == null || title.isBlank()) title = baseName;
|
||||
String description = req.get("description");
|
||||
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
|
||||
String routingType = req.getOrDefault("routingType", "HTTP");
|
||||
String routingType = req.getOrDefault("routingType", "MCI");
|
||||
String moduleName = req.getOrDefault("moduleName", "dat-was-cus");
|
||||
String author = req.get("author");
|
||||
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
|
||||
@@ -290,9 +295,9 @@ public class ScaffoldingController {
|
||||
Generate an MCP Tool scaffold from the user request.
|
||||
Return JSON only. Do not add Markdown, explanations, or code fences.
|
||||
The response must have this exact shape:
|
||||
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","examples":["SUCCESS"],"pattern":"","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
|
||||
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"MCI","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","examples":["SUCCESS"],"pattern":"","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
|
||||
categoryKey must be exactly three lowercase letters or digits.
|
||||
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
|
||||
routingType must be either MCI or HTTP. Default to MCI. Use HTTP only when the user explicitly requests a REST or HTTP integration. httpApiName can contain only letters, digits, hyphens, and underscores.
|
||||
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
|
||||
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
|
||||
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, List. Finite values should be enforced by populating enumValues. List must include itemType and object lists include itemFields.
|
||||
@@ -529,6 +534,19 @@ public class ScaffoldingController {
|
||||
return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
|
||||
}
|
||||
|
||||
private List<String> targetModules(String rawTargetModules) throws Exception {
|
||||
if (rawTargetModules == null || rawTargetModules.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> modules = objectMapper.readValue(rawTargetModules, new TypeReference<List<String>>() { });
|
||||
return modules.stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(name -> name.matches("^dat-was-[a-z0-9-]+$"))
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private record FieldDraft(List<ToolScaffolder.FieldDefinition> fields) {
|
||||
}
|
||||
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
<div class="col-md-6 mt-3 mt-md-0">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="routingType">
|
||||
<option value="MCI">MCI (Legacy)</option>
|
||||
<option value="MCI" selected>MCI (Legacy)</option>
|
||||
<option value="HTTP">HTTP (REST)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -1579,6 +1579,9 @@
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
if (formId === 'podForm') {
|
||||
data.targetModules = JSON.stringify(currentTargetModules());
|
||||
}
|
||||
const btn = this.querySelector('button[type="submit"]');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
@@ -1644,6 +1647,12 @@
|
||||
|
||||
handleFormSubmit('podForm', '/api/v1/scaffold/pod');
|
||||
|
||||
function currentTargetModules() {
|
||||
return Array.from(document.querySelectorAll('#targetModuleSelect option'))
|
||||
.map(option => option.value.trim())
|
||||
.filter(moduleName => /^dat-was-[a-z0-9-]+$/.test(moduleName));
|
||||
}
|
||||
|
||||
async function createPodManifestDraft(button) {
|
||||
const description = document.getElementById('podDescription').value.trim();
|
||||
if (!description) { alert('Pod 업무 설명을 입력해주세요.'); return; }
|
||||
@@ -1651,7 +1660,12 @@
|
||||
try {
|
||||
const response = await fetch('/api/v1/scaffold/pod-draft', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({description, moduleName: document.querySelector('#podForm [name="moduleName"]').value, model: document.getElementById('podAiModelSelect').value})
|
||||
body: JSON.stringify({
|
||||
description,
|
||||
moduleName: document.querySelector('#podForm [name="moduleName"]').value,
|
||||
targetModules: JSON.stringify(currentTargetModules()),
|
||||
model: document.getElementById('podAiModelSelect').value
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'AI Manifest 생성 실패');
|
||||
@@ -2281,7 +2295,14 @@
|
||||
form.elements.categoryKey.value = result.categoryKey || '';
|
||||
if (typeof loadUseCasesForSelection === 'function') loadUseCasesForSelection();
|
||||
}
|
||||
form.elements.routingType.value = result.routingType || 'HTTP';
|
||||
const routingType = String(result.routingType || 'MCI').trim().toUpperCase();
|
||||
form.elements.routingType.value = routingType === 'HTTP' ? 'HTTP' : 'MCI';
|
||||
const useCaseSelect = document.getElementById('toolGroupUseCaseSelect');
|
||||
const useCaseNameInput = document.getElementById('toolGroupUseCaseName');
|
||||
if (useCaseSelect.value === '' && !useCaseNameInput.value.trim() && result.baseName) {
|
||||
useCaseNameInput.value = result.baseName;
|
||||
useCaseNameInput.readOnly = false;
|
||||
}
|
||||
form.elements.httpApiName.value = result.httpApiName || '';
|
||||
form.elements.functionDescription.value = result.functionDescription || '';
|
||||
form.elements.displayDescription.value = result.displayDescription || '';
|
||||
|
||||
@@ -12,11 +12,14 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -83,7 +86,7 @@ class ScaffoldingControllerToolDraftTest {
|
||||
when(requestSpec.options(any(ChatOptions.class))).thenReturn(requestSpec);
|
||||
when(requestSpec.call()).thenReturn(responseSpec);
|
||||
when(responseSpec.content()).thenReturn("""
|
||||
{"baseName":"CustomerContractStatus","title":"계약 상태 조회","description":"고객번호로 계약 상태를 조회합니다.","categoryKey":"cmm","routingType":"HTTP","httpApiName":"contract-status","functionDescription":"고객 계약의 현재 상태를 조회한다.","displayDescription":"고객 계약 상태 조회","whenToUse":"고객번호로 계약 상태 확인을 요청할 때 사용한다.","whenNotToUse":"계약 변경 또는 해지를 요청할 때는 사용하지 않는다.","ioLimits":"고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다.","exampleQueries":["고객 C123의 계약 상태를 알려줘","C123 계약이 정상인지 확인해줘","고객번호 C123 계약 조회해줘"],"tags":["contract","status","search"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"customerId","type":"String","description":"고객번호","examples":["C123"],"required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"결과 코드","examples":["SUCCESS"],"required":true}]}
|
||||
{"baseName":"CustomerContractStatus","title":"계약 상태 조회","description":"고객번호로 계약 상태를 조회합니다.","categoryKey":"cmm","routingType":"MCI","httpApiName":"contract-status","functionDescription":"고객 계약의 현재 상태를 조회한다.","displayDescription":"고객 계약 상태 조회","whenToUse":"고객번호로 계약 상태 확인을 요청할 때 사용한다.","whenNotToUse":"계약 변경 또는 해지를 요청할 때는 사용하지 않는다.","ioLimits":"고객번호 한 건을 입력받아 계약 상태 한 건을 반환한다.","exampleQueries":["고객 C123의 계약 상태를 알려줘","C123 계약이 정상인지 확인해줘","고객번호 C123 계약 조회해줘"],"tags":["contract","status","search"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"customerId","type":"String","description":"고객번호","examples":["C123"],"required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"결과 코드","examples":["SUCCESS"],"required":true}]}
|
||||
""");
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ScaffoldingController(builder, new ObjectMapper()))
|
||||
@@ -95,6 +98,7 @@ class ScaffoldingControllerToolDraftTest {
|
||||
.content("{\"description\":\"고객번호로 계약 상태를 조회\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.baseName").value("CustomerContractStatus"))
|
||||
.andExpect(jsonPath("$.routingType").value("MCI"))
|
||||
.andExpect(jsonPath("$.functionDescription").value("고객 계약의 현재 상태를 조회한다."))
|
||||
.andExpect(jsonPath("$.displayDescription").value("고객 계약 상태 조회"))
|
||||
.andExpect(jsonPath("$.whenToUse").value("고객번호로 계약 상태 확인을 요청할 때 사용한다."))
|
||||
@@ -104,5 +108,49 @@ class ScaffoldingControllerToolDraftTest {
|
||||
.andExpect(jsonPath("$.tags[0]").value("contract"))
|
||||
.andExpect(jsonPath("$.ownerOrg").value("MCP_TOOL"))
|
||||
.andExpect(jsonPath("$.inputFields[0].name").value("customerId"));
|
||||
|
||||
org.mockito.ArgumentCaptor<String> promptCaptor = org.mockito.ArgumentCaptor.forClass(String.class);
|
||||
verify(requestSpec).user(promptCaptor.capture());
|
||||
assertTrue(promptCaptor.getValue().contains("\"routingType\":\"MCI\""));
|
||||
assertTrue(promptCaptor.getValue().contains("Default to MCI"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void podDraftUsesTheCurrentTargetModuleOptionsForConfusableServers() {
|
||||
ChatClient.Builder builder = mock(ChatClient.Builder.class);
|
||||
ChatClient chatClient = mock(ChatClient.class);
|
||||
ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class);
|
||||
ChatClient.CallResponseSpec responseSpec = mock(ChatClient.CallResponseSpec.class);
|
||||
when(builder.build()).thenReturn(chatClient);
|
||||
when(chatClient.prompt()).thenReturn(requestSpec);
|
||||
when(requestSpec.user(anyString())).thenReturn(requestSpec);
|
||||
when(requestSpec.options(any(ChatOptions.class))).thenReturn(requestSpec);
|
||||
when(requestSpec.call()).thenReturn(responseSpec);
|
||||
when(responseSpec.content()).thenReturn("""
|
||||
mcp:
|
||||
manifest:
|
||||
routing-functions:
|
||||
- name: route_to_dat-was-pro
|
||||
server-id: dat-was-pro
|
||||
category-key: pro
|
||||
confusable-servers: [dat-was-hrd, dat-was-pay, dat-was-att]
|
||||
""");
|
||||
|
||||
var response = new ScaffoldingController(builder, new ObjectMapper()).generatePodManifestDraft(java.util.Map.of(
|
||||
"description", "상품 업무를 처리합니다.",
|
||||
"moduleName", "dat-was-pro",
|
||||
"targetModules", "[\"dat-was-cus\",\"dat-was-hr\",\"dat-was-sal\",\"dat-was-pro\",\"dat-was-sys\"]"));
|
||||
|
||||
assertEquals(org.springframework.http.HttpStatus.OK, response.getStatusCode());
|
||||
String manifest = (String) ((java.util.Map<?, ?>) response.getBody()).get("toolServiceManifest");
|
||||
List<String> confusableServers = manifest.lines()
|
||||
.map(String::trim)
|
||||
.filter(line -> line.startsWith("- \"dat-was-"))
|
||||
.map(line -> line.substring(3, line.length() - 1))
|
||||
.toList();
|
||||
assertEquals(List.of("dat-was-cus", "dat-was-hr", "dat-was-sal", "dat-was-sys"), confusableServers);
|
||||
assertFalse(manifest.contains("dat-was-hrd"), manifest);
|
||||
assertFalse(manifest.contains("dat-was-pay"), manifest);
|
||||
assertFalse(manifest.contains("dat-was-att"), manifest);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user