raif 1.2.2 → 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/has_llm.rb +1 -1
- 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 +96 -9
- data/app/models/raif/conversation_entry.rb +37 -8
- 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 +28 -3
- 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 +10 -14
- 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 +74 -18
- 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 +4 -1
- 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/_full_conversation.html.erb +3 -6
- data/app/views/raif/conversations/_initial_chat_message.html.erb +5 -0
- data/app/views/raif/conversations/index.html.erb +23 -0
- data/config/locales/admin.en.yml +33 -1
- data/config/locales/en.yml +41 -4
- data/config/routes.rb +2 -0
- data/db/migrate/20250804013843_add_task_run_args_to_raif_tasks.rb +13 -0
- data/db/migrate/20250811171150_make_raif_task_creator_optional.rb +8 -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/exe/raif +7 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -7
- data/lib/generators/raif/agent/templates/agent.rb.tt +20 -24
- data/lib/generators/raif/agent/templates/agent_eval_set.rb.tt +48 -0
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -3
- data/lib/generators/raif/base_generator.rb +19 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +21 -2
- data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +0 -2
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +34 -32
- data/lib/generators/raif/conversation/templates/conversation_eval_set.rb.tt +70 -0
- data/lib/generators/raif/eval_set/eval_set_generator.rb +28 -0
- data/lib/generators/raif/eval_set/templates/eval_set.rb.tt +21 -0
- data/lib/generators/raif/evals/setup/setup_generator.rb +47 -0
- data/lib/generators/raif/install/install_generator.rb +15 -0
- data/lib/generators/raif/install/templates/initializer.rb +89 -10
- data/lib/generators/raif/model_tool/model_tool_generator.rb +5 -5
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +78 -78
- data/lib/generators/raif/model_tool/templates/model_tool_invocation_partial.html.erb.tt +1 -1
- data/lib/generators/raif/task/task_generator.rb +22 -3
- data/lib/generators/raif/task/templates/application_task.rb.tt +0 -2
- data/lib/generators/raif/task/templates/task.rb.tt +55 -59
- data/lib/generators/raif/task/templates/task_eval_set.rb.tt +54 -0
- data/lib/raif/cli/base.rb +39 -0
- data/lib/raif/cli/evals.rb +47 -0
- data/lib/raif/cli/evals_setup.rb +27 -0
- data/lib/raif/cli.rb +67 -0
- data/lib/raif/configuration.rb +57 -8
- 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/eval.rb +30 -0
- data/lib/raif/evals/eval_set.rb +111 -0
- data/lib/raif/evals/eval_sets/expectations.rb +53 -0
- data/lib/raif/evals/eval_sets/llm_judge_expectations.rb +255 -0
- data/lib/raif/evals/expectation_result.rb +39 -0
- data/lib/raif/evals/llm_judge.rb +32 -0
- data/lib/raif/evals/llm_judges/binary.rb +94 -0
- data/lib/raif/evals/llm_judges/comparative.rb +89 -0
- data/lib/raif/evals/llm_judges/scored.rb +63 -0
- data/lib/raif/evals/llm_judges/summarization.rb +166 -0
- data/lib/raif/evals/run.rb +202 -0
- data/lib/raif/evals/scoring_rubric.rb +174 -0
- data/lib/raif/evals.rb +26 -0
- data/lib/raif/json_schema_builder.rb +14 -0
- data/lib/raif/llm_registry.rb +218 -15
- data/lib/raif/messages.rb +180 -0
- data/lib/raif/migration_checker.rb +3 -3
- data/lib/raif/utils/colors.rb +23 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +13 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/spec/support/current_temperature_test_tool.rb +34 -0
- data/spec/support/rspec_helpers.rb +8 -8
- data/spec/support/test_conversation.rb +1 -1
- metadata +77 -10
- data/app/models/raif/agents/re_act_agent.rb +0 -127
- data/app/models/raif/agents/re_act_step.rb +0 -33
data/app/models/raif/llm.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Raif
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :key,
|
|
9
9
|
:api_name,
|
|
10
|
+
:display_name,
|
|
10
11
|
:default_temperature,
|
|
11
12
|
:default_max_completion_tokens,
|
|
12
13
|
:supports_native_tool_use,
|
|
@@ -25,6 +26,7 @@ module Raif
|
|
|
25
26
|
def initialize(
|
|
26
27
|
key:,
|
|
27
28
|
api_name:,
|
|
29
|
+
display_name: nil,
|
|
28
30
|
model_provider_settings: {},
|
|
29
31
|
supported_provider_managed_tools: [],
|
|
30
32
|
supports_native_tool_use: true,
|
|
@@ -35,6 +37,7 @@ module Raif
|
|
|
35
37
|
)
|
|
36
38
|
@key = key
|
|
37
39
|
@api_name = api_name
|
|
40
|
+
@display_name = display_name
|
|
38
41
|
@provider_settings = model_provider_settings
|
|
39
42
|
@supports_native_tool_use = supports_native_tool_use
|
|
40
43
|
@default_temperature = temperature || 0.7
|
|
@@ -45,11 +48,11 @@ module Raif
|
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def name
|
|
48
|
-
I18n.t("raif.model_names.#{key}")
|
|
51
|
+
I18n.t("raif.model_names.#{key}", default: display_name || key.to_s.humanize)
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def chat(message: nil, messages: nil, response_format: :text, available_model_tools: [], source: nil, system_prompt: nil, temperature: nil,
|
|
52
|
-
max_completion_tokens: nil, &block)
|
|
55
|
+
max_completion_tokens: nil, tool_choice: nil, &block)
|
|
53
56
|
unless response_format.is_a?(Symbol)
|
|
54
57
|
raise ArgumentError,
|
|
55
58
|
"Raif::Llm#chat - Invalid response format: #{response_format}. Must be a symbol (you passed #{response_format.class}) and be one of: #{VALID_RESPONSE_FORMATS.join(", ")}" # rubocop:disable Layout/LineLength
|
|
@@ -67,6 +70,11 @@ module Raif
|
|
|
67
70
|
raise ArgumentError, "Raif::Llm#chat - You must provide either a message: or messages: argument, not both"
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
if tool_choice.present? && !available_model_tools.map(&:to_s).include?(tool_choice.to_s)
|
|
74
|
+
raise ArgumentError,
|
|
75
|
+
"Raif::Llm#chat - Invalid tool choice: #{tool_choice} is not included in the available model tools: #{available_model_tools.join(", ")}"
|
|
76
|
+
end
|
|
77
|
+
|
|
70
78
|
unless Raif.config.llm_api_requests_enabled
|
|
71
79
|
Raif.logger.warn("LLM API requests are disabled. Skipping request to #{api_name}.")
|
|
72
80
|
return
|
|
@@ -77,7 +85,7 @@ module Raif
|
|
|
77
85
|
temperature ||= default_temperature
|
|
78
86
|
max_completion_tokens ||= default_max_completion_tokens
|
|
79
87
|
|
|
80
|
-
model_completion = Raif::ModelCompletion.
|
|
88
|
+
model_completion = Raif::ModelCompletion.create!(
|
|
81
89
|
messages: format_messages(messages),
|
|
82
90
|
system_prompt: system_prompt,
|
|
83
91
|
response_format: response_format,
|
|
@@ -87,20 +95,29 @@ module Raif
|
|
|
87
95
|
temperature: temperature,
|
|
88
96
|
max_completion_tokens: max_completion_tokens,
|
|
89
97
|
available_model_tools: available_model_tools,
|
|
98
|
+
tool_choice: tool_choice&.to_s,
|
|
90
99
|
stream_response: block_given?
|
|
91
100
|
)
|
|
92
101
|
|
|
102
|
+
model_completion.started!
|
|
103
|
+
|
|
93
104
|
retry_with_backoff(model_completion) do
|
|
94
105
|
perform_model_completion!(model_completion, &block)
|
|
95
106
|
end
|
|
96
107
|
|
|
108
|
+
model_completion.completed!
|
|
97
109
|
model_completion
|
|
98
110
|
rescue Raif::Errors::StreamingError => e
|
|
99
111
|
Rails.logger.error("Raif streaming error -- code: #{e.code} -- type: #{e.type} -- message: #{e.message} -- event: #{e.event}")
|
|
112
|
+
model_completion&.record_failure!(e)
|
|
100
113
|
raise e
|
|
101
114
|
rescue Faraday::Error => e
|
|
102
115
|
Raif.logger.error("LLM API request failed (status: #{e.response_status}): #{e.message}")
|
|
103
116
|
Raif.logger.error(e.response_body)
|
|
117
|
+
model_completion&.record_failure!(e)
|
|
118
|
+
raise e
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
model_completion&.record_failure!(e)
|
|
104
121
|
raise e
|
|
105
122
|
end
|
|
106
123
|
|
|
@@ -116,6 +133,14 @@ module Raif
|
|
|
116
133
|
supported_provider_managed_tools&.include?(tool_klass.to_s)
|
|
117
134
|
end
|
|
118
135
|
|
|
136
|
+
# Build the tool_choice parameter to force a specific tool to be called.
|
|
137
|
+
# Each provider implements this to return the correct format.
|
|
138
|
+
# @param tool_name [String] The name of the tool to force
|
|
139
|
+
# @return [Hash] The tool_choice parameter for the provider's API
|
|
140
|
+
def build_forced_tool_choice(tool_name)
|
|
141
|
+
raise NotImplementedError, "#{self.class.name} must implement #build_forced_tool_choice"
|
|
142
|
+
end
|
|
143
|
+
|
|
119
144
|
def validate_provider_managed_tool_support!(tool)
|
|
120
145
|
unless supports_provider_managed_tool?(tool)
|
|
121
146
|
raise Raif::Errors::UnsupportedFeatureError,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Raif::Llms::Anthropic < Raif::Llm
|
|
4
4
|
include Raif::Concerns::Llms::Anthropic::MessageFormatting
|
|
5
5
|
include Raif::Concerns::Llms::Anthropic::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::Anthropic::ResponseToolCalls
|
|
6
7
|
|
|
7
8
|
def perform_model_completion!(model_completion, &block)
|
|
8
9
|
params = build_request_parameters(model_completion)
|
|
@@ -21,7 +22,7 @@ class Raif::Llms::Anthropic < Raif::Llm
|
|
|
21
22
|
private
|
|
22
23
|
|
|
23
24
|
def connection
|
|
24
|
-
@connection ||= Faraday.new(url: "https://api.anthropic.com/v1") do |f|
|
|
25
|
+
@connection ||= Faraday.new(url: "https://api.anthropic.com/v1", request: Raif.default_request_options) do |f|
|
|
25
26
|
f.headers["x-api-key"] = Raif.config.anthropic_api_key
|
|
26
27
|
f.headers["anthropic-version"] = "2023-06-01"
|
|
27
28
|
f.request :json
|
|
@@ -64,6 +65,11 @@ private
|
|
|
64
65
|
if supports_native_tool_use?
|
|
65
66
|
tools = build_tools_parameter(model_completion)
|
|
66
67
|
params[:tools] = tools unless tools.blank?
|
|
68
|
+
|
|
69
|
+
if model_completion.tool_choice.present?
|
|
70
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
71
|
+
params[:tool_choice] = build_forced_tool_choice(tool_klass.tool_name)
|
|
72
|
+
end
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
params[:stream] = true if model_completion.stream_response?
|
|
@@ -92,24 +98,6 @@ private
|
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
|
|
95
|
-
def extract_response_tool_calls(resp)
|
|
96
|
-
return if resp&.dig("content").nil?
|
|
97
|
-
|
|
98
|
-
# Find any tool_use content blocks
|
|
99
|
-
tool_uses = resp&.dig("content")&.select do |content|
|
|
100
|
-
content["type"] == "tool_use"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
return if tool_uses.blank?
|
|
104
|
-
|
|
105
|
-
tool_uses.map do |tool_use|
|
|
106
|
-
{
|
|
107
|
-
"name" => tool_use["name"],
|
|
108
|
-
"arguments" => tool_use["input"]
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
101
|
def extract_citations(resp)
|
|
114
102
|
return [] if resp&.dig("content").nil?
|
|
115
103
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Raif::Llms::Bedrock < Raif::Llm
|
|
4
4
|
include Raif::Concerns::Llms::Bedrock::MessageFormatting
|
|
5
5
|
include Raif::Concerns::Llms::Bedrock::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::Bedrock::ResponseToolCalls
|
|
6
7
|
|
|
7
8
|
def perform_model_completion!(model_completion, &block)
|
|
8
9
|
if Raif.config.aws_bedrock_model_name_prefix.present?
|
|
@@ -72,6 +73,11 @@ private
|
|
|
72
73
|
if supports_native_tool_use?
|
|
73
74
|
tools = build_tools_parameter(model_completion)
|
|
74
75
|
params[:tool_config] = tools unless tools.blank?
|
|
76
|
+
|
|
77
|
+
if model_completion.tool_choice.present?
|
|
78
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
79
|
+
params[:tool_config][:tool_choice] = build_forced_tool_choice(tool_klass.tool_name)
|
|
80
|
+
end
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
params
|
|
@@ -121,26 +127,6 @@ private
|
|
|
121
127
|
end
|
|
122
128
|
end
|
|
123
129
|
|
|
124
|
-
def extract_response_tool_calls(resp)
|
|
125
|
-
# Get the message from the response object
|
|
126
|
-
message = resp.output.message
|
|
127
|
-
return if message.content.nil?
|
|
128
|
-
|
|
129
|
-
# Find any tool_use blocks in the content array
|
|
130
|
-
tool_uses = message.content.select do |content|
|
|
131
|
-
content.respond_to?(:tool_use) && content.tool_use.present?
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
return if tool_uses.blank?
|
|
135
|
-
|
|
136
|
-
tool_uses.map do |content|
|
|
137
|
-
{
|
|
138
|
-
"name" => content.tool_use.name,
|
|
139
|
-
"arguments" => content.tool_use.input
|
|
140
|
-
}
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
130
|
def streaming_chunk_handler(model_completion, &block)
|
|
145
131
|
return unless model_completion.stream_response?
|
|
146
132
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Raif::Llms::Google < Raif::Llm
|
|
4
|
+
include Raif::Concerns::Llms::Google::MessageFormatting
|
|
5
|
+
include Raif::Concerns::Llms::Google::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::Google::ResponseToolCalls
|
|
7
|
+
|
|
8
|
+
def perform_model_completion!(model_completion, &block)
|
|
9
|
+
params = build_request_parameters(model_completion)
|
|
10
|
+
endpoint = build_endpoint(model_completion)
|
|
11
|
+
|
|
12
|
+
response = connection.post(endpoint) do |req|
|
|
13
|
+
req.body = params
|
|
14
|
+
req.options.on_data = streaming_chunk_handler(model_completion, &block) if model_completion.stream_response?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless model_completion.stream_response?
|
|
18
|
+
update_model_completion(model_completion, response.body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
model_completion
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def connection
|
|
27
|
+
@connection ||= Faraday.new(url: "https://generativelanguage.googleapis.com/v1beta") do |f|
|
|
28
|
+
f.headers["x-goog-api-key"] = Raif.config.google_api_key
|
|
29
|
+
f.request :json
|
|
30
|
+
f.response :json
|
|
31
|
+
f.response :raise_error
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_endpoint(model_completion)
|
|
36
|
+
if model_completion.stream_response?
|
|
37
|
+
"models/#{model_completion.model_api_name}:streamGenerateContent?alt=sse"
|
|
38
|
+
else
|
|
39
|
+
"models/#{model_completion.model_api_name}:generateContent"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def streaming_response_type
|
|
44
|
+
Raif::StreamingResponses::Google
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_model_completion(model_completion, response_json)
|
|
48
|
+
model_completion.raw_response = if model_completion.response_format_json?
|
|
49
|
+
extract_json_response(response_json)
|
|
50
|
+
else
|
|
51
|
+
extract_text_response(response_json)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
model_completion.response_array = response_json&.dig("candidates", 0, "content", "parts")
|
|
55
|
+
model_completion.response_tool_calls = extract_response_tool_calls(response_json)
|
|
56
|
+
model_completion.citations = extract_citations(response_json)
|
|
57
|
+
model_completion.completion_tokens = response_json&.dig("usageMetadata", "candidatesTokenCount")
|
|
58
|
+
model_completion.prompt_tokens = response_json&.dig("usageMetadata", "promptTokenCount")
|
|
59
|
+
model_completion.total_tokens = response_json&.dig("usageMetadata", "totalTokenCount") ||
|
|
60
|
+
(model_completion.completion_tokens.to_i + model_completion.prompt_tokens.to_i)
|
|
61
|
+
model_completion.save!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_request_parameters(model_completion)
|
|
65
|
+
params = {
|
|
66
|
+
contents: model_completion.messages
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if model_completion.system_prompt.present?
|
|
70
|
+
params[:system_instruction] = { parts: [{ text: model_completion.system_prompt }] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
params[:generationConfig] = build_generation_config(model_completion)
|
|
74
|
+
|
|
75
|
+
if supports_native_tool_use?
|
|
76
|
+
tools = build_tools_parameter(model_completion)
|
|
77
|
+
params[:tools] = tools unless tools.blank?
|
|
78
|
+
|
|
79
|
+
if model_completion.tool_choice.present?
|
|
80
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
81
|
+
params[:toolConfig] = { functionCallingConfig: build_forced_tool_choice(tool_klass.tool_name) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
params
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_generation_config(model_completion)
|
|
89
|
+
config = {}
|
|
90
|
+
|
|
91
|
+
temperature = model_completion.temperature || default_temperature
|
|
92
|
+
config[:temperature] = temperature.to_f if temperature.present?
|
|
93
|
+
|
|
94
|
+
max_tokens = model_completion.max_completion_tokens || default_max_completion_tokens
|
|
95
|
+
config[:maxOutputTokens] = max_tokens if max_tokens.present?
|
|
96
|
+
|
|
97
|
+
# Use native JSON schema support for structured output
|
|
98
|
+
if model_completion.response_format_json? && model_completion.json_response_schema.present?
|
|
99
|
+
config[:responseMimeType] = "application/json"
|
|
100
|
+
config[:responseSchema] = sanitize_schema_for_google(model_completion.json_response_schema)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
config
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_text_response(resp)
|
|
107
|
+
parts = resp&.dig("candidates", 0, "content", "parts")
|
|
108
|
+
return if parts.blank?
|
|
109
|
+
|
|
110
|
+
parts.select { |p| p.key?("text") }.map { |p| p["text"] }.join
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract_json_response(resp)
|
|
114
|
+
# Google AI supports native JSON schema output, so the response should be in the text field
|
|
115
|
+
extract_text_response(resp)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def extract_citations(resp)
|
|
119
|
+
# Google AI returns grounding metadata for search results
|
|
120
|
+
grounding_metadata = resp&.dig("candidates", 0, "groundingMetadata")
|
|
121
|
+
return [] if grounding_metadata.blank?
|
|
122
|
+
|
|
123
|
+
citations = []
|
|
124
|
+
|
|
125
|
+
# Extract from grounding chunks
|
|
126
|
+
grounding_chunks = grounding_metadata["groundingChunks"] || []
|
|
127
|
+
grounding_chunks.each do |chunk|
|
|
128
|
+
web = chunk["web"]
|
|
129
|
+
next unless web.present?
|
|
130
|
+
|
|
131
|
+
citations << {
|
|
132
|
+
"url" => Raif::Utils::HtmlFragmentProcessor.strip_tracking_parameters(web["uri"]),
|
|
133
|
+
"title" => web["title"]
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
citations.uniq { |citation| citation["url"] }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
@@ -28,11 +28,25 @@ class Raif::Llms::OpenAiBase < Raif::Llm
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
30
|
def connection
|
|
31
|
-
@connection ||=
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
@connection ||= begin
|
|
32
|
+
conn = Faraday.new(url: Raif.config.open_ai_base_url, request: Raif.default_request_options) do |f|
|
|
33
|
+
case Raif.config.open_ai_auth_header_style
|
|
34
|
+
when :bearer
|
|
35
|
+
f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
|
|
36
|
+
when :api_key
|
|
37
|
+
f.headers["api-key"] = Raif.config.open_ai_api_key
|
|
38
|
+
else
|
|
39
|
+
raise Raif::Errors::InvalidConfigError,
|
|
40
|
+
"Raif.config.open_ai_auth_header_style must be either :bearer or :api_key"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
f.request :json
|
|
44
|
+
f.response :json
|
|
45
|
+
f.response :raise_error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
conn.params["api-version"] = Raif.config.open_ai_api_version if Raif.config.open_ai_api_version.present?
|
|
49
|
+
conn
|
|
36
50
|
end
|
|
37
51
|
end
|
|
38
52
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Raif::Llms::OpenAiCompletions < Raif::Llms::OpenAiBase
|
|
4
4
|
include Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
|
5
5
|
include Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::OpenAiCompletions::ResponseToolCalls
|
|
6
7
|
|
|
7
8
|
private
|
|
8
9
|
|
|
@@ -26,17 +27,6 @@ private
|
|
|
26
27
|
)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def extract_response_tool_calls(resp)
|
|
30
|
-
return if resp.dig("choices", 0, "message", "tool_calls").blank?
|
|
31
|
-
|
|
32
|
-
resp.dig("choices", 0, "message", "tool_calls").map do |tool_call|
|
|
33
|
-
{
|
|
34
|
-
"name" => tool_call["function"]["name"],
|
|
35
|
-
"arguments" => JSON.parse(tool_call["function"]["arguments"])
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
30
|
def build_request_parameters(model_completion)
|
|
41
31
|
formatted_system_prompt = format_system_prompt(model_completion)
|
|
42
32
|
|
|
@@ -60,6 +50,11 @@ private
|
|
|
60
50
|
if supports_native_tool_use?
|
|
61
51
|
tools = build_tools_parameter(model_completion)
|
|
62
52
|
parameters[:tools] = tools unless tools.blank?
|
|
53
|
+
|
|
54
|
+
if model_completion.tool_choice.present?
|
|
55
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
56
|
+
parameters[:tool_choice] = build_forced_tool_choice(tool_klass.tool_name)
|
|
57
|
+
end
|
|
63
58
|
end
|
|
64
59
|
|
|
65
60
|
if model_completion.stream_response?
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Raif::Llms::OpenAiResponses < Raif::Llms::OpenAiBase
|
|
4
4
|
include Raif::Concerns::Llms::OpenAiResponses::MessageFormatting
|
|
5
5
|
include Raif::Concerns::Llms::OpenAiResponses::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::OpenAiResponses::ResponseToolCalls
|
|
6
7
|
|
|
7
8
|
private
|
|
8
9
|
|
|
@@ -27,22 +28,6 @@ private
|
|
|
27
28
|
)
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def extract_response_tool_calls(resp)
|
|
31
|
-
return if resp["output"].blank?
|
|
32
|
-
|
|
33
|
-
tool_calls = []
|
|
34
|
-
resp["output"].each do |output_item|
|
|
35
|
-
next unless output_item["type"] == "function_call"
|
|
36
|
-
|
|
37
|
-
tool_calls << {
|
|
38
|
-
"name" => output_item["name"],
|
|
39
|
-
"arguments" => JSON.parse(output_item["arguments"])
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
tool_calls.any? ? tool_calls : nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
31
|
def extract_raw_response(resp)
|
|
47
32
|
text_outputs = []
|
|
48
33
|
|
|
@@ -110,6 +95,11 @@ private
|
|
|
110
95
|
if supports_native_tool_use?
|
|
111
96
|
tools = build_tools_parameter(model_completion)
|
|
112
97
|
parameters[:tools] = tools unless tools.blank?
|
|
98
|
+
|
|
99
|
+
if model_completion.tool_choice.present?
|
|
100
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
101
|
+
parameters[:tool_choice] = build_forced_tool_choice(tool_klass.tool_name)
|
|
102
|
+
end
|
|
113
103
|
end
|
|
114
104
|
|
|
115
105
|
# Add response format if needed. Default will be { "type": "text" }
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Raif::Llms::OpenRouter < Raif::Llm
|
|
4
4
|
include Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
|
5
5
|
include Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
|
|
6
|
+
include Raif::Concerns::Llms::OpenAiCompletions::ResponseToolCalls
|
|
6
7
|
include Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
|
|
7
8
|
|
|
8
9
|
def perform_model_completion!(model_completion, &block)
|
|
@@ -23,7 +24,7 @@ class Raif::Llms::OpenRouter < Raif::Llm
|
|
|
23
24
|
private
|
|
24
25
|
|
|
25
26
|
def connection
|
|
26
|
-
@connection ||= Faraday.new(url: "https://openrouter.ai/api/v1") do |f|
|
|
27
|
+
@connection ||= Faraday.new(url: "https://openrouter.ai/api/v1", request: Raif.default_request_options) do |f|
|
|
27
28
|
f.headers["Authorization"] = "Bearer #{Raif.config.open_router_api_key}"
|
|
28
29
|
f.headers["HTTP-Referer"] = Raif.config.open_router_site_url if Raif.config.open_router_site_url.present?
|
|
29
30
|
f.headers["X-Title"] = Raif.config.open_router_app_name if Raif.config.open_router_app_name.present?
|
|
@@ -85,6 +86,11 @@ private
|
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
params[:tools] = tools unless tools.blank?
|
|
89
|
+
|
|
90
|
+
if model_completion.tool_choice.present?
|
|
91
|
+
tool_klass = model_completion.tool_choice.constantize
|
|
92
|
+
params[:tool_choice] = build_forced_tool_choice(tool_klass.tool_name)
|
|
93
|
+
end
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
if model_completion.stream_response?
|
|
@@ -93,7 +99,9 @@ private
|
|
|
93
99
|
params[:stream_options] = { include_usage: true }
|
|
94
100
|
end
|
|
95
101
|
|
|
96
|
-
|
|
102
|
+
# OpenRouter will sometimes complain about combining response_format json and tool calling.
|
|
103
|
+
# If we're telling it to use the json_response tool, then the json_object response_format should be irrelevant.
|
|
104
|
+
if model_completion.response_format_json? && params[:tools].blank?
|
|
97
105
|
params[:response_format] = { type: "json_object" }
|
|
98
106
|
model_completion.response_format_parameter = "json_object"
|
|
99
107
|
end
|
|
@@ -119,16 +127,4 @@ private
|
|
|
119
127
|
extract_text_response(resp)
|
|
120
128
|
end
|
|
121
129
|
end
|
|
122
|
-
|
|
123
|
-
def extract_response_tool_calls(resp)
|
|
124
|
-
tool_calls = resp.dig("choices", 0, "message", "tool_calls")
|
|
125
|
-
return if tool_calls.blank?
|
|
126
|
-
|
|
127
|
-
tool_calls.map do |tool_call|
|
|
128
|
-
{
|
|
129
|
-
"name" => tool_call["function"]["name"],
|
|
130
|
-
"arguments" => JSON.parse(tool_call["function"]["arguments"])
|
|
131
|
-
}
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
130
|
end
|
|
@@ -1,14 +1,68 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_model_completions
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# available_model_tools :jsonb not null
|
|
9
|
+
# citations :jsonb
|
|
10
|
+
# completed_at :datetime
|
|
11
|
+
# completion_tokens :integer
|
|
12
|
+
# failed_at :datetime
|
|
13
|
+
# failure_error :string
|
|
14
|
+
# failure_reason :text
|
|
15
|
+
# llm_model_key :string not null
|
|
16
|
+
# max_completion_tokens :integer
|
|
17
|
+
# messages :jsonb not null
|
|
18
|
+
# model_api_name :string not null
|
|
19
|
+
# output_token_cost :decimal(10, 6)
|
|
20
|
+
# prompt_token_cost :decimal(10, 6)
|
|
21
|
+
# prompt_tokens :integer
|
|
22
|
+
# raw_response :text
|
|
23
|
+
# response_array :jsonb
|
|
24
|
+
# response_format :integer default("text"), not null
|
|
25
|
+
# response_format_parameter :string
|
|
26
|
+
# response_tool_calls :jsonb
|
|
27
|
+
# retry_count :integer default(0), not null
|
|
28
|
+
# source_type :string
|
|
29
|
+
# started_at :datetime
|
|
30
|
+
# stream_response :boolean default(FALSE), not null
|
|
31
|
+
# system_prompt :text
|
|
32
|
+
# temperature :decimal(5, 3)
|
|
33
|
+
# tool_choice :string
|
|
34
|
+
# total_cost :decimal(10, 6)
|
|
35
|
+
# total_tokens :integer
|
|
36
|
+
# created_at :datetime not null
|
|
37
|
+
# updated_at :datetime not null
|
|
38
|
+
# response_id :string
|
|
39
|
+
# source_id :bigint
|
|
40
|
+
#
|
|
41
|
+
# Indexes
|
|
42
|
+
#
|
|
43
|
+
# index_raif_model_completions_on_completed_at (completed_at)
|
|
44
|
+
# index_raif_model_completions_on_created_at (created_at)
|
|
45
|
+
# index_raif_model_completions_on_failed_at (failed_at)
|
|
46
|
+
# index_raif_model_completions_on_source (source_type,source_id)
|
|
47
|
+
# index_raif_model_completions_on_started_at (started_at)
|
|
48
|
+
#
|
|
3
49
|
class Raif::ModelCompletion < Raif::ApplicationRecord
|
|
4
50
|
include Raif::Concerns::LlmResponseParsing
|
|
5
51
|
include Raif::Concerns::HasAvailableModelTools
|
|
52
|
+
include Raif::Concerns::BooleanTimestamp
|
|
53
|
+
|
|
54
|
+
boolean_timestamp :started_at
|
|
55
|
+
boolean_timestamp :completed_at
|
|
56
|
+
boolean_timestamp :failed_at
|
|
6
57
|
|
|
7
58
|
belongs_to :source, polymorphic: true, optional: true
|
|
8
59
|
|
|
9
60
|
validates :llm_model_key, presence: true, inclusion: { in: ->{ Raif.available_llm_keys.map(&:to_s) } }
|
|
10
61
|
validates :model_api_name, presence: true
|
|
11
62
|
|
|
63
|
+
# Scope to find completions that have response tool calls
|
|
64
|
+
scope :with_response_tool_calls, -> { where_json_not_blank(:response_tool_calls) }
|
|
65
|
+
|
|
12
66
|
delegate :json_response_schema, to: :source, allow_nil: true
|
|
13
67
|
|
|
14
68
|
before_save :set_total_tokens
|
|
@@ -41,6 +95,13 @@ class Raif::ModelCompletion < Raif::ApplicationRecord
|
|
|
41
95
|
end
|
|
42
96
|
end
|
|
43
97
|
|
|
98
|
+
def record_failure!(exception)
|
|
99
|
+
self.failed_at = Time.current
|
|
100
|
+
self.failure_error = exception.class.name
|
|
101
|
+
self.failure_reason = exception.message.truncate(255)
|
|
102
|
+
save!
|
|
103
|
+
end
|
|
104
|
+
|
|
44
105
|
private
|
|
45
106
|
|
|
46
107
|
def llm_config
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
class Raif::ModelTool
|
|
4
4
|
include Raif::Concerns::JsonSchemaDefinition
|
|
5
5
|
|
|
6
|
-
delegate :tool_name, :tool_description, :
|
|
6
|
+
delegate :tool_name, :tool_description, :example_model_invocation, to: :class
|
|
7
7
|
|
|
8
8
|
class << self
|
|
9
9
|
# The description of the tool that will be provided to the model
|
|
@@ -76,8 +76,9 @@ class Raif::ModelTool
|
|
|
76
76
|
false
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def invoke_tool(tool_arguments:, source:)
|
|
79
|
+
def invoke_tool(provider_tool_call_id:, tool_arguments:, source:)
|
|
80
80
|
tool_invocation = Raif::ModelToolInvocation.new(
|
|
81
|
+
provider_tool_call_id: provider_tool_call_id,
|
|
81
82
|
source: source,
|
|
82
83
|
tool_type: name,
|
|
83
84
|
tool_arguments: tool_arguments
|
|
@@ -96,4 +97,11 @@ class Raif::ModelTool
|
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# Instance method to get the tool arguments schema
|
|
101
|
+
# For instance-dependent schemas, builds the schema with this instance as context
|
|
102
|
+
# For class-level schemas, returns the class-level schema
|
|
103
|
+
def tool_arguments_schema
|
|
104
|
+
schema_for_instance(:tool_arguments)
|
|
105
|
+
end
|
|
106
|
+
|
|
99
107
|
end
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_model_tool_invocations
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# completed_at :datetime
|
|
9
|
+
# failed_at :datetime
|
|
10
|
+
# result :jsonb not null
|
|
11
|
+
# source_type :string not null
|
|
12
|
+
# tool_arguments :jsonb not null
|
|
13
|
+
# tool_type :string not null
|
|
14
|
+
# created_at :datetime not null
|
|
15
|
+
# updated_at :datetime not null
|
|
16
|
+
# provider_tool_call_id :string
|
|
17
|
+
# source_id :bigint not null
|
|
18
|
+
#
|
|
19
|
+
# Indexes
|
|
20
|
+
#
|
|
21
|
+
# index_raif_model_tool_invocations_on_source (source_type,source_id)
|
|
22
|
+
#
|
|
3
23
|
class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
4
24
|
belongs_to :source, polymorphic: true
|
|
5
25
|
|
|
@@ -22,14 +42,26 @@ class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
|
22
42
|
@tool ||= tool_type.constantize
|
|
23
43
|
end
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
# Returns tool call in the format expected by LLM message formatting
|
|
46
|
+
# @param assistant_message [String, nil] Optional assistant message accompanying the tool call
|
|
47
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
48
|
+
def as_tool_call_message(assistant_message: nil)
|
|
49
|
+
Raif::Messages::ToolCall.new(
|
|
50
|
+
provider_tool_call_id: provider_tool_call_id,
|
|
51
|
+
name: tool_name,
|
|
52
|
+
arguments: tool_arguments,
|
|
53
|
+
assistant_message: assistant_message
|
|
54
|
+
).to_h
|
|
27
55
|
end
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
# Returns tool result in the format expected by LLM message formatting
|
|
58
|
+
# @return [Hash] Hash representation for JSONB storage and LLM APIs
|
|
59
|
+
def as_tool_call_result_message
|
|
60
|
+
Raif::Messages::ToolCallResult.new(
|
|
61
|
+
provider_tool_call_id: provider_tool_call_id,
|
|
62
|
+
name: tool_name,
|
|
63
|
+
result: result
|
|
64
|
+
).to_h
|
|
33
65
|
end
|
|
34
66
|
|
|
35
67
|
def to_partial_path
|