raif 1.3.0 → 1.4.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -5
  3. data/app/assets/builds/raif.css +4 -1
  4. data/app/assets/builds/raif_admin.css +13 -1
  5. data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
  6. data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
  7. data/app/assets/stylesheets/raif/conversations.scss +3 -0
  8. data/app/assets/stylesheets/raif.scss +2 -1
  9. data/app/controllers/raif/admin/application_controller.rb +16 -0
  10. data/app/controllers/raif/admin/configs_controller.rb +94 -0
  11. data/app/controllers/raif/admin/model_completions_controller.rb +18 -1
  12. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
  13. data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
  14. data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
  15. data/app/controllers/raif/admin/stats_controller.rb +32 -3
  16. data/app/controllers/raif/conversation_entries_controller.rb +1 -0
  17. data/app/controllers/raif/conversations_controller.rb +10 -2
  18. data/app/jobs/raif/conversation_entry_job.rb +8 -6
  19. data/app/models/raif/admin/task_stat.rb +7 -0
  20. data/app/models/raif/agent.rb +63 -2
  21. data/app/models/raif/agents/native_tool_calling_agent.rb +101 -56
  22. data/app/models/raif/application_record.rb +18 -0
  23. data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
  24. data/app/models/raif/concerns/json_schema_definition.rb +40 -5
  25. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +28 -0
  26. data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
  27. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +4 -0
  28. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +36 -0
  29. data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
  30. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +4 -0
  31. data/app/models/raif/concerns/llms/google/message_formatting.rb +109 -0
  32. data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
  33. data/app/models/raif/concerns/llms/google/tool_formatting.rb +72 -0
  34. data/app/models/raif/concerns/llms/message_formatting.rb +11 -5
  35. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
  36. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
  37. data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
  38. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +4 -0
  39. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
  40. data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
  41. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +4 -0
  42. data/app/models/raif/concerns/run_with.rb +127 -0
  43. data/app/models/raif/conversation.rb +91 -8
  44. data/app/models/raif/conversation_entry.rb +32 -1
  45. data/app/models/raif/embedding_model.rb +2 -1
  46. data/app/models/raif/embedding_models/open_ai.rb +1 -1
  47. data/app/models/raif/llm.rb +27 -2
  48. data/app/models/raif/llms/anthropic.rb +7 -19
  49. data/app/models/raif/llms/bedrock.rb +6 -20
  50. data/app/models/raif/llms/google.rb +140 -0
  51. data/app/models/raif/llms/open_ai_base.rb +19 -5
  52. data/app/models/raif/llms/open_ai_completions.rb +6 -11
  53. data/app/models/raif/llms/open_ai_responses.rb +6 -16
  54. data/app/models/raif/llms/open_router.rb +7 -13
  55. data/app/models/raif/model_completion.rb +61 -0
  56. data/app/models/raif/model_tool.rb +10 -2
  57. data/app/models/raif/model_tool_invocation.rb +38 -6
  58. data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
  59. data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
  60. data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
  61. data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
  62. data/app/models/raif/streaming_responses/google.rb +71 -0
  63. data/app/models/raif/task.rb +55 -12
  64. data/app/models/raif/user_tool_invocation.rb +19 -0
  65. data/app/views/layouts/raif/admin.html.erb +12 -1
  66. data/app/views/raif/admin/agents/_agent.html.erb +8 -0
  67. data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
  68. data/app/views/raif/admin/agents/index.html.erb +2 -0
  69. data/app/views/raif/admin/agents/show.html.erb +46 -1
  70. data/app/views/raif/admin/configs/show.html.erb +117 -0
  71. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
  72. data/app/views/raif/admin/conversations/show.html.erb +2 -0
  73. data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
  74. data/app/views/raif/admin/model_completions/index.html.erb +26 -0
  75. data/app/views/raif/admin/model_completions/show.html.erb +124 -61
  76. data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
  77. data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
  78. data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
  79. data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
  80. data/app/views/raif/admin/stats/index.html.erb +71 -88
  81. data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
  82. data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
  83. data/app/views/raif/admin/tasks/index.html.erb +6 -1
  84. data/app/views/raif/admin/tasks/show.html.erb +36 -3
  85. data/app/views/raif/conversation_entries/_form.html.erb +3 -0
  86. data/app/views/raif/conversations/_conversation.html.erb +10 -0
  87. data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
  88. data/app/views/raif/conversations/index.html.erb +23 -0
  89. data/config/locales/admin.en.yml +33 -1
  90. data/config/locales/en.yml +33 -4
  91. data/config/routes.rb +2 -0
  92. data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
  93. data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
  94. data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
  95. data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
  96. data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
  97. data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
  98. data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
  99. data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
  100. data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
  101. data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
  102. data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
  103. data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
  104. data/lib/generators/raif/agent/templates/agent.rb.tt +1 -1
  105. data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -1
  106. data/lib/generators/raif/conversation/templates/conversation.rb.tt +6 -0
  107. data/lib/generators/raif/install/templates/initializer.rb +78 -10
  108. data/lib/generators/raif/task/templates/task.rb.tt +1 -1
  109. data/lib/raif/configuration.rb +37 -2
  110. data/lib/raif/engine.rb +8 -0
  111. data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
  112. data/lib/raif/errors/streaming_error.rb +6 -3
  113. data/lib/raif/errors.rb +1 -0
  114. data/lib/raif/evals/llm_judge.rb +2 -2
  115. data/lib/raif/evals/llm_judges/binary.rb +3 -3
  116. data/lib/raif/evals/llm_judges/comparative.rb +3 -3
  117. data/lib/raif/evals/llm_judges/scored.rb +1 -1
  118. data/lib/raif/evals/llm_judges/summarization.rb +2 -2
  119. data/lib/raif/evals/run.rb +1 -0
  120. data/lib/raif/json_schema_builder.rb +14 -0
  121. data/lib/raif/llm_registry.rb +207 -37
  122. data/lib/raif/messages.rb +180 -0
  123. data/lib/raif/version.rb +1 -1
  124. data/lib/raif.rb +9 -0
  125. data/lib/tasks/annotate_rb.rake +10 -0
  126. data/spec/support/rspec_helpers.rb +8 -8
  127. metadata +44 -9
  128. data/app/models/raif/agents/re_act_agent.rb +0 -127
  129. data/app/models/raif/agents/re_act_step.rb +0 -32
  130. data/app/models/raif/concerns/task_run_args.rb +0 -62
