tool_tailor 0.1.2 → 0.2.0

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: abc31d6d69830be0126f8b6f61aee408be9e17bdb91423b68e9166f30efaf21f
4
- data.tar.gz: 7eb42498a63836eeac46c6996d2e32f34303430b58dcdd36cb89d833cbadeaef
3
+ metadata.gz: 840fcc0f9e8a7f5ccd4db0d8f4a8d88f2f344225d8f759442dc11f625d243300
4
+ data.tar.gz: 53289290ee809ea4e18acf08de8115d4c259b337823fd9ad506de6c335eb12db
5
5
  SHA512:
6
- metadata.gz: 04d10928c7453fbd20980d3c23a6bbbe8cf000baa25f2ffce903910b21607088762f300fde528ee4c7f0b8673ba58a65224926c261486ff033d87c5d25601468
7
- data.tar.gz: 779238cd2409d9d6c669cab52f0d286fc79cf3c9335082d88bf62d871f23ec6439fe479f465d25fab0bd13d2285e6522ee026ea9e5257b019cda483244c28664
6
+ metadata.gz: 682eea6fb802c1a1444cafbe077577594cd682c7e02071313b56c7e737c738575b82098cbc6a4a63287edf449a0cd0aed9c4dcb225386350e4b2653270dac585
7
+ data.tar.gz: 8606a2abd5309a6ab9f7d7fdfa1910c953d674708aa9dce5c2e9fee29168f979632b3f4b8567dd2c5aba68d6fdfc18a2aebc000f464d63f20cc005c7c0452925
data/Gemfile CHANGED
@@ -5,3 +5,5 @@ gemspec
5
5
 
6
6
  gem "rake", "~> 12.0"
7
7
  gem "rspec", "~> 3.0"
8
+
9
+ gem "super_diff", "~> 0.9.0", :group => :test
data/Gemfile.lock CHANGED
@@ -1,13 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tool_tailor (0.1.2)
4
+ tool_tailor (0.2.0)
5
5
  yard (~> 0.9.36)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
+ attr_extras (7.1.0)
10
11
  diff-lcs (1.5.1)
12
+ optimist (3.1.0)
13
+ patience_diff (1.2.0)
14
+ optimist (~> 3.0)
11
15
  rake (12.3.3)
12
16
  rspec (3.13.0)
13
17
  rspec-core (~> 3.13.0)
@@ -22,6 +26,10 @@ GEM
22
26
  diff-lcs (>= 1.2.0, < 2.0)
23
27
  rspec-support (~> 3.13.0)
24
28
  rspec-support (3.13.1)
29
+ super_diff (0.9.0)
30
+ attr_extras (>= 6.2.4)
31
+ diff-lcs
32
+ patience_diff
25
33
  yard (0.9.36)
26
34
 
27
35
  PLATFORMS
@@ -30,6 +38,7 @@ PLATFORMS
30
38
  DEPENDENCIES
31
39
  rake (~> 12.0)
32
40
  rspec (~> 3.0)
41
+ super_diff (~> 0.9.0)
33
42
  tool_tailor!
34
43
 
35
44
  BUNDLED WITH
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,113 +20,115 @@ 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
32
  # @param unit [String] The unit of temperature, either 'celsius' or 'fahrenheit'.
29
- def get_current_weather(location, unit = 'celsius')
33
+ def get_current_weather(location:, unit: 'celsius')
30
34
  # Function implementation goes here
31
35
  end
32
36
  end
33
37
 
