-
-
Notifications
You must be signed in to change notification settings - Fork 699
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support anthropic (non-streaming only) (#512)
Co-authored-by: Jason Liu <[email protected]>
- Loading branch information
Showing
14 changed files
with
889 additions
and
195 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
--- | ||
draft: False | ||
date: 2024-03-20 | ||
authors: | ||
- jxnl | ||
--- | ||
|
||
# Announcing Anthropic Support | ||
|
||
A special shoutout to [Shreya](https://twitter.com/shreyaw_) for her contributions to the anthropic support. As of now, all features are operational with the exception of streaming support. | ||
|
||
For those eager to experiment, simply patch the client with `ANTHROPIC_TOOLS`, which will enable you to leverage the `anthropic` client for making requests. | ||
|
||
``` | ||
pip install instructor[anthropic] | ||
``` | ||
|
||
```python | ||
from pydantic import BaseModel | ||
from typing import List | ||
import anthropic | ||
import instructor | ||
|
||
# Patching the Anthropics client with the instructor for enhanced capabilities | ||
anthropic_client = instructor.patch( | ||
create=anthropic.Anthropic().messages.create, | ||
mode=instructor.Mode.ANTHROPIC_TOOLS | ||
) | ||
|
||
class Properties(BaseModel): | ||
name: str | ||
value: str | ||
|
||
class User(BaseModel): | ||
name: str | ||
age: int | ||
properties: List[Properties] | ||
|
||
user_response = anthropic_client( | ||
model="claude-3-haiku-20240307", | ||
max_tokens=1024, | ||
max_retries=0, | ||
messages=[ | ||
{ | ||
"role": "user", | ||
"content": "Create a user for a model with a name, age, and properties.", | ||
} | ||
], | ||
response_model=User, | ||
) # type: ignore | ||
|
||
print(user_response.model_dump_json(indent=2)) | ||
""" | ||
{ | ||
"name": "John", | ||
"age": 25, | ||
"properties": [ | ||
{ | ||
"key": "favorite_color", | ||
"value": "blue" | ||
} | ||
] | ||
} | ||
``` | ||
We're encountering challenges with deeply nested types and eagerly invite the community to test, provide feedback, and suggest necessary improvements as we enhance the anthropic client's support. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from pydantic import BaseModel | ||
from typing import List | ||
import anthropic | ||
import instructor | ||
|
||
# Patching the Anthropics client with the instructor for enhanced capabilities | ||
anthropic_client = instructor.patch( | ||
create=anthropic.Anthropic().messages.create, mode=instructor.Mode.ANTHROPIC_TOOLS | ||
) | ||
|
||
|
||
class Properties(BaseModel): | ||
key: str | ||
value: str | ||
|
||
|
||
class User(BaseModel): | ||
name: str | ||
age: int | ||
properties: List[Properties] | ||
|
||
|
||
user_response = anthropic_client( | ||
model="claude-3-haiku-20240307", | ||
max_tokens=1024, | ||
max_retries=0, | ||
messages=[ | ||
{ | ||
"role": "user", | ||
"content": "Create a user for a model with a name, age, and properties.", | ||
} | ||
], | ||
response_model=User, | ||
) # type: ignore | ||
|
||
print(user_response.model_dump_json(indent=2)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import re | ||
import xmltodict | ||
from pydantic import BaseModel | ||
import xml.etree.ElementTree as ET | ||
from typing import Type, Any, Dict, TypeVar | ||
|
||
T = TypeVar("T", bound=BaseModel) | ||
|
||
|
||
def json_to_xml(model: Type[BaseModel]) -> str: | ||
"""Takes a Pydantic model and returns XML format for Anthropic function calling.""" | ||
model_dict = model.model_json_schema() | ||
|
||
root = ET.Element("tool_description") | ||
tool_name = ET.SubElement(root, "tool_name") | ||
tool_name.text = model_dict.get("title", "Unknown") | ||
description = ET.SubElement(root, "description") | ||
description.text = ( | ||
"This is the function that must be used to construct the response." | ||
) | ||
parameters = ET.SubElement(root, "parameters") | ||
references = model_dict.get("$defs", {}) | ||
list_type_found = _add_params(parameters, model_dict, references) | ||
|
||
if list_type_found: # Need to append to system prompt for List type handling | ||
return ( | ||
ET.tostring(root, encoding="unicode") | ||
+ "\nFor any List[] types, include multiple <$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME> tags for each item in the list. XML tags should only contain the name of the parameter." | ||
) | ||
else: | ||
return ET.tostring(root, encoding="unicode") | ||
|
||
|
||
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 | ||
|
||
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 | ||
|
||
# Adjust type if array | ||
if "array" in field_type or "List" in field_type: | ||
type_element.text = f"List[{details['title']}]" | ||
list_found = True | ||
else: | ||
type_element.text = field_type | ||
|
||
param_description = ET.SubElement(parameter, "description") | ||
param_description.text = details.get("description", "") | ||
|
||
if ( | ||
isinstance(details, dict) and "$ref" in details | ||
): # Checking if there are nested params | ||
nested_params = ET.SubElement(parameter, "parameters") | ||
list_found |= _add_params( | ||
nested_params, | ||
_resolve_reference(references, details["$ref"]), | ||
references, | ||
) | ||
elif field_type == "array": # Handling for List[] type | ||
nested_params = ET.SubElement(parameter, "parameters") | ||
list_found |= _add_params( | ||
nested_params, | ||
_resolve_reference(references, details["items"]["$ref"]), | ||
references, | ||
) | ||
elif "array" in field_type: # Handling for optional List[] type | ||
nested_params = ET.SubElement(parameter, "parameters") | ||
list_found |= _add_params( | ||
nested_params, | ||
_resolve_reference( | ||
references, details["anyOf"][0]["items"]["$ref"] | ||
), # CHANGE | ||
references, | ||
) | ||
|
||
return list_found | ||
|
||
|
||
def _resolve_reference(references: Dict[str, Any], reference: str) -> Dict[str, Any]: | ||
parts = reference.split("/")[2:] # Remove "#" and "$defs" | ||
for part in parts: | ||
references = references[part] | ||
return references | ||
|
||
|
||
def extract_xml(content: str) -> str: # Currently assumes 1 function call only | ||
"""Extracts XML content in Anthropic's schema from a string.""" | ||
pattern = r"<function_calls>.*?</function_calls>" | ||
matches = re.findall(pattern, content, re.DOTALL) | ||
return "".join(matches) | ||
|
||
|
||
def xml_to_model(model: Type[T], xml_string: str) -> T: | ||
"""Converts XML in Anthropic's schema to an instance of the provided class.""" | ||
parsed_xml = xmltodict.parse(xml_string) | ||
model_dict = parsed_xml["function_calls"]["invoke"]["parameters"] | ||
return model( | ||
**model_dict | ||
) # This sometimes fails if Anthropic's response hallucinates from the schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.