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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::Google::ToolFormatting
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def build_tools_parameter(model_completion)
|
|
7
|
+
tools = []
|
|
8
|
+
function_declarations = []
|
|
9
|
+
|
|
10
|
+
# If we support native tool use and have tools available, add them to the request
|
|
11
|
+
if supports_native_tool_use? && model_completion.available_model_tools.any?
|
|
12
|
+
model_completion.available_model_tools_map.each do |_tool_name, tool|
|
|
13
|
+
if tool.provider_managed?
|
|
14
|
+
# Provider-managed tools are added as separate tool entries
|
|
15
|
+
tools << format_provider_managed_tool(tool)
|
|
16
|
+
else
|
|
17
|
+
function_declarations << {
|
|
18
|
+
name: tool.tool_name,
|
|
19
|
+
description: tool.tool_description,
|
|
20
|
+
parameters: sanitize_schema_for_google(tool.tool_arguments_schema)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add function declarations if any
|
|
27
|
+
if function_declarations.any?
|
|
28
|
+
tools << { functionDeclarations: function_declarations }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
tools
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_provider_managed_tool(tool)
|
|
35
|
+
validate_provider_managed_tool_support!(tool)
|
|
36
|
+
|
|
37
|
+
case tool.name
|
|
38
|
+
when "Raif::ModelTools::ProviderManaged::WebSearch"
|
|
39
|
+
{ google_search: {} }
|
|
40
|
+
when "Raif::ModelTools::ProviderManaged::CodeExecution"
|
|
41
|
+
{ code_execution: {} }
|
|
42
|
+
else
|
|
43
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
|
44
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_forced_tool_choice(tool_name)
|
|
49
|
+
{ mode: "ANY", allowedFunctionNames: [tool_name] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Google's API doesn't support additionalProperties in JSON schemas
|
|
55
|
+
# This method recursively removes it from the schema
|
|
56
|
+
def sanitize_schema_for_google(schema)
|
|
57
|
+
return schema unless schema.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
sanitized = schema.except(:additionalProperties, "additionalProperties")
|
|
60
|
+
|
|
61
|
+
sanitized.transform_values do |value|
|
|
62
|
+
case value
|
|
63
|
+
when Hash
|
|
64
|
+
sanitize_schema_for_google(value)
|
|
65
|
+
when Array
|
|
66
|
+
value.map { |item| sanitize_schema_for_google(item) }
|
|
67
|
+
else
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -5,11 +5,17 @@ module Raif::Concerns::Llms::MessageFormatting
|
|
|
5
5
|
|
|
6
6
|
def format_messages(messages)
|
|
7
7
|
messages.map do |message|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
if message.is_a?(Hash) && message["type"] == "tool_call"
|
|
9
|
+
format_tool_call_message(message)
|
|
10
|
+
elsif message.is_a?(Hash) && message["type"] == "tool_call_result"
|
|
11
|
+
format_tool_call_result_message(message)
|
|
12
|
+
else
|
|
13
|
+
role = message["role"] || message[:role]
|
|
14
|
+
{
|
|
15
|
+
"role" => role,
|
|
16
|
+
"content" => format_message_content(message["content"] || message[:content], role: role)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
|
|
@@ -23,7 +23,7 @@ module Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
|
|
|
23
23
|
# Check properties count (max 100 total)
|
|
24
24
|
validate_properties_count(schema, errors)
|
|
25
25
|
|
|
26
|
-
# Check nesting depth (max
|
|
26
|
+
# Check nesting depth (max 10 levels)
|
|
27
27
|
validate_nesting_depth(schema, errors)
|
|
28
28
|
|
|
29
29
|
# Check for unsupported anyOf at root level
|
|
@@ -118,8 +118,8 @@ private
|
|
|
118
118
|
def validate_nesting_depth(schema, errors, depth = 1)
|
|
119
119
|
return unless schema.is_a?(Hash)
|
|
120
120
|
|
|
121
|
-
if depth >
|
|
122
|
-
errors << "Schema exceeds maximum nesting depth of
|
|
121
|
+
if depth > 10
|
|
122
|
+
errors << "Schema exceeds maximum nesting depth of 10 levels"
|
|
123
123
|
return
|
|
124
124
|
end
|
|
125
125
|
|
|
@@ -38,4 +38,26 @@ module Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
|
|
38
38
|
raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
def format_tool_call_message(tool_call)
|
|
43
|
+
{
|
|
44
|
+
"role" => "assistant",
|
|
45
|
+
"tool_calls" => [{
|
|
46
|
+
"id" => tool_call["provider_tool_call_id"],
|
|
47
|
+
"type" => "function",
|
|
48
|
+
"function" => {
|
|
49
|
+
"name" => tool_call["name"],
|
|
50
|
+
"arguments" => JSON.generate(tool_call["arguments"])
|
|
51
|
+
}
|
|
52
|
+
}]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_tool_call_result_message(tool_call_result)
|
|
57
|
+
{
|
|
58
|
+
"role" => "tool",
|
|
59
|
+
"tool_call_id" => tool_call_result["provider_tool_call_id"],
|
|
60
|
+
"content" => tool_call_result["result"].is_a?(String) ? tool_call_result["result"] : JSON.generate(tool_call_result["result"])
|
|
61
|
+
}
|
|
62
|
+
end
|
|
41
63
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::OpenAiCompletions::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
tool_calls = resp.dig("choices", 0, "message", "tool_calls")
|
|
8
|
+
return if tool_calls.blank?
|
|
9
|
+
|
|
10
|
+
tool_calls.map do |tool_call|
|
|
11
|
+
{
|
|
12
|
+
"provider_tool_call_id" => tool_call["id"],
|
|
13
|
+
"name" => tool_call["function"]["name"],
|
|
14
|
+
"arguments" => begin
|
|
15
|
+
JSON.parse(tool_call["function"]["arguments"])
|
|
16
|
+
rescue JSON::ParserError
|
|
17
|
+
tool_call["function"]["arguments"]
|
|
18
|
+
end
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -40,4 +40,21 @@ module Raif::Concerns::Llms::OpenAiResponses::MessageFormatting
|
|
|
40
40
|
raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
def format_tool_call_message(tool_call)
|
|
45
|
+
{
|
|
46
|
+
"type" => "function_call",
|
|
47
|
+
"call_id" => tool_call["provider_tool_call_id"],
|
|
48
|
+
"name" => tool_call["name"],
|
|
49
|
+
"arguments" => JSON.generate(tool_call["arguments"])
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def format_tool_call_result_message(tool_call_result)
|
|
54
|
+
{
|
|
55
|
+
"type" => "function_call_output",
|
|
56
|
+
"call_id" => tool_call_result["provider_tool_call_id"],
|
|
57
|
+
"output" => tool_call_result["result"].is_a?(String) ? tool_call_result["result"] : JSON.generate(tool_call_result["result"])
|
|
58
|
+
}
|
|
59
|
+
end
|
|
43
60
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::Llms::OpenAiResponses::ResponseToolCalls
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def extract_response_tool_calls(resp)
|
|
7
|
+
return if resp["output"].blank?
|
|
8
|
+
|
|
9
|
+
tool_calls = []
|
|
10
|
+
resp["output"].each do |output_item|
|
|
11
|
+
next unless output_item["type"] == "function_call"
|
|
12
|
+
|
|
13
|
+
tool_calls << {
|
|
14
|
+
"provider_tool_call_id" => output_item["call_id"],
|
|
15
|
+
"name" => output_item["name"],
|
|
16
|
+
"arguments" => begin
|
|
17
|
+
JSON.parse(output_item["arguments"])
|
|
18
|
+
rescue JSON::ParserError
|
|
19
|
+
output_item["arguments"]
|
|
20
|
+
end
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
tool_calls.any? ? tool_calls : nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raif::Concerns::RunWith
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
class_attribute :_run_with_args, instance_writer: false, default: []
|
|
8
|
+
|
|
9
|
+
# Backward compatibility alias
|
|
10
|
+
class_attribute :_task_run_args, instance_writer: false, default: []
|
|
11
|
+
|
|
12
|
+
# Automatically serialize run_with args before validation on create
|
|
13
|
+
before_validation :serialize_run_with_to_column, on: :create
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
# Scope for querying records by run_with arguments
|
|
18
|
+
# @param args [Hash] Key-value pairs to match in the run_with column
|
|
19
|
+
# @example
|
|
20
|
+
# Task.having_run_with(document: doc)
|
|
21
|
+
# Task.having_run_with(user: user, options: { foo: "bar" })
|
|
22
|
+
def having_run_with(**args)
|
|
23
|
+
return all if args.empty?
|
|
24
|
+
|
|
25
|
+
# Serialize args the same way we do for storage (handles GID conversion)
|
|
26
|
+
serialized = serialize_run_with(args)
|
|
27
|
+
|
|
28
|
+
# Avoid matching all records if args didn't match declared run_with arguments
|
|
29
|
+
return none if args.any? && serialized.empty?
|
|
30
|
+
|
|
31
|
+
# Use database-specific JSON containment query
|
|
32
|
+
case connection.adapter_name.downcase
|
|
33
|
+
when "postgresql"
|
|
34
|
+
# PostgreSQL: Use JSONB containment operator
|
|
35
|
+
where("run_with @> ?", serialized.to_json)
|
|
36
|
+
when "mysql2", "trilogy"
|
|
37
|
+
# MySQL: Use JSON_CONTAINS function
|
|
38
|
+
where("JSON_CONTAINS(run_with, ?)", serialized.to_json)
|
|
39
|
+
else
|
|
40
|
+
raise "Unsupported database: #{connection.adapter_name}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# DSL for declaring persistent run arguments that will be serialized to the database
|
|
45
|
+
# @param name [Symbol] The name of the argument
|
|
46
|
+
def run_with(name)
|
|
47
|
+
# Ensure each class has its own array copy
|
|
48
|
+
self._run_with_args = _run_with_args.dup
|
|
49
|
+
_run_with_args << name.to_sym
|
|
50
|
+
|
|
51
|
+
# Keep backward compatibility for _task_run_args class attribute
|
|
52
|
+
self._task_run_args = _task_run_args.dup
|
|
53
|
+
_task_run_args << name.to_sym
|
|
54
|
+
|
|
55
|
+
# Define getter that pulls from run_with JSON column
|
|
56
|
+
define_method(name) do
|
|
57
|
+
return instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}")
|
|
58
|
+
|
|
59
|
+
value = run_with&.dig(name.to_s)
|
|
60
|
+
return unless value
|
|
61
|
+
|
|
62
|
+
# Deserialize GID if it's a string starting with gid://
|
|
63
|
+
deserialized = if value.is_a?(String) && value.start_with?("gid://")
|
|
64
|
+
begin
|
|
65
|
+
GlobalID::Locator.locate(value)
|
|
66
|
+
rescue ActiveRecord::RecordNotFound
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
instance_variable_set("@#{name}", deserialized)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Define setter that stores in memory (for use during run)
|
|
77
|
+
define_method("#{name}=") do |value|
|
|
78
|
+
instance_variable_set("@#{name}", value)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Backward compatibility alias
|
|
83
|
+
alias_method :task_run_arg, :run_with
|
|
84
|
+
|
|
85
|
+
# Transform run args into a hash that can be stored in the run_with database column
|
|
86
|
+
def serialize_run_with(args)
|
|
87
|
+
serialized_args = {}
|
|
88
|
+
_run_with_args.each do |arg_name|
|
|
89
|
+
next unless args.key?(arg_name)
|
|
90
|
+
|
|
91
|
+
value = args[arg_name]
|
|
92
|
+
serialized_args[arg_name.to_s] = if value.respond_to?(:to_global_id)
|
|
93
|
+
value.to_global_id.to_s
|
|
94
|
+
else
|
|
95
|
+
value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
serialized_args
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Backward compatibility alias
|
|
103
|
+
alias_method :serialize_task_run_args, :serialize_run_with
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Automatically called before validation on create to serialize run_with args
|
|
109
|
+
# Collects all declared run_with arguments from instance variables and serializes them
|
|
110
|
+
# to the run_with JSON column
|
|
111
|
+
def serialize_run_with_to_column
|
|
112
|
+
args = {}
|
|
113
|
+
|
|
114
|
+
# Collect all run_with args that were set via instance variables
|
|
115
|
+
self.class._run_with_args.each do |arg_name|
|
|
116
|
+
if instance_variable_defined?("@#{arg_name}")
|
|
117
|
+
args[arg_name] = instance_variable_get("@#{arg_name}")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Merge serialized args into run_with hash if any args were set
|
|
122
|
+
if args.any?
|
|
123
|
+
self.run_with ||= {}
|
|
124
|
+
self.run_with = self.run_with.merge(self.class.serialize_run_with(args))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_conversations
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# available_model_tools :jsonb not null
|
|
9
|
+
# available_user_tools :jsonb not null
|
|
10
|
+
# conversation_entries_count :integer default(0), not null
|
|
11
|
+
# creator_type :string not null
|
|
12
|
+
# generating_entry_response :boolean default(FALSE), not null
|
|
13
|
+
# llm_messages_max_length :integer
|
|
14
|
+
# llm_model_key :string not null
|
|
15
|
+
# requested_language_key :string
|
|
16
|
+
# response_format :integer default("text"), not null
|
|
17
|
+
# source_type :string
|
|
18
|
+
# system_prompt :text
|
|
19
|
+
# type :string not null
|
|
20
|
+
# created_at :datetime not null
|
|
21
|
+
# updated_at :datetime not null
|
|
22
|
+
# creator_id :bigint not null
|
|
23
|
+
# source_id :bigint
|
|
24
|
+
#
|
|
25
|
+
# Indexes
|
|
26
|
+
#
|
|
27
|
+
# index_raif_conversations_on_created_at (created_at)
|
|
28
|
+
# index_raif_conversations_on_creator (creator_type,creator_id)
|
|
29
|
+
# index_raif_conversations_on_source (source_type,source_id)
|
|
30
|
+
#
|
|
3
31
|
class Raif::Conversation < Raif::ApplicationRecord
|
|
4
32
|
include Raif::Concerns::HasLlm
|
|
5
33
|
include Raif::Concerns::HasRequestedLanguage
|
|
@@ -7,6 +35,27 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
|
7
35
|
include Raif::Concerns::LlmResponseParsing
|
|
8
36
|
|
|
9
37
|
belongs_to :creator, polymorphic: true
|
|
38
|
+
belongs_to :source, polymorphic: true, optional: true
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
def before_prompt_model_for_entry_response(&block)
|
|
42
|
+
@before_prompt_model_for_entry_response_blocks ||= []
|
|
43
|
+
@before_prompt_model_for_entry_response_blocks << block if block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def before_prompt_model_for_entry_response_blocks
|
|
47
|
+
blocks = []
|
|
48
|
+
|
|
49
|
+
# Collect blocks from ancestors (in reverse order so parent blocks run first)
|
|
50
|
+
ancestors.reverse_each do |klass|
|
|
51
|
+
if klass.instance_variable_defined?(:@before_prompt_model_for_entry_response_blocks)
|
|
52
|
+
blocks.concat(klass.instance_variable_get(:@before_prompt_model_for_entry_response_blocks))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
blocks
|
|
57
|
+
end
|
|
58
|
+
end
|
|
10
59
|
|
|
11
60
|
has_many :entries, class_name: "Raif::ConversationEntry", dependent: :destroy, foreign_key: :raif_conversation_id, inverse_of: :raif_conversation
|
|
12
61
|
|
|
@@ -14,6 +63,7 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
|
14
63
|
|
|
15
64
|
after_initialize -> { self.available_model_tools ||= [] }
|
|
16
65
|
after_initialize -> { self.available_user_tools ||= [] }
|
|
66
|
+
after_initialize -> { self.llm_messages_max_length ||= Raif.config.conversation_llm_messages_max_length_default }
|
|
17
67
|
|
|
18
68
|
before_validation ->{ self.type ||= "Raif::Conversation" }, on: :create
|
|
19
69
|
|
|
@@ -39,9 +89,15 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
|
39
89
|
end
|
|
40
90
|
|
|
41
91
|
def prompt_model_for_entry_response(entry:, &block)
|
|
42
|
-
|
|
92
|
+
self.class.before_prompt_model_for_entry_response_blocks.each do |callback_block|
|
|
93
|
+
instance_exec(entry, &callback_block)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
self.system_prompt = build_system_prompt
|
|
97
|
+
self.generating_entry_response = true
|
|
98
|
+
save!
|
|
43
99
|
|
|
44
|
-
llm.chat(
|
|
100
|
+
model_completion = llm.chat(
|
|
45
101
|
messages: llm_messages,
|
|
46
102
|
source: entry,
|
|
47
103
|
response_format: response_format.to_sym,
|
|
@@ -49,7 +105,15 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
|
49
105
|
available_model_tools: available_model_tools,
|
|
50
106
|
&block
|
|
51
107
|
)
|
|
108
|
+
|
|
109
|
+
self.generating_entry_response = false
|
|
110
|
+
save!
|
|
111
|
+
|
|
112
|
+
model_completion
|
|
52
113
|
rescue StandardError => e
|
|
114
|
+
self.generating_entry_response = false
|
|
115
|
+
save!
|
|
116
|
+
|
|
53
117
|
Rails.logger.error("Error processing conversation entry ##{entry.id}. #{e.message}")
|
|
54
118
|
Rails.logger.error(e.backtrace.join("\n"))
|
|
55
119
|
|
|
@@ -75,14 +139,33 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
|
75
139
|
def llm_messages
|
|
76
140
|
messages = []
|
|
77
141
|
|
|
78
|
-
entries
|
|
79
|
-
|
|
142
|
+
# Apply max length limit to entries if configured (nil means no limit)
|
|
143
|
+
included_entries = entries.oldest_first.includes(:raif_model_tool_invocations)
|
|
144
|
+
included_entries = included_entries.last(llm_messages_max_length) if llm_messages_max_length.present?
|
|
145
|
+
|
|
146
|
+
included_entries.each do |entry|
|
|
147
|
+
unless entry.user_message.blank?
|
|
148
|
+
messages << Raif::Messages::UserMessage.new(content: entry.user_message).to_h
|
|
149
|
+
end
|
|
150
|
+
|
|
80
151
|
next unless entry.completed?
|
|
81
152
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
153
|
+
tool_invocations = entry.raif_model_tool_invocations.to_a
|
|
154
|
+
|
|
155
|
+
if tool_invocations.any?
|
|
156
|
+
# First tool call includes the assistant's message (if any)
|
|
157
|
+
first_invocation = tool_invocations.shift
|
|
158
|
+
messages << first_invocation.as_tool_call_message(assistant_message: entry.model_response_message.presence)
|
|
159
|
+
messages << first_invocation.as_tool_call_result_message
|
|
160
|
+
|
|
161
|
+
# Remaining tool calls (if multiple)
|
|
162
|
+
tool_invocations.each do |tool_invocation|
|
|
163
|
+
messages << tool_invocation.as_tool_call_message
|
|
164
|
+
messages << tool_invocation.as_tool_call_result_message
|
|
165
|
+
end
|
|
166
|
+
elsif entry.model_response_message.present?
|
|
167
|
+
# No tool calls, just a regular assistant response
|
|
168
|
+
messages << Raif::Messages::AssistantMessage.new(content: entry.model_response_message).to_h
|
|
86
169
|
end
|
|
87
170
|
end
|
|
88
171
|
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# == Schema Information
|
|
4
|
+
#
|
|
5
|
+
# Table name: raif_conversation_entries
|
|
6
|
+
#
|
|
7
|
+
# id :bigint not null, primary key
|
|
8
|
+
# completed_at :datetime
|
|
9
|
+
# creator_type :string not null
|
|
10
|
+
# failed_at :datetime
|
|
11
|
+
# model_response_message :text
|
|
12
|
+
# raw_response :text
|
|
13
|
+
# started_at :datetime
|
|
14
|
+
# user_message :text
|
|
15
|
+
# created_at :datetime not null
|
|
16
|
+
# updated_at :datetime not null
|
|
17
|
+
# creator_id :bigint not null
|
|
18
|
+
# raif_conversation_id :bigint not null
|
|
19
|
+
#
|
|
20
|
+
# Indexes
|
|
21
|
+
#
|
|
22
|
+
# index_raif_conversation_entries_on_created_at (created_at)
|
|
23
|
+
# index_raif_conversation_entries_on_creator (creator_type,creator_id)
|
|
24
|
+
# index_raif_conversation_entries_on_raif_conversation_id (raif_conversation_id)
|
|
25
|
+
#
|
|
26
|
+
# Foreign Keys
|
|
27
|
+
#
|
|
28
|
+
# fk_rails_... (raif_conversation_id => raif_conversations.id)
|
|
29
|
+
#
|
|
3
30
|
class Raif::ConversationEntry < Raif::ApplicationRecord
|
|
4
31
|
include Raif::Concerns::InvokesModelTools
|
|
5
32
|
include Raif::Concerns::HasAvailableModelTools
|
|
@@ -98,7 +125,11 @@ private
|
|
|
98
125
|
tool_klass = available_model_tools_map[tool_call["name"]]
|
|
99
126
|
next if tool_klass.nil?
|
|
100
127
|
|
|
101
|
-
tool_klass.invoke_tool(
|
|
128
|
+
tool_klass.invoke_tool(
|
|
129
|
+
provider_tool_call_id: tool_call["provider_tool_call_id"],
|
|
130
|
+
tool_arguments: tool_call["arguments"],
|
|
131
|
+
source: self
|
|
132
|
+
)
|
|
102
133
|
end
|
|
103
134
|
|
|
104
135
|
completed!
|
|
@@ -5,6 +5,7 @@ class Raif::EmbeddingModel
|
|
|
5
5
|
|
|
6
6
|
attr_accessor :key,
|
|
7
7
|
:api_name,
|
|
8
|
+
:display_name,
|
|
8
9
|
:input_token_cost,
|
|
9
10
|
:default_output_vector_size
|
|
10
11
|
|
|
@@ -13,7 +14,7 @@ class Raif::EmbeddingModel
|
|
|
13
14
|
validates :key, presence: true
|
|
14
15
|
|
|
15
16
|
def name
|
|
16
|
-
I18n.t("raif.embedding_model_names.#{key}")
|
|
17
|
+
I18n.t("raif.embedding_model_names.#{key}", default: display_name || key.to_s.humanize)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def generate_embedding!(input, dimensions: nil)
|
|
@@ -30,7 +30,7 @@ private
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def connection
|
|
33
|
-
@connection ||= Faraday.new(url:
|
|
33
|
+
@connection ||= Faraday.new(url: Raif.config.open_ai_embedding_base_url, request: Raif.default_request_options) do |f|
|
|
34
34
|
f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
|
|
35
35
|
f.request :json
|
|
36
36
|
f.response :json
|
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
|
|
@@ -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,
|