Enhance ToolScaffolder with Pattern and Examples validation, support AI prompt generation for Pattern
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m22s

This commit is contained in:
jade
2026-08-14 10:29:27 +09:00
parent b8d6fa28df
commit b53dfe3601
3 changed files with 105 additions and 55 deletions

View File

@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.*;
public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "Enum", "List");
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free",
@@ -96,7 +96,7 @@ public class ScaffoldingController {
List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields"));
if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", List.of("example"), "", false));
}
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"),
@@ -187,9 +187,10 @@ public class ScaffoldingController {
Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List.
Enum fields must include enumValues. List fields must include itemType; use Object plus itemFields for object lists.
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","examples":["example1","example2"],"pattern":"^regex$","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, List.
Finite values should be enforced by populating enumValues on standard types. List fields must include itemType; use Object plus itemFields for object lists.
If applicable, provide a regex for pattern.
Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
@@ -217,12 +218,13 @@ 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","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
{"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":[]}]}
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.
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, Enum, List. Enum must include enumValues; List must include itemType and object lists include itemFields.
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.
If applicable, provide a regex for pattern.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s
@@ -302,7 +304,8 @@ public class ScaffoldingController {
field.name().trim(),
field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(),
field.example() == null ? "" : field.example().trim(),
field.examples() == null ? List.of() : field.examples().stream().map(String::trim).toList(),
field.pattern() == null ? "" : field.pattern().trim(),
field.required(),
field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(),
@@ -323,9 +326,6 @@ public class ScaffoldingController {
}
private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("Enum".equals(field.type()) && field.enumValues().isEmpty()) {
throw new IllegalArgumentException("Enum field needs enumValues: " + field.name());
}
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name());
}

View File