34
- # Unbound method
35
- TestClass.instance_method(:get_current_weather).to_json_schema # => {
36
- # "type" => "function",
37
- # "function" => {
38
- # "name" => "get_current_weather",
39
- # "description" => "Get the current weather in a given location.",
40
- # "parameters" => {
41
- # "type" => "object",
42
- # "properties" => {
43
- # "location" => {
44
- # "type" => "string",
45
- # "description" => "The city and state, e.g., San Francisco, CA."
46
- # },
47
- # "unit" => {
48
- # "type" => "string",
49
- # "description" => "The unit of temperature, either 'celsius' or 'fahrenheit'."
50
- # },
51
- # "api_key" => {
52
- # "type" => "number",
53
- # "description" => "The API key for the weather service."
54
- # }
55
- # },
56
- # "required" => ["location", "unit", "api_key"]
57
- # }
58
- # }
59
-
60
- # Bound method
61
- example_instance = TestClass.new
62
- example_instance.method(:get_current_weather).to_json_schema # => {
63
- # "type" => "function",
64
- # "function" => {
65
- # "name" => "get_current_weather",
66
- # "description" => "Get the current weather in a given location.",
67
- # "parameters" => {
68
- # "type" => "object",
69
- # "properties" => {
70
- # "location" => {
71
- # "type" => "string",
72
- # "description" => "The city and state, e.g., San Francisco, CA."
73
- # },
74
- # "unit" => {
75
- # "type" => "string",
76
- # "description" => "The unit of temperature, either 'celsius' or 'fahrenheit'."
77
- # },
78
- # "api_key" => {
79
- # "type" => "number",
80
- # "description" => "The API key for the weather service."
81
- # }
82
- # },
83
- # "required" => ["location", "unit", "api_key"]
84
- # }
85
- # }
86
- # }
38
+ # Convert an instance method
39
+ schema = ToolTailor.convert(WeatherService.instance_method(:get_current_weather))
40
+
41
+ # Using to_json_schema on an unbound method
42
+ schema = WeatherService.instance_method(:get_current_weather).to_json_schema
43
+
44
+ # Using to_json_schema on a bound method
45
+ weather_service = WeatherService.new
46
+ schema = weather_service.method(:get_current_weather).to_json_schema
47
+ ```
48
+
49
+ ### Converting Classes
50
+
51
+ When passing a class, ToolTailor assumes you want to use the `new` method and generates the schema based on the `initialize` method:
52
+
53
+ ```ruby
54
+ class User
55
+ # Create a new user
56
+ #
57
+ # @param name [String] The user's name
58
+ # @param age [Integer] The user's age
59
+ def initialize(name:, age:)
60
+ @name = name
61
+ @age = age
62
+ end
63
+ end
64
+
65
+ # Convert a class
66
+ schema = ToolTailor.convert(User)
67
+
68
+ # or
69
+ schema = User.to_json_schema
70
+
71
+ # This is equivalent to:
72
+ schema = ToolTailor.convert(User.instance_method(:initialize))
87
73
  ```
88
74
 
89
- And with [ruby-openai](https://github.com/alexrudall/ruby-openai):
90
-
91
- ```rb
92
- response =
93
- client.chat(
94
- parameters: {
95
- model: "gpt-4o",
96
- messages: [
97
- {
98
- "role": "user",
99
- "content": "What is the weather like in San Francisco?",
75
+ The resulting schema will look like this:
76
+
77
+ ```ruby
78
+ {
79
+ "type" => "function",
80
+ "function" => {
81
+ "name" => "User",
82
+ "description" => "Create a new user",
83
+ "parameters" => {
84
+ "type" => "object",
85
+ "properties" => {
86
+ "name" => {
87
+ "type" => "string",
88
+ "description" => "The user's name"
100
89
  },
101
- ],
102
- tools: [
103
- TestClass.instance_method(:get_current_weather).to_json_schema
104
- ],
105
- tool_choice: {
106
- type: "function",
107
- function: {
108
- name: "get_current_weather"
90
+ "age" => {
91
+ "type" => "integer",
92
+ "description" => "The user's age"
109
93
  }
110
- }
111
- },
112
- )
94
+ },
95
+ "required" => ["name", "age"]
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Using with ruby-openai
102
+
103
+ Here's an example of how to use ToolTailor with the [ruby-openai](https://github.com/alexrudall/ruby-openai) gem:
104
+
105
+ ```ruby
106
+ response = client.chat(
107
+ parameters: {
108
+ model: "gpt-4",
109
+ messages: [
110
+ { role: "user", content: "Create a user named Alice who is 30 years old" }
111
+ ],
112
+ tools: [ToolTailor.convert(User)],
113
+ tool_choice: { type: "function", function: { name: "User" } }
114
+ }
115
+ )
113
116
 
114
117
  message = response.dig("choices", 0, "message")
115
118
 
116
119
  if message["role"] == "assistant" && message["tool_calls"]
117
120
  function_name = message.dig("tool_calls", 0, "function", "name")
118
- args =
119
- JSON.parse(
120
- message.dig("tool_calls", 0, "function", "arguments"),
121
- { symbolize_names: true },
122
- )
121
+ args = JSON.parse(
122
+ message.dig("tool_calls", 0, "function", "arguments"),
123
+ { symbolize_names: true }
124
+ )
123
125
 
124
126
  case function_name
125
- when "get_current_weather"
126
- TestClass.get_current_weather(**args)
127
+ when "User"
128
+ user = User.new(**args)
129
+ puts "Created user: #{user.name}, age #{user.age}"
127
130
  end
128
131
  end
129
- # => "The weather is nice 🌞"
130
132
  ```
131
133
 
132
134
  ## Development
@@ -137,10 +139,12 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
137
139
 
138
140
  ## Contributing
139
141
 
140
- 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).
142
+ 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).
141
143
 
142
144
  ## License
143
145
 
144
146
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
145
147
 
146
148
  ## Code of Conduct
149
+
150
+ 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.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/tool_tailor.rb CHANGED
@@ -5,38 +5,83 @@ require "yard"
5
5
  module ToolTailor
6
6
  class Error < StandardError; end
7
7
 
8
- # Converts a function to a JSON schema representation.
8
+ # Converts a function or class to a JSON schema representation.
9
9
  #
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.
13
- def self.convert(function)
14
- unless function.is_a?(Method) || function.is_a?(UnboundMethod)
15
- raise ArgumentError, "Unsupported object type: #{function.class}"
10
+ # @param object [Method, UnboundMethod, Class] The function or class to convert.
11
+ # @return [String] The JSON schema representation of the function or class.
12
+ # @raise [ArgumentError] If the provided object is not a Method, UnboundMethod, or Class.
13
+ #
14
+ # @example
15
+ # def example_method(param1:, param2:)
16
+ # # method implementation
17
+ # end
18
+ #
19
+ # ToolTailor.convert(method(:example_method))
20
+ #
21
+ # @example
22
+ # class ExampleClass
23
+ # def initialize(param1:, param2:)
24
+ # # initialization
25
+ # end
26
+ # end
27
+ #
28
+ # ToolTailor.convert(ExampleClass)
29
+ def self.convert(object)
30
+ case object
31
+ when Method, UnboundMethod
32
+ convert_method(object)
33
+ when Class
34
+ convert_class(object)
35
+ else
36
+ raise ArgumentError, "Unsupported object type: #{object.class}"
16
37
  end
38
+ end
17
39
 
18
- file_path, line_number = function.source_location
40
+ # Converts a method to a JSON schema representation.
41
+ #
42
+ # @param method [Method, UnboundMethod] The method to convert.
43
+ # @return [String] The JSON schema representation of the method.
44
+ def self.convert_method(method)
45
+ # Ensure only named arguments are allowed
46
+ unless method.parameters.all? { |type, _| type == :keyreq || type == :key }
47
+ raise ArgumentError, "Only named arguments are supported"
48
+ end
49
+
50
+ file_path, line_number = method.source_location
19
51
  YARD.parse(file_path)
20
52
 
21
- method_path = "#{function.owner}##{function.name}"
53
+ method_path = "#{method.owner}##{method.name}"
22
54
  yard_object = YARD::Registry.at(method_path)
23
- raise "Documentation for #{method_path} not found." if yard_object.nil?
24
-
25
- function_description = yard_object.docstring
26
55
 
27
- parameters = yard_object.tags("param").map do |tag|
56
+ # Extract parameters from the method definition
57
+ parameters = method.parameters.map do |_, name|
28
58
  {
29
- name: tag.name,
30
- type: type_mapping(tag.types.first),
31
- description: tag.text
59
+ name: name.to_s,
60
+ type: "string",
61
+ description: ""
32
62
  }
33
63
  end
34
64
 
65
+ method_description = ""
66
+
67
+ if yard_object
68
+ method_description = yard_object.docstring
69
+
70
+ yard_object.tags("param").each do |tag|
71
+ param_name = tag.name.chomp(':')
72
+ param = parameters.find { |p| p[:name] == param_name }
73
+ if param
74
+ param[:type] = type_mapping(tag.types.first)
75
+ param[:description] = tag.text
76
+ end
77
+ end
78
+ end
79
+
35
80
  {
36
81
  type: "function",
37
82
  function: {
38
- name: function.name.to_s,
39
- description: function_description,
83
+ name: method.name.to_s,
84
+ description: method_description,
40
85
  parameters: {
41
86
  type: "object",
42
87
  properties: parameters.map do |param|
@@ -48,12 +93,29 @@ module ToolTailor
48
93
  }
49
94
  ]
50
95
  end.to_h,
51
- required: parameters.map { |param| param[:name].to_s }
96
+ required: method.parameters.select { |type, _| type == :keyreq }.map { |_, name| name.to_s }
52
97
  }
53
98
  }
54
99
  }.to_json
55
100
  end
56
101
 
102
+ def self.convert_class(klass)
103
+ initialize_method = klass.instance_method(:initialize)
104
+ schema = JSON.parse(convert_method(initialize_method))
105
+ schema["function"]["name"] = klass.name
106
+
107
+ # Re-parse YARD documentation for the class
108
+ file_path, _ = initialize_method.source_location
109
+ YARD.parse(file_path)
110
+ class_object = YARD::Registry.at(klass.name)
111
+
112
+ if class_object
113
+ schema["function"]["description"] = class_object.docstring.to_s
114
+ end
115
+
116
+ schema.to_json
117
+ end
118
+
57
119
  # Maps Ruby types to JSON schema types.
58
120
  #
59
121
  # @param type [String] The Ruby type to map.
@@ -77,38 +139,6 @@ end
77
139
  class UnboundMethod
78
140
  # Converts an UnboundMethod to a JSON schema.
79
141
  #
80
- # @example
81
- # class ExampleClass
82
- # # @param name [String] The name of the person.
83
- # # @param age [Integer] The age of the person.
84
- # def greet(name, age)
85
- # puts "Hello, #{name}! You are #{age} years old."
86
- # end
87
- # end
88
- #
89
- # ExampleClass.instance_method(:greet).to_json_schema
90
- # # => {
91
- # # "type" => "function",
92
- # # "function" => {
93
- # # "name" => "greet",
94
- # # "description" => "",
95
- # # "parameters" => {
96
- # # "type" => "object",
97
- # # "properties" => {
98
- # # "name" => {
99
- # # "type" => "string",
100
- # # "description" => "The name of the person."
101
- # # },
102
- # # "age" => {
103
- # # "type" => "integer",
104
- # # "description" => "The age of the person."
105
- # # }
106
- # # },
107
- # # "required" => ["name", "age"]
108
- # # }
109
- # # }
110
- # # }
111
- #
112
142
  # @return [String] The JSON schema representation of the method.
113
143
  def to_json_schema
114
144
  ToolTailor.convert(self)
@@ -118,40 +148,17 @@ end
118
148
  class Method
119
149
  # Converts a Method to a JSON schema.
120
150
  #
121
- # @example
122
- # class ExampleClass
123
- # # @param name [String] The name of the person.
124
- # # @param age [Integer] The age of the person.
125
- # def greet(name, age)
126
- # puts "Hello, #{name}! You are #{age} years old."
127
- # end
128
- # end
129
- #
130
- # ExampleClass.new.method(:greet).to_json_schema
131
- # # => {
132
- # # "type" => "function",
133
- # # "function" => {
134
- # # "name" => "greet",
135
- # # "description" => "",
136
- # # "parameters" => {
137
- # # "type" => "object",
138
- # # "properties" => {
139
- # # "name" => {
140
- # # "type" => "string",
141
- # # "description" => "The name of the person."
142
- # # },
143
- # # "age" => {
144
- # # "type" => "integer",
145
- # # "description" => "The age of the person."
146
- # # }
147
- # # },
148
- # # "required" => ["name", "age"]
149
- # # }
150
- # # }
151
- # # }
152
- #
153
151
  # @return [String] The JSON schema representation of the method.
154
152
  def to_json_schema
155
153
  ToolTailor.convert(self)
156
154
  end
157
155
  end
156
+
157
+ class Class
158
+ # Converts a Class to a JSON schema.
159
+ #
160
+ # @return [String] The JSON schema representation of the class's initialize method.
161
+ def to_json_schema
162
+ ToolTailor.convert(self)
163
+ end
164
+ end
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.2
4
+ version: 0.2.0
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-23 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