Skip to content

Commit 2c64594

Browse files
SamSaffronCopilot
andauthored
DEV: use a proper object for tool definition (#1337)
* DEV: use a proper object for tool definition This moves away from using a loose hash to define tools, which is error prone. Instead given a proper object we will also be able to coerce the return values to match tool definition correctly * fix xml tools * fix anthropic tools * fix specs... a few more to go * specs are passing * FIX: coerce values for XML tool calls * Update spec/lib/completions/tool_definition_spec.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c34fcc8 commit 2c64594

22 files changed

+814
-231
lines changed

lib/completions/dialects/claude_tools.rb

+1-26
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,8 @@ def initialize(tools)
99
end
1010

1111
def translated_tools
12-
# Transform the raw tools into the required Anthropic Claude API format
1312
raw_tools.map do |t|
14-
properties = {}
15-
required = []
16-
17-
if t[:parameters]
18-
properties = {}
19-
20-
t[:parameters].each do |param|
21-
mapped = { type: param[:type], description: param[:description] }
22-
mapped[:items] = { type: param[:item_type] } if param[:item_type]
23-
mapped[:enum] = param[:enum] if param[:enum]
24-
properties[param[:name]] = mapped
25-
end
26-
required =
27-
t[:parameters].select { |param| param[:required] }.map { |param| param[:name] }
28-
end
29-
30-
{
31-
name: t[:name],
32-
description: t[:description],
33-
input_schema: {
34-
type: "object",
35-
properties: properties,
36-
required: required,
37-
},
38-
}
13+
{ name: t.name, description: t.description, input_schema: t.parameters_json_schema }
3914
end
4015
end
4116

lib/completions/dialects/cohere_tools.rb

+15-17
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,24 @@ def tool_results(messages)
3838
end
3939

4040
def translated_tools
41-
raw_tools.map do |t|
42-
tool = t.dup
41+
raw_tools.map do |tool|
42+
defs = {}
4343

44-
tool[:parameter_definitions] = t[:parameters]
45-
.to_a
46-
.reduce({}) do |memo, p|
47-
name = p[:name]
48-
memo[name] = {
49-
description: p[:description],
50-
type: cohere_type(p[:type], p[:item_type]),
51-
required: p[:required],
52-
}
44+
tool.parameters.each do |p|
45+
name = p.name
46+
defs[name] = {
47+
description: p.description,
48+
type: cohere_type(p.type, p.item_type),
49+
required: p.required,
50+
}
5351

54-
memo[name][:default] = p[:default] if p[:default]
55-
memo
56-
end
52+
#defs[name][:default] = p.default if p.default
53+
end
5754

5855
{
59-
name: tool[:name] == "search" ? "search_local" : tool[:name],
60-
description: tool[:description],
61-
parameter_definitions: tool[:parameter_definitions],
56+
name: tool.name == "search" ? "search_local" : tool.name,
57+
description: tool.description,
58+
parameter_definitions: defs,
6259
}
6360
end
6461
end
@@ -72,6 +69,7 @@ def instructions
7269
attr_reader :raw_tools
7370

7471
def cohere_type(type, item_type)
72+
type = type.to_s
7573
case type
7674
when "string"
7775
"str"

lib/completions/dialects/gemini.rb

+2-16
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,8 @@ def tools
4747

4848
translated_tools =
4949
prompt.tools.map do |t|
50-
tool = t.slice(:name, :description)
51-
52-
if t[:parameters]
53-
tool[:parameters] = t[:parameters].reduce(
54-
{ type: "object", required: [], properties: {} },
55-
) do |memo, p|
56-
name = p[:name]
57-
memo[:required] << name if p[:required]
58-
59-
memo[:properties][name] = p.except(:name, :required, :item_type)
60-
61-
memo[:properties][name][:items] = { type: p[:item_type] } if p[:item_type]
62-
memo
63-
end
64-
end
65-
50+
tool = { name: t.name, description: t.description }
51+
tool[:parameters] = t.parameters_json_schema if t.parameters
6652
tool
6753
end
6854

lib/completions/dialects/nova_tools.rb

+3-29
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ def translated_tools
1717
@raw_tools.map do |tool|
1818
{
1919
toolSpec: {
20-
name: tool[:name],
21-
description: tool[:description],
20+
name: tool.name,
21+
description: tool.description,
2222
inputSchema: {
23-
json: convert_tool_to_input_schema(tool),
23+
json: tool.parameters_json_schema,
2424
},
2525
},
2626
}
@@ -51,32 +51,6 @@ def from_raw_tool(raw_message)
5151
},
5252
}
5353
end
54-
55-
private
56-
57-
def convert_tool_to_input_schema(tool)
58-
tool = tool.transform_keys(&:to_sym)
59-
properties = {}
60-
tool[:parameters].each do |param|
61-
schema = {}
62-
type = param[:type]
63-
type = "string" if !%w[string number boolean integer array].include?(type)
64-
65-
schema[:type] = type
66-
67-
if enum = param[:enum]
68-
schema[:enum] = enum
69-
end
70-
71-
schema[:items] = { type: param[:item_type] } if type == "array"
72-
73-
schema[:required] = true if param[:required]
74-
75-
properties[param[:name]] = schema
76-
end
77-
78-
{ type: "object", properties: properties }
79-
end
8054
end
8155
end
8256
end

