From cf81f0031f74e835e1efe74cb1b788227fb9410b Mon Sep 17 00:00:00 2001 From: Iristack <107678261+Iristack@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:47:25 +0800 Subject: [PATCH 1/5] feat: add list parsing support to MarkdownSkillParser --- .../core/skill/util/MarkdownSkillParser.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java index 5bce28ced..163b156f6 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java @@ -170,6 +170,8 @@ static Map parse(String yaml) { } String[] lines = yaml.split("[\\r\\n]+"); + String curKey = null; + List yamlList = new ArrayList<>(); for (String line : lines) { // Skip empty lines @@ -182,16 +184,39 @@ static Map parse(String yaml) { continue; } + // Handle list items + if (line.trim().startsWith("-")) { + // Start with a list item but no key, throw it + if (curKey == null) { + throw new IllegalArgumentException( + "List item without a preceding key: " + line); + } + String yamlListItem = line.trim().substring(1).trim(); + yamlList.add(yamlListItem); + continue; + } + + // check if list item exists + if (curKey != null && !yamlList.isEmpty()) { + result.put(curKey, String.join(",", yamlList)); + yamlList.clear(); + } + Matcher matcher = KEY_VALUE_PATTERN.matcher(line.trim()); if (!matcher.matches()) { throw new IllegalArgumentException( "Invalid YAML line (expected 'key: value' format): " + line); } - String key = matcher.group(1); + curKey = matcher.group(1); String value = parseValue(matcher.group(2)); - result.put(key, value); + result.put(curKey, value); + } + + // when list in the end + if (curKey != null && !yamlList.isEmpty()) { + result.put(curKey, String.join(",", yamlList)); } return result; From 2af9905b6a02dcc124de7fb08987b62b0278e366 Mon Sep 17 00:00:00 2001 From: Iristack <107678261+Iristack@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:05:52 +0800 Subject: [PATCH 2/5] test: add list parsing support to MarkdownSkillParser --- .../skill/util/MarkdownSkillParserTest.java | 259 +++++++++++++++++- 1 file changed, 257 insertions(+), 2 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java index 3d4b642b4..6eacd9d59 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java @@ -296,7 +296,7 @@ void testInvalidYaml() { } @Test - @DisplayName("Should throw exception for list format") + @DisplayName("Should throw exception for preceding list format") void testListFormat() { String markdown = "---\n- item1\n- item2\n---\nContent"; @@ -304,7 +304,7 @@ void testListFormat() { assertThrows( IllegalArgumentException.class, () -> MarkdownSkillParser.parse(markdown)); - assertTrue(exception.getMessage().contains("Invalid YAML line")); + assertTrue(exception.getMessage().contains("List item without a preceding key")); } } @@ -535,4 +535,259 @@ void testToString() { assertTrue(toString.contains("content")); } } + + @Nested + @DisplayName("ParseMarkdownList Tests") + class ParseMarkdownListTests { + @Test + @DisplayName("Test parsing YAML with single list item") + void testParseSingleListItem() { + String yaml = "name: test\n" + + "tags:\n" + + " - feature1"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("feature1", result.get("tags")); + } + + @Test + @DisplayName("Test parsing YAML with multiple list items") + void testParseMultipleListItems() { + String yaml = "name: test\n" + + "tags:\n" + + " - feature1\n" + + " - feature2\n" + + " - feature3"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("feature1,feature2,feature3", result.get("tags")); + } + + @Test + @DisplayName("Test parsing YAML with list in the middle") + void testParseListInMiddle() { + String yaml = "name: test\n" + + "tags:\n" + + " - feature1\n" + + " - feature2\n" + + "description: A test skill"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("feature1,feature2", result.get("tags")); + assertEquals("A test skill", result.get("description")); + } + + @Test + @DisplayName("Test parsing YAML with multiple lists") + void testParseMultipleLists() { + String yaml = "name: test\n" + + "tags:\n" + + " - tag1\n" + + " - tag2\n" + + "description: A test skill\n" + + "inputs:\n" + + " - input1\n" + + " - input2\n" + + " - input3"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("tag1,tag2", result.get("tags")); + assertEquals("A test skill", result.get("description")); + assertEquals("input1,input2,input3", result.get("inputs")); + } + + @Test + @DisplayName("Test parsing YAML with list item without key should throw exception") + void testParseListItemWithoutKey() { + String yaml = "- item1\n" + + "- item2"; + + assertThrows(IllegalArgumentException.class, () -> parseYaml(yaml)); + } + + @Test + @DisplayName("Test parsing YAML with empty list") + void testParseEmptyList() { + String yaml = "name: test\n" + + "tags:\n" + + "description: A test skill"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("A test skill", result.get("description")); + } + + @Test + @DisplayName("Test parsing YAML with list containing spaces") + void testParseListWithSpaces() { + String yaml = "name: test\n" + + "tags:\n" + + " - feature1 \n" + + " - feature2"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("feature1,feature2", result.get("tags")); + } + + @Test + @DisplayName("Test parsing markdown with list in frontmatter") + void testParseMarkdownWithListInFrontmatter() { + String markdown = "---\n" + + "name: test_skill\n" + + "tags:\n" + + " - ai\n" + + " - ml\n" + + " - deep-learning\n" + + "version: 1.0.0\n" + + "---\n" + + "# Test Skill\n" + + "This is a test skill description."; + + ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + + assertTrue(parsed.hasFrontmatter()); + assertEquals("test_skill", parsed.getMetadata().get("name")); + assertEquals("ai,ml,deep-learning", parsed.getMetadata().get("tags")); + assertEquals("1.0.0", parsed.getMetadata().get("version")); + assertTrue(parsed.getContent().contains("# Test Skill")); + } + + @Test + @DisplayName("Test parsing markdown with multiple lists in frontmatter") + void testParseMarkdownWithMultipleLists() { + String markdown = "---\n" + + "name: vm_renew_skill\n" + + "tags:\n" + + " - vm\n" + + " - renew\n" + + "inputs:\n" + + " - vm_id\n" + + " - duration\n" + + "outputs:\n" + + " - result\n" + + " - status\n" + + "version: 2.0.0\n" + + "---\n" + + "# VM Renew Skill\n" + + "This skill renews virtual machines."; + + ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + + assertTrue(parsed.hasFrontmatter()); + assertEquals("vm_renew_skill", parsed.getMetadata().get("name")); + assertEquals("vm,renew", parsed.getMetadata().get("tags")); + assertEquals("vm_id,duration", parsed.getMetadata().get("inputs")); + assertEquals("result,status", parsed.getMetadata().get("outputs")); + assertEquals("2.0.0", parsed.getMetadata().get("version")); + assertTrue(parsed.getContent().contains("# VM Renew Skill")); + } + + @Test + @DisplayName("Test generating markdown with list values") + void testGenerateMarkdownWithList() { + Map metadata = Map.of( + "name", "test_skill", + "tags", "ai,ml,deep-learning", + "version", "1.0.0" + ); + String content = "# Test Skill\nThis is content."; + + String generated = MarkdownSkillParser.generate(metadata, content); + + assertTrue(generated.startsWith("---\n")); + assertTrue(generated.contains("name: test_skill")); + assertTrue(generated.contains("version: 1.0.0")); + assertTrue(generated.contains("# Test Skill")); + } + + @Test + @DisplayName("Test round-trip: parse and generate with list") + void testRoundTripWithList() { + String original = "---\n" + + "name: round_trip_test\n" + + "tags:\n" + + " - tag1\n" + + " - tag2\n" + + " - tag3\n" + + "version: 1.0.0\n" + + "---\n" + + "# Content"; + + ParsedMarkdown parsed = MarkdownSkillParser.parse(original); + String regenerated = MarkdownSkillParser.generate(parsed.getMetadata(), parsed.getContent()); + + // Parse again to verify round-trip + ParsedMarkdown reparsed = MarkdownSkillParser.parse(regenerated); + + assertEquals(parsed.getMetadata().get("name"), reparsed.getMetadata().get("name")); + assertEquals("tag1,tag2,tag3", reparsed.getMetadata().get("tags")); + assertEquals(parsed.getMetadata().get("version"), reparsed.getMetadata().get("version")); + } + + + @Test + @DisplayName("Test parsing empty markdown") + void testParseEmptyMarkdown() { + ParsedMarkdown parsed = MarkdownSkillParser.parse(""); + + assertFalse(parsed.hasFrontmatter()); + assertTrue(parsed.getMetadata().isEmpty()); + assertEquals("", parsed.getContent()); + } + + @Test + @DisplayName("Test parsing markdown without frontmatter") + void testParseMarkdownWithoutFrontmatter() { + String markdown = "# Just Content\nNo frontmatter here."; + + ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + + assertFalse(parsed.hasFrontmatter()); + assertTrue(parsed.getMetadata().isEmpty()); + assertEquals("# Just Content\nNo frontmatter here.", parsed.getContent()); + } + + @Test + @DisplayName("Test parsing null markdown") + void testParseNullMarkdown() { + ParsedMarkdown parsed = MarkdownSkillParser.parse(null); + + assertFalse(parsed.hasFrontmatter()); + assertTrue(parsed.getMetadata().isEmpty()); + assertEquals("", parsed.getContent()); + } + + @Test + @DisplayName("Test parsing list with special characters in items") + void testParseListWithSpecialCharacters() { + String yaml = "name: test\n" + + "tags:\n" + + " - tag-with-dash\n" + + " - tag_with_underscore\n" + + " - tag123"; + + Map result = parseYaml(yaml); + + assertEquals("test", result.get("name")); + assertEquals("tag-with-dash,tag_with_underscore,tag123", result.get("tags")); + } + + + private Map parseYaml(String yaml) { + String markdown = "---\n" + yaml + "\n---\n"; + ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + return parsed.getMetadata(); + } + } } From 719ef373bcfe7a555da8ff7084cb22559d123c25 Mon Sep 17 00:00:00 2001 From: Iristack <107678261+Iristack@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:26:07 +0800 Subject: [PATCH 3/5] Update MarkdownSkillParser.java --- .../java/io/agentscope/core/skill/util/MarkdownSkillParser.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java index 163b156f6..8dcf2e2dd 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java @@ -16,6 +16,8 @@ package io.agentscope.core.skill.util; +import java.util.ArrayList; +import java.util.List; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; From 728d47f96296ea019d6cd46e7f6ea40fc195a72c Mon Sep 17 00:00:00 2001 From: Iristack <107678261+Iristack@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:38:54 +0800 Subject: [PATCH 4/5] Update MarkdownSkillParserTest.java --- .../skill/util/MarkdownSkillParserTest.java | 157 +++++++++--------- 1 file changed, 78 insertions(+), 79 deletions(-) diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java index 6eacd9d59..b2d4441fa 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java @@ -542,9 +542,7 @@ class ParseMarkdownListTests { @Test @DisplayName("Test parsing YAML with single list item") void testParseSingleListItem() { - String yaml = "name: test\n" + - "tags:\n" + - " - feature1"; + String yaml = "name: test\n" + "tags:\n" + " - feature1"; Map result = parseYaml(yaml); @@ -555,11 +553,12 @@ void testParseSingleListItem() { @Test @DisplayName("Test parsing YAML with multiple list items") void testParseMultipleListItems() { - String yaml = "name: test\n" + - "tags:\n" + - " - feature1\n" + - " - feature2\n" + - " - feature3"; + String yaml = + "name: test\n" + + "tags:\n" + + " - feature1\n" + + " - feature2\n" + + " - feature3"; Map result = parseYaml(yaml); @@ -570,11 +569,12 @@ void testParseMultipleListItems() { @Test @DisplayName("Test parsing YAML with list in the middle") void testParseListInMiddle() { - String yaml = "name: test\n" + - "tags:\n" + - " - feature1\n" + - " - feature2\n" + - "description: A test skill"; + String yaml = + "name: test\n" + + "tags:\n" + + " - feature1\n" + + " - feature2\n" + + "description: A test skill"; Map result = parseYaml(yaml); @@ -586,15 +586,16 @@ void testParseListInMiddle() { @Test @DisplayName("Test parsing YAML with multiple lists") void testParseMultipleLists() { - String yaml = "name: test\n" + - "tags:\n" + - " - tag1\n" + - " - tag2\n" + - "description: A test skill\n" + - "inputs:\n" + - " - input1\n" + - " - input2\n" + - " - input3"; + String yaml = + "name: test\n" + + "tags:\n" + + " - tag1\n" + + " - tag2\n" + + "description: A test skill\n" + + "inputs:\n" + + " - input1\n" + + " - input2\n" + + " - input3"; Map result = parseYaml(yaml); @@ -607,8 +608,7 @@ void testParseMultipleLists() { @Test @DisplayName("Test parsing YAML with list item without key should throw exception") void testParseListItemWithoutKey() { - String yaml = "- item1\n" + - "- item2"; + String yaml = "- item1\n" + "- item2"; assertThrows(IllegalArgumentException.class, () -> parseYaml(yaml)); } @@ -616,9 +616,7 @@ void testParseListItemWithoutKey() { @Test @DisplayName("Test parsing YAML with empty list") void testParseEmptyList() { - String yaml = "name: test\n" + - "tags:\n" + - "description: A test skill"; + String yaml = "name: test\n" + "tags:\n" + "description: A test skill"; Map result = parseYaml(yaml); @@ -629,10 +627,7 @@ void testParseEmptyList() { @Test @DisplayName("Test parsing YAML with list containing spaces") void testParseListWithSpaces() { - String yaml = "name: test\n" + - "tags:\n" + - " - feature1 \n" + - " - feature2"; + String yaml = "name: test\n" + "tags:\n" + " - feature1 \n" + " - feature2"; Map result = parseYaml(yaml); @@ -643,16 +638,17 @@ void testParseListWithSpaces() { @Test @DisplayName("Test parsing markdown with list in frontmatter") void testParseMarkdownWithListInFrontmatter() { - String markdown = "---\n" + - "name: test_skill\n" + - "tags:\n" + - " - ai\n" + - " - ml\n" + - " - deep-learning\n" + - "version: 1.0.0\n" + - "---\n" + - "# Test Skill\n" + - "This is a test skill description."; + String markdown = + "---\n" + + "name: test_skill\n" + + "tags:\n" + + " - ai\n" + + " - ml\n" + + " - deep-learning\n" + + "version: 1.0.0\n" + + "---\n" + + "# Test Skill\n" + + "This is a test skill description."; ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); @@ -666,21 +662,22 @@ void testParseMarkdownWithListInFrontmatter() { @Test @DisplayName("Test parsing markdown with multiple lists in frontmatter") void testParseMarkdownWithMultipleLists() { - String markdown = "---\n" + - "name: vm_renew_skill\n" + - "tags:\n" + - " - vm\n" + - " - renew\n" + - "inputs:\n" + - " - vm_id\n" + - " - duration\n" + - "outputs:\n" + - " - result\n" + - " - status\n" + - "version: 2.0.0\n" + - "---\n" + - "# VM Renew Skill\n" + - "This skill renews virtual machines."; + String markdown = + "---\n" + + "name: vm_renew_skill\n" + + "tags:\n" + + " - vm\n" + + " - renew\n" + + "inputs:\n" + + " - vm_id\n" + + " - duration\n" + + "outputs:\n" + + " - result\n" + + " - status\n" + + "version: 2.0.0\n" + + "---\n" + + "# VM Renew Skill\n" + + "This skill renews virtual machines."; ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); @@ -696,11 +693,11 @@ void testParseMarkdownWithMultipleLists() { @Test @DisplayName("Test generating markdown with list values") void testGenerateMarkdownWithList() { - Map metadata = Map.of( - "name", "test_skill", - "tags", "ai,ml,deep-learning", - "version", "1.0.0" - ); + Map metadata = + Map.of( + "name", "test_skill", + "tags", "ai,ml,deep-learning", + "version", "1.0.0"); String content = "# Test Skill\nThis is content."; String generated = MarkdownSkillParser.generate(metadata, content); @@ -714,28 +711,30 @@ void testGenerateMarkdownWithList() { @Test @DisplayName("Test round-trip: parse and generate with list") void testRoundTripWithList() { - String original = "---\n" + - "name: round_trip_test\n" + - "tags:\n" + - " - tag1\n" + - " - tag2\n" + - " - tag3\n" + - "version: 1.0.0\n" + - "---\n" + - "# Content"; + String original = + "---\n" + + "name: round_trip_test\n" + + "tags:\n" + + " - tag1\n" + + " - tag2\n" + + " - tag3\n" + + "version: 1.0.0\n" + + "---\n" + + "# Content"; ParsedMarkdown parsed = MarkdownSkillParser.parse(original); - String regenerated = MarkdownSkillParser.generate(parsed.getMetadata(), parsed.getContent()); + String regenerated = + MarkdownSkillParser.generate(parsed.getMetadata(), parsed.getContent()); // Parse again to verify round-trip ParsedMarkdown reparsed = MarkdownSkillParser.parse(regenerated); assertEquals(parsed.getMetadata().get("name"), reparsed.getMetadata().get("name")); assertEquals("tag1,tag2,tag3", reparsed.getMetadata().get("tags")); - assertEquals(parsed.getMetadata().get("version"), reparsed.getMetadata().get("version")); + assertEquals( + parsed.getMetadata().get("version"), reparsed.getMetadata().get("version")); } - @Test @DisplayName("Test parsing empty markdown") void testParseEmptyMarkdown() { @@ -771,11 +770,12 @@ void testParseNullMarkdown() { @Test @DisplayName("Test parsing list with special characters in items") void testParseListWithSpecialCharacters() { - String yaml = "name: test\n" + - "tags:\n" + - " - tag-with-dash\n" + - " - tag_with_underscore\n" + - " - tag123"; + String yaml = + "name: test\n" + + "tags:\n" + + " - tag-with-dash\n" + + " - tag_with_underscore\n" + + " - tag123"; Map result = parseYaml(yaml); @@ -783,7 +783,6 @@ void testParseListWithSpecialCharacters() { assertEquals("tag-with-dash,tag_with_underscore,tag123", result.get("tags")); } - private Map parseYaml(String yaml) { String markdown = "---\n" + yaml + "\n---\n"; ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); From 7c5a3386a63ba4427b1a9213469693b861a08a78 Mon Sep 17 00:00:00 2001 From: Iristack <107678261+Iristack@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:40:48 +0800 Subject: [PATCH 5/5] Update MarkdownSkillParser.java --- .../io/agentscope/core/skill/util/MarkdownSkillParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java index 8dcf2e2dd..609ce6a66 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java @@ -17,8 +17,8 @@ package io.agentscope.core.skill.util; import java.util.ArrayList; -import java.util.List; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -172,7 +172,7 @@ static Map parse(String yaml) { } String[] lines = yaml.split("[\\r\\n]+"); - String curKey = null; + String curKey = null; List yamlList = new ArrayList<>(); for (String line : lines) { @@ -189,7 +189,7 @@ static Map parse(String yaml) { // Handle list items if (line.trim().startsWith("-")) { // Start with a list item but no key, throw it - if (curKey == null) { + if (curKey == null) { throw new IllegalArgumentException( "List item without a preceding key: " + line); }