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.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -5
  3. data/app/assets/builds/raif.css +4 -1
  4. data/app/assets/builds/raif_admin.css +13 -1
  5. data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
  6. data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
  7. data/app/assets/stylesheets/raif/conversations.scss +3 -0
  8. data/app/assets/stylesheets/raif.scss +2 -1
  9. data/app/controllers/raif/admin/application_controller.rb +16 -0
  10. data/app/controllers/raif/admin/configs_controller.rb +94 -0
  11. data/app/controllers/raif/admin/model_completions_controller.rb +18 -1
  12. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
  13. data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
  14. data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
  15. data/app/controllers/raif/admin/stats_controller.rb +32 -3
  16. data/app/controllers/raif/conversation_entries_controller.rb +1 -0
  17. data/app/controllers/raif/conversations_controller.rb +10 -2
  18. data/app/jobs/raif/conversation_entry_job.rb +8 -6
  19. data/app/models/raif/admin/task_stat.rb +7 -0
  20. data/app/models/raif/agent.rb +63 -2
  21. data/app/models/raif/agents/native_tool_calling_agent.rb +101 -56
  22. data/app/models/raif/application_record.rb +18 -0
  23. data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
  24. data/app/models/raif/concerns/has_llm.rb +1 -1
  25. data/app/models/raif/concerns/json_schema_definition.rb +40 -5
  26. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +28 -0
  27. data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
  28. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +4 -0
  29. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +36 -0
  30. data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
  31. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +4 -0
  32. data/app/models/raif/concerns/llms/google/message_formatting.rb +109 -0
  33. data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
  34. data/app/models/raif/concerns/llms/google/tool_formatting.rb +72 -0
  35. data/app/models/raif/concerns/llms/message_formatting.rb +11 -5
  36. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
  37. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
  38. data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
  39. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +4 -0
  40. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
  41. data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
  42. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +4 -0
  43. data/app/models/raif/concerns/run_with.rb +127 -0
  44. data/app/models/raif/conversation.rb +96 -9
  45. data/app/models/raif/conversation_entry.rb +37 -8
  46. data/app/models/raif/embedding_model.rb +2 -1
  47. data/app/models/raif/embedding_models/open_ai.rb +1 -1
  48. data/app/models/raif/llm.rb +28 -3
  49. data/app/models/raif/llms/anthropic.rb +7 -19
  50. data/app/models/raif/llms/bedrock.rb +6 -20
  51. data/app/models/raif/llms/google.rb +140 -0
  52. data/app/models/raif/llms/open_ai_base.rb +19 -5
  53. data/app/models/raif/llms/open_ai_completions.rb +6 -11
  54. data/app/models/raif/llms/open_ai_responses.rb +6 -16
  55. data/app/models/raif/llms/open_router.rb +10 -14
  56. data/app/models/raif/model_completion.rb +61 -0
  57. data/app/models/raif/model_tool.rb +10 -2
  58. data/app/models/raif/model_tool_invocation.rb +38 -6
  59. data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
  60. data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
  61. data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
  62. data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
  63. data/app/models/raif/streaming_responses/google.rb +71 -0
  64. data/app/models/raif/task.rb +74 -18
  65. data/app/models/raif/user_tool_invocation.rb +19 -0
  66. data/app/views/layouts/raif/admin.html.erb +12 -1
  67. data/app/views/raif/admin/agents/_agent.html.erb +8 -0
  68. data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
  69. data/app/views/raif/admin/agents/index.html.erb +2 -0
  70. data/app/views/raif/admin/agents/show.html.erb +46 -1
  71. data/app/views/raif/admin/configs/show.html.erb +117 -0
  72. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
  73. data/app/views/raif/admin/conversations/show.html.erb +2 -0
  74. data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
  75. data/app/views/raif/admin/model_completions/index.html.erb +26 -0
  76. data/app/views/raif/admin/model_completions/show.html.erb +124 -61
  77. data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
  78. data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
  79. data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
  80. data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
  81. data/app/views/raif/admin/stats/index.html.erb +71 -88
  82. data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
  83. data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
  84. data/app/views/raif/admin/tasks/index.html.erb +6 -1
  85. data/app/views/raif/admin/tasks/show.html.erb +36 -3
  86. data/app/views/raif/conversation_entries/_form.html.erb +4 -1
  87. data/app/views/raif/conversations/_conversation.html.erb +10 -0
  88. data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
  89. data/app/views/raif/conversations/_full_conversation.html.erb +3 -6
  90. data/app/views/raif/conversations/_initial_chat_message.html.erb +5 -0
  91. data/app/views/raif/conversations/index.html.erb +23 -0
  92. data/config/locales/admin.en.yml +33 -1
  93. data/config/locales/en.yml +41 -4
  94. data/config/routes.rb +2 -0
  95. data/db/migrate/20250804013843_add_task_run_args_to_raif_tasks.rb +13 -0
  96. data/db/migrate/20250811171150_make_raif_task_creator_optional.rb +8 -0
  97. data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
  98. data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
  99. data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
  100. data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
  101. data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
  102. data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
  103. data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
  104. data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
  105. data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
  106. data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
  107. data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
  108. data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
  109. data/exe/raif +7 -0
  110. data/lib/generators/raif/agent/agent_generator.rb +22 -7
  111. data/lib/generators/raif/agent/templates/agent.rb.tt +20 -24
  112. data/lib/generators/raif/agent/templates/agent_eval_set.rb.tt +48 -0
  113. data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -3
  114. data/lib/generators/raif/base_generator.rb +19 -0
  115. data/lib/generators/raif/conversation/conversation_generator.rb +21 -2
  116. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +0 -2
  117. data/lib/generators/raif/conversation/templates/conversation.rb.tt +34 -32
  118. data/lib/generators/raif/conversation/templates/conversation_eval_set.rb.tt +70 -0
  119. data/lib/generators/raif/eval_set/eval_set_generator.rb +28 -0
  120. data/lib/generators/raif/eval_set/templates/eval_set.rb.tt +21 -0
  121. data/lib/generators/raif/evals/setup/setup_generator.rb +47 -0
  122. data/lib/generators/raif/install/install_generator.rb +15 -0
  123. data/lib/generators/raif/install/templates/initializer.rb +89 -10
  124. data/lib/generators/raif/model_tool/model_tool_generator.rb +5 -5
  125. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +78 -78
  126. data/lib/generators/raif/model_tool/templates/model_tool_invocation_partial.html.erb.tt +1 -1
  127. data/lib/generators/raif/task/task_generator.rb +22 -3
  128. data/lib/generators/raif/task/templates/application_task.rb.tt +0 -2
  129. data/lib/generators/raif/task/templates/task.rb.tt +55 -59
  130. data/lib/generators/raif/task/templates/task_eval_set.rb.tt +54 -0
  131. data/lib/raif/cli/base.rb +39 -0
  132. data/lib/raif/cli/evals.rb +47 -0
  133. data/lib/raif/cli/evals_setup.rb +27 -0
  134. data/lib/raif/cli.rb +67 -0
  135. data/lib/raif/configuration.rb +57 -8
  136. data/lib/raif/engine.rb +8 -0
  137. data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
  138. data/lib/raif/errors/streaming_error.rb +6 -3
  139. data/lib/raif/errors.rb +1 -0
  140. data/lib/raif/evals/eval.rb +30 -0
  141. data/lib/raif/evals/eval_set.rb +111 -0
  142. data/lib/raif/evals/eval_sets/expectations.rb +53 -0
  143. data/lib/raif/evals/eval_sets/llm_judge_expectations.rb +255 -0
  144. data/lib/raif/evals/expectation_result.rb +39 -0
  145. data/lib/raif/evals/llm_judge.rb +32 -0
  146. data/lib/raif/evals/llm_judges/binary.rb +94 -0
  147. data/lib/raif/evals/llm_judges/comparative.rb +89 -0
  148. data/lib/raif/evals/llm_judges/scored.rb +63 -0
  149. data/lib/raif/evals/llm_judges/summarization.rb +166 -0
  150. data/lib/raif/evals/run.rb +202 -0
  151. data/lib/raif/evals/scoring_rubric.rb +174 -0
  152. data/lib/raif/evals.rb +26 -0
  153. data/lib/raif/json_schema_builder.rb +14 -0
  154. data/lib/raif/llm_registry.rb +218 -15
  155. data/lib/raif/messages.rb +180 -0
  156. data/lib/raif/migration_checker.rb +3 -3
  157. data/lib/raif/utils/colors.rb +23 -0
  158. data/lib/raif/utils.rb +1 -0
  159. data/lib/raif/version.rb +1 -1
  160. data/lib/raif.rb +13 -0
  161. data/lib/tasks/annotate_rb.rake +10 -0
  162. data/spec/support/current_temperature_test_tool.rb +34 -0
  163. data/spec/support/rspec_helpers.rb +8 -8
  164. data/spec/support/test_conversation.rb +1 -1
  165. metadata +77 -10
  166. data/app/models/raif/agents/re_act_agent.rb +0 -127
  167. data/app/models/raif/agents/re_act_step.rb +0 -33
