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..939b1b814 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 @@ -20,6 +20,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility for parsing and generating Markdown files with YAML frontmatter. @@ -54,6 +56,8 @@ */ public class MarkdownSkillParser { + private static final Logger logger = LoggerFactory.getLogger(MarkdownSkillParser.class); + /** * Private constructor to prevent instantiation. */ @@ -81,7 +85,6 @@ private MarkdownSkillParser() {} * * @param markdown Markdown content (may or may not have frontmatter) * @return ParsedMarkdown containing metadata and content - * @throws IllegalArgumentException if YAML syntax is invalid */ public static ParsedMarkdown parse(String markdown) { if (markdown == null || markdown.isEmpty()) { @@ -102,14 +105,8 @@ public static ParsedMarkdown parse(String markdown) { return new ParsedMarkdown(Map.of(), markdownContent); } - try { - Map metadata = SimpleYamlParser.parse(yamlContent); - return new ParsedMarkdown(metadata, markdownContent); - } catch (IllegalArgumentException e) { - throw e; - } catch (RuntimeException e) { - throw new IllegalArgumentException("Invalid YAML frontmatter syntax", e); - } + Map metadata = SimpleYamlParser.parse(yamlContent); + return new ParsedMarkdown(metadata, markdownContent); } /** @@ -158,9 +155,14 @@ private static class SimpleYamlParser { /** * Parse YAML string into a map of key-value pairs. * + *

This is a simplified parser designed for flat string-to-string mappings. + * Block-style complex YAML structures (such as multi-line lists or indented + * nested objects) are not supported and will be gracefully skipped. + * However, flow-style inline structures (e.g., single-line JSON strings) + * are treated as standard scalar values and will be parsed as raw strings. + * * @param yaml YAML content to parse * @return Map of key-value pairs - * @throws IllegalArgumentException if YAML syntax is invalid */ static Map parse(String yaml) { Map result = new LinkedHashMap<>(); @@ -184,19 +186,44 @@ static Map parse(String yaml) { Matcher matcher = KEY_VALUE_PATTERN.matcher(line.trim()); if (!matcher.matches()) { - throw new IllegalArgumentException( - "Invalid YAML line (expected 'key: value' format): " + line); + logger.debug( + "Skipping unsupported YAML line (expected 'key: value' format): {}", + line); + continue; } String key = matcher.group(1); - String value = parseValue(matcher.group(2)); + String rawValue = matcher.group(2); - result.put(key, value); + if (isBlockScalarModifier(rawValue)) { + logger.debug( + "Skipping key '{}': block-style values ('{}') are unsupported", + key, + rawValue.trim()); + continue; + } + + result.put(key, parseValue(rawValue)); } return result; } + /** + * Check if the raw value is a YAML block scalar modifier ('|' or '>'). + * + * @param rawValue The raw string captured after the colon + * @return true if it is a block scalar modifier + */ + private static boolean isBlockScalarModifier(String rawValue) { + if (rawValue == null) { + return false; + } + + String trimmed = rawValue.trim(); + return "|".equals(trimmed) || ">".equals(trimmed); + } + /** * Parse a YAML value, handling quoted strings. * 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..52597615f 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 @@ -20,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import io.agentscope.core.skill.util.MarkdownSkillParser.ParsedMarkdown; @@ -283,28 +282,103 @@ void testParseUnicodeCharacters() { class ErrorHandlingTests { @Test - @DisplayName("Should throw exception for invalid YAML") + @DisplayName("Should gracefully ignore invalid YAML lines instead of throwing exception") void testInvalidYaml() { String markdown = "---\nname: test\nthis is not a valid line\n---\nContent"; - IllegalArgumentException exception = - assertThrows( - IllegalArgumentException.class, - () -> MarkdownSkillParser.parse(markdown)); - assertTrue(exception.getMessage().contains("Invalid YAML line")); - assertTrue(exception.getMessage().contains("expected 'key: value' format")); + MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + Map metadata = parsed.getMetadata(); + + assertEquals("test", metadata.get("name")); + assertFalse(metadata.containsKey("this is not a valid line")); + assertEquals("Content", parsed.getContent()); } @Test - @DisplayName("Should throw exception for list format") + @DisplayName("Should gracefully ignore list format instead of throwing exception") void testListFormat() { - String markdown = "---\n- item1\n- item2\n---\nContent"; + String markdown = "---\nname: test_skill\n- item1\n- item2\n---\nContent"; + + MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + Map metadata = parsed.getMetadata(); + + assertEquals("test_skill", metadata.get("name")); + assertFalse(metadata.containsKey("- item1")); + assertFalse(metadata.containsKey("- item2")); + } + + @Test + @DisplayName( + "Should parse basic scalars and gracefully ignore complex YAML structures like" + + " lists or JSON") + void testParseAndIgnoreComplexMetadata() { + String markdown = + """ + --- + name: Agent Browser + description: A fast Rust-based headless browser automation CLI + read_when: + - Automating web interactions + - Extracting structured data from pages + metadata: {"clawdbot":{"emoji":"🌐"}} + allowed-tools: Bash(agent-browser:*) + --- + + # Content + This is the content.\ + """; + + MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + Map metadata = parsed.getMetadata(); + + assertEquals("Agent Browser", metadata.get("name")); + assertEquals( + "A fast Rust-based headless browser automation CLI", + metadata.get("description")); + assertEquals("Bash(agent-browser:*)", metadata.get("allowed-tools")); + + assertEquals("{\"clawdbot\":{\"emoji\":\"🌐\"}}", metadata.get("metadata")); + + assertEquals("", metadata.get("read_when")); + assertNull(metadata.get("- Automating web interactions")); + + assertTrue(parsed.getContent().contains("# Content")); + } + + @Test + @DisplayName( + "Should gracefully skip keys with block-style modifiers (| or >) instead of" + + " recording them as literal values") + void testSkipBlockStyleModifiers() { + String markdown = + """ + --- + name: test_skill + description: | + This is a multi-line description. + It should be ignored by the simple parser. + summary: > + This is a folded multi-line summary. + It should also be ignored. + version: "1.0" + --- + Content\ + """; + + MarkdownSkillParser.ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown); + Map metadata = parsed.getMetadata(); + + assertEquals("test_skill", metadata.get("name")); + assertEquals("1.0", metadata.get("version")); + + assertNull( + metadata.get("description"), + "Block scalar modifier '|' should not be parsed as a literal value"); + assertNull( + metadata.get("summary"), + "Block scalar modifier '>' should not be parsed as a literal value"); - IllegalArgumentException exception = - assertThrows( - IllegalArgumentException.class, - () -> MarkdownSkillParser.parse(markdown)); - assertTrue(exception.getMessage().contains("Invalid YAML line")); + assertFalse(metadata.containsKey(" This is a multi-line description.")); } }