2024-11-11 16:14:30 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
RSpec.describe DiscourseAi::Completions::XmlToolProcessor do
|
|
|
|
let(:processor) { DiscourseAi::Completions::XmlToolProcessor.new }
|
|
|
|
|
|
|
|
it "can process simple text" do
|
|
|
|
result = []
|
|
|
|
result << (processor << "hello")
|
|
|
|
result << (processor << " world ")
|
|
|
|
expect(result).to eq([["hello"], [" world "]])
|
|
|
|
expect(processor.finish).to eq([])
|
|
|
|
expect(processor.should_cancel?).to eq(false)
|
|
|
|
end
|
|
|
|
|
2024-11-18 17:22:39 -05:00
|
|
|
it "can handle partial tool calls" do
|
|
|
|
processor = DiscourseAi::Completions::XmlToolProcessor.new(partial_tool_calls: true)
|
|
|
|
|
|
|
|
xml = (<<~XML).strip
|
|
|
|
<function|_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>h|ell|o<|/tool_name>
|
|
|
|
<parameters>
|
|
|
|
<hello>wo|r|ld</hello>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>tool|2</tool_name>
|
|
|
|
<parameters>
|
|
|
|
<param>v|alue</param>
|
|
|
|
<para|m2><![CDA|TA[va|lue2]]></param2>
|
|
|
|
</parame|ters>
|
|
|
|
</invoke>
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
xml.split("|").each { |part| result << (processor << part).map(&:dup) }
|
|
|
|
|
|
|
|
result << (processor.finish)
|
|
|
|
result.flatten!
|
|
|
|
|
|
|
|
tool1_params =
|
|
|
|
result
|
|
|
|
.select do |r|
|
|
|
|
r.is_a?(DiscourseAi::Completions::ToolCall) && r.name == "hello" && r.partial
|
|
|
|
end
|
|
|
|
.map(&:parameters)
|
|
|
|
|
|
|
|
expect(tool1_params).to eq([{ hello: "wo" }, { hello: "wor" }, { hello: "world" }])
|
|
|
|
|
|
|
|
tool2_params =
|
|
|
|
result
|
|
|
|
.select do |r|
|
|
|
|
r.is_a?(DiscourseAi::Completions::ToolCall) && r.name == "tool2" && r.partial
|
|
|
|
end
|
|
|
|
.map(&:parameters)
|
|
|
|
|
|
|
|
expect(tool2_params).to eq(
|
|
|
|
[
|
|
|
|
{ param: "v" },
|
|
|
|
{ param: "value" },
|
|
|
|
{ param: "value", param2: "va" },
|
|
|
|
{ param: "value", param2: "value2" },
|
|
|
|
],
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "can handle mix and match xml cause tool llms may not encode" do
|
2024-11-11 16:14:30 -05:00
|
|
|
xml = (<<~XML).strip
|
|
|
|
<function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>hello</tool_name>
|
2024-11-18 17:22:39 -05:00
|
|
|
<parameters>
|
|
|
|
<hello>world <sam>sam</sam></hello>
|
|
|
|
<test><![CDATA[</h1>\n</div>\n]]></test>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
tool_call = result.last.first
|
|
|
|
expect(tool_call.parameters).to eq(hello: "world <sam>sam</sam>", test: "</h1>\n</div>\n")
|
|
|
|
end
|
|
|
|
|
|
|
|
it "is usable for simple single message mode" do
|
|
|
|
xml = (<<~XML)
|
|
|
|
world <function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>hello</tool_name>
|
2024-11-11 16:14:30 -05:00
|
|
|
<parameters>
|
|
|
|
<hello>world</hello>
|
|
|
|
<test>value</test>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
2024-11-18 17:22:39 -05:00
|
|
|
result << (processor << "hello")
|
2024-11-11 16:14:30 -05:00
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
tool_call =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(
|
|
|
|
id: "tool_0",
|
|
|
|
name: "hello",
|
|
|
|
parameters: {
|
|
|
|
hello: "world",
|
|
|
|
test: "value",
|
|
|
|
},
|
|
|
|
)
|
2024-11-18 17:22:39 -05:00
|
|
|
expect(result).to eq([["hello"], [" world"], [tool_call]])
|
2024-11-11 16:14:30 -05:00
|
|
|
expect(processor.should_cancel?).to eq(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "handles multiple tool calls in sequence" do
|
|
|
|
xml = (<<~XML).strip
|
|
|
|
start
|
|
|
|
<function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>first_tool</tool_name>
|
|
|
|
<parameters>
|
|
|
|
<param1>value1</param1>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>second_tool</tool_name>
|
|
|
|
<parameters>
|
|
|
|
<param2>value2</param2>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
</function_calls>
|
|
|
|
end
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
first_tool =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(
|
|
|
|
id: "tool_0",
|
|
|
|
name: "first_tool",
|
|
|
|
parameters: {
|
|
|
|
param1: "value1",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
second_tool =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(
|
|
|
|
id: "tool_1",
|
|
|
|
name: "second_tool",
|
|
|
|
parameters: {
|
|
|
|
param2: "value2",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(result).to eq([["start"], [first_tool, second_tool]])
|
|
|
|
expect(processor.should_cancel?).to eq(true)
|
|
|
|
end
|
|
|
|
|
|
|
|
it "handles non-English parameters correctly" do
|
|
|
|
xml = (<<~XML).strip
|
|
|
|
こんにちは
|
|
|
|
<function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>translator</tool_name>
|
|
|
|
<parameters>
|
|
|
|
<text>世界</text>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
tool_call =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(
|
|
|
|
id: "tool_0",
|
|
|
|
name: "translator",
|
|
|
|
parameters: {
|
|
|
|
text: "世界",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(result).to eq([["こんにちは"], [tool_call]])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "processes input character by character" do
|
|
|
|
xml =
|
|
|
|
"hi<function_calls><invoke><tool_name>test</tool_name><parameters><p>v</p></parameters></invoke>"
|
|
|
|
|
|
|
|
result = []
|
|
|
|
xml.each_char { |char| result << (processor << char) }
|
|
|
|
result << processor.finish
|
|
|
|
|
|
|
|
tool_call =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(id: "tool_0", name: "test", parameters: { p: "v" })
|
|
|
|
|
|
|
|
filtered_result = result.reject(&:empty?)
|
|
|
|
expect(filtered_result).to eq([["h"], ["i"], [tool_call]])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "handles malformed XML gracefully" do
|
|
|
|
xml = (<<~XML).strip
|
|
|
|
text
|
|
|
|
<function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>test</tool_name>
|
|
|
|
<parameters>
|
|
|
|
<param>value
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
malformed
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
# Should just do its best to parse the XML
|
2024-11-18 17:22:39 -05:00
|
|
|
tool_call = DiscourseAi::Completions::ToolCall.new(id: "tool_0", name: "test", parameters: {})
|
2024-11-11 16:14:30 -05:00
|
|
|
expect(result).to eq([["text"], [tool_call]])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "correctly processes empty parameter sets" do
|
|
|
|
xml = (<<~XML).strip
|
|
|
|
hello
|
|
|
|
<function_calls>
|
|
|
|
<invoke>
|
|
|
|
<tool_name>no_params</tool_name>
|
|
|
|
<parameters>
|
|
|
|
</parameters>
|
|
|
|
</invoke>
|
|
|
|
XML
|
|
|
|
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor.finish)
|
|
|
|
|
|
|
|
tool_call =
|
|
|
|
DiscourseAi::Completions::ToolCall.new(id: "tool_0", name: "no_params", parameters: {})
|
|
|
|
|
|
|
|
expect(result).to eq([["hello"], [tool_call]])
|
|
|
|
end
|
|
|
|
|
|
|
|
it "properly handles cancelled processing" do
|
|
|
|
xml = "start<function_calls></function_calls>"
|
|
|
|
result = []
|
|
|
|
result << (processor << xml)
|
|
|
|
result << (processor << "more text")
|
|
|
|
result << processor.finish
|
|
|
|
|
|
|
|
expect(result).to eq([["start"], [], []])
|
|
|
|
expect(processor.should_cancel?).to eq(true)
|
|
|
|
end
|
|
|
|
end
|