From 2dc0c19c5a26815b2482531c84e28f87b705fa0f Mon Sep 17 00:00:00 2001 From: jade Date: Wed, 2 Sep 2026 13:29:42 +0900 Subject: [PATCH] feat: add AI manifest generation to pod scaffold --- .../presentation/ScaffoldingController.java | 24 ++++++++++- .../main/resources/static/admin/scaffold.html | 36 +++++++++++++++- .../dat/lib/util/PodScaffolder.java | 42 ++++++++++++++++++- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java index 1c97f2f9..66333ac2 100644 --- a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java +++ b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java @@ -78,12 +78,34 @@ public class ScaffoldingController { } System.setProperty("AXHUB_SOURCE_DIR", workspacePath); - return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date); + return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date, req.get("toolServiceManifest")); } catch (Exception e) { return "오류 발생: " + e.getMessage(); } } + @PostMapping("/pod-draft") + public ResponseEntity generatePodManifestDraft(@RequestBody Map req) { + String description = req.getOrDefault("description", "").trim(); + if (description.isBlank()) return ResponseEntity.badRequest().body(Map.of("error", "Pod 업무 설명을 입력해주세요.")); + try { + String prompt = """ + Generate only YAML for an MCP tool service manifest. + The root must be mcp.manifest.routing-functions with one routing function. + Include name, description, server-id, category-key, product-boundary, business-domain, + business-outcome, primary-entities, capabilities, select-if, reject-if, + confidence-server-ids, and decision-policy. + 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"))); + return ResponseEntity.ok(Map.of("toolServiceManifest", content)); + } catch (Exception e) { + return ResponseEntity.internalServerError().body(Map.of("error", "AI Manifest 초안 생성 실패: " + safeMessage(e))); + } + } + @GetMapping("/browse-folder") public String browseFolder() { try { diff --git a/dat-gateway/src/main/resources/static/admin/scaffold.html b/dat-gateway/src/main/resources/static/admin/scaffold.html index 4e4e6e5a..c00ffee3 100644 --- a/dat-gateway/src/main/resources/static/admin/scaffold.html +++ b/dat-gateway/src/main/resources/static/admin/scaffold.html @@ -839,6 +839,24 @@
Unique port for Gateway routing and local development.
+
+ + +
+
설명을 입력하면 AI가 tool-service-manifest.yml 초안을 작성합니다.
+
+ + +
+
+
+
+ + +
@@ -926,8 +944,8 @@
@@ -1626,6 +1644,22 @@ handleFormSubmit('podForm', '/api/v1/scaffold/pod'); + async function createPodManifestDraft(button) { + const description = document.getElementById('podDescription').value.trim(); + if (!description) { alert('Pod 업무 설명을 입력해주세요.'); return; } + if (button) { button.disabled = true; button.textContent = 'AI 생성 중...'; } + 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}) + }); + const result = await response.json(); + if (!response.ok) throw new Error(result.error || 'AI Manifest 생성 실패'); + document.getElementById('toolServiceManifest').value = result.toolServiceManifest || ''; + } catch (error) { alert(error.message); } + finally { if (button) { button.disabled = false; button.textContent = 'AI로 Manifest 채우기'; } } + } + const groupedTools = []; function toMethodName(baseName) { diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java index 0e1271a6..5a366987 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/PodScaffolder.java @@ -50,11 +50,24 @@ public class PodScaffolder { envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); } Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate); + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null); + } + + public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, + String createDate, String toolServiceManifest) throws IOException { + String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (envSourceDir == null || envSourceDir.isBlank()) envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, toolServiceManifest); } static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate, null); + } + + static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, + String author, String createDate, String toolServiceManifest) throws IOException { Path modulePath = rootDir.resolve(Paths.get(moduleName)); if (Files.exists(modulePath)) { return "[오류] 이미 존재하는 모듈입니다: " + moduleName; @@ -135,6 +148,8 @@ public class PodScaffolder { spring: application: name: %s + config: + import: optional:classpath:tool-service-manifest.yml profiles: active: local logging: @@ -151,6 +166,9 @@ public class PodScaffolder { TESTER-DEV: ALL """.formatted(portStr, moduleName, moduleName.replace("dap-", "")); writeUtf8(resPath.resolve("application.yml"), applicationYml); + String manifest = toolServiceManifest == null || toolServiceManifest.isBlank() + ? defaultToolServiceManifest(moduleName) : toolServiceManifest.trim() + System.lineSeparator(); + writeUtf8(resPath.resolve("tool-service-manifest.yml"), manifest); String applicationLocalYml = """ # Local 환경 전용 설정 (H2 메모리 DB 등) @@ -368,6 +386,28 @@ public class PodScaffolder { Files.writeString(path, content, StandardCharsets.UTF_8); } + private static String defaultToolServiceManifest(String moduleName) { + String key = moduleName.replace("dat-was-", ""); + return """ + mcp: + manifest: + routing-functions: + - name: route_to_%s + description: %s 업무 서버로 요청을 라우팅합니다. + server-id: %s + category-key: %s + product-boundary: insurance + business-domain: %s 업무 + business-outcome: %s 관련 업무를 처리합니다. + primary-entities: [] + capabilities: [] + select-if: %s 관련 요청인 경우 + reject-if: 다른 업무 영역이 주된 요청인 경우 + confidence-server-ids: [%s] + decision-policy: 요청의 주요 업무 영역을 기준으로 서버를 선택합니다. + """.formatted(moduleName.replace("-", "_"), key, moduleName, key, key, key, key, moduleName); + } + private static String capitalize(String str) { if (str == null || str.isEmpty()) return str; return str.substring(0, 1).toUpperCase() + str.substring(1);