diff --git a/ballerina-interpreter/parser.bal b/ballerina-interpreter/parser.bal index 8758272..6f8beb2 100644 --- a/ballerina-interpreter/parser.bal +++ b/ballerina-interpreter/parser.bal @@ -20,9 +20,18 @@ import ballerina/os; function parseAfm(string content) returns AFMRecord|error { string resolvedContent = check resolveVariables(content); + string[] resolvedLines = splitLines(resolvedContent); + int firstNonBlank = 0; + while firstNonBlank < resolvedLines.length() && resolvedLines[firstNonBlank].trim() == "" { + firstNonBlank += 1; + } + if firstNonBlank > 0 { + resolvedContent = string:'join("\n", ...resolvedLines.slice(firstNonBlank)); + resolvedLines = splitLines(resolvedContent); + } + AgentMetadata? metadata = (); string body; - string[] resolvedLines = splitLines(resolvedContent); if resolvedLines.length() > 0 && resolvedLines[0].trim() == FRONTMATTER_DELIMITER { map frontmatterMap; [frontmatterMap, body] = check extractFrontMatter(resolvedContent); diff --git a/ballerina-interpreter/tests/agent_test.bal b/ballerina-interpreter/tests/agent_test.bal index c117be2..9896e95 100644 --- a/ballerina-interpreter/tests/agent_test.bal +++ b/ballerina-interpreter/tests/agent_test.bal @@ -339,6 +339,29 @@ function testGetModelUnsupportedProvider() returns error? { test:assertTrue((result).message().includes("not yet supported")); } +@test:Config +function testParseAfmIgnoresLeadingBlankLines() returns error? { + string content = "\n\n---\n" + + "model:\n" + + " provider: \"ollama\"\n" + + " name: \"llama3\"\n" + + "---\n\n" + + "# Role\n" + + "You are a helpful assistant.\n\n" + + "# Instructions\n" + + "Be concise.\n"; + + AFMRecord afm = check parseAfm(content); + + AgentMetadata? metadata = afm.metadata; + test:assertTrue(metadata is AgentMetadata); + Model? model = (metadata).model; + test:assertTrue(model is Model); + test:assertEquals((model).provider, "ollama"); + test:assertEquals((model).name, "llama3"); + test:assertEquals(afm.role, "You are a helpful assistant."); +} + function extractJsonFromCodeBlockDataProvider() returns [string, string, string][] { return [ ["json marker", "Here is the result:\n```json\n{\"name\": \"Alice\"}\n```\nDone.", "{\"name\": \"Alice\"}"], diff --git a/python-interpreter/packages/afm-core/src/afm/parser.py b/python-interpreter/packages/afm-core/src/afm/parser.py index 9c8a6ba..60433c6 100644 --- a/python-interpreter/packages/afm-core/src/afm/parser.py +++ b/python-interpreter/packages/afm-core/src/afm/parser.py @@ -36,11 +36,15 @@ def extract_raw_frontmatter(content: str) -> tuple[dict | None, str]: """ lines = content.splitlines() - if not lines or lines[0].strip() != FRONTMATTER_DELIMITER: + start = 0 + while start < len(lines) and not lines[start].strip(): + start += 1 + + if start >= len(lines) or lines[start].strip() != FRONTMATTER_DELIMITER: return None, content end_index = None - for i in range(1, len(lines)): + for i in range(start + 1, len(lines)): if lines[i].strip() == FRONTMATTER_DELIMITER: end_index = i break @@ -48,7 +52,7 @@ def extract_raw_frontmatter(content: str) -> tuple[dict | None, str]: if end_index is None: raise ValueError("Unclosed frontmatter - missing closing '---'") - yaml_content = "\n".join(lines[1:end_index]) + yaml_content = "\n".join(lines[start + 1 : end_index]) body = "\n".join(lines[end_index + 1 :]) if not yaml_content.strip(): diff --git a/python-interpreter/packages/afm-core/tests/test_parser.py b/python-interpreter/packages/afm-core/tests/test_parser.py index 0b517ff..8084cb9 100644 --- a/python-interpreter/packages/afm-core/tests/test_parser.py +++ b/python-interpreter/packages/afm-core/tests/test_parser.py @@ -141,6 +141,31 @@ def test_parse_no_frontmatter(self, sample_no_frontmatter_path: Path) -> None: assert result.role == "This is the role without frontmatter." assert result.instructions == "These are instructions without frontmatter." + def test_parse_frontmatter_with_leading_blank_lines(self) -> None: + content = """ + +--- +spec_version: "0.3.0" +model: + provider: "ollama" + name: "llama3" +--- + +# Role +The role. + +# Instructions +The instructions. +""" + result = parse_afm(content) + + assert result.metadata.spec_version == "0.3.0" + assert result.metadata.model is not None + assert result.metadata.model.provider == "ollama" + assert result.metadata.model.name == "llama3" + assert result.role == "The role." + assert result.instructions == "The instructions." + def test_parse_unclosed_frontmatter(self) -> None: content = """--- spec_version: "0.3.0"