@@ -1,5 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # == Schema Information
4
+ #
5
+ # Table name: raif_agents
6
+ #
7
+ # id :bigint not null, primary key
8
+ # available_model_tools :jsonb not null
9
+ # completed_at :datetime
10
+ # conversation_history :jsonb not null
11
+ # creator_type :string not null
12
+ # failed_at :datetime
13
+ # failure_reason :text
14
+ # final_answer :text
15
+ # iteration_count :integer default(0), not null
16
+ # llm_model_key :string not null
17
+ # max_iterations :integer default(10), not null
18
+ # requested_language_key :string
19
+ # run_with :jsonb
20
+ # source_type :string
21
+ # started_at :datetime
22
+ # system_prompt :text
23
+ # task :text
24
+ # type :string not null
25
+ # created_at :datetime not null
26
+ # updated_at :datetime not null
27
+ # creator_id :bigint not null
28
+ # source_id :bigint
29
+ #
30
+ # Indexes
31
+ #
32
+ # index_raif_agents_on_created_at (created_at)
33
+ # index_raif_agents_on_creator (creator_type,creator_id)
34
+ # index_raif_agents_on_source (source_type,source_id)
35
+ #
3
36
  module Raif
4
37
  module Agents
5
38
  class NativeToolCallingAgent < Raif::Agent
@@ -12,7 +45,10 @@ module Raif
12
45
  }
13
46
 
14
47
  before_validation -> {
15
- available_model_tools << "Raif::ModelTools::AgentFinalAnswer" unless available_model_tools.include?("Raif::ModelTools::AgentFinalAnswer")
48
+ # If there is no final answer tool added, add it
49
+ unless available_model_tools_map.key?("agent_final_answer")
50
+ available_model_tools << "Raif::ModelTools::AgentFinalAnswer"
51
+ end
16
52
  }
17
53
 
18
54
  def build_system_prompt
@@ -28,7 +64,7 @@ module Raif
28
64
  6. Provide a final answer to the user's request.
29
65
 
