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.
- checksums.yaml +4 -4
- data/README.md +6 -5
- data/app/assets/builds/raif.css +4 -1
- data/app/assets/builds/raif_admin.css +13 -1
- data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
- data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
- data/app/assets/stylesheets/raif/conversations.scss +3 -0
- data/app/assets/stylesheets/raif.scss +2 -1
- data/app/controllers/raif/admin/application_controller.rb +16 -0
- data/app/controllers/raif/admin/configs_controller.rb +94 -0
- data/app/controllers/raif/admin/model_completions_controller.rb +18 -1
- data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
- data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
- data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
- data/app/controllers/raif/admin/stats_controller.rb +32 -3
- data/app/controllers/raif/conversation_entries_controller.rb +1 -0
- data/app/controllers/raif/conversations_controller.rb +10 -2
- data/app/jobs/raif/conversation_entry_job.rb +8 -6
- data/app/models/raif/admin/task_stat.rb +7 -0
- data/app/models/raif/agent.rb +63 -2
- data/app/models/raif/agents/native_tool_calling_agent.rb +101 -56
- data/app/models/raif/application_record.rb +18 -0
- data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
- data/app/models/raif/concerns/json_schema_definition.rb +40 -5
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +28 -0
- data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +36 -0
- data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/google/message_formatting.rb +109 -0
- data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
- data/app/models/raif/concerns/llms/google/tool_formatting.rb +72 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +11 -5
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
- data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
- data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
- data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/run_with.rb +127 -0
- data/app/models/raif/conversation.rb +91 -8
- data/app/models/raif/conversation_entry.rb +32 -1
- data/app/models/raif/embedding_model.rb +2 -1
- data/app/models/raif/embedding_models/open_ai.rb +1 -1
- data/app/models/raif/llm.rb +27 -2
- data/app/models/raif/llms/anthropic.rb +7 -19
- data/app/models/raif/llms/bedrock.rb +6 -20
- data/app/models/raif/llms/google.rb +140 -0
- data/app/models/raif/llms/open_ai_base.rb +19 -5
- data/app/models/raif/llms/open_ai_completions.rb +6 -11
- data/app/models/raif/llms/open_ai_responses.rb +6 -16
- data/app/models/raif/llms/open_router.rb +7 -13
- data/app/models/raif/model_completion.rb +61 -0
- data/app/models/raif/model_tool.rb +10 -2
- data/app/models/raif/model_tool_invocation.rb +38 -6
- data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
- data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
- data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
- data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
- data/app/models/raif/streaming_responses/google.rb +71 -0
- data/app/models/raif/task.rb +55 -12
- data/app/models/raif/user_tool_invocation.rb +19 -0
- data/app/views/layouts/raif/admin.html.erb +12 -1
- data/app/views/raif/admin/agents/_agent.html.erb +8 -0
- data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
- data/app/views/raif/admin/agents/index.html.erb +2 -0
- data/app/views/raif/admin/agents/show.html.erb +46 -1
- data/app/views/raif/admin/configs/show.html.erb +117 -0
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
- data/app/views/raif/admin/conversations/show.html.erb +2 -0
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
- data/app/views/raif/admin/model_completions/index.html.erb +26 -0
- data/app/views/raif/admin/model_completions/show.html.erb +124 -61
- data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
- data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
- data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
- data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
- data/app/views/raif/admin/stats/index.html.erb +71 -88
- data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
- data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
- data/app/views/raif/admin/tasks/index.html.erb +6 -1
- data/app/views/raif/admin/tasks/show.html.erb +36 -3
- data/app/views/raif/conversation_entries/_form.html.erb +3 -0
- data/app/views/raif/conversations/_conversation.html.erb +10 -0
- data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
- data/app/views/raif/conversations/index.html.erb +23 -0
- data/config/locales/admin.en.yml +33 -1
- data/config/locales/en.yml +33 -4
- data/config/routes.rb +2 -0
- data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
- data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
- data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
- data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
- data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
- data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
- data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
- data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
- data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
- data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
- data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
- data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
- data/lib/generators/raif/agent/templates/agent.rb.tt +1 -1
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -1
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +6 -0
- data/lib/generators/raif/install/templates/initializer.rb +78 -10
- data/lib/generators/raif/task/templates/task.rb.tt +1 -1
- data/lib/raif/configuration.rb +37 -2
- data/lib/raif/engine.rb +8 -0
- data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
- data/lib/raif/errors/streaming_error.rb +6 -3
- data/lib/raif/errors.rb +1 -0
- data/lib/raif/evals/llm_judge.rb +2 -2
- data/lib/raif/evals/llm_judges/binary.rb +3 -3
- data/lib/raif/evals/llm_judges/comparative.rb +3 -3
- data/lib/raif/evals/llm_judges/scored.rb +1 -1
- data/lib/raif/evals/llm_judges/summarization.rb +2 -2
- data/lib/raif/evals/run.rb +1 -0
- data/lib/raif/json_schema_builder.rb +14 -0
- data/lib/raif/llm_registry.rb +207 -37
- data/lib/raif/messages.rb +180 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +9 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/spec/support/rspec_helpers.rb +8 -8
- metadata +44 -9
- data/app/models/raif/agents/re_act_agent.rb +0 -127
- data/app/models/raif/agents/re_act_step.rb +0 -32
- 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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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
|