@@ -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
- role = message["role"] || message[:role]
9
- {
10
- "role" => role,
11
- "content" => format_message_content(message["content"] || message[:content], role: role)
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 5 levels)
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 > 5
122
- errors << "Schema exceeds maximum nesting depth of 5 levels"
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
@@ -23,4 +23,8 @@ module Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
23
23
  end
24
24
  end
25
25
  end
26
+
27
+ def build_forced_tool_choice(tool_name)
28
+ { "type" => "function", "function" => { "name" => tool_name } }
29
+ end
26
30
  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
@@ -39,4 +39,8 @@ module Raif::Concerns::Llms::OpenAiResponses::ToolFormatting
39
39
  "Invalid provider-managed tool: #{tool.name} for #{key}"
40
40
  end
41
41
  end
42
+
43
+ def build_forced_tool_choice(tool_name)
44
+ { "type" => "function", "name" => tool_name }
45
+ end
42
46
  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
 
@@ -34,10 +84,20 @@ class Raif::Conversation < Raif::ApplicationRecord
34
84
  I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
35
85
  end
36
86
 
87
+ def initial_chat_message_partial_path
88
+ "raif/conversations/initial_chat_message"
89
+ end
90
+
37
91
  def prompt_model_for_entry_response(entry:, &block)
38
- update(system_prompt: build_system_prompt)
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!
39
99
 
