Skip to content

Commit 8c35e5f

Browse files
committed
Merge branch 'main' of github.com:comfuture/function-schema
2 parents 9fc4e20 + 734b565 commit 8c35e5f

File tree

4 files changed

+121
-18
lines changed

4 files changed

+121
-18
lines changed

README.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
This is a small utility to generate JSON schemas for python functions.
88
With power of type annotations, it is possible to generate a schema for a function without describing it twice.
99

10-
At this moment, extracting schema from a function is only useful for [OpenAI API function-call](https://platform.openai.com/docs/guides/gpt/function-calling) feature.
11-
But it can be used for other purposes for example to generate documentation in the future.
10+
At this moment, extracting schema from a function is useful for [OpenAI Assistant Toll Calling](https://platform.openai.com/docs/assistants/tools/function-calling), [OpenAI API function-call](https://platform.openai.com/docs/guides/function-calling), and [Anthropic Claude Toll calling](https://docs.anthropic.com/claude/docs/tool-use) feature.
11+
And it can be used for other purposes for example to generate documentation in the future.
1212

1313
## Installation
1414

@@ -79,20 +79,64 @@ Will output:
7979
}
8080
```
8181

82+
for claude, you should pass 2nd argument as SchemaFormat.claude or `claude`:
83+
84+
```python
85+
from function_schema import get_function_schema
86+
87+
schema = get_function_schema(get_weather, "claude")
88+
```
89+
90+
Please refer to the [Claude tool use](https://docs.anthropic.com/claude/docs/tool-use) documentation for more information.
91+
8292
You can use this schema to make a function call in OpenAI API:
8393
```python
8494
import openai
8595
openai.api_key = "sk-..."
8696

87-
result = openai.ChatCompletion.create(
97+
# Create an assistant with the function
98+
assistant = client.beta.assistants.create(
99+
instructions="You are a weather bot. Use the provided functions to answer questions.",
100+
model="gpt-4-turbo-preview",
101+
tools=[{
102+
"type": "function",
103+
"function": get_function_schema(get_weather),
104+
}]
105+
)
106+
107+
run = client.beta.messages.create(
108+
assistant_id=assistant.id,
109+
messages=[
110+
{"role": "user", "content": "What's the weather like in Seoul?"}
111+
]
112+
)
113+
114+
# or with chat completion
115+
116+
result = openai.chat.completion.create(
88117
model="gpt-3.5-turbo",
89118
messages=[
90-
{"role": "user", "content": "What's the weather like in Seoul?"}
91-
],
92-
functions=[
93-
get_function_schema(get_weather)
119+
{"role": "user", "content": "What's the weather like in Seoul?"}
94120
],
95-
function_call="auto",
121+
tools=[get_function_schema(get_weather)],
122+
tool_call="auto",
123+
)
124+
```
125+
126+
In claude api,
127+
128+
```python
129+
import anthropic
130+
131+
client = anthropic.Client()
132+
133+
response = client.beta.tools.messages.create(
134+
model="claude-3-opus-20240229",
135+
max_tokens=4096,
136+
tools=[get_function_schema(get_weather, "claude")],
137+
messages=[
138+
{"role": "user", "content": "What's the weather like in Seoul?"}
139+
]
96140
)
97141
```
98142

function_schema/cli.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77

88
def print_usage():
9-
print("Usage: function_schema <file_path> <function_name>")
9+
print("Usage: function_schema <file_path> <function_name> [format='openai']")
10+
print("Example: function_schema ./tests/test_schema.py test_simple_function")
11+
print("Example: function_schema ./tests/test_schema.py test_simple_function claude")
1012

1113

1214
def main():
@@ -18,14 +20,25 @@ def main():
1820
spec.loader.exec_module(module)
1921
members = inspect.getmembers(module)
2022

23+
try:
24+
format = sys.argv[3]
25+
except IndexError:
26+
format = "openai"
27+
if format not in ["openai", "claude"]:
28+
print(
29+
"Invalid format. Use 'openai' or 'claude'. using 'openai'",
30+
file=sys.stderr,
31+
)
32+
2133
for name, func in members:
22-
if name == sys.argv[2]:
23-
print(json.dumps(get_function_schema(func), indent=2))
34+
if name >= sys.argv[2]:
35+
print(json.dumps(get_function_schema(func, format), indent=2))
2436
sys.exit(0)
2537
print(f"Function {sys.argv[2]} not found in {file_path}")
2638
except IndexError:
2739
print_usage()
2840
sys.exit(1)
2941

42+
3043
if __name__ == "__main__":
3144
main()

function_schema/core.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@
33
import inspect
44

55

6+
class SchemaFormat(str, enum.Enum):
7+
openai = "openai"
8+
claude = "claude"
9+
10+
611
def get_function_schema(
7-
func: typing.Annotated[typing.Callable, "The function to get the schema for"]
12+
func: typing.Annotated[typing.Callable, "The function to get the schema for"],
13+
format: typing.Annotated[
14+
typing.Optional[str], SchemaFormat, "The format of the schema to return"
15+
] = "openai",
816
) -> typing.Annotated[dict[str, typing.Any], "The JSON schema for the given function"]:
917
"""
1018
Returns a JSON schema for the given function.
@@ -103,15 +111,18 @@ def get_function_schema(
103111

104112
if not isinstance(None, T) and default_value is inspect._empty:
105113
schema["required"].append(name)
114+
115+
parms_key = "input_schema" if format == "claude" else "parameters"
116+
106117
return {
107118
"name": func.__name__,
108119
"description": inspect.getdoc(func),
109-
"parameters": schema,
120+
parms_key: schema,
110121
}
111122

112123

113124
def guess_type(
114-
T: typing.Annotated[type, "The type to guess the JSON schema type for"]
125+
T: typing.Annotated[type, "The type to guess the JSON schema type for"],
115126
) -> typing.Annotated[
116127
typing.Union[str, list[str]], "str | list of str that representing JSON schema type"
117128
]:
@@ -123,11 +134,11 @@ def guess_type(
123134
_types = []
124135
for union_type in union_types:
125136
_types.append(guess_type(union_type))
126-
_types = [t for t in _types if t is not None] # exclude None
137+
_types = [t for t in _types if t is not None] # exclude None
127138

128139
# number contains integer in JSON schema
129-
if 'number' in _types and 'integer' in _types:
130-
_types.remove('integer')
140+
if "number" in _types and "integer" in _types:
141+
_types.remove("integer")
131142

132143
if len(_types) == 1:
133144
return _types[0]
@@ -136,7 +147,7 @@ def guess_type(
136147
if not isinstance(T, type):
137148
return
138149

139-
if T.__name__ == 'NoneType':
150+
if T.__name__ == "NoneType":
140151
return
141152

142153
if issubclass(T, str):

test/test_schema_type.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Annotated
2+
import enum
3+
from function_schema.core import get_function_schema
4+
5+
6+
def test_function_schema_type():
7+
"""Test a function schema for claude"""
8+
9+
def func1(
10+
animal: Annotated[
11+
str,
12+
"The animal you want to pet",
13+
enum.Enum("Animal", "Cat Dog"),
14+
],
15+
):
16+
"""My function"""
17+
...
18+
19+
schema = get_function_schema(func1)
20+
assert (
21+
schema["parameters"]["properties"]["animal"]["type"] == "string"
22+
), "parameter animal should be a string"
23+
assert schema["parameters"]["properties"]["animal"]["enum"] == [
24+
"Cat",
25+
"Dog",
26+
], "parameter animal should have an enum"
27+
28+
schema2 = get_function_schema(func1, format="claude")
29+
assert (
30+
schema2["input_schema"]["properties"]["animal"]["type"] == "string"
31+
), "parameter animal should be a string"
32+
assert schema2["input_schema"]["properties"]["animal"]["enum"] == [
33+
"Cat",
34+
"Dog",
35+
], "parameter animal should have an enum"

0 commit comments

Comments
 (0)