agent_search_gateway.llm_search_parser
Restricted Markdown parser for LLM search results.
1"""Restricted Markdown parser for LLM search results.""" 2 3from .errors import InputFailure, ParserFailure 4from .models import SearchRecord 5from .url_normalization import normalize_url 6 7_RESULT_HEADING = "## Result" 8_URL_PREFIX = "URL:" 9_ABSTRACT_PREFIX = "Abstract:" 10 11 12def parse_search_markdown(markdown: str) -> list[SearchRecord]: 13 lines = markdown.splitlines() 14 headings = [index for index, line in enumerate(lines) if line.strip() == _RESULT_HEADING] 15 if not headings: 16 raise ParserFailure("LLM search response contained no Result blocks") 17 18 records: list[SearchRecord] = [] 19 for position, start in enumerate(headings): 20 end = headings[position + 1] if position + 1 < len(headings) else len(lines) 21 record = _parse_block(lines[start + 1 : end]) 22 if record is not None: 23 records.append(record) 24 return records 25 26 27def _parse_block(lines: list[str]) -> SearchRecord | None: 28 urls = [ 29 line.strip()[len(_URL_PREFIX) :].strip() 30 for line in lines 31 if line.strip().startswith(_URL_PREFIX) 32 ] 33 abstracts = [ 34 line.strip()[len(_ABSTRACT_PREFIX) :].strip() 35 for line in lines 36 if line.strip().startswith(_ABSTRACT_PREFIX) 37 ] 38 if len(urls) != 1 or len(abstracts) != 1: 39 raise ParserFailure("Result block must contain exactly one URL and one Abstract") 40 if not urls[0]: 41 raise ParserFailure("Result block URL is empty") 42 if not abstracts[0]: 43 return None 44 try: 45 normalized = normalize_url(urls[0]) 46 except InputFailure as exc: 47 raise ParserFailure("Result block URL is invalid") from exc 48 return SearchRecord(url=normalized, abstract=abstracts[0])
13def parse_search_markdown(markdown: str) -> list[SearchRecord]: 14 lines = markdown.splitlines() 15 headings = [index for index, line in enumerate(lines) if line.strip() == _RESULT_HEADING] 16 if not headings: 17 raise ParserFailure("LLM search response contained no Result blocks") 18 19 records: list[SearchRecord] = [] 20 for position, start in enumerate(headings): 21 end = headings[position + 1] if position + 1 < len(headings) else len(lines) 22 record = _parse_block(lines[start + 1 : end]) 23 if record is not None: 24 records.append(record) 25 return records