30
66
  For your final answer:
31
- - Use the agent_final_answer tool/function with your complete answer as the "final_answer" parameter.
67
+ - You **MUST** use the agent_final_answer tool/function to provide your final answer.
32
68
  - Your answer should be comprehensive and directly address the user's request.
33
69
 
34
70
  Guidelines
@@ -48,82 +84,91 @@ module Raif
48
84
  available_model_tools
49
85
  end
50
86
 
87
+ def final_answer_tool
88
+ available_model_tools_map["agent_final_answer"]
89
+ end
90
+
91
+ # Warn the agent that it must provide a final answer on the next iteration
92
+ def before_iteration_llm_chat
93
+ return unless final_iteration?
94
+
95
+ warning_message = Raif::Messages::UserMessage.new(
96
+ content: I18n.t("raif.agents.native_tool_calling_agent.final_answer_warning")
97
+ )
98
+ add_conversation_history_entry(warning_message.to_h)
99
+ end
100
+
101
+ # On the final iteration, force the agent to use the agent_final_answer tool
102
+ def tool_choice_for_iteration
103
+ return unless final_iteration?
104
+
105
+ final_answer_tool
106
+ end
107
+
51
108
  def process_iteration_model_completion(model_completion)
52
- if model_completion.parsed_response.present?
53
- add_conversation_history_entry({
54
- role: "assistant",
55
- content: model_completion.parsed_response
56
- })
57
- end
109
+ assistant_response_message = model_completion.parsed_response if model_completion.parsed_response.present?
58
110
 
111
+ # The model made no tool call in this completion. Tell it to make a tool call.
59
112
  if model_completion.response_tool_calls.blank?
60
- add_conversation_history_entry({
61
- role: "assistant",
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
63
- })
113
+ if assistant_response_message.present?
114
+ assistant_message = Raif::Messages::AssistantMessage.new(content: assistant_response_message)
115
+ add_conversation_history_entry(assistant_message.to_h)
116
+ end
117
+
118
+ error_message = Raif::Messages::UserMessage.new(
119
+ content: "Error: Previous message contained no tool call. Make a tool call at each step. Available tools: #{available_model_tools_map.keys.join(", ")}" # rubocop:disable Layout/LineLength
120
+ )
121
+ add_conversation_history_entry(error_message.to_h)
122
+
64
123
  return
65
124
  end
66
125
 
67
126
  tool_call = model_completion.response_tool_calls.first
68
127
 
69
- unless tool_call["name"] && tool_call["arguments"]
70
- add_conversation_history_entry({
71
- role: "assistant",
72
- content: "<observation>Error: Invalid action specified. Please provide a valid action, formatted as a JSON object with 'tool' and 'arguments' keys.</observation>" # rubocop:disable Layout/LineLength
73
- })
74
- return
75
- end
128
+ # Add the tool call to history
129
+ tool_call_message = Raif::Messages::ToolCall.new(
130
+ provider_tool_call_id: tool_call["provider_tool_call_id"],
131
+ name: tool_call["name"],
132
+ arguments: tool_call["arguments"],
133
+ assistant_message: assistant_response_message,
134
+ provider_metadata: tool_call["provider_metadata"]
135
+ )
136
+ add_conversation_history_entry(tool_call_message.to_h)
76
137
 
77
138
  tool_name = tool_call["name"]
78
139
  tool_arguments = tool_call["arguments"]
79
-
80
- # Add assistant's response to conversation history (without the actual tool calls)
81
- # add_conversation_history_entry({
82
- # role: "assistant",
83
- # content: "<thought>I need to use the #{tool_name} tool to help with this task.</thought>"
84
- # })
85
-
86
- # Check if we have a final answer. If yes, we're done.
87
- if tool_name == "agent_final_answer"
88
- self.final_answer = tool_arguments["final_answer"]
89
- add_conversation_history_entry({ role: "assistant", content: "<answer>#{final_answer}</answer>" })
90
- return
91
- end
92
-
93
- # Add the tool call to conversation history
94
- add_conversation_history_entry({
95
- role: "assistant",
96
- content: "<action>#{JSON.pretty_generate(tool_call)}</action>"
97
- })
98
-
99
- # Find the tool class and process it
100
140
  tool_klass = available_model_tools_map[tool_name]
