raif 1.0.0 → 1.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 +4 -4
- data/README.md +346 -43
- data/app/assets/builds/raif.css +26 -1
- data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
- data/app/assets/stylesheets/raif/loader.scss +27 -1
- 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 +42 -14
- 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/anthropic/tool_formatting.rb +56 -0
- data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +70 -0
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +42 -0
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
- data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +41 -0
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
- data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
- data/app/models/raif/conversation.rb +28 -7
- data/app/models/raif/conversation_entry.rb +40 -8
- data/app/models/raif/embedding_model.rb +22 -0
- data/app/models/raif/embedding_models/bedrock.rb +34 -0
- data/app/models/raif/embedding_models/open_ai.rb +40 -0
- data/app/models/raif/llm.rb +108 -9
- data/app/models/raif/llms/anthropic.rb +72 -57
- data/app/models/raif/llms/bedrock.rb +165 -0
- data/app/models/raif/llms/open_ai_base.rb +66 -0
- data/app/models/raif/llms/open_ai_completions.rb +100 -0
- data/app/models/raif/llms/open_ai_responses.rb +144 -0
- data/app/models/raif/llms/open_router.rb +88 -0
- data/app/models/raif/model_completion.rb +23 -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 +82 -52
- 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/provider_managed/base.rb +9 -0
- data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
- data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
- data/app/models/raif/streaming_responses/anthropic.rb +63 -0
- data/app/models/raif/streaming_responses/bedrock.rb +89 -0
- data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
- data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
- 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/_conversation_entry.html.erb +48 -0
- data/app/views/raif/admin/conversations/show.html.erb +4 -2
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +8 -0
- data/app/views/raif/admin/model_completions/index.html.erb +2 -0
- data/app/views/raif/admin/model_completions/show.html.erb +58 -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/_citations.html.erb +9 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +22 -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 +14 -3
- data/config/locales/admin.en.yml +16 -0
- data/config/locales/en.yml +47 -3
- data/config/routes.rb +6 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
- 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/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
- data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
- data/db/migrate/20250603202013_add_stream_response_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 +16 -14
- data/lib/generators/raif/install/templates/initializer.rb +62 -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 +63 -4
- data/lib/raif/embedding_model_registry.rb +83 -0
- data/lib/raif/engine.rb +56 -7
- 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/streaming_error.rb +18 -0
- data/lib/raif/errors/unsupported_feature_error.rb +8 -0
- data/lib/raif/errors.rb +4 -2
- data/lib/raif/json_schema_builder.rb +104 -0
- data/lib/raif/llm_registry.rb +315 -0
- data/lib/raif/migration_checker.rb +74 -0
- data/lib/raif/utils/html_fragment_processor.rb +169 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +7 -32
- data/lib/tasks/raif_tasks.rake +9 -4
- metadata +62 -12
- data/app/models/raif/llms/bedrock_claude.rb +0 -134
- data/app/models/raif/llms/open_ai.rb +0 -259
- data/lib/raif/default_llms.rb +0 -37
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Admin
|
5
|
+
module Stats
|
6
|
+
class TasksController < Raif::Admin::ApplicationController
|
7
|
+
def index
|
8
|
+
@selected_period = params[:period] || "day"
|
9
|
+
@time_range = get_time_range(@selected_period)
|
10
|
+
|
11
|
+
@task_count = Raif::Task.where(created_at: @time_range).count
|
12
|
+
|
13
|
+
# Get task counts by type
|
14
|
+
@task_counts_by_type = Raif::Task.where(created_at: @time_range).group(:type).count
|
15
|
+
|
16
|
+
# Get costs by task type
|
17
|
+
@task_costs_by_type = Raif::Task.joins(:raif_model_completion)
|
18
|
+
.where(created_at: @time_range)
|
19
|
+
.group(:type)
|
20
|
+
.sum("raif_model_completions.total_cost")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Admin
|
5
|
+
class StatsController < Raif::Admin::ApplicationController
|
6
|
+
def index
|
7
|
+
@selected_period = params[:period] || "day"
|
8
|
+
@time_range = get_time_range(@selected_period)
|
9
|
+
|
10
|
+
@model_completion_count = Raif::ModelCompletion.where(created_at: @time_range).count
|
11
|
+
@model_completion_total_cost = Raif::ModelCompletion.where(created_at: @time_range).sum(:total_cost)
|
12
|
+
@task_count = Raif::Task.where(created_at: @time_range).count
|
13
|
+
@conversation_count = Raif::Conversation.where(created_at: @time_range).count
|
14
|
+
@conversation_entry_count = Raif::ConversationEntry.where(created_at: @time_range).count
|
15
|
+
@agent_count = Raif::Agent.where(created_at: @time_range).count
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -7,10 +7,26 @@ module Raif
|
|
7
7
|
|
8
8
|
def index
|
9
9
|
@task_types = Raif::Task.distinct.pluck(:type)
|
10
|
-
@
|
10
|
+
@selected_type = params[:task_types].present? ? params[:task_types] : "all"
|
11
|
+
|
12
|
+
@task_statuses = [:all, :completed, :failed, :in_progress, :pending]
|
13
|
+
@selected_statuses = params[:task_statuses].present? ? params[:task_statuses].to_sym : :all
|
11
14
|
|
12
15
|
tasks = Raif::Task.order(created_at: :desc)
|
13
|
-
tasks = tasks.where(type: @
|
16
|
+
tasks = tasks.where(type: @selected_type) if @selected_type.present? && @selected_type != "all"
|
17
|
+
|
18
|
+
if @selected_statuses.present? && @selected_statuses != :all
|
19
|
+
case @selected_statuses
|
20
|
+
when :completed
|
21
|
+
tasks = tasks.completed
|
22
|
+
when :failed
|
23
|
+
tasks = tasks.failed
|
24
|
+
when :in_progress
|
25
|
+
tasks = tasks.in_progress
|
26
|
+
when :pending
|
27
|
+
tasks = tasks.pending
|
28
|
+
end
|
29
|
+
end
|
14
30
|
|
15
31
|
@pagy, @tasks = pagy(tasks)
|
16
32
|
end
|
@@ -34,7 +34,11 @@ private
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def validate_conversation_type
|
37
|
-
|
37
|
+
unless Raif.config.conversation_types.include?(conversation_type_param)
|
38
|
+
logger.error("Invalid Raif conversation type - not in Raif.config.conversation_types: #{conversation_type_param}")
|
39
|
+
logger.debug("\n\n\e[33m!!! Make sure to add the conversation type in Raif.config.conversation_types\e[0m\n")
|
40
|
+
head :bad_request
|
41
|
+
end
|
38
42
|
end
|
39
43
|
|
40
44
|
def raif_conversation_type
|
data/app/models/raif/agent.rb
CHANGED
@@ -22,14 +22,12 @@ module Raif
|
|
22
22
|
validates :task, presence: true
|
23
23
|
validates :system_prompt, presence: true
|
24
24
|
validates :max_iterations, presence: true, numericality: { greater_than: 0 }
|
25
|
-
validates :available_model_tools, length: {
|
26
|
-
minimum: 1,
|
27
|
-
message: ->(_object, _data) {
|
28
|
-
I18n.t("raif.agents.errors.available_model_tools.too_short")
|
29
|
-
}
|
30
|
-
}
|
31
25
|
|
32
|
-
before_validation -> {
|
26
|
+
before_validation -> {
|
27
|
+
populate_default_model_tools
|
28
|
+
self.system_prompt ||= build_system_prompt
|
29
|
+
},
|
30
|
+
on: :create
|
33
31
|
|
34
32
|
attr_accessor :on_conversation_history_entry
|
35
33
|
|
@@ -59,7 +57,6 @@ module Raif
|
|
59
57
|
def run!(&block)
|
60
58
|
self.on_conversation_history_entry = block_given? ? block : nil
|
61
59
|
self.started_at = Time.current
|
62
|
-
self.available_model_tools += ["Raif::ModelTools::AgentFinalAnswer"] unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
|
63
60
|
save!
|
64
61
|
|
65
62
|
logger.debug <<~DEBUG
|
@@ -111,8 +108,12 @@ module Raif
|
|
111
108
|
|
112
109
|
private
|
113
110
|
|
111
|
+
def populate_default_model_tools
|
112
|
+
# no-op by default. Can be overridden by subclasses to add default model tools
|
113
|
+
end
|
114
|
+
|
114
115
|
def process_iteration_model_completion(model_completion)
|
115
|
-
raise NotImplementedError, "#{self.class.name} must implement
|
116
|
+
raise NotImplementedError, "#{self.class.name} must implement process_iteration_model_completion"
|
116
117
|
end
|
117
118
|
|
118
119
|
def native_model_tools
|
@@ -122,6 +123,7 @@ module Raif
|
|
122
123
|
def add_conversation_history_entry(entry)
|
123
124
|
entry_stringified = entry.stringify_keys
|
124
125
|
conversation_history << entry_stringified
|
126
|
+
save!
|
125
127
|
on_conversation_history_entry.call(entry_stringified) if on_conversation_history_entry.present?
|
126
128
|
end
|
127
129
|
|
@@ -4,6 +4,16 @@ module Raif
|
|
4
4
|
module Agents
|
5
5
|
class NativeToolCallingAgent < Raif::Agent
|
6
6
|
validate :ensure_llm_supports_native_tool_use
|
7
|
+
validates :available_model_tools, length: {
|
8
|
+
minimum: 2,
|
9
|
+
message: ->(_object, _data) {
|
10
|
+
I18n.t("raif.agents.native_tool_calling_agent.errors.available_model_tools.too_short")
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
before_validation -> {
|
15
|
+
available_model_tools << "Raif::ModelTools::AgentFinalAnswer" unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
|
16
|
+
}
|
7
17
|
|
8
18
|
def build_system_prompt
|
9
19
|
<<~PROMPT.strip
|
@@ -49,7 +59,7 @@ module Raif
|
|
49
59
|
if model_completion.response_tool_calls.blank?
|
50
60
|
add_conversation_history_entry({
|
51
61
|
role: "assistant",
|
52
|
-
content: "<observation>Error: No tool call found. I need make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>" # rubocop:disable Layout/LineLength
|
62
|
+
content: "<observation>Error: No tool call found. I need to make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>" # rubocop:disable Layout/LineLength
|
53
63
|
})
|
54
64
|
return
|
55
65
|
end
|
@@ -3,6 +3,12 @@
|
|
3
3
|
module Raif
|
4
4
|
module Agents
|
5
5
|
class ReActAgent < Raif::Agent
|
6
|
+
validates :available_model_tools, length: {
|
7
|
+
minimum: 1,
|
8
|
+
message: ->(_object, _data) {
|
9
|
+
I18n.t("raif.agents.re_act_agent.errors.available_model_tools.too_short")
|
10
|
+
}
|
11
|
+
}
|
6
12
|
|
7
13
|
def build_system_prompt
|
8
14
|
<<~PROMPT.strip
|
@@ -4,7 +4,7 @@ module Raif::Concerns::HasAvailableModelTools
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
def available_model_tools_map
|
7
|
-
|
7
|
+
available_model_tools&.map do |tool_name|
|
8
8
|
tool_klass = tool_name.is_a?(String) ? tool_name.constantize : tool_name
|
9
9
|
[tool_klass.tool_name, tool_klass]
|
10
10
|
end.to_h
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module Concerns
|
5
|
+
module JsonSchemaDefinition
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def json_schema_definition(schema_name, &block)
|
10
|
+
raise ArgumentError, "A block must be provided to define the JSON schema" unless block_given?
|
11
|
+
|
12
|
+
@schemas ||= {}
|
13
|
+
@schemas[schema_name] = Raif::JsonSchemaBuilder.new
|
14
|
+
@schemas[schema_name].instance_eval(&block)
|
15
|
+
@schemas[schema_name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def schema_defined?(schema_name)
|
19
|
+
@schemas&.dig(schema_name).present?
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema_for(schema_name)
|
23
|
+
@schemas[schema_name].to_schema
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -3,12 +3,33 @@
|
|
3
3
|
module Raif::Concerns::LlmResponseParsing
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
ASCII_CONTROL_CHARS = /[\x00-\x1f\x7f]/
|
7
|
+
|
6
8
|
included do
|
7
9
|
normalizes :raw_response, with: ->(text){ text&.strip }
|
8
10
|
|
9
11
|
enum :response_format, Raif::Llm.valid_response_formats, prefix: true
|
10
12
|
|
11
13
|
validates :response_format, presence: true, inclusion: { in: response_formats.keys }
|
14
|
+
|
15
|
+
class_attribute :allowed_tags
|
16
|
+
class_attribute :allowed_attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
class_methods do
|
20
|
+
def llm_response_format(format)
|
21
|
+
raise ArgumentError, "response_format must be one of: #{response_formats.keys.join(", ")}" unless response_formats.keys.include?(format.to_s)
|
22
|
+
|
23
|
+
after_initialize -> { self.response_format = format }, if: :new_record?
|
24
|
+
end
|
25
|
+
|
26
|
+
def llm_response_allowed_tags(tags)
|
27
|
+
self.allowed_tags = tags
|
28
|
+
end
|
29
|
+
|
30
|
+
def llm_response_allowed_attributes(attributes)
|
31
|
+
self.allowed_attributes = attributes
|
32
|
+
end
|
12
33
|
end
|
13
34
|
|
14
35
|
# Parses the response from the LLM into a structured format, based on the response_format.
|
@@ -16,29 +37,36 @@ module Raif::Concerns::LlmResponseParsing
|
|
16
37
|
# If the response format is HTML, it will be sanitized via ActionController::Base.helpers.sanitize.
|
17
38
|
#
|
18
39
|
# @return [Object] The parsed response.
|
19
|
-
def parsed_response
|
40
|
+
def parsed_response(force_reparse: false)
|
20
41
|
return if raw_response.blank?
|
42
|
+
return @parsed_response if @parsed_response.present? && !force_reparse
|
21
43
|
|
22
|
-
@parsed_response
|
23
|
-
|
24
|
-
JSON.parse(json)
|
44
|
+
@parsed_response = if response_format_json?
|
45
|
+
parse_json_response
|
25
46
|
elsif response_format_html?
|
26
|
-
|
27
|
-
clean_html_fragment(html)
|
47
|
+
parse_html_response
|
28
48
|
else
|
29
49
|
raw_response.strip
|
30
50
|
end
|
31
51
|
end
|
32
52
|
|
33
|
-
def
|
34
|
-
|
53
|
+
def parse_json_response
|
54
|
+
json = raw_response.gsub(/#{ASCII_CONTROL_CHARS}|^```json|```$/, "").strip
|
35
55
|
|
36
|
-
|
37
|
-
if node.text? && node.text.strip.empty?
|
38
|
-
node.remove
|
39
|
-
end
|
40
|
-
end
|
56
|
+
raise JSON::ParserError, "Invalid JSON" if json.blank?
|
41
57
|
|
42
|
-
|
58
|
+
JSON.parse(json)
|
43
59
|
end
|
60
|
+
|
61
|
+
def parse_html_response
|
62
|
+
html = raw_response.strip.gsub("```html", "").chomp("```")
|
63
|
+
|
64
|
+
html_with_converted_links = Raif::Utils::HtmlFragmentProcessor.convert_markdown_links_to_html(html)
|
65
|
+
Raif::Utils::HtmlFragmentProcessor.clean_html_fragment(
|
66
|
+
html_with_converted_links,
|
67
|
+
allowed_tags: allowed_tags,
|
68
|
+
allowed_attributes: allowed_attributes
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
44
72
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::LlmTemperature
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class_attribute :temperature, instance_writer: false
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def llm_temperature(temperature)
|
12
|
+
raise ArgumentError, "temperature must be a number between 0 and 1" unless temperature.is_a?(Numeric) && temperature.between?(0, 1)
|
13
|
+
|
14
|
+
self.temperature = temperature
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Anthropic::MessageFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def format_model_image_input_message(image_input)
|
7
|
+
if image_input.source_type == :url
|
8
|
+
{
|
9
|
+
"type" => "image",
|
10
|
+
"source" => {
|
11
|
+
"type" => "url",
|
12
|
+
"url" => image_input.url
|
13
|
+
}
|
14
|
+
}
|
15
|
+
elsif image_input.source_type == :file_content
|
16
|
+
{
|
17
|
+
"type" => "image",
|
18
|
+
"source" => {
|
19
|
+
"type" => "base64",
|
20
|
+
"media_type" => image_input.content_type,
|
21
|
+
"data" => image_input.base64_data
|
22
|
+
}
|
23
|
+
}
|
24
|
+
else
|
25
|
+
raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_model_file_input_message(file_input)
|
30
|
+
if file_input.source_type == :url
|
31
|
+
{
|
32
|
+
"type" => "document",
|
33
|
+
"source" => {
|
34
|
+
"type" => "url",
|
35
|
+
"url" => file_input.url
|
36
|
+
}
|
37
|
+
}
|
38
|
+
elsif file_input.source_type == :file_content
|
39
|
+
{
|
40
|
+
"type" => "document",
|
41
|
+
"source" => {
|
42
|
+
"type" => "base64",
|
43
|
+
"media_type" => file_input.content_type,
|
44
|
+
"data" => file_input.base64_data
|
45
|
+
}
|
46
|
+
}
|
47
|
+
else
|
48
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Anthropic::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
tools = []
|
8
|
+
|
9
|
+
# If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
|
10
|
+
if model_completion.response_format_json? && model_completion.json_response_schema.present?
|
11
|
+
tools << {
|
12
|
+
name: "json_response",
|
13
|
+
description: "Generate a structured JSON response based on the provided schema.",
|
14
|
+
input_schema: model_completion.json_response_schema
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
# If we support native tool use and have tools available, add them to the request
|
19
|
+
if supports_native_tool_use? && model_completion.available_model_tools.any?
|
20
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
21
|
+
tools << if tool.provider_managed?
|
22
|
+
format_provider_managed_tool(tool)
|
23
|
+
else
|
24
|
+
{
|
25
|
+
name: tool.tool_name,
|
26
|
+
description: tool.tool_description,
|
27
|
+
input_schema: tool.tool_arguments_schema
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
tools
|
34
|
+
end
|
35
|
+
|
36
|
+
def format_provider_managed_tool(tool)
|
37
|
+
validate_provider_managed_tool_support!(tool)
|
38
|
+
|
39
|
+
case tool.name
|
40
|
+
when "Raif::ModelTools::ProviderManaged::WebSearch"
|
41
|
+
{
|
42
|
+
type: "web_search_20250305",
|
43
|
+
name: "web_search",
|
44
|
+
max_uses: 5
|
45
|
+
}
|
46
|
+
when "Raif::ModelTools::ProviderManaged::CodeExecution"
|
47
|
+
{
|
48
|
+
type: "code_execution_20250522",
|
49
|
+
name: "code_execution"
|
50
|
+
}
|
51
|
+
else
|
52
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
53
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Bedrock::MessageFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def format_string_message(content, role: nil)
|
7
|
+
{ "text" => content }
|
8
|
+
end
|
9
|
+
|
10
|
+
def format_model_image_input_message(image_input)
|
11
|
+
if image_input.source_type == :url
|
12
|
+
raise Raif::Errors::UnsupportedFeatureError, "AWS Bedrock does not support providing an image by URL"
|
13
|
+
elsif image_input.source_type == :file_content
|
14
|
+
# The AWS Bedrock SDK requires data sent as bytes (and doesn't support base64 like everyone else)
|
15
|
+
# The ModelCompletion stores the messages as JSON though, so it can't be raw bytes (it will throw an encoding error).
|
16
|
+
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::Bedrock#perform_model_completion!
|
17
|
+
# before sending to AWS.
|
18
|
+
{
|
19
|
+
"image" => {
|
20
|
+
"format" => format_for_content_type(image_input.content_type),
|
21
|
+
"source" => {
|
22
|
+
"tmp_base64_data" => image_input.base64_data
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
else
|
27
|
+
raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_model_file_input_message(file_input)
|
32
|
+
if file_input.source_type == :url
|
33
|
+
raise Raif::Errors::UnsupportedFeatureError, "AWS Bedrock does not support providing a file by URL"
|
34
|
+
elsif file_input.source_type == :file_content
|
35
|
+
# The AWS Bedrock SDK requires data sent as bytes (and doesn't support base64 like everyone else)
|
36
|
+
# The ModelCompletion stores the messages as JSON though, so it can't be raw bytes (it will throw an encoding error).
|
37
|
+
# We store the image data as base64 and then it will get converted to bytes in Raif::Llms::Bedrock#perform_model_completion!
|
38
|
+
# before sending to AWS.
|
39
|
+
{
|
40
|
+
"document" => {
|
41
|
+
"format" => format_for_content_type(file_input.content_type),
|
42
|
+
"name" => File.basename(file_input.filename, File.extname(file_input.filename)), # AWS requires a filename and it cannot include dots from the extension # rubocop:disable Layout/LineLength
|
43
|
+
"source" => {
|
44
|
+
"tmp_base64_data" => file_input.base64_data
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
else
|
49
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def format_for_content_type(content_type)
|
54
|
+
{
|
55
|
+
"image/png" => "png",
|
56
|
+
"image/jpeg" => "jpeg",
|
57
|
+
"image/gif" => "gif",
|
58
|
+
"image/webp" => "webp",
|
59
|
+
"application/pdf" => "pdf",
|
60
|
+
"text/csv" => "csv",
|
61
|
+
"application/msword" => "doc",
|
62
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
|
63
|
+
"application/vnd.ms-excel" => "xls",
|
64
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xlsx",
|
65
|
+
"text/html" => "html",
|
66
|
+
"text/plain" => "txt",
|
67
|
+
"text/markdown" => "md"
|
68
|
+
}[content_type]
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::Bedrock::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
tools = []
|
8
|
+
|
9
|
+
# If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
|
10
|
+
if model_completion.response_format_json? && model_completion.json_response_schema.present?
|
11
|
+
tools << {
|
12
|
+
name: "json_response",
|
13
|
+
description: "Generate a structured JSON response based on the provided schema.",
|
14
|
+
input_schema: { json: model_completion.json_response_schema }
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
19
|
+
tools << if tool.provider_managed?
|
20
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
21
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
22
|
+
else
|
23
|
+
{
|
24
|
+
name: tool.tool_name,
|
25
|
+
description: tool.tool_description,
|
26
|
+
input_schema: { json: tool.tool_arguments_schema }
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
return {} if tools.blank?
|
32
|
+
|
33
|
+
{
|
34
|
+
tools: tools.map{|tool| { tool_spec: tool } }
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::MessageFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def format_messages(messages)
|
7
|
+
messages.map do |message|
|
8
|
+
role = message["role"] || message[:role]
|
9
|
+
{
|
10
|
+
"role" => role,
|
11
|
+
"content" => format_message_content(message["content"] || message[:content], role: role)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Content could be a string or an array.
|
17
|
+
# If it's an array, it could contain Raif::ModelImageInput or Raif::ModelFileInput objects,
|
18
|
+
# which need to be formatted according to each model provider's API.
|
19
|
+
def format_message_content(content, role: nil)
|
20
|
+
raise ArgumentError,
|
21
|
+
"Message content must be an array or a string. Content was: #{content.inspect}" unless content.is_a?(Array) || content.is_a?(String)
|
22
|
+
|
23
|
+
return [format_string_message(content, role: role)] if content.is_a?(String)
|
24
|
+
|
25
|
+
content.map do |item|
|
26
|
+
if item.is_a?(Raif::ModelImageInput)
|
27
|
+
format_model_image_input_message(item)
|
28
|
+
elsif item.is_a?(Raif::ModelFileInput)
|
29
|
+
format_model_file_input_message(item)
|
30
|
+
elsif item.is_a?(String)
|
31
|
+
format_string_message(item, role: role)
|
32
|
+
else
|
33
|
+
item
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_string_message(content, role: nil)
|
39
|
+
{ "type" => "text", "text" => content }
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|