diff --git a/libs/core/langchain_core/output_parsers/xml.py b/libs/core/langchain_core/output_parsers/xml.py index 704c67b8e7..890a4d7c71 100644 --- a/libs/core/langchain_core/output_parsers/xml.py +++ b/libs/core/langchain_core/output_parsers/xml.py @@ -155,7 +155,7 @@ class XMLOutputParser(BaseTransformOutputParser): def get_format_instructions(self) -> str: return XML_FORMAT_INSTRUCTIONS.format(tags=self.tags) - def parse(self, text: str) -> Dict[str, List[Any]]: + def parse(self, text: str) -> Dict[str, Union[str, List[Any]]]: # Try to find XML string within triple backticks # Imports are temporarily placed here to avoid issue with caching on CI # likely if you're reading this you can move them to the top of the file @@ -207,9 +207,13 @@ class XMLOutputParser(BaseTransformOutputParser): yield output streaming_parser.close() - def _root_to_dict(self, root: ET.Element) -> Dict[str, List[Any]]: + def _root_to_dict(self, root: ET.Element) -> Dict[str, Union[str, List[Any]]]: """Converts xml tree to python dictionary.""" - result: Dict[str, List[Any]] = {root.tag: []} + if root.text and bool(re.search(r"\S", root.text)): + # If root text contains any non-whitespace character it + # returns {root.tag: root.text} + return {root.tag: root.text} + result: Dict = {root.tag: []} for child in root: if len(child) == 0: result[root.tag].append({child.tag: child.text}) diff --git a/libs/core/tests/unit_tests/output_parsers/test_xml_parser.py b/libs/core/tests/unit_tests/output_parsers/test_xml_parser.py index c30d09ea1b..c71ed7f992 100644 --- a/libs/core/tests/unit_tests/output_parsers/test_xml_parser.py +++ b/libs/core/tests/unit_tests/output_parsers/test_xml_parser.py @@ -45,8 +45,8 @@ DEF_RESULT_EXPECTED = { async def _test_parser(parser: XMLOutputParser, content: str) -> None: """Test parser.""" - xml_content = parser.parse(content) - assert DEF_RESULT_EXPECTED == xml_content + assert parser.parse(content) == DEF_RESULT_EXPECTED + assert await parser.aparse(content) == DEF_RESULT_EXPECTED assert list(parser.transform(iter(content))) == [ {"foo": [{"bar": [{"baz": None}]}]}, @@ -54,10 +54,6 @@ async def _test_parser(parser: XMLOutputParser, content: str) -> None: {"foo": [{"baz": "tag"}]}, ] - async def _as_iter(iterable: Iterable[str]) -> AsyncIterator[str]: - for item in iterable: - yield item - chunks = [chunk async for chunk in parser.atransform(_as_iter(content))] assert list(chunks) == [ @@ -67,6 +63,30 @@ async def _test_parser(parser: XMLOutputParser, content: str) -> None: ] +ROOT_LEVEL_ONLY = """ +Text of the body. +""" + +ROOT_LEVEL_ONLY_EXPECTED = {"body": "Text of the body."} + + +async def _as_iter(iterable: Iterable[str]) -> AsyncIterator[str]: + for item in iterable: + yield item + + +async def test_root_only_xml_output_parser() -> None: + """Test XMLOutputParser when xml only contains the root level tag""" + xml_parser = XMLOutputParser(parser="xml") + assert xml_parser.parse(ROOT_LEVEL_ONLY) == {"body": "Text of the body."} + assert await xml_parser.aparse(ROOT_LEVEL_ONLY) == {"body": "Text of the body."} + assert list(xml_parser.transform(iter(ROOT_LEVEL_ONLY))) == [ + {"body": "Text of the body."} + ] + chunks = [chunk async for chunk in xml_parser.atransform(_as_iter(ROOT_LEVEL_ONLY))] + assert chunks == [{"body": "Text of the body."}] + + @pytest.mark.parametrize( "content", [