101
141
 
102
142
  # The model tried to use a tool that doesn't exist
103
- unless tool_klass
104
- add_conversation_history_entry({
105
- role: "assistant",
106
- content: "<observation>Error: Tool '#{tool_name}' not found. Available tools: #{available_model_tools_map.keys.join(", ")}</observation>"
107
- })
143
+ if tool_klass.blank?
144
+ error_content = "Error: Tool '#{tool_name}' is not a valid tool. " \
145
+ "Available tools: #{available_model_tools_map.keys.join(", ")}"
146
+ error_message = Raif::Messages::UserMessage.new(content: error_content)
147
+ add_conversation_history_entry(error_message.to_h)
108
148
  return
109
149
  end
110
150
 
151
+ # Make sure the tool arguments match the tool's schema
111
152
  unless JSON::Validator.validate(tool_klass.tool_arguments_schema, tool_arguments)
112
- add_conversation_history_entry({
113
- role: "assistant",
114
- content: "<observation>Error: Invalid tool arguments. Please provide valid arguments for the tool '#{tool_name}'. Tool arguments schema: #{tool_klass.tool_arguments_schema.to_json}</observation>" # rubocop:disable Layout/LineLength
115
- })
153
+ error_content = "Error: Invalid tool arguments for the tool '#{tool_name}'. " \
154
+ "Tool arguments schema: #{tool_klass.tool_arguments_schema.to_json}"
155
+ error_message = Raif::Messages::UserMessage.new(content: error_content)
156
+ add_conversation_history_entry(error_message.to_h)
116
157
  return
117
158
  end
118
159
 
119
- # Process the tool and add observation to history
120
- tool_invocation = tool_klass.invoke_tool(tool_arguments: tool_arguments, source: self)
121
- observation = tool_klass.observation_for_invocation(tool_invocation)
160
+ # Process the tool invocation and add observation/result to history
161
+ tool_invocation = tool_klass.invoke_tool(
162
+ provider_tool_call_id: tool_call["provider_tool_call_id"],
163
+ tool_arguments: tool_arguments,
164
+ source: self
165
+ )
122
166
 
123
- add_conversation_history_entry({
124
- role: "assistant",
125
- content: "<observation>#{observation}</observation>"
126
- })
167
+ if tool_name == "agent_final_answer"
168
+ self.final_answer = tool_invocation.result
169
+ else
170
+ add_conversation_history_entry(tool_invocation.as_tool_call_result_message)
171
+ end
127
172
  end
128
173
 
129
174
  def ensure_llm_supports_native_tool_use
@@ -8,6 +8,24 @@ class Raif::ApplicationRecord < Raif.config.model_superclass.constantize
8
8
  scope :newest_first, -> { order(created_at: :desc) }
9
9
  scope :oldest_first, -> { order(created_at: :asc) }
10
10
 
11
+ # Returns a scope that checks if a JSON column is not blank (not null and not empty array)
12
+ # @param column_name [Symbol, String] the name of the JSON column
13
+ # @return [ActiveRecord::Relation]
14
+ def self.where_json_not_blank(column_name)
15
+ quoted_column = connection.quote_column_name(column_name.to_s)
16
+
17
+ case connection.adapter_name.downcase
18
+ when "postgresql"
19
+ where.not(column_name => nil)
20
+ .where("jsonb_array_length(#{quoted_column}) > 0")
21
+ when "mysql2", "trilogy"
22
+ where.not(column_name => nil)
23
+ .where("JSON_LENGTH(#{quoted_column}) > 0")
24
+ else
25
+ raise "Unsupported database: #{connection.adapter_name}"
26
+ end
27
+ end
28
+
11
29
  def self.table_name_prefix
12
30
  "raif_"