lib/completions/dialects/ollama_tools.rb

+9-17
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,15 @@ def instructions
1414
end
1515

1616
def translated_tools
17-
raw_tools.map do |t|
18-
tool = t.dup
19-
20-
tool[:parameters] = t[:parameters]
21-
.to_a
22-
.reduce({ type: "object", properties: {}, required: [] }) do |memo, p|
23-
name = p[:name]
24-
memo[:required] << name if p[:required]
25-
26-
except = %i[name required item_type]
27-
except << :enum if p[:enum].blank?
28-
29-
memo[:properties][name] = p.except(*except)
30-
memo
31-
end
32-
33-
{ type: "function", function: tool }
17+
raw_tools.map do |tool|
18+
{
19+
type: "function",
20+
function: {
21+
name: tool.name,
22+
description: tool.description,
23+
parameters: tool.parameters_json_schema,
24+
},
25+
}
3426
end
3527
end
3628

lib/completions/dialects/open_ai_tools.rb

+9-19
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,15 @@ def initialize(tools)
99
end
1010

1111
def translated_tools
12-
raw_tools.map do |t|
13-
tool = t.dup
14-
15-
tool[:parameters] = t[:parameters]
16-
.to_a
17-
.reduce({ type: "object", properties: {}, required: [] }) do |memo, p|
18-
name = p[:name]
19-
memo[:required] << name if p[:required]
20-
21-
except = %i[name required item_type]
22-
except << :enum if p[:enum].blank?
23-
24-
memo[:properties][name] = p.except(*except)
25-
26-
memo[:properties][name][:items] = { type: p[:item_type] } if p[:item_type]
27-
memo
28-
end
29-
30-
{ type: "function", function: tool }
12+
raw_tools.map do |tool|
13+
{
14+
type: "function",
15+
function: {
16+
name: tool.name,
17+
description: tool.description,
18+
parameters: tool.parameters_json_schema,
19+
},
20+
}
3121
end
3222
end
3323

lib/completions/dialects/xml_tools.rb

+17-14
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,45 @@ def initialize(tools)
99
end
1010

