tool_tailor 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +94 -92
- data/lib/tool_tailor/version.rb +1 -1
- data/lib/tool_tailor.rb +76 -85
- data/lib/yard_custom_tags.rb +46 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 908a6461c07d131d14e6bd8be1a3fa6fa578e4f0261110d084238dbc657e8f33
|
4
|
+
data.tar.gz: 65541821e6c287d82e1bffd57e04da0c541654ea3e8edc84621cc0fbfbe11604
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 798fb99b7ac1fba192eec08684ac51d1f39f11e7bb6ff79757987a2aaabba1bb749ed865278dc37639e08ef6cd04b11de68f461ebbb0ee09433db3cb3d090f97
|
7
|
+
data.tar.gz: 4a2aeda57a2757776a05845d29fb1a09c479d2352820d5be0930c6b7b75c01c59f4712cd9367cda3e758e5c1d3c6d00385080b1768be256a1fde20ea73eb871d
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ToolTailor
|
2
2
|
|
3
|
-
ToolTailor is a Ruby gem that converts methods to OpenAI JSON schemas for use with tools, making it easier to integrate with OpenAI's API.
|
3
|
+
ToolTailor is a Ruby gem that converts methods and classes to OpenAI JSON schemas for use with tools, making it easier to integrate with OpenAI's API.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -20,116 +20,116 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
ToolTailor can convert both methods and classes to JSON schemas:
|
24
|
+
|
25
|
+
### Converting Methods
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class WeatherService
|
25
29
|
# Get the current weather in a given location.
|
26
30
|
#
|
27
31
|
# @param location [String] The city and state, e.g., San Francisco, CA.
|
28
|
-
# @param unit [String] The unit
|
29
|
-
|
32
|
+
# @param unit [String] The temperature unit to use. Infer this from the user's location.
|
33
|
+
# @values unit ["Celsius", "Fahrenheit"]
|
34
|
+
def get_current_temperature(location:, unit:)
|
30
35
|
# Function implementation goes here
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
34
|
-
#
|
35
|
-
ToolTailor.convert(
|
36
|
-
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
#
|
68
|
-
|
69
|
-
# "description" => "Get the current weather in a given location.",
|
70
|
-
# "parameters" => {
|
71
|
-
# "type" => "object",
|
72
|
-
# "properties" => {
|
73
|
-
# "location" => {
|
74
|
-
# "type" => "string",
|
75
|
-
# "description" => "The city and state, e.g., San Francisco, CA."
|
76
|
-
# },
|
77
|
-
# "unit" => {
|
78
|
-
# "type" => "string",
|
79
|
-
# "description" => "The unit of temperature, either 'celsius' or 'fahrenheit'."
|
80
|
-
# },
|
81
|
-
# "api_key" => {
|
82
|
-
# "type" => "number",
|
83
|
-
# "description" => "The API key for the weather service."
|
84
|
-
# }
|
85
|
-
# },
|
86
|
-
# "required" => ["location", "unit", "api_key"]
|
87
|
-
# }
|
88
|
-
# }
|
89
|
-
# }
|
39
|
+
# Convert an instance method
|
40
|
+
schema = ToolTailor.convert(WeatherService.instance_method(:get_current_temperature))
|
41
|
+
|
42
|
+
# Using to_json_schema on an unbound method
|
43
|
+
schema = WeatherService.instance_method(:get_current_temperature).to_json_schema
|
44
|
+
|
45
|
+
# Using to_json_schema on a bound method
|
46
|
+
weather_service = WeatherService.new
|
47
|
+
schema = weather_service.method(:get_current_temperature).to_json_schema
|
48
|
+
```
|
49
|
+
|
50
|
+
### Converting Classes
|
51
|
+
|
52
|
+
When passing a class, ToolTailor assumes you want to use the `new` method and generates the schema based on the `initialize` method:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class User
|
56
|
+
# Create a new user
|
57
|
+
#
|
58
|
+
# @param name [String] The user's name
|
59
|
+
# @param age [Integer] The user's age
|
60
|
+
def initialize(name:, age:)
|
61
|
+
@name = name
|
62
|
+
@age = age
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Convert a class
|
67
|
+
schema = ToolTailor.convert(User)
|
68
|
+
|
69
|
+
# or
|
70
|
+
schema = User.to_json_schema
|
71
|
+
|
72
|
+
# This is equivalent to:
|
73
|
+
schema = ToolTailor.convert(User.instance_method(:initialize))
|
90
74
|
```
|
91
75
|
|
92
|
-
|
93
|
-
|
94
|
-
```
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
76
|
+
The resulting schema will look like this:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
{
|
80
|
+
"type" => "function",
|
81
|
+
"function" => {
|
82
|
+
"name" => "User",
|
83
|
+
"description" => "Create a new user",
|
84
|
+
"parameters" => {
|
85
|
+
"type" => "object",
|
86
|
+
"properties" => {
|
87
|
+
"name" => {
|
88
|
+
"type" => "string",
|
89
|
+
"description" => "The user's name"
|
103
90
|
},
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
],
|
108
|
-
tool_choice: {
|
109
|
-
type: "function",
|
110
|
-
function: {
|
111
|
-
name: "get_current_weather"
|
91
|
+
"age" => {
|
92
|
+
"type" => "integer",
|
93
|
+
"description" => "The user's age"
|
112
94
|
}
|
113
|
-
}
|
114
|
-
|
115
|
-
|
95
|
+
},
|
96
|
+
"required" => ["name", "age"]
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
```
|
101
|
+
|
102
|
+
### Using with ruby-openai
|
103
|
+
|
104
|
+
Here's an example of how to use ToolTailor with the [ruby-openai](https://github.com/alexrudall/ruby-openai) gem:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
response = client.chat(
|
108
|
+
parameters: {
|
109
|
+
model: "gpt-4",
|
110
|
+
messages: [
|
111
|
+
{ role: "user", content: "Create a user named Alice who is 30 years old" }
|
112
|
+
],
|
113
|
+
tools: [ToolTailor.convert(User)],
|
114
|
+
tool_choice: { type: "function", function: { name: "User" } }
|
115
|
+
}
|
116
|
+
)
|
116
117
|
|
117
118
|
message = response.dig("choices", 0, "message")
|
118
119
|
|
119
120
|
if message["role"] == "assistant" && message["tool_calls"]
|
120
121
|
function_name = message.dig("tool_calls", 0, "function", "name")
|
121
|
-
args =
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
)
|
122
|
+
args = JSON.parse(
|
123
|
+
message.dig("tool_calls", 0, "function", "arguments"),
|
124
|
+
{ symbolize_names: true }
|
125
|
+
)
|
126
126
|
|
127
127
|
case function_name
|
128
|
-
when "
|
129
|
-
|
128
|
+
when "User"
|
129
|
+
user = User.new(**args)
|
130
|
+
puts "Created user: #{user.name}, age #{user.age}"
|
130
131
|
end
|
131
132
|
end
|
132
|
-
# => "The weather is nice 🌞"
|
133
133
|
```
|
134
134
|
|
135
135
|
## Development
|
@@ -140,10 +140,12 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
140
140
|
|
141
141
|
## Contributing
|
142
142
|
|
143
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
143
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kieranklaassen/tool_tailor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/kieranklaassen/tool_tailor/blob/master/CODE_OF_CONDUCT.md).
|
144
144
|
|
145
145
|
## License
|
146
146
|
|
147
147
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
148
148
|
|
149
149
|
## Code of Conduct
|
150
|
+
|
151
|
+
Everyone interacting in the ToolTailor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kieranklaassen/tool_tailor/blob/master/CODE_OF_CONDUCT.md).
|
data/lib/tool_tailor/version.rb
CHANGED
data/lib/tool_tailor.rb
CHANGED
@@ -1,51 +1,73 @@
|
|
1
1
|
require "tool_tailor/version"
|
2
2
|
require "json"
|
3
3
|
require "yard"
|
4
|
+
require "yard_custom_tags"
|
4
5
|
|
5
6
|
module ToolTailor
|
6
7
|
class Error < StandardError; end
|
7
8
|
|
8
|
-
# Converts a function to a JSON schema representation.
|
9
|
+
# Converts a function or class to a JSON schema representation.
|
9
10
|
#
|
10
|
-
# @param
|
11
|
-
# @return [String] The JSON schema representation of the function.
|
12
|
-
# @raise [ArgumentError] If the provided object is not a Method or
|
11
|
+
# @param object [Method, UnboundMethod, Class] The function or class to convert.
|
12
|
+
# @return [String] The JSON schema representation of the function or class.
|
13
|
+
# @raise [ArgumentError] If the provided object is not a Method, UnboundMethod, or Class.
|
13
14
|
#
|
14
15
|
# @example
|
15
|
-
# def example_method(param1
|
16
|
+
# def example_method(param1:, param2:)
|
16
17
|
# # method implementation
|
17
18
|
# end
|
18
19
|
#
|
19
20
|
# ToolTailor.convert(method(:example_method))
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# class ExampleClass
|
24
|
+
# def initialize(param1:, param2:)
|
25
|
+
# # initialization
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# ToolTailor.convert(ExampleClass)
|
30
|
+
def self.convert(object)
|
31
|
+
case object
|
32
|
+
when Method, UnboundMethod
|
33
|
+
convert_method(object)
|
34
|
+
when Class
|
35
|
+
convert_class(object)
|
36
|
+
else
|
37
|
+
raise ArgumentError, "Unsupported object type: #{object.class}"
|
23
38
|
end
|
39
|
+
end
|
24
40
|
|
41
|
+
# Converts a method to a JSON schema representation.
|
42
|
+
#
|
43
|
+
# @param method [Method, UnboundMethod] The method to convert.
|
44
|
+
# @return [String] The JSON schema representation of the method.
|
45
|
+
def self.convert_method(method)
|
25
46
|
# Ensure only named arguments are allowed
|
26
|
-
unless
|
47
|
+
unless method.parameters.all? { |type, _| type == :keyreq || type == :key }
|
27
48
|
raise ArgumentError, "Only named arguments are supported"
|
28
49
|
end
|
29
50
|
|
30
|
-
file_path, line_number =
|
51
|
+
file_path, line_number = method.source_location
|
31
52
|
YARD.parse(file_path)
|
32
53
|
|
33
|
-
method_path = "#{
|
54
|
+
method_path = "#{method.owner}##{method.name}"
|
34
55
|
yard_object = YARD::Registry.at(method_path)
|
35
56
|
|
36
|
-
# Extract parameters from the
|
37
|
-
parameters =
|
57
|
+
# Extract parameters from the method definition
|
58
|
+
parameters = method.parameters.map do |_, name|
|
38
59
|
{
|
39
60
|
name: name.to_s,
|
40
61
|
type: "string",
|
41
|
-
description: ""
|
62
|
+
description: "",
|
63
|
+
enum: nil
|
42
64
|
}
|
43
65
|
end
|
44
66
|
|
45
|
-
|
67
|
+
method_description = ""
|
46
68
|
|
47
69
|
if yard_object
|
48
|
-
|
70
|
+
method_description = yard_object.docstring
|
49
71
|
|
50
72
|
yard_object.tags("param").each do |tag|
|
51
73
|
param_name = tag.name.chomp(':')
|
@@ -55,13 +77,19 @@ module ToolTailor
|
|
55
77
|
param[:description] = tag.text
|
56
78
|
end
|
57
79
|
end
|
80
|
+
|
81
|
+
yard_object.tags("values").each do |tag|
|
82
|
+
param_name = tag.name.chomp(':')
|
83
|
+
param = parameters.find { |p| p[:name] == param_name }
|
84
|
+
param[:enum] = tag.text if param
|
85
|
+
end
|
58
86
|
end
|
59
87
|
|
60
88
|
{
|
61
89
|
type: "function",
|
62
90
|
function: {
|
63
|
-
name:
|
64
|
-
description:
|
91
|
+
name: method.name.to_s,
|
92
|
+
description: method_description,
|
65
93
|
parameters: {
|
66
94
|
type: "object",
|
67
95
|
properties: parameters.map do |param|
|
@@ -69,16 +97,34 @@ module ToolTailor
|
|
69
97
|
param[:name],
|
70
98
|
{
|
71
99
|
type: param[:type],
|
72
|
-
description: param[:description]
|
73
|
-
|
100
|
+
description: param[:description],
|
101
|
+
enum: param[:enum]
|
102
|
+
}.compact
|
74
103
|
]
|
75
104
|
end.to_h,
|
76
|
-
required:
|
105
|
+
required: method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name.to_s }
|
77
106
|
}
|
78
107
|
}
|
79
108
|
}.to_json
|
80
109
|
end
|
81
110
|
|
111
|
+
def self.convert_class(klass)
|
112
|
+
initialize_method = klass.instance_method(:initialize)
|
113
|
+
schema = JSON.parse(convert_method(initialize_method))
|
114
|
+
schema["function"]["name"] = klass.name
|
115
|
+
|
116
|
+
# Re-parse YARD documentation for the class
|
117
|
+
file_path, _ = initialize_method.source_location
|
118
|
+
YARD.parse(file_path)
|
119
|
+
class_object = YARD::Registry.at(klass.name)
|
120
|
+
|
121
|
+
if class_object
|
122
|
+
schema["function"]["description"] = class_object.docstring.to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
schema.to_json
|
126
|
+
end
|
127
|
+
|
82
128
|
# Maps Ruby types to JSON schema types.
|
83
129
|
#
|
84
130
|
# @param type [String] The Ruby type to map.
|
@@ -102,38 +148,6 @@ end
|
|
102
148
|
class UnboundMethod
|
103
149
|
# Converts an UnboundMethod to a JSON schema.
|
104
150
|
#
|
105
|
-
# @example
|
106
|
-
# class ExampleClass
|
107
|
-
# # @param name [String] The name of the person.
|
108
|
-
# # @param age [Integer] The age of the person.
|
109
|
-
# def greet(name, age)
|
110
|
-
# puts "Hello, #{name}! You are #{age} years old."
|
111
|
-
# end
|
112
|
-
# end
|
113
|
-
#
|
114
|
-
# ExampleClass.instance_method(:greet).to_json_schema
|
115
|
-
# # => {
|
116
|
-
# # "type" => "function",
|
117
|
-
# # "function" => {
|
118
|
-
# # "name" => "greet",
|
119
|
-
# # "description" => "",
|
120
|
-
# # "parameters" => {
|
121
|
-
# # "type" => "object",
|
122
|
-
# # "properties" => {
|
123
|
-
# # "name" => {
|
124
|
-
# # "type" => "string",
|
125
|
-
# # "description" => "The name of the person."
|
126
|
-
# # },
|
127
|
-
# # "age" => {
|
128
|
-
# # "type" => "integer",
|
129
|
-
# # "description" => "The age of the person."
|
130
|
-
# # }
|
131
|
-
# # },
|
132
|
-
# # "required" => ["name", "age"]
|
133
|
-
# # }
|
134
|
-
# # }
|
135
|
-
# # }
|
136
|
-
#
|
137
151
|
# @return [String] The JSON schema representation of the method.
|
138
152
|
def to_json_schema
|
139
153
|
ToolTailor.convert(self)
|
@@ -143,40 +157,17 @@ end
|
|
143
157
|
class Method
|
144
158
|
# Converts a Method to a JSON schema.
|
145
159
|
#
|
146
|
-
# @example
|
147
|
-
# class ExampleClass
|
148
|
-
# # @param name [String] The name of the person.
|
149
|
-
# # @param age [Integer] The age of the person.
|
150
|
-
# def greet(name, age)
|
151
|
-
# puts "Hello, #{name}! You are #{age} years old."
|
152
|
-
# end
|
153
|
-
# end
|
154
|
-
#
|
155
|
-
# ExampleClass.new.method(:greet).to_json_schema
|
156
|
-
# # => {
|
157
|
-
# # "type" => "function",
|
158
|
-
# # "function" => {
|
159
|
-
# # "name" => "greet",
|
160
|
-
# # "description" => "",
|
161
|
-
# # "parameters" => {
|
162
|
-
# # "type" => "object",
|
163
|
-
# # "properties" => {
|
164
|
-
# # "name" => {
|
165
|
-
# # "type" => "string",
|
166
|
-
# # "description" => "The name of the person."
|
167
|
-
# # },
|
168
|
-
# # "age" => {
|
169
|
-
# # "type" => "integer",
|
170
|
-
# # "description" => "The age of the person."
|
171
|
-
# # }
|
172
|
-
# # },
|
173
|
-
# # "required" => ["name", "age"]
|
174
|
-
# # }
|
175
|
-
# # }
|
176
|
-
# # }
|
177
|
-
#
|
178
160
|
# @return [String] The JSON schema representation of the method.
|
179
161
|
def to_json_schema
|
180
162
|
ToolTailor.convert(self)
|
181
163
|
end
|
182
164
|
end
|
165
|
+
|
166
|
+
class Class
|
167
|
+
# Converts a Class to a JSON schema.
|
168
|
+
#
|
169
|
+
# @return [String] The JSON schema representation of the class's initialize method.
|
170
|
+
def to_json_schema
|
171
|
+
ToolTailor.convert(self)
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "yard"
|
2
|
+
|
3
|
+
module YARD
|
4
|
+
module Tags
|
5
|
+
# Custom tag class for handling `@values` tags.
|
6
|
+
class ValuesTag < YARD::Tags::Tag
|
7
|
+
TAG_FORMAT = /^(\S+)\s+\[(.+)\]$/
|
8
|
+
|
9
|
+
def initialize(tag_name, text)
|
10
|
+
name, values = parse_text(text)
|
11
|
+
super(tag_name, values, nil, name)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Parses the text to match the expected format and extract the name and values.
|
17
|
+
def parse_text(text)
|
18
|
+
match = text.match(TAG_FORMAT)
|
19
|
+
unless match
|
20
|
+
raise ArgumentError, "Invalid @values tag format. Expected: @values <name> [value1, value2, ...]. Values should be a JSON array."
|
21
|
+
end
|
22
|
+
|
23
|
+
name, values_text = match.captures
|
24
|
+
values = parse_values(values_text)
|
25
|
+
[name, values]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parses the values text as a JSON array to ensure correct types.
|
29
|
+
def parse_values(values_text)
|
30
|
+
json_text = "[#{values_text}]"
|
31
|
+
JSON.parse(json_text)
|
32
|
+
rescue JSON::ParserError => e
|
33
|
+
raise ArgumentError, "Invalid values format: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Library
|
38
|
+
def self.define_custom_tag
|
39
|
+
# Defines a new custom tag `@values` using the ValuesTag class.
|
40
|
+
YARD::Tags::Library.define_tag("Values", :values, ValuesTag)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
YARD::Tags::Library.define_custom_tag
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tool_tailor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kieran Klaassen
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: yard
|
@@ -45,6 +45,7 @@ files:
|
|
45
45
|
- bin/setup
|
46
46
|
- lib/tool_tailor.rb
|
47
47
|
- lib/tool_tailor/version.rb
|
48
|
+
- lib/yard_custom_tags.rb
|
48
49
|
- tool_tailor.gemspec
|
49
50
|
homepage: https://github.com/kieranklaassen/tool_tailor
|
50
51
|
licenses:
|