13
31
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::AgentInferenceStats
4
+ extend ActiveSupport::Concern
5
+
6
+ # Returns the total number of prompt tokens across all model completions
7
+ def total_prompt_tokens
8
+ @total_prompt_tokens ||= raif_model_completions.sum(:prompt_tokens)
9
+ end
10
+
11
+ # Returns the total number of completion tokens across all model completions
12
+ def total_completion_tokens
13
+ @total_completion_tokens ||= raif_model_completions.sum(:completion_tokens)
14
+ end
15
+
16
+ # Returns the total number of tokens across all model completions
17
+ def total_tokens_sum
18
+ @total_tokens_sum ||= raif_model_completions.sum(:total_tokens)
19
+ end
20
+
21
+ # Returns the total cost of prompt tokens across all model completions
22
+ def total_prompt_token_cost
23
+ @total_prompt_token_cost ||= raif_model_completions.sum(:prompt_token_cost)
24
+ end
25
+
26
+ # Returns the total cost of output tokens across all model completions
27
+ def total_output_token_cost
28
+ @total_output_token_cost ||= raif_model_completions.sum(:output_token_cost)
29
+ end
30
+
31
+ # Returns the total cost across all model completions
32
+ def total_cost
33
+ @total_cost ||= raif_model_completions.sum(:total_cost)
34
+ end
35
+ end
@@ -9,19 +9,54 @@ module Raif
9
9
  def json_schema_definition(schema_name, &block)
10
10
  raise ArgumentError, "A block must be provided to define the JSON schema" unless block_given?
11
11
 
12
- @schemas ||= {}
13
- @schemas[schema_name] = Raif::JsonSchemaBuilder.new
14
- @schemas[schema_name].instance_eval(&block)
15
- @schemas[schema_name]
12
+ # Check if block expects an instance parameter (arity == 1)
13
+ # arity == 0: no parameters (class-level schema)
14
+ # arity == 1: one parameter (instance-dependent schema)
15
+ if block.arity == 1
16
+ # Store block for instance-dependent schema building
17
+ @schema_blocks ||= {}
18
+ @schema_blocks[schema_name] = block
19
+ else
20
+ # Build schema immediately for class-level (backward compatible)
21
+ @schemas ||= {}
22
+ @schemas[schema_name] = Raif::JsonSchemaBuilder.new
23
+ @schemas[schema_name].instance_eval(&block)
24
+ end
16
25
  end
17
26
 
18
27
  def schema_defined?(schema_name)
19
- @schemas&.dig(schema_name).present?
28
+ @schemas&.dig(schema_name).present? || @schema_blocks&.dig(schema_name).present?
20
29
  end
21
30
 
22
31
  def schema_for(schema_name)
32
+ # Check if this is an instance-dependent schema
33
+ if @schema_blocks&.dig(schema_name).present?
34
+ raise Raif::Errors::InstanceDependentSchemaError,
35
+ "The schema '#{schema_name}' is instance-dependent and cannot be accessed at the class level. " \
36
+ "Call this method on an instance instead."
37
+ end
38
+
23
39
  @schemas[schema_name].to_schema
24
40
  end
41
+
42
+ def instance_dependent_schema?(schema_name)
43
+ @schema_blocks&.dig(schema_name).present?
44
+ end
45
+ end
46
+
47
+ # Instance method to build schema with instance context
48
+ def schema_for_instance(schema_name)
49
+ block = self.class.instance_variable_get(:@schema_blocks)&.[](schema_name)
50
+
51
+ if block
52
+ # Build schema with instance context
53
+ builder = Raif::JsonSchemaBuilder.new
54
+ builder.build_with_instance(self, &block)
55
+ builder.to_schema
56
+ elsif self.class.schema_defined?(schema_name)
57
+ # Fall back to class-level schema
58
+ self.class.schema_for(schema_name)
59
+ end
25
60
  end
26
61
  end
27
62
  end
@@ -48,4 +48,32 @@ module Raif::Concerns::Llms::Anthropic::MessageFormatting
48
48
  raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
49
49
  end
50
50
  end
