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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33d87f65353ea72f0715a86b7759529f68f6678eb6a24d47fac0547bade1b59e
4
- data.tar.gz: 874c949f555e93aa445147d89981e1207e11fdc4520977a668e9724354ca7d95
3
+ metadata.gz: 908a6461c07d131d14e6bd8be1a3fa6fa578e4f0261110d084238dbc657e8f33
4
+ data.tar.gz: 65541821e6c287d82e1bffd57e04da0c541654ea3e8edc84621cc0fbfbe11604
5
5
  SHA512:
6
- metadata.gz: 4500dc6778e3bb5373a4630160b22a2ad7f0669ddb097d221eb93b3f1c27a93b48310ef4fcc434ec2c154ea004beb84d157bd1550d4ada0eb1e8fe7b8daa83da
7
- data.tar.gz: 036f48ffa4b31354b24c85392cf52520215afcb02afac201289e19e650f4491be5fa0122aca606fc6d421d833ee2b8a64533b53a5936ea90608d2197caecd2b2
6
+ metadata.gz: 798fb99b7ac1fba192eec08684ac51d1f39f11e7bb6ff79757987a2aaabba1bb749ed865278dc37639e08ef6cd04b11de68f461ebbb0ee09433db3cb3d090f97
7
+ data.tar.gz: 4a2aeda57a2757776a05845d29fb1a09c479d2352820d5be0930c6b7b75c01c59f4712cd9367cda3e758e5c1d3c6d00385080b1768be256a1fde20ea73eb871d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tool_tailor (0.1.3)
4
+ tool_tailor (0.2.0)
5
5
  yard (~> 0.9.36)
6
6
 
7
7
  GEM
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
- ```rb
24
- class TestClass
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 of temperature, either 'celsius' or 'fahrenheit'.
29
- def get_current_weather(location:, unit: 'celsius')
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
- # Simple
35
- ToolTailor.convert(TestClass.instance_method(:get_current_weather))
36
-
37
- # Unbound method with to_json_schema
38
- TestClass.instance_method(:get_current_weather).to_json_schema # => {
39
- # "type" => "function",
40
- # "function" => {
41
- # "name" => "get_current_weather",
42
- # "description" => "Get the current weather in a given location.",
43
- # "parameters" => {
44
- # "type" => "object",
45
- # "properties" => {
46
- # "location" => {
47
- # "type" => "string",
48
- # "description" => "The city and state, e.g., San Francisco, CA."
49
- # },
50
- # "unit" => {
51
- # "type" => "string",
52
- # "description" => "The unit of temperature, either 'celsius' or 'fahrenheit'."
53
- # },
54
- # "api_key" => {
55
- # "type" => "number",
56
- # "description" => "The API key for the weather service."
57
- # }
58
- # },
59
- # "required" => ["location", "unit", "api_key"]
60
- # }
61
- # }
62
-
63
- # Bound method with to_json_schema
64
- example_instance = TestClass.new
65
- example_instance.method(:get_current_weather).to_json_schema # => {
66
- # "type" => "function",
67
- # "function" => {
68
- # "name" => "get_current_weather",
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
- And with [ruby-openai](https://github.com/alexrudall/ruby-openai):
93
-
94
- ```rb
95
- response =
96
- client.chat(
97
- parameters: {
98
- model: "gpt-4o",
99
- messages: [
100
- {
101
- "role": "user",
102
- "content": "What is the weather like in San Francisco?",
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
- tools: [
106
- TestClass.instance_method(:get_current_weather).to_json_schema
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
- JSON.parse(
123
- message.dig("tool_calls", 0, "function", "arguments"),
124
- { symbolize_names: true },
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 "get_current_weather"
129
- TestClass.get_current_weather(**args)
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/[USERNAME]/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/[USERNAME]/tool_tailor/blob/master/CODE_OF_CONDUCT.md).
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).
@@ -1,3 +1,3 @@
1
1
  module ToolTailor
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.1"
3
3
  end
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 function [Method, UnboundMethod] The function to convert.
11
- # @return [String] The JSON schema representation of the function.
12
- # @raise [ArgumentError] If the provided object is not a Method or UnboundMethod.
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, param2)
16
+ # def example_method(param1:, param2:)
16
17
  # # method implementation
17
18
  # end
18
19
  #
19
20
  # ToolTailor.convert(method(:example_method))
20
- def self.convert(function)
21
- unless function.is_a?(Method) || function.is_a?(UnboundMethod)
22
- raise ArgumentError, "Unsupported object type: #{function.class}"
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 function.parameters.all? { |type, _| type == :keyreq || type == :key }
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 = function.source_location
51
+ file_path, line_number = method.source_location
31
52
  YARD.parse(file_path)
32
53
 
33
- method_path = "#{function.owner}##{function.name}"
54
+ method_path = "#{method.owner}##{method.name}"
34
55
  yard_object = YARD::Registry.at(method_path)
35
56
 
36
- # Extract parameters from the function definition
37
- parameters = function.parameters.map do |_, name|
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
- function_description = ""
67
+ method_description = ""
46
68
 
47
69
  if yard_object
48
- function_description = yard_object.docstring
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: function.name.to_s,
64
- description: function_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: function.parameters.select { |type, _| type == :keyreq }.map { |_, name| name.to_s }
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.3
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-05-30 00:00:00.000000000 Z
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: