|
1 | 1 | package info.jab.pml; |
2 | 2 |
|
3 | | -import com.fasterxml.jackson.databind.JsonNode; |
4 | | -import com.fasterxml.jackson.databind.ObjectMapper; |
5 | 3 | import java.io.IOException; |
6 | 4 | import java.io.InputStream; |
7 | 5 | import java.net.JarURLConnection; |
8 | 6 | import java.net.URISyntaxException; |
9 | 7 | import java.net.URL; |
10 | | -import java.nio.charset.StandardCharsets; |
11 | 8 | import java.nio.file.Files; |
12 | 9 | import java.nio.file.Path; |
13 | 10 | import java.nio.file.Paths; |
|
16 | 13 | import java.util.jar.JarEntry; |
17 | 14 | import java.util.jar.JarFile; |
18 | 15 | import java.util.stream.Stream; |
| 16 | +import org.w3c.dom.Document; |
| 17 | +import org.w3c.dom.Element; |
| 18 | +import org.w3c.dom.NodeList; |
19 | 19 |
|
20 | 20 | /** |
21 | | - * Inventory of skills to generate, loaded from {@code skill-inventory.json}. |
| 21 | + * Inventory of skills to generate, loaded from {@code skill-inventory.xml}. |
22 | 22 | * <p> |
23 | 23 | * Each entry has an {@code id} (numeric or string like "010"). When {@code requiresSystemPrompt} |
24 | 24 | * is true (default), the skillId is derived by matching system-prompts with prefix {@code {id}-}. |
25 | 25 | * When false, the entry must specify {@code skillId} and no system-prompt is required. |
26 | | - * Each skill must have a summary in {@code skills/{id}-skill.md}. |
| 26 | + * Each skill must have a summary in {@code skills/{id}-skill.md} or {@code skills/{id}-skill.xml} |
| 27 | + * when {@code xml="true"} on the entry. |
27 | 28 | */ |
28 | 29 | public final class SkillsInventory { |
29 | 30 |
|
30 | | - private static final String INVENTORY_RESOURCE = "skill-inventory.json"; |
| 31 | + private static final String INVENTORY_RESOURCE = "skill-inventory.xml"; |
31 | 32 | private static final String SYSTEM_PROMPTS_PREFIX = "system-prompts/"; |
32 | 33 |
|
33 | 34 | private SkillsInventory() {} |
@@ -150,64 +151,79 @@ private static URL getResourceUrl(String name) { |
150 | 151 | } |
151 | 152 |
|
152 | 153 | /** |
153 | | - * Loads and parses skill-inventory.json. |
| 154 | + * Loads and parses skill-inventory.xml. |
154 | 155 | */ |
155 | 156 | public static List<InventoryEntry> loadInventory() { |
156 | 157 | try (InputStream stream = getResource(INVENTORY_RESOURCE)) { |
157 | 158 | if (stream == null) { |
158 | 159 | throw new RuntimeException("Skill inventory not found: " + INVENTORY_RESOURCE); |
159 | 160 | } |
160 | | - String json = new String(stream.readAllBytes(), StandardCharsets.UTF_8); |
161 | | - return parseInventory(json); |
| 161 | + return parseInventory(stream); |
162 | 162 | } catch (Exception e) { |
163 | 163 | throw new RuntimeException("Failed to load skill inventory", e); |
164 | 164 | } |
165 | 165 | } |
166 | 166 |
|
167 | | - private static List<InventoryEntry> parseInventory(String json) { |
| 167 | + private static List<InventoryEntry> parseInventory(InputStream in) { |
168 | 168 | try { |
169 | | - ObjectMapper mapper = new ObjectMapper(); |
170 | | - JsonNode root = mapper.readTree(json); |
171 | | - if (!root.isArray()) { |
172 | | - throw new RuntimeException("Skill inventory must be a JSON array"); |
| 169 | + Document doc = InventoryXmlLoader.parse(in); |
| 170 | + Element root = doc.getDocumentElement(); |
| 171 | + if (!"skill-inventory".equals(root.getNodeName())) { |
| 172 | + throw new RuntimeException("Skill inventory root must be <skill-inventory>"); |
173 | 173 | } |
174 | | - |
| 174 | + NodeList skillNodes = root.getElementsByTagName("skill"); |
175 | 175 | List<InventoryEntry> entries = new ArrayList<>(); |
176 | | - for (JsonNode node : root) { |
177 | | - String numericId = node.get("id").isTextual() |
178 | | - ? node.get("id").asText() |
179 | | - : String.valueOf(node.get("id").asInt()); |
180 | | - boolean requiresSystemPrompt = node.has("requiresSystemPrompt") |
181 | | - ? node.get("requiresSystemPrompt").asBoolean() |
182 | | - : true; |
183 | | - String skillId = node.has("skillId") ? node.get("skillId").asText() : null; |
| 176 | + for (int i = 0; i < skillNodes.getLength(); i++) { |
| 177 | + if (!(skillNodes.item(i) instanceof Element skillEl)) { |
| 178 | + continue; |
| 179 | + } |
| 180 | + if (skillEl.getParentNode() != root) { |
| 181 | + continue; |
| 182 | + } |
| 183 | + String numericId = skillEl.getAttribute("id"); |
| 184 | + if (numericId == null || numericId.isBlank()) { |
| 185 | + throw new RuntimeException("skill-inventory entry missing id attribute"); |
| 186 | + } |
| 187 | + boolean requiresSystemPrompt = parseBooleanAttribute(skillEl, "requiresSystemPrompt", true); |
| 188 | + String skillId = skillEl.hasAttribute("skillId") |
| 189 | + ? skillEl.getAttribute("skillId").trim() |
| 190 | + : null; |
| 191 | + if (skillId != null && skillId.isEmpty()) { |
| 192 | + skillId = null; |
| 193 | + } |
184 | 194 |
|
185 | 195 | if (!requiresSystemPrompt && (skillId == null || skillId.isBlank())) { |
186 | 196 | throw new RuntimeException("Entry with id " + numericId |
187 | 197 | + " has requiresSystemPrompt=false but no skillId specified."); |
188 | 198 | } |
189 | | - boolean useXml = parseXmlFlag(node); |
| 199 | + boolean useXml = parseXmlAttribute(skillEl); |
190 | 200 | entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId, useXml)); |
191 | 201 | } |
| 202 | + if (entries.isEmpty()) { |
| 203 | + throw new RuntimeException("Skill inventory must contain at least one <skill> entry"); |
| 204 | + } |
192 | 205 | return entries; |
| 206 | + } catch (RuntimeException e) { |
| 207 | + throw e; |
193 | 208 | } catch (Exception e) { |
194 | 209 | throw new RuntimeException("Failed to parse skill inventory", e); |
195 | 210 | } |
196 | 211 | } |
197 | 212 |
|
198 | | - private static boolean parseXmlFlag(JsonNode node) { |
199 | | - if (!node.has("xml")) { |
200 | | - return false; |
201 | | - } |
202 | | - JsonNode xmlNode = node.get("xml"); |
203 | | - if (xmlNode.isBoolean()) { |
204 | | - return xmlNode.asBoolean(); |
| 213 | + private static boolean parseBooleanAttribute(Element el, String name, boolean defaultValue) { |
| 214 | + if (!el.hasAttribute(name)) { |
| 215 | + return defaultValue; |
205 | 216 | } |
206 | | - if (xmlNode.isTextual()) { |
207 | | - String s = xmlNode.asText().toLowerCase(); |
208 | | - return "true".equals(s) || "yes".equals(s) || "1".equals(s); |
| 217 | + String v = el.getAttribute(name).trim().toLowerCase(); |
| 218 | + return "true".equals(v) || "yes".equals(v) || "1".equals(v); |
| 219 | + } |
| 220 | + |
| 221 | + private static boolean parseXmlAttribute(Element skillEl) { |
| 222 | + if (!skillEl.hasAttribute("xml")) { |
| 223 | + return false; |
209 | 224 | } |
210 | | - return false; |
| 225 | + String s = skillEl.getAttribute("xml").trim().toLowerCase(); |
| 226 | + return "true".equals(s) || "yes".equals(s) || "1".equals(s); |
211 | 227 | } |
212 | 228 |
|
213 | 229 | private static void validateSkillSummaryExists(String numericId, boolean useXml) { |
@@ -241,7 +257,7 @@ private static InputStream getResource(String name) { |
241 | 257 | } |
242 | 258 |
|
243 | 259 | /** |
244 | | - * Single entry from skill-inventory.json. When requiresSystemPrompt is true, |
| 260 | + * Single entry from skill-inventory.xml. When requiresSystemPrompt is true, |
245 | 261 | * skillId is derived by matching system-prompts with prefix {@code {numericId}-}. |
246 | 262 | * When false, skillId must be provided and no system-prompt is required. |
247 | 263 | * When useXml is true, skill summary is loaded from skills/{numericId}-skill.xml |
|
0 commit comments