51
+
52
+ def format_tool_call_message(tool_call)
53
+ content_array = []
54
+ content_array << format_string_message(tool_call["assistant_message"]) if tool_call["assistant_message"].present?
55
+
56
+ content_array << {
57
+ "type" => "tool_use",
58
+ "id" => tool_call["provider_tool_call_id"],
59
+ "name" => tool_call["name"],
60
+ "input" => tool_call["arguments"]
61
+ }
62
+
63
+ {
64
+ "role" => "assistant",
65
+ "content" => content_array
66
+ }
67
+ end
68
+
69
+ def format_tool_call_result_message(tool_call_result)
70
+ {
71
+ "role" => "user",
72
+ "content" => [{
73
+ "type" => "tool_result",
74
+ "tool_use_id" => tool_call_result["provider_tool_call_id"],
75
+ "content" => tool_call_result["result"].is_a?(String) ? tool_call_result["result"] : JSON.generate(tool_call_result["result"])
76
+ }]
77
+ }
78
+ end
51
79
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::Anthropic::ResponseToolCalls
4
+ extend ActiveSupport::Concern
5
+
6
+ def extract_response_tool_calls(resp)
7
+ return if resp&.dig("content").nil?
8
+
9
+ # Find any tool_use content blocks
10
+ tool_uses = resp&.dig("content")&.select do |content|
11
+ content["type"] == "tool_use"
12
+ end
13
+
14
+ return if tool_uses.blank?
15
+
16
+ tool_uses.map do |tool_use|
17
+ {
18
+ "provider_tool_call_id" => tool_use["id"],
19
+ "name" => tool_use["name"],
20
+ "arguments" => tool_use["input"],
21
+ }
22
+ end
23
+ end
24
+ end
@@ -53,4 +53,8 @@ module Raif::Concerns::Llms::Anthropic::ToolFormatting
53
53
  "Invalid provider-managed tool: #{tool.name} for #{key}"
54
54
  end
55
55
  end
56
+
57
+ def build_forced_tool_choice(tool_name)
58
+ { "type" => "tool", "name" => tool_name }
59
+ end
56
60
  end
@@ -67,4 +67,40 @@ module Raif::Concerns::Llms::Bedrock::MessageFormatting
67
67
  "text/markdown" => "md"
68
68
  }[content_type]
69
69
  end
70
+
71
+ def format_tool_call_message(tool_call)
72
+ content_array = []
73
+ content_array << format_string_message(tool_call["assistant_message"]) if tool_call["assistant_message"].present?
74
+
75
+ content_array << {
76
+ "tool_use" => {
77
+ "tool_use_id" => tool_call["provider_tool_call_id"],
78
+ "name" => tool_call["name"],
79
+ "input" => tool_call["arguments"]
80
+ }
81
+ }
82
+
83
+ {
84
+ "role" => "assistant",
85
+ "content" => content_array
86
+ }
87
+ end
88
+
89
+ def format_tool_call_result_message(tool_call_result)
90
+ tool_result_content = if tool_call_result["result"].is_a?(String)
91
+ { "text" => tool_call_result["result"] }
92
+ else
93
+ { "json" => tool_call_result["result"] }
94
+ end
95
+
96
+ {
97
+ "role" => "user",
98
+ "content" => [{
99
+ "tool_result" => {
100
+ "tool_use_id" => tool_call_result["provider_tool_call_id"],
101
+ "content" => [tool_result_content]
102
+ }
103
+ }]
104
+ }
105
+ end
70
106
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::Bedrock::ResponseToolCalls
4
+ extend ActiveSupport::Concern
5
+
6
+ def extract_response_tool_calls(resp)
7
+ # Get the message from the response object
8
+ message = resp.output.message
9
+ return if message.content.nil?
10
+
11
+ # Find any tool_use blocks in the content array
12
+ tool_uses = message.content.select do |content|
13
+ content.respond_to?(:tool_use) && content.tool_use.present?
14
+ end
15
+
16
+ return if tool_uses.blank?
17
+
18
+ tool_uses.map do |content|
19
+ {
20
+ "provider_tool_call_id" => content.tool_use.tool_use_id,
21
+ "name" => content.tool_use.name,
22
+ "arguments" => content.tool_use.input
23
+ }
24
+ end
25
+ end
26
+ end
@@ -34,4 +34,8 @@ module Raif::Concerns::Llms::Bedrock::ToolFormatting
34
34
  tools: tools.map{|tool| { tool_spec: tool } }
35
35
  }
36
36
  end