40
- llm.chat(
100
+ model_completion = llm.chat(
41
101
  messages: llm_messages,
42
102
  source: entry,
43
103
  response_format: response_format.to_sym,
@@ -45,7 +105,15 @@ class Raif::Conversation < Raif::ApplicationRecord
45
105
  available_model_tools: available_model_tools,
46
106
  &block
47
107
  )
108
+
109
+ self.generating_entry_response = false
110
+ save!
111
+
112
+ model_completion
48
113
  rescue StandardError => e
114
+ self.generating_entry_response = false
115
+ save!
116
+
49
117
  Rails.logger.error("Error processing conversation entry ##{entry.id}. #{e.message}")
50
118
  Rails.logger.error(e.backtrace.join("\n"))
51
119
 
@@ -59,7 +127,7 @@ class Raif::Conversation < Raif::ApplicationRecord
59
127
  Airbrake.notify(notice)
60
128
  end
61
129
 
62
- entry
130
+ nil
63
131
  end
64
132
 
65
133
  def process_model_response_message(message:, entry:)
@@ -71,14 +139,33 @@ class Raif::Conversation < Raif::ApplicationRecord
71
139
  def llm_messages
72
140
  messages = []
73
141
 
74
- entries.oldest_first.includes(:raif_model_tool_invocations).each do |entry|
75
- messages << { "role" => "user", "content" => entry.user_message } unless entry.user_message.blank?
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
+
76
151
  next unless entry.completed?
77
152
 
78
- messages << { "role" => "assistant", "content" => entry.model_response_message } unless entry.model_response_message.blank?
79
- entry.raif_model_tool_invocations.each do |tool_invocation|
80
- messages << { "role" => "assistant", "content" => tool_invocation.as_llm_message }
81
- messages << { "role" => "assistant", "content" => tool_invocation.result_llm_message } if tool_invocation.result_llm_message.present?
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
82
169
  end
83
170
  end
84
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
@@ -63,7 +90,7 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
63
90
  broadcast_replace_to raif_conversation
64
91
  end
65
92
 
66
- if raif_model_completion.parsed_response.present? || raif_model_completion.response_tool_calls.present?
93
+ if raif_model_completion.present? && (raif_model_completion.parsed_response.present? || raif_model_completion.response_tool_calls.present?)
67
94
  extract_message_and_invoke_tools!
68
95
  create_entry_for_observation! if triggers_observation_to_model?
69
96
  else
@@ -83,7 +110,7 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
83
110
  def create_entry_for_observation!
84
111
  follow_up_entry = raif_conversation.entries.create!(creator: creator)
85
112
  Raif::ConversationEntryJob.perform_later(conversation_entry: follow_up_entry)
86
- follow_up_entry.broadcast_append_to raif_conversation, target: dom_id(raif_conversation, :entries)
113
+ follow_up_entry.broadcast_append_to raif_conversation, target: ActionView::RecordIdentifier.dom_id(raif_conversation, :entries)
87
114
  end
88
115
 
89
116
  private
@@ -94,13 +121,15 @@ private
94
121
  self.model_response_message = raif_conversation.process_model_response_message(message: raif_model_completion.parsed_response, entry: self)
95
122
  save!
96
123
 
97
- if raif_model_completion.response_tool_calls.present?
98
- raif_model_completion.response_tool_calls.each do |tool_call|
99
- tool_klass = available_model_tools_map[tool_call["name"]]
100
- next if tool_klass.nil?
124
+ raif_model_completion.response_tool_calls&.each do |tool_call|
125
+ tool_klass = available_model_tools_map[tool_call["name"]]
126
+ next if tool_klass.nil?
101
127
 
102
- tool_klass.invoke_tool(tool_arguments: tool_call["arguments"], source: self)
103
- end
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
+ )
104
133
  end
105
134
 
106
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: "https://api.openai.com/v1") do |f|
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