raif 1.0.0 → 1.1.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 +4 -4
- data/README.md +200 -41
- data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
- data/app/controllers/raif/admin/application_controller.rb +14 -0
- data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
- data/app/controllers/raif/admin/stats_controller.rb +19 -0
- data/app/controllers/raif/admin/tasks_controller.rb +18 -2
- data/app/controllers/raif/conversations_controller.rb +5 -1
- data/app/models/raif/agent.rb +11 -9
- data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
- data/app/models/raif/agents/re_act_agent.rb +6 -0
- data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
- data/app/models/raif/concerns/json_schema_definition.rb +28 -0
- data/app/models/raif/concerns/llm_response_parsing.rb +23 -1
- data/app/models/raif/concerns/llm_temperature.rb +17 -0
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
- data/app/models/raif/concerns/llms/bedrock_claude/message_formatting.rb +70 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +41 -0
- data/app/models/raif/concerns/llms/open_ai/message_formatting.rb +41 -0
- data/app/models/raif/conversation.rb +11 -3
- data/app/models/raif/conversation_entry.rb +22 -6
- data/app/models/raif/embedding_model.rb +22 -0
- data/app/models/raif/embedding_models/bedrock_titan.rb +34 -0
- data/app/models/raif/embedding_models/open_ai.rb +40 -0
- data/app/models/raif/llm.rb +39 -6
- data/app/models/raif/llms/anthropic.rb +23 -28
- data/app/models/raif/llms/bedrock_claude.rb +33 -19
- data/app/models/raif/llms/open_ai.rb +14 -17
- data/app/models/raif/llms/open_router.rb +93 -0
- data/app/models/raif/model_completion.rb +21 -2
- data/app/models/raif/model_file_input.rb +113 -0
- data/app/models/raif/model_image_input.rb +4 -0
- data/app/models/raif/model_tool.rb +77 -51
- data/app/models/raif/model_tool_invocation.rb +8 -6
- data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
- data/app/models/raif/model_tools/fetch_url.rb +27 -36
- data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
- data/app/models/raif/task.rb +71 -16
- data/app/views/layouts/raif/admin.html.erb +10 -0
- data/app/views/raif/admin/agents/show.html.erb +3 -1
- data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
- data/app/views/raif/admin/conversations/show.html.erb +3 -1
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +1 -0
- data/app/views/raif/admin/model_completions/index.html.erb +1 -0
- data/app/views/raif/admin/model_completions/show.html.erb +30 -3
- data/app/views/raif/admin/stats/index.html.erb +128 -0
- data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
- data/app/views/raif/admin/tasks/_task.html.erb +5 -4
- data/app/views/raif/admin/tasks/index.html.erb +20 -2
- data/app/views/raif/admin/tasks/show.html.erb +3 -1
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +18 -14
- data/app/views/raif/conversation_entries/_form.html.erb +1 -1
- data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
- data/app/views/raif/conversation_entries/_message.html.erb +10 -3
- data/config/locales/admin.en.yml +14 -0
- data/config/locales/en.yml +25 -3
- data/config/routes.rb +6 -0
- data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
- data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
- data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
- data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
- data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -12
- data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
- data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +13 -11
- data/lib/generators/raif/install/templates/initializer.rb +50 -6
- data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
- data/lib/generators/raif/task/templates/task.rb.tt +34 -23
- data/lib/raif/configuration.rb +40 -3
- data/lib/raif/embedding_model_registry.rb +83 -0
- data/lib/raif/engine.rb +34 -1
- data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
- data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
- data/lib/raif/errors/unsupported_feature_error.rb +8 -0
- data/lib/raif/errors.rb +3 -2
- data/lib/raif/json_schema_builder.rb +104 -0
- data/lib/raif/llm_registry.rb +205 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +5 -32
- data/lib/tasks/raif_tasks.rake +9 -4
- metadata +32 -19
- data/lib/raif/default_llms.rb +0 -37
@@ -1,13 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::Llms::BedrockClaude < Raif::Llm
|
4
|
+
include Raif::Concerns::Llms::BedrockClaude::MessageFormatting
|
4
5
|
|
5
6
|
def perform_model_completion!(model_completion)
|
6
7
|
if Raif.config.aws_bedrock_model_name_prefix.present?
|
7
8
|
model_completion.model_api_name = "#{Raif.config.aws_bedrock_model_name_prefix}.#{model_completion.model_api_name}"
|
8
9
|
end
|
9
10
|
|
10
|
-
params =
|
11
|
+
params = build_request_parameters(model_completion)
|
11
12
|
resp = bedrock_client.converse(params)
|
12
13
|
|
13
14
|
model_completion.raw_response = if model_completion.response_format_json?
|
@@ -30,25 +31,41 @@ protected
|
|
30
31
|
@bedrock_client ||= Aws::BedrockRuntime::Client.new(region: Raif.config.aws_bedrock_region)
|
31
32
|
end
|
32
33
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
content: [{ text: message[:content] }]
|
38
|
-
}
|
39
|
-
end
|
40
|
-
end
|
34
|
+
def build_request_parameters(model_completion)
|
35
|
+
# The AWS Bedrock SDK requires symbols for keys
|
36
|
+
messages_param = model_completion.messages.map(&:deep_symbolize_keys)
|
37
|
+
replace_tmp_base64_data_with_bytes(messages_param)
|
41
38
|
|
42
|
-
def build_api_parameters(model_completion)
|
43
39
|
params = {
|
44
40
|
model_id: model_completion.model_api_name,
|
45
41
|
inference_config: { max_tokens: model_completion.max_completion_tokens || 8192 },
|
46
|
-
messages:
|
42
|
+
messages: messages_param
|
47
43
|
}
|
48
44
|
|
49
45
|
params[:system] = [{ text: model_completion.system_prompt }] if model_completion.system_prompt.present?
|
50
46
|
|
51
|
-
|
47
|
+
tool_config = build_tool_parameters(model_completion)
|
48
|
+
params[:tool_config] = tool_config if tool_config.present?
|
49
|
+
|
50
|
+
params
|
51
|
+
end
|
52
|
+
|
53
|
+
def replace_tmp_base64_data_with_bytes(messages)
|
54
|
+
# The AWS Bedrock SDK requires data sent as bytes (and doesn't support base64 like everyone else)
|
55
|
+
# The ModelCompletion stores the messages as JSON though, so it can't be raw bytes.
|
56
|
+
# We store the image data as base64, so we need to convert that to bytes before sending to AWS.
|
57
|
+
messages.each do |message|
|
58
|
+
message[:content].each do |content|
|
59
|
+
next unless content[:image] || content[:document]
|
60
|
+
|
61
|
+
type_key = content[:image] ? :image : :document
|
62
|
+
base64_data = content[type_key][:source].delete(:tmp_base64_data)
|
63
|
+
content[type_key][:source][:bytes] = Base64.strict_decode64(base64_data)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_tool_parameters(model_completion)
|
52
69
|
tools = []
|
53
70
|
|
54
71
|
# If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
|
@@ -71,14 +88,11 @@ protected
|
|
71
88
|
end
|
72
89
|
end
|
73
90
|
|
74
|
-
|
75
|
-
if tools.any?
|
76
|
-
params[:tool_config] = {
|
77
|
-
tools: tools.map { |tool| { tool_spec: tool } }
|
78
|
-
}
|
79
|
-
end
|
91
|
+
return if tools.blank?
|
80
92
|
|
81
|
-
|
93
|
+
{
|
94
|
+
tools: tools.map{|tool| { tool_spec: tool } }
|
95
|
+
}
|
82
96
|
end
|
83
97
|
|
84
98
|
def extract_text_response(resp)
|
@@ -1,29 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::Llms::OpenAi < Raif::Llm
|
4
|
+
include Raif::Concerns::Llms::OpenAi::MessageFormatting
|
4
5
|
|
5
6
|
def perform_model_completion!(model_completion)
|
6
7
|
model_completion.temperature ||= default_temperature
|
7
|
-
parameters =
|
8
|
+
parameters = build_request_parameters(model_completion)
|
8
9
|
|
9
10
|
response = connection.post("chat/completions") do |req|
|
10
|
-
req.body = parameters
|
11
|
+
req.body = parameters
|
11
12
|
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
# Handle API errors
|
16
|
-
unless response.success?
|
17
|
-
error_message = resp["error"]&.dig("message") || "OpenAI API error: #{response.status}"
|
18
|
-
raise Raif::Errors::OpenAi::ApiError, error_message
|
19
|
-
end
|
14
|
+
response_json = response.body
|
20
15
|
|
21
16
|
model_completion.update!(
|
22
|
-
response_tool_calls: extract_response_tool_calls(
|
23
|
-
raw_response:
|
24
|
-
completion_tokens:
|
25
|
-
prompt_tokens:
|
26
|
-
total_tokens:
|
17
|
+
response_tool_calls: extract_response_tool_calls(response_json),
|
18
|
+
raw_response: response_json.dig("choices", 0, "message", "content"),
|
19
|
+
completion_tokens: response_json.dig("usage", "completion_tokens"),
|
20
|
+
prompt_tokens: response_json.dig("usage", "prompt_tokens"),
|
21
|
+
total_tokens: response_json.dig("usage", "total_tokens"),
|
27
22
|
response_format_parameter: parameters.dig(:response_format, :type)
|
28
23
|
)
|
29
24
|
|
@@ -32,8 +27,10 @@ class Raif::Llms::OpenAi < Raif::Llm
|
|
32
27
|
|
33
28
|
def connection
|
34
29
|
@connection ||= Faraday.new(url: "https://api.openai.com/v1") do |f|
|
35
|
-
f.headers["Content-Type"] = "application/json"
|
36
30
|
f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
|
31
|
+
f.request :json
|
32
|
+
f.response :json
|
33
|
+
f.response :raise_error
|
37
34
|
end
|
38
35
|
end
|
39
36
|
|
@@ -88,7 +85,7 @@ private
|
|
88
85
|
end
|
89
86
|
end
|
90
87
|
|
91
|
-
def
|
88
|
+
def build_request_parameters(model_completion)
|
92
89
|
formatted_system_prompt = model_completion.system_prompt.to_s.strip
|
93
90
|
|
94
91
|
# If the response format is JSON, we need to include "as json" in the system prompt.
|
@@ -162,7 +159,7 @@ private
|
|
162
159
|
def supports_structured_outputs?
|
163
160
|
# Not all OpenAI models support structured outputs:
|
164
161
|
# https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat#supported-models
|
165
|
-
provider_settings[:supports_structured_outputs]
|
162
|
+
provider_settings.key?(:supports_structured_outputs) ? provider_settings[:supports_structured_outputs] : true
|
166
163
|
end
|
167
164
|
|
168
165
|
def validate_object_properties(schema, errors)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::Llms::OpenRouter < Raif::Llm
|
4
|
+
include Raif::Concerns::Llms::OpenAi::MessageFormatting
|
5
|
+
|
6
|
+
def perform_model_completion!(model_completion)
|
7
|
+
model_completion.temperature ||= default_temperature
|
8
|
+
parameters = build_request_parameters(model_completion)
|
9
|
+
response = connection.post("chat/completions") do |req|
|
10
|
+
req.body = parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
response_json = response.body
|
14
|
+
|
15
|
+
model_completion.update!(
|
16
|
+
response_tool_calls: extract_response_tool_calls(response_json),
|
17
|
+
raw_response: response_json.dig("choices", 0, "message", "content"),
|
18
|
+
completion_tokens: response_json.dig("usage", "completion_tokens"),
|
19
|
+
prompt_tokens: response_json.dig("usage", "prompt_tokens"),
|
20
|
+
total_tokens: response_json.dig("usage", "total_tokens")
|
21
|
+
)
|
22
|
+
|
23
|
+
model_completion
|
24
|
+
end
|
25
|
+
|
26
|
+
def connection
|
27
|
+
@connection ||= Faraday.new(url: "https://openrouter.ai/api/v1") do |f|
|
28
|
+
f.headers["Authorization"] = "Bearer #{Raif.config.open_router_api_key}"
|
29
|
+
f.headers["HTTP-Referer"] = Raif.config.open_router_site_url if Raif.config.open_router_site_url.present?
|
30
|
+
f.headers["X-Title"] = Raif.config.open_router_app_name if Raif.config.open_router_app_name.present?
|
31
|
+
f.request :json
|
32
|
+
f.response :json
|
33
|
+
f.response :raise_error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def build_request_parameters(model_completion)
|
40
|
+
params = {
|
41
|
+
model: model_completion.model_api_name,
|
42
|
+
messages: model_completion.messages,
|
43
|
+
temperature: model_completion.temperature.to_f,
|
44
|
+
max_tokens: model_completion.max_completion_tokens || default_max_completion_tokens,
|
45
|
+
stream: false
|
46
|
+
}
|
47
|
+
|
48
|
+
# Add system message to the messages array if present
|
49
|
+
if model_completion.system_prompt.present?
|
50
|
+
params[:messages].unshift({ "role" => "system", "content" => model_completion.system_prompt })
|
51
|
+
end
|
52
|
+
|
53
|
+
if model_completion.available_model_tools.any?
|
54
|
+
tools = []
|
55
|
+
|
56
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
57
|
+
tools << {
|
58
|
+
type: "function",
|
59
|
+
function: {
|
60
|
+
name: tool.tool_name,
|
61
|
+
description: tool.tool_description,
|
62
|
+
parameters: tool.tool_arguments_schema
|
63
|
+
}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
params[:tools] = tools
|
68
|
+
end
|
69
|
+
|
70
|
+
params
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_response_tool_calls(response_json)
|
74
|
+
tool_calls = response_json.dig("choices", 0, "message", "tool_calls")
|
75
|
+
return [] unless tool_calls.is_a?(Array)
|
76
|
+
|
77
|
+
tool_calls.map do |tool_call|
|
78
|
+
next unless tool_call["type"] == "function"
|
79
|
+
|
80
|
+
function = tool_call["function"]
|
81
|
+
next unless function.is_a?(Hash)
|
82
|
+
|
83
|
+
{
|
84
|
+
"id" => tool_call["id"],
|
85
|
+
"type" => "function",
|
86
|
+
"function" => {
|
87
|
+
"name" => function["name"],
|
88
|
+
"arguments" => function["arguments"]
|
89
|
+
}
|
90
|
+
}
|
91
|
+
end.compact
|
92
|
+
end
|
93
|
+
end
|
@@ -12,6 +12,7 @@ class Raif::ModelCompletion < Raif::ApplicationRecord
|
|
12
12
|
delegate :json_response_schema, to: :source, allow_nil: true
|
13
13
|
|
14
14
|
before_save :set_total_tokens
|
15
|
+
before_save :calculate_costs
|
15
16
|
|
16
17
|
after_initialize -> { self.messages ||= [] }
|
17
18
|
after_initialize -> { self.available_model_tools ||= [] }
|
@@ -20,9 +21,27 @@ class Raif::ModelCompletion < Raif::ApplicationRecord
|
|
20
21
|
source.json_response_schema if source&.respond_to?(:json_response_schema)
|
21
22
|
end
|
22
23
|
|
23
|
-
protected
|
24
|
-
|
25
24
|
def set_total_tokens
|
26
25
|
self.total_tokens ||= completion_tokens.present? && prompt_tokens.present? ? completion_tokens + prompt_tokens : nil
|
27
26
|
end
|
27
|
+
|
28
|
+
def calculate_costs
|
29
|
+
if prompt_tokens.present? && llm_config[:input_token_cost].present?
|
30
|
+
self.prompt_token_cost = llm_config[:input_token_cost] * prompt_tokens
|
31
|
+
end
|
32
|
+
|
33
|
+
if completion_tokens.present? && llm_config[:output_token_cost].present?
|
34
|
+
self.output_token_cost = llm_config[:output_token_cost] * completion_tokens
|
35
|
+
end
|
36
|
+
|
37
|
+
if prompt_token_cost.present? || output_token_cost.present?
|
38
|
+
self.total_cost = (prompt_token_cost || 0) + (output_token_cost || 0)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def llm_config
|
45
|
+
@llm_config ||= Raif.llm_config(llm_model_key.to_sym)
|
46
|
+
end
|
28
47
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelFileInput
|
4
|
+
include ActiveModel::Model
|
5
|
+
|
6
|
+
attr_accessor :input, :url, :base64_data, :filename, :content_type, :source_type
|
7
|
+
|
8
|
+
validates :url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: "is not a valid URL" }, allow_nil: true
|
9
|
+
validates :base64_data, presence: { message: "could not be read from input" }, if: -> { input.present? }
|
10
|
+
validates :content_type, presence: { message: "could not be determined" }, if: -> { input.present? }
|
11
|
+
|
12
|
+
def initialize(input: nil, url: nil)
|
13
|
+
raise ArgumentError, "You must provide either an input or a URL" if input.blank? && url.blank?
|
14
|
+
raise ArgumentError, "Provide either input or URL, not both" if input.present? && url.present?
|
15
|
+
|
16
|
+
super(input: input, url: url)
|
17
|
+
|
18
|
+
if url.present?
|
19
|
+
@source_type = :url
|
20
|
+
elsif input.present?
|
21
|
+
@source_type = :file_content
|
22
|
+
process_input!
|
23
|
+
end
|
24
|
+
|
25
|
+
unless valid?
|
26
|
+
raise Raif::Errors::InvalidModelFileInputError, errors.full_messages.join(", ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<#{self.class.name} input=#{input.inspect} url=#{url.inspect} base64_data=#{base64_data&.truncate(20)} filename=#{filename.inspect} content_type=#{content_type.inspect} source_type=#{source_type.inspect}>" # rubocop:disable Layout/LineLength
|
32
|
+
end
|
33
|
+
|
34
|
+
def file_bytes
|
35
|
+
Base64.strict_decode64(base64_data)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def process_input!
|
41
|
+
if input_is_active_storage_blob?
|
42
|
+
process_active_storage_blob(input)
|
43
|
+
return
|
44
|
+
elsif input_has_active_storage_blob?
|
45
|
+
process_active_storage_blob(input.blob)
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
case input
|
50
|
+
when String
|
51
|
+
process_string_input
|
52
|
+
when Pathname
|
53
|
+
process_from_path
|
54
|
+
when File, Tempfile, IO, StringIO
|
55
|
+
read_from_io
|
56
|
+
else
|
57
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid input type for #{self.class.name} (got #{input.class})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_string_input
|
62
|
+
if File.exist?(input)
|
63
|
+
@input = Pathname.new(input)
|
64
|
+
process_from_path
|
65
|
+
else
|
66
|
+
raise Raif::Errors::InvalidModelFileInputError, "File does not exist: #{input}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_active_storage_blob(blob)
|
71
|
+
@filename = blob.filename.to_s
|
72
|
+
@content_type = blob.content_type
|
73
|
+
@base64_data = Base64.strict_encode64(blob.download)
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_from_path
|
77
|
+
@filename = input.basename.to_s
|
78
|
+
@content_type = Marcel::MimeType.for(input)
|
79
|
+
@base64_data = Base64.strict_encode64(input.binread)
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_from_io
|
83
|
+
@filename = input.respond_to?(:path) ? File.basename(input.path) : nil
|
84
|
+
@content_type = Marcel::MimeType.for(input)
|
85
|
+
try_rewind
|
86
|
+
@base64_data = Base64.strict_encode64(input.read)
|
87
|
+
end
|
88
|
+
|
89
|
+
def try_rewind
|
90
|
+
input.rewind if input.respond_to?(:rewind)
|
91
|
+
rescue IOError => e
|
92
|
+
logger.error "Failed to rewind IO: #{e.message}"
|
93
|
+
logger.error e.backtrace.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
def input_looks_like_base64?
|
97
|
+
input.match?(%r{\A[a-zA-Z0-9+/\n\r]+={0,2}\z})
|
98
|
+
end
|
99
|
+
|
100
|
+
def input_has_active_storage_blob?
|
101
|
+
return false unless defined?(ActiveStorage)
|
102
|
+
return false unless input.respond_to?(:blob)
|
103
|
+
|
104
|
+
input.blob.is_a?(ActiveStorage::Blob)
|
105
|
+
end
|
106
|
+
|
107
|
+
def input_is_active_storage_blob?
|
108
|
+
return false unless defined?(ActiveStorage)
|
109
|
+
|
110
|
+
input.is_a?(ActiveStorage::Blob)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -1,69 +1,95 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTool
|
4
|
+
include Raif::Concerns::JsonSchemaDefinition
|
4
5
|
|
5
|
-
delegate :tool_name, :tool_description, :tool_arguments_schema, to: :class
|
6
|
+
delegate :tool_name, :tool_description, :tool_arguments_schema, :example_model_invocation, to: :class
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# The name of the tool as it will be provided to the model & used in the model invocation.
|
21
|
-
# Default for something like Raif::ModelTools::WikipediaSearch would be "wikipedia_search"
|
22
|
-
def self.tool_name
|
23
|
-
name.split("::").last.underscore
|
24
|
-
end
|
8
|
+
class << self
|
9
|
+
# The description of the tool that will be provided to the model
|
10
|
+
# when giving it a list of available tools.
|
11
|
+
def description_for_llm
|
12
|
+
<<~DESCRIPTION
|
13
|
+
Name: #{tool_name}
|
14
|
+
Description: #{tool_description}
|
15
|
+
Arguments Schema:
|
16
|
+
#{JSON.pretty_generate(tool_arguments_schema)}
|
17
|
+
Example Usage:
|
18
|
+
#{JSON.pretty_generate(example_model_invocation)}
|
19
|
+
DESCRIPTION
|
20
|
+
end
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
# The name of the tool as it will be provided to the model & used in the model invocation.
|
23
|
+
# Default for something like Raif::ModelTools::WikipediaSearch would be "wikipedia_search"
|
24
|
+
def tool_name
|
25
|
+
name.split("::").last.underscore
|
26
|
+
end
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
def tool_description(&block)
|
29
|
+
if block_given?
|
30
|
+
@tool_description = block.call
|
31
|
+
elsif @tool_description.present?
|
32
|
+
@tool_description
|
33
|
+
else
|
34
|
+
raise NotImplementedError, "#{name}#tool_description is not implemented"
|
35
|
+
end
|
36
|
+
end
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
def example_model_invocation(&block)
|
39
|
+
if block_given?
|
40
|
+
@example_model_invocation = block.call
|
41
|
+
elsif @example_model_invocation.present?
|
42
|
+
@example_model_invocation
|
43
|
+
else
|
44
|
+
raise NotImplementedError, "#{name}#example_model_invocation is not implemented"
|
45
|
+
end
|
46
|
+
end
|
37
47
|
|
38
|
-
|
39
|
-
|
40
|
-
|
48
|
+
def process_invocation(invocation)
|
49
|
+
raise NotImplementedError, "#{name}#process_invocation is not implemented"
|
50
|
+
end
|
41
51
|
|
42
|
-
|
43
|
-
|
44
|
-
|
52
|
+
def invocation_partial_name
|
53
|
+
name.gsub("Raif::ModelTools::", "").underscore
|
54
|
+
end
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
|
56
|
+
def tool_arguments_schema(&block)
|
57
|
+
if block_given?
|
58
|
+
json_schema_definition(:tool_arguments, &block)
|
59
|
+
elsif schema_defined?(:tool_arguments)
|
60
|
+
schema_for(:tool_arguments)
|
61
|
+
else
|
62
|
+
raise NotImplementedError,
|
63
|
+
"#{name} must define tool arguments schema via tool_arguments_schema or override #{name}.tool_arguments_schema"
|
64
|
+
end
|
65
|
+
end
|
49
66
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
tool_type: name,
|
54
|
-
tool_arguments: tool_arguments
|
55
|
-
)
|
67
|
+
def renderable?
|
68
|
+
true
|
69
|
+
end
|
56
70
|
|
57
|
-
|
58
|
-
|
59
|
-
process_invocation(tool_invocation)
|
60
|
-
tool_invocation.completed!
|
71
|
+
def triggers_observation_to_model?
|
72
|
+
false
|
61
73
|
end
|
62
74
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
75
|
+
def invoke_tool(tool_arguments:, source:)
|
76
|
+
tool_invocation = Raif::ModelToolInvocation.new(
|
77
|
+
source: source,
|
78
|
+
tool_type: name,
|
79
|
+
tool_arguments: tool_arguments
|
80
|
+
)
|
81
|
+
|
82
|
+
ActiveRecord::Base.transaction do
|
83
|
+
tool_invocation.save!
|
84
|
+
process_invocation(tool_invocation)
|
85
|
+
tool_invocation.completed!
|
86
|
+
end
|
87
|
+
|
88
|
+
tool_invocation
|
89
|
+
rescue StandardError => e
|
90
|
+
tool_invocation.failed!
|
91
|
+
raise e
|
92
|
+
end
|
67
93
|
end
|
68
94
|
|
69
95
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json-schema"
|
4
|
-
|
5
3
|
class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
6
4
|
belongs_to :source, polymorphic: true
|
7
5
|
|
@@ -11,7 +9,11 @@ class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
11
9
|
validates :tool_type, presence: true
|
12
10
|
validate :ensure_valid_tool_argument_schema, if: -> { tool_type.present? && tool_arguments_schema.present? }
|
13
11
|
|
14
|
-
delegate :tool_arguments_schema,
|
12
|
+
delegate :tool_arguments_schema,
|
13
|
+
:renderable?,
|
14
|
+
:tool_name,
|
15
|
+
:triggers_observation_to_model?,
|
16
|
+
to: :tool
|
15
17
|
|
16
18
|
boolean_timestamp :completed_at
|
17
19
|
boolean_timestamp :failed_at
|
@@ -25,9 +27,9 @@ class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def result_llm_message
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
return unless tool.respond_to?(:observation_for_invocation)
|
31
|
+
|
32
|
+
tool.observation_for_invocation(self)
|
31
33
|
end
|
32
34
|
|
33
35
|
def to_partial_path
|
@@ -1,46 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTools::AgentFinalAnswer < Raif::ModelTool
|
4
|
+
tool_arguments_schema do
|
5
|
+
string "final_answer", description: "Your complete and final answer to the user's question or task"
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
example_model_invocation do
|
6
9
|
{
|
7
10
|
"name" => tool_name,
|
8
11
|
"arguments" => { "final_answer": "The answer to the user's question or task" }
|
9
12
|
}
|
10
13
|
end
|
11
14
|
|
12
|
-
|
13
|
-
{
|
14
|
-
type: "object",
|
15
|
-
additionalProperties: false,
|
16
|
-
required: ["final_answer"],
|
17
|
-
properties: {
|
18
|
-
final_answer: {
|
19
|
-
type: "string",
|
20
|
-
description: "Your complete and final answer to the user's question or task"
|
21
|
-
}
|
22
|
-
}
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.tool_description
|
15
|
+
tool_description do
|
27
16
|
"Provide your final answer to the user's question or task"
|
28
17
|
end
|
29
18
|
|
30
|
-
|
31
|
-
|
19
|
+
class << self
|
20
|
+
def observation_for_invocation(tool_invocation)
|
21
|
+
return "No answer provided" unless tool_invocation.result.present?
|
32
22
|
|
33
|
-
|
34
|
-
|
23
|
+
tool_invocation.result["final_answer"]
|
24
|
+
end
|
35
25
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
26
|
+
def process_invocation(tool_invocation)
|
27
|
+
tool_invocation.update!(
|
28
|
+
result: {
|
29
|
+
final_answer: tool_invocation.tool_arguments["final_answer"]
|
30
|
+
}
|
31
|
+
)
|
42
32
|
|
43
|
-
|
33
|
+
tool_invocation.result
|
34
|
+
end
|
44
35
|
end
|
45
36
|
|
46
37
|
end
|