37
+
38
+ def build_forced_tool_choice(tool_name)
39
+ { tool: { name: tool_name } }
40
+ end
37
41
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::Google::MessageFormatting
4
+ extend ActiveSupport::Concern
5
+
6
+ # Override the base format_messages to use Google's message format
7
+ def format_messages(messages)
8
+ messages.map do |message|
9
+ if message.is_a?(Hash) && message["type"] == "tool_call"
10
+ format_tool_call_message(message)
11
+ elsif message.is_a?(Hash) && message["type"] == "tool_call_result"
12
+ format_tool_call_result_message(message)
13
+ else
14
+ role = message["role"] || message[:role]
15
+ # Google uses "model" instead of "assistant"
16
+ google_role = role == "assistant" ? "model" : role
17
+ {
18
+ "role" => google_role,
19
+ "parts" => format_message_content(message["content"] || message[:content], role: role)
20
+ }
21
+ end
22
+ end
23
+ end
24
+
25
+ def format_string_message(content, role: nil)
26
+ { "text" => content }
27
+ end
28
+
29
+ def format_model_image_input_message(image_input)
30
+ if image_input.source_type == :url
31
+ {
32
+ "fileData" => {
33
+ "mimeType" => image_input.content_type,
34
+ "fileUri" => image_input.url
35
+ }
36
+ }
37
+ elsif image_input.source_type == :file_content
38
+ {
39
+ "inlineData" => {
40
+ "mimeType" => image_input.content_type,
41
+ "data" => image_input.base64_data
42
+ }
43
+ }
44
+ else
45
+ raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
46
+ end
47
+ end
48
+
49
+ def format_model_file_input_message(file_input)
50
+ if file_input.source_type == :url
51
+ {
52
+ "fileData" => {
53
+ "mimeType" => file_input.content_type,
54
+ "fileUri" => file_input.url
55
+ }
56
+ }
57
+ elsif file_input.source_type == :file_content
58
+ {
59
+ "inlineData" => {
60
+ "mimeType" => file_input.content_type,
61
+ "data" => file_input.base64_data
62
+ }
63
+ }
64
+ else
65
+ raise Raif::Errors::InvalidModelFileInputError, "Invalid model file input source type: #{file_input.source_type}"
66
+ end
67
+ end
68
+
69
+ def format_tool_call_message(tool_call)
70
+ parts = []
71
+
72
+ if tool_call["assistant_message"].present?
73
+ parts << format_string_message(tool_call["assistant_message"])
74
+ end
75
+
76
+ function_call_part = {
77
+ "functionCall" => {
78
+ "name" => tool_call["name"],
79
+ "args" => tool_call["arguments"]
80
+ }
81
+ }
82
+
83
+ # Include thoughtSignature if present (required for Gemini 2.5+ thinking models)
84
+ thought_signature = tool_call.dig("provider_metadata", "thought_signature")
85
+ function_call_part["thoughtSignature"] = thought_signature if thought_signature.present?
86
+
87
+ parts << function_call_part
88
+
89
+ {
90
+ "role" => "model",
91
+ "parts" => parts
92
+ }
93
+ end
94
+
95
+ def format_tool_call_result_message(tool_call_result)
96
+ result = tool_call_result["result"]
97
+ response_content = result.is_a?(String) ? { "output" => result } : result
98
+
99
+ {
100
+ "role" => "user",
101
+ "parts" => [{
102
+ "functionResponse" => {
103
+ "name" => tool_call_result["name"],
104
+ "response" => response_content
105
+ }
106
+ }]
107
+ }
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::Google::ResponseToolCalls
4
+ extend ActiveSupport::Concern
5
+
6
+ def extract_response_tool_calls(resp)
7
+ parts = resp&.dig("candidates", 0, "content", "parts")
8
+ return if parts.blank?
9
+
10
+ # Find any functionCall parts
11
+ function_calls = parts.select { |part| part.key?("functionCall") }
12
+
13
+ return if function_calls.blank?
14
+
15
+ function_calls.map do |part|
16
+ function_call = part["functionCall"]
17
+ tool_call = {
18
+ # Google doesn't provide a unique ID for function calls, so we generate one
19
+ "provider_tool_call_id" => SecureRandom.uuid,
20
+ "name" => function_call["name"],
21
+ "arguments" => function_call["args"]
22
+ }
23
+
24
+ # Capture thoughtSignature if present (required for Gemini 2.5+ thinking models)
25
+ if part["thoughtSignature"].present?
26
+ tool_call["provider_metadata"] = { "thought_signature" => part["thoughtSignature"] }
27
+ end
28
+
29
+ tool_call
30
+ end
31
+ end
32
+ end