orchestrata_ultimate.agents.volks.analyst.parsers
Reference › orchestrata_ultimate › agents › volks › analyst › parsers
parsers holds the regular-expression and line parsers that turn the Analyst's LLM reply into data. Only agent imports it, and the labelled sections it reads are defined by the template in prompts. _extract_section cuts the text between a start marker and the first end marker, and _parse_kv_lines reads key: value lines. The public parsers are parse_project_type, parse_technology, parse_spec_metadata, parse_user_stories (stories with their acceptance criteria, assembled line by line through _parse_stories_from_section and _process_story_line), parse_requirements (trimmed at the next section boundary by _trim_requirements_section, entries read by _extract_req_lines) and parse_nfrs (via _extract_nfr_lines). Most carry @traces markers for specification 019.
At a glance
| Source | src/orchestrata_ultimate/agents/volks/analyst/parsers.py |
| Lines of code | 362 |
| Classes | 0 |
| Functions | 13 |
| Imports (internal) | orchestrata_ultimate.core.base.traces |
| Imported by | orchestrata_ultimate.agents.volks.analyst.agent |
| Tests that import it | tests/agents/volks/analyst/test_parsers.py, tests/generated_agents/test_volks_analyst_parsers_generated.py, tests/generated_recovery/test_agents_volks_analyst_parsers.py |
Class diagram
No classes; see Functions.
Dependency diagram
API
Functions
| Function | Signature | Summary |
|---|---|---|
_extract_section |
_extract_section(response: str, start_marker: str, end_markers: list[str]) -> str |
Extract text between a start marker and the first end marker. |
parse_project_type traces |
parse_project_type(response: str) -> str |
Extract PROJECT_TYPE from LLM response. |
parse_technology traces |
parse_technology(response: str) -> dict[str, str] |
Extract TECHNOLOGY key-value pairs from LLM response. |
_parse_kv_lines |
_parse_kv_lines(section: str) -> dict[str, str] |
Parse key: value lines from a section. |
parse_spec_metadata traces |
parse_spec_metadata(response: str) -> dict[str, str] |
Extract SPEC_METADATA key-value pairs from LLM response. |
parse_user_stories traces |
parse_user_stories(response: str) -> list[dict[str, Any]] |
Extract USER_STORIES from LLM response. |
_parse_stories_from_section traces |
_parse_stories_from_section(section: str) -> list[dict[str, Any]] |
Parse individual stories from a USER_STORIES section. |
_process_story_line traces |
_process_story_line(stripped: str, current: dict[str, Any] | None, in_criteria: bool, stories: list[dict[str, Any]]) -> tuple[dict[str, Any] | None, bool] |
Process a single line within the USER_STORIES section. |
parse_requirements traces |
parse_requirements(response: str) -> list[dict[str, Any]] |
Extract REQUIREMENTS from LLM response. |
_trim_requirements_section traces |
_trim_requirements_section(section: str) -> str |
Trim the requirements section at the next section boundary. |
_extract_req_lines traces |
_extract_req_lines(section: str) -> list[dict[str, Any]] |
Extract requirement entries from section lines. |
parse_nfrs traces |
parse_nfrs(response: str) -> list[dict[str, str]] |
Extract NON_FUNCTIONAL requirements from LLM response. |
_extract_nfr_lines traces |
_extract_nfr_lines(section: str) -> list[dict[str, str]] |
Extract NFR entries from section lines. |
Source
"""LLM output parsers for the Analyst volk (U:BA).
Regex-based extraction of structured sections from LLM responses:
PROJECT_TYPE, TECHNOLOGY, SPEC_METADATA, USER_STORIES,
REQUIREMENTS, and NON_FUNCTIONAL.
"""
import re
from typing import Any
from orchestrata_ultimate.core.base.traces import traces
# Compiled patterns for requirement/story/NFR extraction.
#
# REQ_PATTERN matches the CANONICAL story-scoped id only (RUL-031): ``US``, a
# hyphen, then three digits. It previously read ``US\d+`` — the legacy
# spelling — which meant this parser could not read a single id from the
# migrated corpus and silently returned an empty list for every requirement
# the analyst produced. Accepting the legacy form "just in case an LLM emits
# it" is what this parser must NOT do: its output flows into the corpus, so
# tolerating legacy here re-introduces exactly what RUL-031 eliminated.
REQ_PATTERN = re.compile(r"(REQ-\d{3}-US-\d{3}-\d{3}):\s*(.+)")
US_PATTERN = re.compile(r"(US-\d{3}):\s*(.+)")
NFR_PATTERN = re.compile(r"(NFR-\d{3}):\s*(.+)")
# The story segment of a canonical requirement id, for deriving ``story_id``.
REQ_STORY_PATTERN = re.compile(r"US-(\d{3})")
def _extract_section(
response: str,
start_marker: str,
end_markers: list[str],
) -> str:
"""Extract text between a start marker and the first end marker.
Args:
response: Full LLM response text.
start_marker: Section header to find (e.g. "TECHNOLOGY:").
end_markers: List of subsequent section headers that end
this section.
Returns:
The extracted section text, or empty string if not found.
"""
if start_marker not in response:
return ""
section = response.split(start_marker)[1]
for marker in end_markers:
if marker in section:
section = section.split(marker)[0]
break
return section.strip()
@traces(spec="019", story="US-002", reqs=["REQ-019-013", "REQ-019-014", "REQ-019-015"])
def parse_project_type(response: str) -> str:
"""Extract PROJECT_TYPE from LLM response.
Args:
response: Full LLM response text.
Returns:
Lowercased project type string, or empty string.
"""
if "PROJECT_TYPE:" not in response:
return ""
line = response.split("PROJECT_TYPE:")[1].split("\n")[0].strip()
return line.lower()
@traces(spec="019", story="US-002", reqs=["REQ-019-016", "REQ-019-017", "REQ-019-018"])
def parse_technology(response: str) -> dict[str, str]:
"""Extract TECHNOLOGY key-value pairs from LLM response.
Args:
response: Full LLM response text.
Returns:
Dictionary of technology field to value.
"""
section = _extract_section(
response,
"TECHNOLOGY:",
["SPEC_METADATA:", "USER_STORIES:", "REQUIREMENTS:"],
)
if not section:
return {}
return _parse_kv_lines(section)
def _parse_kv_lines(section: str) -> dict[str, str]:
"""Parse key: value lines from a section.
Args:
section: Multi-line text with "- key: value" lines.
Returns:
Dictionary of parsed key-value pairs.
"""
result: dict[str, str] = {}
for line in section.split("\n"):
line = line.strip().lstrip("- ")
if ":" in line:
key, val = line.split(":", 1)
result[key.strip()] = val.strip()
return result
@traces(spec="019", story="US-002", reqs=["REQ-019-019", "REQ-019-020", "REQ-019-021"])
def parse_spec_metadata(response: str) -> dict[str, str]:
"""Extract SPEC_METADATA key-value pairs from LLM response.
Args:
response: Full LLM response text.
Returns:
Dictionary of spec metadata fields.
"""
section = _extract_section(
response,
"SPEC_METADATA:",
["USER_STORIES:", "REQUIREMENTS:", "NON_FUNCTIONAL:"],
)
if not section:
return {}
return _parse_kv_lines(section)
@traces(spec="019", story="US-002", reqs=["REQ-019-022", "REQ-019-023", "REQ-019-024"])
def parse_user_stories(
response: str,
) -> list[dict[str, Any]]:
"""Extract USER_STORIES from LLM response.
Parses story ID/title, role/want/benefit fields, and
acceptance criteria lists.
Args:
response: Full LLM response text.
Returns:
List of story dicts with id, title, as_a, i_want,
so_that, and acceptance_criteria keys.
"""
section = _extract_section(
response,
"USER_STORIES:",
["REQUIREMENTS:", "NON_FUNCTIONAL:"],
)
if not section:
return []
return _parse_stories_from_section(section)
@traces(spec="019", story="US-002", reqs=["REQ-019-022", "REQ-019-023", "REQ-019-024"])
def _parse_stories_from_section(
section: str,
) -> list[dict[str, Any]]:
"""Parse individual stories from a USER_STORIES section.
Args:
section: Text content of the USER_STORIES section.
Returns:
List of parsed story dictionaries.
"""
stories: list[dict[str, Any]] = []
current: dict[str, Any] | None = None
in_criteria = False
for line in section.split("\n"):
stripped = line.strip()
if not stripped:
continue
current, in_criteria = _process_story_line(
stripped, current, in_criteria, stories
)
if current:
stories.append(current)
return stories
@traces(spec="019", story="US-002", reqs=["REQ-019-022", "REQ-019-023"])
def _process_story_line(
stripped: str,
current: dict[str, Any] | None,
in_criteria: bool,
stories: list[dict[str, Any]],
) -> tuple[dict[str, Any] | None, bool]:
"""Process a single line within the USER_STORIES section.
Args:
stripped: Stripped line text.
current: Currently-being-built story dict, or None.
in_criteria: Whether we are inside acceptance criteria.
stories: Accumulator list for completed stories.
Returns:
Tuple of (current_story, in_criteria) after processing.
"""
us_match = US_PATTERN.match(stripped)
if us_match:
if current:
stories.append(current)
new_story: dict[str, Any] = {
"id": us_match.group(1),
"title": us_match.group(2),
"as_a": "",
"i_want": "",
"so_that": "",
"acceptance_criteria": [],
}
return new_story, False
if current is None:
return current, in_criteria
lower = stripped.lower()
if lower.startswith("as a "):
current["as_a"] = stripped[5:]
return current, False
if lower.startswith("i want "):
current["i_want"] = stripped[7:]
return current, False
if lower.startswith("so that "):
current["so_that"] = stripped[8:]
return current, False
if "acceptance criteria" in lower:
return current, True
if in_criteria and stripped.startswith("- "):
current["acceptance_criteria"].append(stripped[2:])
return current, in_criteria
@traces(spec="019", story="US-002", reqs=["REQ-019-025", "REQ-019-026"])
def parse_requirements(
response: str,
) -> list[dict[str, Any]]:
"""Extract REQUIREMENTS from LLM response.
Args:
response: Full LLM response text.
Returns:
List of requirement dicts with id, description,
story_id, priority, and category keys.
"""
if "REQUIREMENTS:" not in response:
return []
section = response.split("REQUIREMENTS:")[1]
section = _trim_requirements_section(section)
return _extract_req_lines(section)
@traces(spec="019", story="US-002", reqs=["REQ-019-025"])
def _trim_requirements_section(section: str) -> str:
"""Trim the requirements section at the next section boundary.
Args:
section: Raw text after "REQUIREMENTS:".
Returns:
Trimmed section text.
"""
end_markers = ["NON_FUNCTIONAL:", "NFR-"]
for marker in end_markers:
if marker in section:
idx = section.index(marker)
if marker == "NFR-":
nl = section.rfind("\n", 0, idx)
if nl >= 0:
section = section[:nl]
else:
section = section.split(marker)[0]
break
return section
@traces(spec="019", story="US-002", reqs=["REQ-019-025", "REQ-019-026"])
def _extract_req_lines(
section: str,
) -> list[dict[str, Any]]:
"""Extract requirement entries from section lines.
Args:
section: Trimmed requirements section text.
Returns:
List of requirement dictionaries.
"""
requirements: list[dict[str, Any]] = []
for line in section.strip().split("\n"):
stripped = line.strip()
match = REQ_PATTERN.match(stripped)
if not match:
continue
req_id = match.group(1)
description = match.group(2)
us_match = REQ_STORY_PATTERN.search(req_id)
story_id = f"US-{us_match.group(1)}" if us_match else ""
requirements.append(
{
"id": req_id,
"description": description,
"story_id": story_id,
"priority": "NORMAL",
"category": "",
}
)
return requirements
@traces(spec="019", story="US-002", reqs=["REQ-019-027", "REQ-019-028", "REQ-019-029"])
def parse_nfrs(response: str) -> list[dict[str, str]]:
"""Extract NON_FUNCTIONAL requirements from LLM response.
Args:
response: Full LLM response text.
Returns:
List of NFR dicts with id, description, and category.
"""
if "NON_FUNCTIONAL:" not in response:
return []
section = response.split("NON_FUNCTIONAL:")[1]
return _extract_nfr_lines(section)
@traces(spec="019", story="US-002", reqs=["REQ-019-027", "REQ-019-028", "REQ-019-029"])
def _extract_nfr_lines(
section: str,
) -> list[dict[str, str]]:
"""Extract NFR entries from section lines.
Args:
section: Raw text after "NON_FUNCTIONAL:".
Returns:
List of NFR dictionaries.
"""
nfrs: list[dict[str, str]] = []
for line in section.strip().split("\n"):
stripped = line.strip()
match = NFR_PATTERN.match(stripped)
if not match:
continue
nfr_id = match.group(1)
rest = match.group(2)
category = ""
cat_match = re.search(r"\(category:\s*(\w+)\)", rest)
if cat_match:
category = cat_match.group(1)
rest = rest[: cat_match.start()].strip()
nfrs.append(
{
"id": nfr_id,
"description": rest,
"category": category,
}
)
return nfrs