Skip to content

xml_tool_message

langroid/agent/xml_tool_message.py

XMLToolMessage

Bases: ToolMessage

Abstract class for tools formatted using XML instead of JSON.

When a subclass defines a field with the attribute verbatim=True, instructions are sent to the LLM to ensure the field's content is: - preserved as is, including whitespace, indents, quotes, newlines, etc with no escaping, and - enclosed in a CDATA section in the XML output. This is useful for LLMs sending code as part of a tool; results can be far superior compared to sending code in JSON-formatted tools, where code needs to confirm to JSON's strict rules and escaping requirements. (see test_xml_tool_message.py for an example).

extract_field_values(formatted_string) classmethod

Extracts field values from an XML-formatted string.

Parameters:

Name Type Description Default
formatted_string str

The XML-formatted string to parse.

required

Returns:

Type Description
Optional[Dict[str, Any]]

Optional[Dict[str, Any]]: A dictionary containing the extracted field values, where keys are the XML element names and values are their corresponding contents.

Optional[Dict[str, Any]]

Returns None if parsing fails or the root element is not a dictionary.

Raises:

Type Description
XMLSyntaxError

If the input string is not valid XML.

Source code in langroid/agent/xml_tool_message.py
@classmethod
def extract_field_values(cls, formatted_string: str) -> Optional[Dict[str, Any]]:
    """
    Extracts field values from an XML-formatted string.

    Args:
        formatted_string (str): The XML-formatted string to parse.

    Returns:
        Optional[Dict[str, Any]]: A dictionary containing the extracted field
            values, where keys are the XML element names and values are their
            corresponding contents.
        Returns None if parsing fails or the root element is not a dictionary.

    Raises:
        etree.XMLSyntaxError: If the input string is not valid XML.
    """
    parser = etree.XMLParser(strip_cdata=False)
    root = etree.fromstring(formatted_string.encode("utf-8"), parser=parser)

    def parse_element(element: etree._Element) -> Any:
        # Skip elements starting with underscore
        if element.tag.startswith("_"):
            return {}

        field_info = cls.__fields__.get(element.tag)
        is_verbatim = field_info and field_info.field_info.extra.get(
            "verbatim", False
        )

        if is_verbatim:
            # For code elements, preserve the content as is, including whitespace
            content = element.text if element.text else ""
            # Strip leading and trailing triple backticks if present,
            # accounting for whitespace
            return (
                content.strip().removeprefix("```").removesuffix("```").strip()
                if content.strip().startswith("```")
                and content.strip().endswith("```")
                else content
            )
        elif len(element) == 0:
            # For non-code leaf elements, strip whitespace
            return element.text.strip() if element.text else ""
        else:
            # For branch elements, handle potential lists or nested structures
            children = [parse_element(child) for child in element]
            if all(child.tag == element[0].tag for child in element):
                # If all children have the same tag, treat as a list
                return children
            else:
                # Otherwise, treat as a dictionary
                result = {child.tag: parse_element(child) for child in element}
                # Check if this corresponds to a nested Pydantic model
                if field_info and issubclass(field_info.type_, BaseModel):
                    return field_info.type_(**result)
                return result

    result = parse_element(root)
    if not isinstance(result, dict):
        return None
    # Filter out empty dictionaries from skipped underscore fields
    return {k: v for k, v in result.items() if v != {}}

parse(formatted_string) classmethod

Parses the XML-formatted string and returns an instance of the class.

Parameters:

Name Type Description Default
formatted_string str

The XML-formatted string to parse.

required

Returns:

Type Description
Optional[XMLToolMessage]

Optional["XMLToolMessage"]: An instance of the class if parsing succeeds, None otherwise.

