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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +346 -43
  3. data/app/assets/builds/raif.css +26 -1
  4. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  5. data/app/assets/stylesheets/raif/loader.scss +27 -1
  6. data/app/controllers/raif/admin/application_controller.rb +14 -0
  7. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  8. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  9. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  10. data/app/controllers/raif/conversations_controller.rb +5 -1
  11. data/app/models/raif/agent.rb +11 -9
  12. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  13. data/app/models/raif/agents/re_act_agent.rb +6 -0
  14. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  15. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  16. data/app/models/raif/concerns/llm_response_parsing.rb +42 -14
  17. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  18. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  19. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
  20. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +70 -0
  21. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
  22. data/app/models/raif/concerns/llms/message_formatting.rb +42 -0
  23. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
  24. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +41 -0
  25. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
  26. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
  27. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
  28. data/app/models/raif/conversation.rb +28 -7
  29. data/app/models/raif/conversation_entry.rb +40 -8
  30. data/app/models/raif/embedding_model.rb +22 -0
  31. data/app/models/raif/embedding_models/bedrock.rb +34 -0
  32. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  33. data/app/models/raif/llm.rb +108 -9
  34. data/app/models/raif/llms/anthropic.rb +72 -57
  35. data/app/models/raif/llms/bedrock.rb +165 -0
  36. data/app/models/raif/llms/open_ai_base.rb +66 -0
  37. data/app/models/raif/llms/open_ai_completions.rb +100 -0
  38. data/app/models/raif/llms/open_ai_responses.rb +144 -0
  39. data/app/models/raif/llms/open_router.rb +88 -0
  40. data/app/models/raif/model_completion.rb +23 -2
  41. data/app/models/raif/model_file_input.rb +113 -0
  42. data/app/models/raif/model_image_input.rb +4 -0
  43. data/app/models/raif/model_tool.rb +82 -52
  44. data/app/models/raif/model_tool_invocation.rb +8 -6
  45. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  46. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  47. data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
  48. data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
  49. data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
  50. data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
  51. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  52. data/app/models/raif/streaming_responses/anthropic.rb +63 -0
  53. data/app/models/raif/streaming_responses/bedrock.rb +89 -0
  54. data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
  55. data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
  56. data/app/models/raif/task.rb +71 -16
  57. data/app/views/layouts/raif/admin.html.erb +10 -0
  58. data/app/views/raif/admin/agents/show.html.erb +3 -1
  59. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  60. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
  61. data/app/views/raif/admin/conversations/show.html.erb +4 -2
  62. data/app/views/raif/admin/model_completions/_model_completion.html.erb +8 -0
  63. data/app/views/raif/admin/model_completions/index.html.erb +2 -0
  64. data/app/views/raif/admin/model_completions/show.html.erb +58 -3
  65. data/app/views/raif/admin/stats/index.html.erb +128 -0
  66. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  67. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  68. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  69. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  70. data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
  71. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +22 -14
  72. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  73. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  74. data/app/views/raif/conversation_entries/_message.html.erb +14 -3
  75. data/config/locales/admin.en.yml +16 -0
  76. data/config/locales/en.yml +47 -3
  77. data/config/routes.rb +6 -0
  78. data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
  79. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  80. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  81. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  82. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  83. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  84. data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
  85. data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
  86. data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
  87. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  88. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  89. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  90. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  91. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  92. data/lib/generators/raif/conversation/templates/conversation.rb.tt +16 -14
  93. data/lib/generators/raif/install/templates/initializer.rb +62 -6
  94. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  95. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  96. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  97. data/lib/raif/configuration.rb +63 -4
  98. data/lib/raif/embedding_model_registry.rb +83 -0
  99. data/lib/raif/engine.rb +56 -7
  100. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  101. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  102. data/lib/raif/errors/streaming_error.rb +18 -0
  103. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  104. data/lib/raif/errors.rb +4 -2
  105. data/lib/raif/json_schema_builder.rb +104 -0
  106. data/lib/raif/llm_registry.rb +315 -0
  107. data/lib/raif/migration_checker.rb +74 -0
  108. data/lib/raif/utils/html_fragment_processor.rb +169 -0
  109. data/lib/raif/utils.rb +1 -0
  110. data/lib/raif/version.rb +1 -1
  111. data/lib/raif.rb +7 -32
  112. data/lib/tasks/raif_tasks.rake +9 -4
  113. metadata +62 -12
  114. data/app/models/raif/llms/bedrock_claude.rb +0 -134
  115. data/app/models/raif/llms/open_ai.rb +0 -259
  116. 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
- @selected_types = params[:task_types] || []
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: @selected_types) if @selected_types.present?
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
- head :bad_request unless Raif.config.conversation_types.include?(conversation_type_param)
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
@@ -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 -> { self.system_prompt ||= build_system_prompt }, on: :create
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 execute_agent_iteration"
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
- @available_model_tools_map ||= available_model_tools&.map do |tool_name|
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 ||= if response_format_json?
23
- json = raw_response.gsub("```json", "").gsub("```", "")
24
- JSON.parse(json)
44
+ @parsed_response = if response_format_json?
45
+ parse_json_response
25
46
  elsif response_format_html?
26
- html = raw_response.strip.gsub("```html", "").chomp("```")
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 clean_html_fragment(html)
34
- fragment = Nokogiri::HTML.fragment(html)
53
+ def parse_json_response
54
+ json = raw_response.gsub(/#{ASCII_CONTROL_CHARS}|^```json|```$/, "").strip
35
55
 
36
- fragment.traverse do |node|
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
- ActionController::Base.helpers.sanitize(fragment.to_html).strip
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