1111
def translated_tools
12-
raw_tools.reduce(+"") do |tools, function|
12+
result = +""
13+
14+
raw_tools.each do |tool|
1315
parameters = +""
14-
if function[:parameters].present?
15-
function[:parameters].each do |parameter|
16+
if tool.parameters.present?
17+
tool.parameters.each do |parameter|
1618
parameters << <<~PARAMETER
1719
<parameter>
18-
<name>#{parameter[:name]}</name>
19-
<type>#{parameter[:type]}</type>
20-
<description>#{parameter[:description]}</description>
21-
<required>#{parameter[:required]}</required>
20+
<name>#{parameter.name}</name>
21+
<type>#{parameter.type}</type>
22+
<description>#{parameter.description}</description>
23+
<required>#{parameter.required}</required>
2224
PARAMETER
23-
if parameter[:enum]
24-
parameters << "<options>#{parameter[:enum].join(",")}</options>\n"
25+
if parameter.item_type
26+
parameters << "<array_item_type>#{parameter.item_type}</array_item_type>\n"
2527
end
28+
parameters << "<options>#{parameter.enum.join(",")}</options>\n" if parameter.enum
2629
parameters << "</parameter>\n"
2730
end
2831
end
2932

30-
tools << <<~TOOLS
33+
result << <<~TOOLS
3134
<tool_description>
32-
<tool_name>#{function[:name]}</tool_name>
33-
<description>#{function[:description]}</description>
35+
<tool_name>#{tool.name}</tool_name>
36+
<description>#{tool.description}</description>
3437
<parameters>
3538
#{parameters}</parameters>
3639
</tool_description>
3740
TOOLS
3841
end
42+
result
3943
end
4044

4145
def instructions
4246
return "" if raw_tools.blank?
4347

4448
@instructions ||=
4549
begin
46-
has_arrays =
47-
raw_tools.any? { |tool| tool[:parameters]&.any? { |p| p[:type] == "array" } }
50+
has_arrays = raw_tools.any? { |tool| tool.parameters&.any? { |p| p.type == "array" } }
4851

4952
(<<~TEXT).strip
5053
#{tool_preamble(include_array_tip: has_arrays)}

lib/completions/endpoints/base.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def endpoint_for(provider_name)
2828
DiscourseAi::Completions::Endpoints::OpenRouter,
2929
]
3030

31-
endpoints << DiscourseAi::Completions::Endpoints::Ollama if Rails.env.development?
31+
endpoints << DiscourseAi::Completions::Endpoints::Ollama if !Rails.env.production?
3232

3333
if Rails.env.test? || Rails.env.development?
3434
endpoints << DiscourseAi::Completions::Endpoints::Fake
@@ -166,6 +166,7 @@ def perform_completion!(
166166
xml_tool_processor =
167167
XmlToolProcessor.new(
168168
partial_tool_calls: partial_tool_calls,
169+
tool_definitions: dialect.prompt.tools,
169170
) if xml_tools_enabled? && dialect.prompt.has_tools?
170171

171172
to_strip = xml_tags_to_strip(dialect)

lib/completions/prompt.rb

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ module Completions
55
class Prompt
66
INVALID_TURN = Class.new(StandardError)
77

8-
attr_reader :messages
9-
attr_accessor :tools, :topic_id, :post_id, :max_pixels, :tool_choice
8+
attr_reader :messages, :tools
9+
attr_accessor :topic_id, :post_id, :max_pixels, :tool_choice
1010

1111
def initialize(
1212
system_message_text = nil,
@@ -37,10 +37,25 @@ def initialize(
3737
@messages.each { |message| validate_message(message) }
3838
@messages.each_cons(2) { |last_turn, new_turn| validate_turn(last_turn, new_turn) }
3939

40-
@tools = tools
40+
self.tools = tools
4141
@tool_choice = tool_choice
4242
end
4343

44+
def tools=(tools)
45+
raise ArgumentError, "tools must be an array" if !tools.is_a?(Array) && !tools.nil?
46+
47+
@tools =
48+
tools.map do |tool|
49+
if tool.is_a?(Hash)
50+
ToolDefinition.from_hash(tool)
51+
elsif tool.is_a?(ToolDefinition)
52+
tool
53+
else
54+
raise ArgumentError, "tool must be a hash or a ToolDefinition was #{tool.class}"
55+
end
56+
end
57+
end
58+
4459
# this new api tries to create symmetry between responses and prompts
4560
# this means anything we get back from the model via endpoint can be easily appended
4661
def push_model_response(response)

0 commit comments

Comments
 (0)