This commit is contained in:
@@ -154,6 +154,11 @@ public class McpRouterController {
|
|||||||
return activeTools;
|
return activeTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Report 화면처럼 내부 메타데이터 전체가 필요한 Gateway 컴포넌트에 활성 Tool 목록을 제공합니다. */
|
||||||
|
List<ToolMetadata> activeToolsForReport() {
|
||||||
|
return fetchAllActiveTools();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/tools/list")
|
@GetMapping("/tools/list")
|
||||||
public ResponseEntity<JsonRpcResponse> listTools(
|
public ResponseEntity<JsonRpcResponse> listTools(
|
||||||
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
|
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
|
||||||
@@ -280,4 +285,4 @@ public class McpRouterController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.shinhanlife.dap.mcg.presentation;
|
package io.shinhanlife.dap.mcg.presentation;
|
||||||
|
|
||||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -30,7 +29,7 @@ public class ToolReportProxyController {
|
|||||||
public ToolReportProxyController(
|
public ToolReportProxyController(
|
||||||
RestClient.Builder restClientBuilder,
|
RestClient.Builder restClientBuilder,
|
||||||
McpRouterController mcpRouterController,
|
McpRouterController mcpRouterController,
|
||||||
@Value("${report.service-url:http://127.0.0.1:8092}") String reportServiceUrl) {
|
@Value("${report.service-url}") String reportServiceUrl) {
|
||||||
this.restClient = restClientBuilder.build();
|
this.restClient = restClientBuilder.build();
|
||||||
this.mcpRouterController = mcpRouterController;
|
this.mcpRouterController = mcpRouterController;
|
||||||
this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", "");
|
this.reportServiceUrl = reportServiceUrl.replaceAll("/+$", "");
|
||||||
@@ -75,18 +74,7 @@ public class ToolReportProxyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<ToolMetadata> activeTools() {
|
private List<ToolMetadata> activeTools() {
|
||||||
ResponseEntity<JsonRpcResponse> response = mcpRouterController.listTools(null);
|
return mcpRouterController.activeToolsForReport();
|
||||||
JsonRpcResponse body = response.getBody();
|
|
||||||
if (body == null || !(body.getResult() instanceof Map<?, ?> result)
|
|
||||||
|| !(result.get("tools") instanceof List<?> tools)) {
|
|
||||||
throw new IllegalStateException("MCP tools/list response does not contain result.tools");
|
|
||||||
}
|
|
||||||
return tools.stream().map(item -> {
|
|
||||||
if (!(item instanceof ToolMetadata metadata)) {
|
|
||||||
throw new IllegalStateException("MCP tools/list contains an invalid tool entry");
|
|
||||||
}
|
|
||||||
return metadata;
|
|
||||||
}).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<byte[]> copy(ResponseEntity<byte[]> response) {
|
private ResponseEntity<byte[]> copy(ResponseEntity<byte[]> response) {
|
||||||
|
|||||||
@@ -52,3 +52,6 @@ mcp:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.apache.kafka: ERROR
|
org.apache.kafka: ERROR
|
||||||
|
|
||||||
|
report:
|
||||||
|
service-url: ${REPORT_SERVICE_URL:http://127.0.0.1:8092}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ server:
|
|||||||
enabled: true
|
enabled: true
|
||||||
shutdown: graceful
|
shutdown: graceful
|
||||||
|
|
||||||
|
report:
|
||||||
|
service-url: ${REPORT_SERVICE_URL:http://tool-report:8092}
|
||||||
|
|
||||||
mcp:
|
mcp:
|
||||||
gateway:
|
gateway:
|
||||||
fallback:
|
fallback:
|
||||||
|
|||||||
@@ -760,6 +760,7 @@
|
|||||||
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
|
<a href="/chat.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Chat</a>
|
||||||
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
|
<a href="/tester.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Tester</a>
|
||||||
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
|
<a href="/tool-test-console.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Console</a>
|
||||||
|
<a href="/tool-report.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Report</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -156,8 +156,15 @@
|
|||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/report/api/report-tools');
|
const response = await fetch('/report/api/report-tools');
|
||||||
if (!response.ok) throw new Error('툴 목록 조회에 실패했습니다.');
|
const body = await response.json().catch(() => null);
|
||||||
state.tools = await response.json();
|
if (!response.ok) {
|
||||||
|
throw new Error(body?.error || body?.message || `툴 목록 조회에 실패했습니다. (${response.status})`);
|
||||||
|
}
|
||||||
|
const tools = Array.isArray(body) ? body : body?.result?.tools;
|
||||||
|
if (!Array.isArray(tools)) {
|
||||||
|
throw new Error('툴 목록 응답 형식이 올바르지 않습니다.');
|
||||||
|
}
|
||||||
|
state.tools = tools;
|
||||||
[...new Set(state.tools.map(tool => tool.categoryKey))].sort().forEach(category => {
|
[...new Set(state.tools.map(tool => tool.categoryKey))].sort().forEach(category => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = category; option.textContent = category; el('category').appendChild(option);
|
option.value = category; option.textContent = category; el('category').appendChild(option);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ plugins {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
|
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||||
implementation 'com.github.javaparser:javaparser-core:3.26.3'
|
implementation 'com.github.javaparser:javaparser-core:3.26.3'
|
||||||
implementation 'org.apache.poi:poi-ooxml:5.3.0'
|
implementation 'org.apache.poi:poi-ooxml:5.3.0'
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class ToolReportExcelWriter {
|
|||||||
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||||
Styles styles = new Styles(workbook);
|
Styles styles = new Styles(workbook);
|
||||||
writeSummary(workbook, reports, styles);
|
writeSummary(workbook, reports, styles);
|
||||||
|
writeDefinitions(workbook, reports, styles);
|
||||||
writeFields(workbook, reports, styles);
|
writeFields(workbook, reports, styles);
|
||||||
writeDiagnostics(workbook, reports, styles);
|
writeDiagnostics(workbook, reports, styles);
|
||||||
workbook.write(output);
|
workbook.write(output);
|
||||||
@@ -45,6 +46,29 @@ public class ToolReportExcelWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeDefinitions(Workbook workbook, List<ToolReportModel> reports, Styles styles) {
|
||||||
|
String[] headers = {"No.", "툴명", "버전", "표시 설명", "기능 설명", "사용 시점", "사용 제외",
|
||||||
|
"입출력 제한", "예시 질의", "태그", "필수 환경변수", "소유 조직", "정의 파일"};
|
||||||
|
Sheet sheet = workbook.createSheet("툴 정의");
|
||||||
|
decorateSheet(sheet, "Tool Definition Report", "tool-definitions YAML 메타데이터", reports,
|
||||||
|
headers.length, styles);
|
||||||
|
header(sheet, headers, styles);
|
||||||
|
int rowIndex = FIRST_DATA_ROW;
|
||||||
|
int sequence = 1;
|
||||||
|
for (ToolReportModel report : reports) {
|
||||||
|
ToolSummary tool = report.tool();
|
||||||
|
Row row = sheet.createRow(rowIndex++);
|
||||||
|
Object[] values = {sequence++, tool.name(), tool.version(), tool.displayDescription(),
|
||||||
|
tool.functionDescription(), tool.whenToUse(), tool.whenNotToUse(), tool.ioLimits(),
|
||||||
|
tool.exampleQueries(), tool.tags(), tool.requiredEnvKeys(), tool.ownerOrg(), tool.definitionFile()};
|
||||||
|
values(row, values, styles.body);
|
||||||
|
row.getCell(0).setCellStyle(styles.bodyCenter);
|
||||||
|
row.getCell(2).setCellStyle(styles.bodyCenter);
|
||||||
|
}
|
||||||
|
finishTable(sheet, rowIndex, headers.length,
|
||||||
|
new int[]{7, 30, 12, 40, 48, 48, 48, 48, 60, 30, 30, 20, 65});
|
||||||
|
}
|
||||||
|
|
||||||
private void writeSummary(Workbook workbook, List<ToolReportModel> reports, Styles styles) {
|
private void writeSummary(Workbook workbook, List<ToolReportModel> reports, Styles styles) {
|
||||||
String[] headers = {"No.", "툴명", "제목", "설명", "카테고리", "연계 ID", "등록", "승인 필요",
|
String[] headers = {"No.", "툴명", "제목", "설명", "카테고리", "연계 ID", "등록", "승인 필요",
|
||||||
"Read Only", "Destructive", "Idempotent", "Open World", "Request", "Response", "UseCase", "원천 파일"};
|
"Read Only", "Destructive", "Idempotent", "Open World", "Request", "Response", "UseCase", "원천 파일"};
|
||||||
|
|||||||
@@ -18,5 +18,25 @@ public record ToolSummary(
|
|||||||
String useCaseClass,
|
String useCaseClass,
|
||||||
String sourceFile,
|
String sourceFile,
|
||||||
String inputSchemaResource,
|
String inputSchemaResource,
|
||||||
String outputSchemaResource) {
|
String outputSchemaResource,
|
||||||
|
String version,
|
||||||
|
String functionDescription,
|
||||||
|
String whenToUse,
|
||||||
|
String whenNotToUse,
|
||||||
|
String ioLimits,
|
||||||
|
String displayDescription,
|
||||||
|
String exampleQueries,
|
||||||
|
String tags,
|
||||||
|
String requiredEnvKeys,
|
||||||
|
String ownerOrg,
|
||||||
|
String definitionFile) {
|
||||||
|
|
||||||
|
public ToolSummary(String name, String title, String description, String categoryKey, String mappingId,
|
||||||
|
boolean register, boolean requiresApproval, boolean readOnlyHint, boolean destructiveHint,
|
||||||
|
boolean idempotentHint, boolean openWorldHint, String requestType, String responseType,
|
||||||
|
String useCaseClass, String sourceFile, String inputSchemaResource, String outputSchemaResource) {
|
||||||
|
this(name, title, description, categoryKey, mappingId, register, requiresApproval, readOnlyHint,
|
||||||
|
destructiveHint, idempotentHint, openWorldHint, requestType, responseType, useCaseClass, sourceFile,
|
||||||
|
inputSchemaResource, outputSchemaResource, "", "", "", "", "", "", "", "", "", "", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.shinhanlife.dap.report.source;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
import com.github.javaparser.StaticJavaParser;
|
import com.github.javaparser.StaticJavaParser;
|
||||||
import com.github.javaparser.ast.CompilationUnit;
|
import com.github.javaparser.ast.CompilationUnit;
|
||||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||||
@@ -26,10 +27,12 @@ public class ToolDetailAnalyzer {
|
|||||||
|
|
||||||
private final ToolSourceDiscovery discovery;
|
private final ToolSourceDiscovery discovery;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ObjectMapper yamlMapper;
|
||||||
|
|
||||||
public ToolDetailAnalyzer(ToolSourceDiscovery discovery) {
|
public ToolDetailAnalyzer(ToolSourceDiscovery discovery) {
|
||||||
this.discovery = discovery;
|
this.discovery = discovery;
|
||||||
this.objectMapper = new ObjectMapper();
|
this.objectMapper = new ObjectMapper();
|
||||||
|
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolReportModel analyze(ToolSummary tool) {
|
public ToolReportModel analyze(ToolSummary tool) {
|
||||||
@@ -39,10 +42,12 @@ public class ToolDetailAnalyzer {
|
|||||||
|
|
||||||
collectSchema(tool, tool.inputSchemaResource(), "INPUT", fields, diagnostics);
|
collectSchema(tool, tool.inputSchemaResource(), "INPUT", fields, diagnostics);
|
||||||
collectSchema(tool, tool.outputSchemaResource(), "OUTPUT", fields, diagnostics);
|
collectSchema(tool, tool.outputSchemaResource(), "OUTPUT", fields, diagnostics);
|
||||||
if (tool.inputSchemaResource().isBlank()) {
|
boolean definitionInput = collectDefinitionSchema(tool, "parameters_schema", "INPUT", fields, diagnostics);
|
||||||
|
boolean definitionOutput = collectDefinitionSchema(tool, "output_schema", "OUTPUT", fields, diagnostics);
|
||||||
|
if (tool.inputSchemaResource().isBlank() && !definitionInput) {
|
||||||
collectJavaType(tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics);
|
collectJavaType(tool, javaFiles.get(tool.requestType()), "DTO", "INPUT", fields, diagnostics);
|
||||||
}
|
}
|
||||||
if (tool.outputSchemaResource().isBlank()) {
|
if (tool.outputSchemaResource().isBlank() && !definitionOutput) {
|
||||||
collectJavaType(tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics);
|
collectJavaType(tool, javaFiles.get(tool.responseType()), "DTO", "OUTPUT", fields, diagnostics);
|
||||||
}
|
}
|
||||||
if (!tool.mappingId().isBlank()) {
|
if (!tool.mappingId().isBlank()) {
|
||||||
@@ -52,6 +57,21 @@ public class ToolDetailAnalyzer {
|
|||||||
return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics));
|
return new ToolReportModel(tool, List.copyOf(fields), List.copyOf(diagnostics));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean collectDefinitionSchema(ToolSummary tool, String schemaKey, String direction,
|
||||||
|
List<FieldDefinition> fields, List<String> diagnostics) {
|
||||||
|
if (tool.definitionFile() == null || tool.definitionFile().isBlank()) return false;
|
||||||
|
Path path = discovery.sourceRoot().resolve(tool.definitionFile()).normalize();
|
||||||
|
try {
|
||||||
|
JsonNode schema = yamlMapper.readTree(path.toFile()).path(schemaKey);
|
||||||
|
if (schema.isMissingNode() || schema.isNull() || schema.isEmpty()) return false;
|
||||||
|
collectSchemaFields(tool, schema, "TOOL_DEFINITION", direction, "", "", relative(path), fields);
|
||||||
|
return true;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
diagnostics.add("Tool definition schema 분석 실패: " + relative(path) + " (" + schemaKey + ")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Path> indexJavaFiles() {
|
private Map<String, Path> indexJavaFiles() {
|
||||||
Map<String, Path> result = new LinkedHashMap<>();
|
Map<String, Path> result = new LinkedHashMap<>();
|
||||||
try (var paths = Files.walk(discovery.sourceRoot())) {
|
try (var paths = Files.walk(discovery.sourceRoot())) {
|
||||||
@@ -83,23 +103,41 @@ public class ToolDetailAnalyzer {
|
|||||||
Path path = matches.get(0);
|
Path path = matches.get(0);
|
||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(path.toFile());
|
JsonNode root = objectMapper.readTree(path.toFile());
|
||||||
JsonNode properties = root.path("properties");
|
collectSchemaFields(tool, root, "JSON_SCHEMA", direction, "", "", relative(path), fields);
|
||||||
List<String> required = new ArrayList<>();
|
|
||||||
root.path("required").forEach(node -> required.add(node.asText()));
|
|
||||||
Iterator<Map.Entry<String, JsonNode>> iterator = properties.fields();
|
|
||||||
while (iterator.hasNext()) {
|
|
||||||
Map.Entry<String, JsonNode> entry = iterator.next();
|
|
||||||
JsonNode definition = entry.getValue();
|
|
||||||
fields.add(new FieldDefinition(tool.name(), "JSON_SCHEMA", direction, "",
|
|
||||||
entry.getKey(), definition.path("type").asText("object"),
|
|
||||||
required.contains(entry.getKey()), definition.path("description").asText(""),
|
|
||||||
constraints(definition), relative(path)));
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
diagnostics.add("Schema 분석 실패: " + relative(path));
|
diagnostics.add("Schema 분석 실패: " + relative(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void collectSchemaFields(ToolSummary tool, JsonNode schema, String sourceKind, String direction,
|
||||||
|
String ownerPath, String fieldPrefix, String sourceFile,
|
||||||
|
List<FieldDefinition> fields) {
|
||||||
|
List<String> required = new ArrayList<>();
|
||||||
|
schema.path("required").forEach(node -> required.add(node.asText()));
|
||||||
|
Iterator<Map.Entry<String, JsonNode>> iterator = schema.path("properties").fields();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, JsonNode> entry = iterator.next();
|
||||||
|
JsonNode definition = entry.getValue();
|
||||||
|
String fieldName = fieldPrefix.isBlank() ? entry.getKey() : fieldPrefix + "." + entry.getKey();
|
||||||
|
String type = schemaType(definition);
|
||||||
|
fields.add(new FieldDefinition(tool.name(), sourceKind, direction, ownerPath, fieldName, type,
|
||||||
|
required.contains(entry.getKey()), definition.path("description").asText(""),
|
||||||
|
constraints(definition), sourceFile));
|
||||||
|
JsonNode nested = definition.path("properties").isObject() ? definition : definition.path("items");
|
||||||
|
if (nested.path("properties").isObject()) {
|
||||||
|
collectSchemaFields(tool, nested, sourceKind, direction, fieldName, fieldName, sourceFile, fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String schemaType(JsonNode definition) {
|
||||||
|
String type = definition.path("type").asText("object");
|
||||||
|
if ("array".equals(type)) {
|
||||||
|
return "array<" + definition.path("items").path("type").asText("object") + ">";
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
private void collectJavaType(ToolSummary tool, Path path, String sourceKind, String direction,
|
private void collectJavaType(ToolSummary tool, Path path, String sourceKind, String direction,
|
||||||
List<FieldDefinition> fields, List<String> diagnostics) {
|
List<FieldDefinition> fields, List<String> diagnostics) {
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import com.github.javaparser.StaticJavaParser;
|
|||||||
import com.github.javaparser.ast.CompilationUnit;
|
import com.github.javaparser.ast.CompilationUnit;
|
||||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
import io.shinhanlife.dap.report.config.ReportProperties;
|
import io.shinhanlife.dap.report.config.ReportProperties;
|
||||||
import io.shinhanlife.dap.report.model.ToolSummary;
|
import io.shinhanlife.dap.report.model.ToolSummary;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -13,6 +16,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -22,6 +26,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class ToolSourceDiscovery {
|
public class ToolSourceDiscovery {
|
||||||
|
|
||||||
private final Path sourceRoot;
|
private final Path sourceRoot;
|
||||||
|
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||||
|
|
||||||
public ToolSourceDiscovery(ReportProperties properties) {
|
public ToolSourceDiscovery(ReportProperties properties) {
|
||||||
String configuredRoot = properties.sourceRoot();
|
String configuredRoot = properties.sourceRoot();
|
||||||
@@ -45,15 +50,89 @@ public class ToolSourceDiscovery {
|
|||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception);
|
throw new IllegalStateException("Failed to scan tool sources: " + sourceRoot, exception);
|
||||||
}
|
}
|
||||||
return tools.stream()
|
Map<String, ToolSummary> discovered = tools.stream()
|
||||||
.sorted(Comparator.comparing(ToolSummary::sourceFile))
|
.sorted(Comparator.comparing(ToolSummary::sourceFile))
|
||||||
.collect(Collectors.toMap(ToolSummary::name, Function.identity(),
|
.collect(Collectors.toMap(ToolSummary::name, Function.identity(),
|
||||||
(first, duplicate) -> first, LinkedHashMap::new))
|
(first, duplicate) -> first, LinkedHashMap::new));
|
||||||
.values().stream()
|
discoverDefinitions().forEach((name, definition) ->
|
||||||
|
discovered.merge(name, definition, this::mergeDefinition));
|
||||||
|
return discovered.values().stream()
|
||||||
.sorted(Comparator.comparing(ToolSummary::name))
|
.sorted(Comparator.comparing(ToolSummary::name))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, ToolSummary> discoverDefinitions() {
|
||||||
|
Map<String, ToolSummary> definitions = new LinkedHashMap<>();
|
||||||
|
try (var paths = Files.walk(sourceRoot)) {
|
||||||
|
paths.filter(this::isToolDefinition).sorted().forEach(path -> {
|
||||||
|
try {
|
||||||
|
JsonNode root = yamlMapper.readTree(path.toFile());
|
||||||
|
String name = text(root, "name");
|
||||||
|
if (name.isBlank()) return;
|
||||||
|
JsonNode description = root.path("description");
|
||||||
|
String function = text(description, "function");
|
||||||
|
definitions.putIfAbsent(name, new ToolSummary(
|
||||||
|
name, text(root, "display_name"), function, text(root, "category_key"),
|
||||||
|
text(root, "legacy_interface_id"), true, false,
|
||||||
|
bool(root, "read_only"), bool(root, "destructive"), bool(root, "idempotent"), false,
|
||||||
|
"", "", "", "", "", "", text(root, "version"), function,
|
||||||
|
text(description, "when_to_use"), text(description, "when_not_to_use"),
|
||||||
|
text(description, "io_limits"), text(root, "display_description"),
|
||||||
|
joined(root.path("example_queries")), joined(root.path("tags")),
|
||||||
|
joined(root.path("required_env_keys")), text(root, "owner_org"), relative(path)));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Failed to parse tool definition: " + path, exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("Failed to scan tool definitions: " + sourceRoot, exception);
|
||||||
|
}
|
||||||
|
return definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ToolSummary mergeDefinition(ToolSummary source, ToolSummary definition) {
|
||||||
|
return new ToolSummary(source.name(), prefer(definition.title(), source.title()),
|
||||||
|
prefer(definition.description(), source.description()),
|
||||||
|
prefer(definition.categoryKey(), source.categoryKey()),
|
||||||
|
prefer(definition.mappingId(), source.mappingId()), source.register(), source.requiresApproval(),
|
||||||
|
definition.readOnlyHint(), definition.destructiveHint(), definition.idempotentHint(),
|
||||||
|
source.openWorldHint(), source.requestType(), source.responseType(), source.useCaseClass(),
|
||||||
|
source.sourceFile(), source.inputSchemaResource(), source.outputSchemaResource(), definition.version(),
|
||||||
|
definition.functionDescription(), definition.whenToUse(), definition.whenNotToUse(),
|
||||||
|
definition.ioLimits(), definition.displayDescription(), definition.exampleQueries(), definition.tags(),
|
||||||
|
definition.requiredEnvKeys(), definition.ownerOrg(), definition.definitionFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isToolDefinition(Path path) {
|
||||||
|
String normalized = path.toString().replace('\\', '/');
|
||||||
|
String fileName = path.getFileName().toString().toLowerCase();
|
||||||
|
return Files.isRegularFile(path) && (fileName.endsWith(".yml") || fileName.endsWith(".yaml"))
|
||||||
|
&& normalized.contains("/src/main/resources/tool-definitions/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
return node.path(field).asText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean bool(JsonNode node, String field) {
|
||||||
|
return node.path(field).asBoolean(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joined(JsonNode node) {
|
||||||
|
if (!node.isArray()) return node.asText("");
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
node.forEach(value -> values.add(value.asText()));
|
||||||
|
return String.join(" | ", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String prefer(String primary, String fallback) {
|
||||||
|
return primary == null || primary.isBlank() ? fallback : primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String relative(Path path) {
|
||||||
|
return sourceRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
private Path resolveProjectRoot(Path configuredPath) {
|
private Path resolveProjectRoot(Path configuredPath) {
|
||||||
Path candidate = configuredPath;
|
Path candidate = configuredPath;
|
||||||
while (candidate != null) {
|
while (candidate != null) {
|
||||||
@@ -88,7 +167,8 @@ public class ToolSourceDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ToolSummary toSummary(Path path, String owner, MethodDeclaration method, AnnotationExpr tool) {
|
private ToolSummary toSummary(Path path, String owner, MethodDeclaration method, AnnotationExpr tool) {
|
||||||
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "ToolHint").orElse(null);
|
AnnotationExpr hint = JavaAnnotationReader.find(method.getAnnotations(), "GrowToolHint")
|
||||||
|
.or(() -> JavaAnnotationReader.find(method.getAnnotations(), "ToolHint")).orElse(null);
|
||||||
AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null);
|
AnnotationExpr annotations = JavaAnnotationReader.nested(tool, "annotations").orElse(null);
|
||||||
String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString());
|
String name = JavaAnnotationReader.string(tool, "name", method.getNameAsString());
|
||||||
String title = JavaAnnotationReader.string(tool, "title", name);
|
String title = JavaAnnotationReader.string(tool, "title", name);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
server:
|
server:
|
||||||
address: ${REPORT_SERVER_ADDRESS:127.0.0.1}
|
address: ${REPORT_SERVER_ADDRESS:0.0.0.0}
|
||||||
port: ${REPORT_SERVER_PORT:8092}
|
port: ${REPORT_SERVER_PORT:8092}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ class ToolReportExcelWriterTest {
|
|||||||
byte[] content = new ToolReportExcelWriter().write(List.of(new ToolReportModel(tool, List.of(field), List.of())));
|
byte[] content = new ToolReportExcelWriter().write(List.of(new ToolReportModel(tool, List.of(field), List.of())));
|
||||||
|
|
||||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||||
assertThat(workbook.getNumberOfSheets()).isEqualTo(3);
|
assertThat(workbook.getNumberOfSheets()).isEqualTo(4);
|
||||||
assertThat(workbook.getSheet("툴 기본정보").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name());
|
assertThat(workbook.getSheet("툴 기본정보").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name());
|
||||||
|
assertThat(workbook.getSheet("툴 정의").getRow(8).getCell(1).getStringCellValue()).isEqualTo(tool.name());
|
||||||
assertThat(workbook.getSheet("수집 필드").getRow(8).getCell(5).getStringCellValue()).isEqualTo("customerNo");
|
assertThat(workbook.getSheet("수집 필드").getRow(8).getCell(5).getStringCellValue()).isEqualTo("customerNo");
|
||||||
assertThat(workbook.getSheet("툴 기본정보").getRow(0).getCell(0).getCellStyle().getFillForegroundColorColor().getARGBHex())
|
assertThat(workbook.getSheet("툴 기본정보").getRow(0).getCell(0).getCellStyle().getFillForegroundColorColor().getARGBHex())
|
||||||
.endsWith("262626");
|
.endsWith("262626");
|
||||||
|
|||||||
@@ -16,6 +16,36 @@ class ToolSourceDiscoveryTest {
|
|||||||
|
|
||||||
assertThat(discovery.discover())
|
assertThat(discovery.discover())
|
||||||
.extracting(tool -> tool.name())
|
.extracting(tool -> tool.name())
|
||||||
.contains("oth.cmm.customer.detail", "sms.sms.msg.send");
|
.contains("cmm_customer_tool", "cmm_claim_search", "iam_system_status");
|
||||||
|
|
||||||
|
assertThat(discovery.discover().stream()
|
||||||
|
.filter(tool -> tool.name().equals("cmm_customer_tool"))
|
||||||
|
.findFirst().orElseThrow())
|
||||||
|
.satisfies(tool -> {
|
||||||
|
assertThat(tool.version()).isEqualTo("1.0.0");
|
||||||
|
assertThat(tool.mappingId()).isEqualTo("ONILD0320");
|
||||||
|
assertThat(tool.definitionFile()).endsWith("tool-definitions/cmm/cmm_customer_tool.yml");
|
||||||
|
assertThat(tool.whenToUse()).contains("고객 ID");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collectsJsonSchemaFieldsEmbeddedInToolDefinitionYaml() {
|
||||||
|
Path projectRoot = Path.of("..").toAbsolutePath().normalize();
|
||||||
|
ToolSourceDiscovery discovery = new ToolSourceDiscovery(
|
||||||
|
new ReportProperties(projectRoot.toString(), "tool-report"));
|
||||||
|
var tool = discovery.discover().stream()
|
||||||
|
.filter(candidate -> candidate.name().equals("cmm_customer_tool"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
|
||||||
|
var report = new ToolDetailAnalyzer(discovery).analyze(tool);
|
||||||
|
|
||||||
|
assertThat(report.fields())
|
||||||
|
.anySatisfy(field -> {
|
||||||
|
assertThat(field.sourceKind()).isEqualTo("TOOL_DEFINITION");
|
||||||
|
assertThat(field.direction()).isEqualTo("INPUT");
|
||||||
|
assertThat(field.fieldName()).isEqualTo("csNo");
|
||||||
|
assertThat(field.required()).isTrue();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
<a href="/chat.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Chat</a>
|
<a href="/chat.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Chat</a>
|
||||||
<a href="/tester.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Tester</a>
|
<a href="/tester.html" style="color:#a1a1aa; text-decoration:none;" class="hover:text-white transition-colors">Tester</a>
|
||||||
<a href="/tool-test-console.html" style="color:#ffffff; text-decoration:none;" class="font-semibold">Console</a>
|
<a href="/tool-test-console.html" style="color:#ffffff; text-decoration:none;" class="font-semibold">Console</a>
|
||||||
|
<a href="/tool-report.html" style="color:#a1a1aa;" class="hover:text-white transition-colors">Report</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user