jxnl / instructor

structured outputs for llms
https://python.useinstructor.com/
MIT License
6.61k stars 522 forks source link

Anthropic Claude 3 function calling: List[Literal] and List[Literal] | None fail #532

Open timothyasp opened 3 months ago

timothyasp commented 3 months ago

What Model are you using?

Describe the bug

Here's some sample code that fails:

languages = Literal["python", "javascript", "typescript", "java", "haskell", "php"]
class ListLiteral(BaseModel):
    languages: List[languages]

resp = create(
    model="claude-3-haiku-20240307",
    max_tokens=256,
    max_retries=0,
    messages=[
        {
            "role": "user",
            "content": "What are your top 3 favorite programming languages?",
        }
    ],
    response_model=ListLiteral,
)  # type: ignore

assert isinstance(resp, ListLiteral)
assert len(resp.languages) == 3

with error:

self = <xml.dom.expatbuilder.ExpatBuilderNS object at 0x1115f9cf0>
string = '<tool_description><tool_name>ListLiteral</tool_name><description>This is the function that must be used to construct ...AMETER_VALUE</$PARAMETER_NAME> tags for each item in the list. XML tags should only contain the name of the parameter.'

    def parseString(self, string):
        """Parse a document from a string, returning the document node."""
        parser = self.getParser()
        try:
>           parser.Parse(string, True)
E           xml.parsers.expat.ExpatError: junk after document element: line 2, column 0

/usr/local/Cellar/python@3.10/3.10.11/Frameworks/Python.framework/Versions/3.10/lib/python3.10/xml/dom/expatbuilder.py:223: ExpatError

And a different error when allowing anyOf with List[Literal] | None Code:

languages = Literal["python", "javascript", "typescript", "java", "haskell", "php"]

def test_anthropic_list_literal_or_none():
    class ListLiteral(BaseModel):
        languages: List[languages] | None

    resp = create(
        model="claude-3-haiku-20240307",
        max_tokens=256,
        max_retries=0,
        messages=[
            {
                "role": "user",
                "content": "What are your top 3 favorite programming languages?",
            }
        ],
        response_model=ListLiteral,
    )  # type: ignore

    assert isinstance(resp, ListLiteral)
    assert len(resp.languages) == 3

With error:

root = <Element 'parameters' at 0x10f747d80>
model_dict = {'properties': {'languages': {'anyOf': [{'items': {'enum': [...], 'type': 'string'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Languages'}}, 'required': ['languages'], 'title': 'ListLiteral', 'type': 'object'}
references = {}

    def _add_params(
        root: ET.Element, model_dict: Dict[str, Any], references: Dict[str, Any]
    ) -> bool:  # Return value indiciates if we ever came across a param with type List
        # TODO: handling of nested params with the same name
        properties = model_dict.get("properties", {})
        list_found = False
        nested_list_found = False

        for field_name, details in properties.items():
            parameter = ET.SubElement(root, "parameter")
            name = ET.SubElement(parameter, "name")
            name.text = field_name
            type_element = ET.SubElement(parameter, "type")

            # Get type
            if "anyOf" in details:  # Case where there can be multiple types
                # supports:
                # case 1: List type (example json: {'anyOf': [{'items': {'$ref': '#/$defs/PartialUser'}, 'type': 'array'}, {'type': 'null'}], 'default': None, 'title': 'Users'})
                # case 2: nested model (example json: {'anyOf': [{'$ref': '#/$defs/PartialDate'}, {'type': 'null'}], 'default': {}})
                field_type = " or ".join(
                    [
                        d["type"]
                        if "type" in d
                        else (d["$ref"] if "$ref" in d else "unknown")
                        for d in details["anyOf"]
                    ]
                )
            else:
                field_type = details.get(
                    "type", "unknown"
                )  # Might be better to fail here if there is no type since pydantic models require types

            if "array" in field_type and "items" not in details:
>               raise ValueError("Invalid array item.")
E               ValueError: Invalid array item.

../../instructor/anthropic_utils.py:80: ValueError

Expected behavior This works well with OpenAI with instructor, so I'd expect the same results. It looks like it's an edge case in the json_to_xml function. I'll work on a fix, but opening this just in case someone already has a fix or idea how best to fix it.

jxnl commented 3 months ago

ty!

BenDLH commented 3 months ago

Hitting this issue too 👍

virattt commented 3 months ago

Same here, getting this issue too - unable to use claude (but openai works fine!)

Cruppelt commented 3 months ago

Very similar thing needs to happen as https://github.com/jxnl/instructor/pull/524. I've added this to my test cases but the current structure needs to be revamped.

Added support for Literal https://github.com/jxnl/instructor/pull/534 here. Union (|) not supported yet but getting there.