@@ -1181,7 +1181,8 @@
<th style="min-width: 170px;">Enum values / List item type</th>
<th style="min-width: 260px;">Object list item fields (JSON)</th>
<th style="min-width: 210px;">Description</th>
<th style="min-width: 180px;">Example</th>
<th style="min-width: 150px;">Pattern (Regex)</th>
<th style="min-width: 180px;">Examples (Enter 단위 구분)</th>
<th style="min-width: 125px;">Required</th>
<th style="width: 1%;">삭제</th>
</tr>
@@ -1205,7 +1206,7 @@
<!-- Sub Field Editor Modal -->
<div class="modal fade" id="subFieldEditorModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">하위 필드(itemFields) 편집</h5>
@@ -1218,9 +1219,10 @@
<tr>
<th style="width:15%">Name</th>
<th style="width:15%">Type</th>
<th style="width:20%">Details</th>
<th style="width:20%">Description</th>
<th style="width:15%">Example</th>
<th style="width:15%">Details</th>
<th style="width:15%">Description</th>
<th style="width:10%">Pattern</th>
<th style="width:15%">Examples</th>
<th style="width:10%">Required</th>
<th style="width:5%"></th>
</tr>
@@ -1728,7 +1730,7 @@
}
}
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum', 'List'];
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'List'];
const typeExamples = {
String: 'example',
Integer: '1',
@@ -1736,7 +1738,6 @@
Double: '1.0',
Boolean: 'true',
BigDecimal: '1000.00',
Enum: 'OPEN',
List: 'C001'
};
const fieldTemplates = {
@@ -1805,24 +1806,27 @@
};
const n = makeInput(field.name, 'name'); n.dataset.field = 'name';
const d = makeInput(field.description, 'desc'); d.dataset.field = 'description';
const e = makeInput(field.example, 'ex'); e.dataset.field = 'example';
const p = makeInput(field.pattern, 'pattern'); p.dataset.field = 'pattern';
const e = document.createElement('textarea');
e.className = 'form-control form-control-sm';
e.rows = 2; e.dataset.field = 'example';
e.value = (field.examples || []).join('\n') || '';
const t = document.createElement('select');
t.className = 'form-select form-select-sm'; t.dataset.field = 'type';
['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum'].forEach(type => {
['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal'].forEach(type => {
t.add(new Option(type, type, false, (field.type || 'String') === type));
});
const det = makeInput(field.type === 'Enum' ? (field.enumValues||[]).join(',') : '', 'Enum csv');
const det = makeInput(field.type !== 'List' ? (field.enumValues||[]).join(',') : '', 'Enum csv (콤마 구분)');
det.dataset.field = 'details';
t.addEventListener('change', () => {
if (t.value === 'Enum') det.placeholder = 'Enum csv';
else det.value = '';
det.placeholder = 'Enum csv (콤마 구분)';
});
const req = document.createElement('select');
req.className = 'form-select form-select-sm'; req.dataset.field = 'required';
req.add(new Option('Required', 'true', false, field.required === true || field.required === 'true'));
req.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true')));
[n, t, det, d, e, req].forEach(ctrl => {
[n, t, det, d, p, e, req].forEach(ctrl => {
const td = document.createElement('td'); td.appendChild(ctrl); row.appendChild(td);
});
const del = document.createElement('button');
@@ -1841,9 +1845,10 @@
name: row.querySelector('[data-field="name"]').value.trim(),
type,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
pattern: row.querySelector('[data-field="pattern"]').value.trim(),
examples: row.querySelector('[data-field="example"]').value.split('\n').map(s=>s.trim()).filter(Boolean),
required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(s=>s.trim()).filter(Boolean) : [],
enumValues: type !== 'List' ? details.split(',').map(s=>s.trim()).filter(Boolean) : [],
itemType: null, itemFields: []
};
}).filter(f => f.name);
@@ -1898,8 +1903,16 @@
nameInput.dataset.field = 'name';
const descriptionInput = makeInput(field.description, 'e.g. Employee ID');
descriptionInput.dataset.field = 'description';
const exampleInput = makeInput(field.example, 'e.g. EMP10001');
const patternInput = makeInput(field.pattern, 'e.g. ^[0-9]{4}$');
patternInput.dataset.field = 'pattern';
const exampleInput = document.createElement('textarea');
exampleInput.className = 'form-control form-control-sm';
exampleInput.placeholder = 'e.g. EMP10001\nEMP10002';
exampleInput.rows = 2;
exampleInput.dataset.field = 'example';
exampleInput.value = (field.examples || []).join('\n') || '';
const typeSelect = document.createElement('select');
typeSelect.className = 'form-select form-select-sm';
@@ -1917,13 +1930,13 @@
input.add(new Option(itemType, itemType, false, (value || 'String') === itemType));
});
} else {
input = makeInput(type === 'Enum' ? value : '', 'Enum: OPEN, CLOSED');
input = makeInput(value, 'Enum csv (콤마 구분)');
}
input.dataset.field = 'details';
return input;
};
let detailsInput = createDetailsInput(field.type || 'String',
field.type === 'Enum' ? (field.enumValues || []).join(', ') : field.itemType);
field.type !== 'List' ? (field.enumValues || []).join(', ') : field.itemType);
const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm';
@@ -1978,12 +1991,12 @@
});
bindDetailsInput();
updateObjectListFieldsState();
[nameInput, descriptionInput, exampleInput, requiredSelect, itemFieldsInput].forEach(control => {
[nameInput, descriptionInput, patternInput, exampleInput, requiredSelect, itemFieldsInput].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview);
});
[nameInput, typeSelect, detailsInput, itemFieldsContainer, descriptionInput, exampleInput, requiredSelect].forEach(control => {
[nameInput, typeSelect, detailsInput, itemFieldsContainer, descriptionInput, patternInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
row.appendChild(cell);
@@ -2021,9 +2034,10 @@
return {
name: row.querySelector('[data-field="name"]').value.trim(), type,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
pattern: row.querySelector('[data-field="pattern"]').value.trim(),
examples: row.querySelector('[data-field="example"]').value.split('\n').map(e => e.trim()).filter(Boolean),
required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(value => value.trim()).filter(Boolean) : [],
enumValues: type !== 'List' ? details.split(',').map(value => value.trim()).filter(Boolean) : [],
itemType: type === 'List' ? (details || 'String') : null,
itemFields
};