Source code in langroid/agent/xml_tool_message.py
@classmethod
def parse(cls, formatted_string: str) -> Optional["XMLToolMessage"]:
    """
    Parses the XML-formatted string and returns an instance of the class.

    Args:
        formatted_string (str): The XML-formatted string to parse.

    Returns:
        Optional["XMLToolMessage"]: An instance of the class if parsing succeeds,
            None otherwise.
    """
    try:
        parsed_data = cls.extract_field_values(formatted_string)
        if parsed_data is None:
            return None

        # Use Pydantic's parse_obj to create and validate the instance
        return cls.parse_obj(parsed_data)
    except Exception as e:
        from langroid.exceptions import XMLException

        raise XMLException(f"Error parsing XML: {str(e)}")

format_example()

Format the current instance as an XML example.

Returns:

Name Type Description
str str

A string representation of the current instance in XML format.

Raises:

Type Description
ValueError

If the result from etree.tostring is not a string.

Source code in langroid/agent/xml_tool_message.py
def format_example(self) -> str:
    """
    Format the current instance as an XML example.

    Returns:
        str: A string representation of the current instance in XML format.

    Raises:
        ValueError: If the result from etree.tostring is not a string.
    """

    def create_element(
        parent: etree._Element, name: str, value: Any, path: str = ""
    ) -> None:
        if value is None:
            return

        elem = etree.SubElement(parent, name)
        current_path = f"{path}.{name}" if path else name

        if isinstance(value, list):
            for item in value:
                create_element(elem, "item", item, current_path)
        elif isinstance(value, dict):
            for k, v in value.items():
                create_element(elem, k, v, current_path)
        elif isinstance(value, BaseModel):
            # Handle nested Pydantic models
            for field_name, field_value in value.dict().items():
                create_element(elem, field_name, field_value, current_path)
        else:
            if current_path in self.__class__.find_verbatim_fields():
                elem.text = etree.CDATA(str(value))
            else:
                elem.text = str(value)

    root = etree.Element(self.Config.root_element)
    exclude_fields = self.Config.schema_extra.get("exclude", set())
    for name, value in self.dict().items():
        if name not in exclude_fields:
            create_element(root, name, value)

    result = etree.tostring(root, encoding="unicode", pretty_print=True)
    if not isinstance(result, str):
        raise ValueError("Unexpected non-string result from etree.tostring")
    return result

find_candidates(text) classmethod

Find and extract all potential XML tool messages from the given text.

This method searches for XML-like structures in the input text that match the expected format of the tool message. It looks for opening and closing tags that correspond to the root element defined in the XMLToolMessage class, which is by default .

Parameters:

Name Type Description Default
text str

The input text to search for XML tool messages.

required

Returns:

Type Description
List[str]

List[str]: A list of strings, each representing a potential XML tool message. These candidates include both the opening and closing tags, so that they are individually parseable.

Note

This method ensures that all candidates are valid and parseable by inserting a closing tag if it's missing for the last candidate.

Source code in langroid/agent/xml_tool_message.py
@classmethod
def find_candidates(cls, text: str) -> List[str]:
    """
    Find and extract all potential XML tool messages from the given text.

    This method searches for XML-like structures in the input text that match
    the expected format of the tool message. It looks for opening and closing
    tags that correspond to the root element defined in the XMLToolMessage class,
    which is by default <tool>.

    Args:
        text (str): The input text to search for XML tool messages.

    Returns:
        List[str]: A list of strings, each representing a potential XML tool
                   message.
                   These candidates include both the opening and
                   closing tags, so that they are individually parseable.

    Note:
        This method ensures that all candidates are valid and parseable by
        inserting a closing tag if it's missing for the last candidate.
    """
    root_tag = cls.Config.root_element
    opening_tag = f"<{root_tag}>"
    closing_tag = f"</{root_tag}>"

    candidates = []
    start = 0
    while True:
        start = text.find(opening_tag, start)
        if start == -1:
            break
        end = text.find(closing_tag, start)
        if end == -1:
            # For the last candidate, insert the closing tag if it's missing
            candidate = text[start:]
            if not candidate.strip().endswith(closing_tag):
                candidate += closing_tag
            candidates.append(candidate)
            break
        candidates.append(text[start : end + len(closing_tag)])
        start = end + len(closing_tag)

    return candidates