raif 1.0.0 → 1.2.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +346 -43
  3. data/app/assets/builds/raif.css +26 -1
  4. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  5. data/app/assets/stylesheets/raif/loader.scss +27 -1
  6. data/app/controllers/raif/admin/application_controller.rb +14 -0
  7. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  8. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  9. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  10. data/app/controllers/raif/conversations_controller.rb +5 -1
  11. data/app/models/raif/agent.rb +11 -9
  12. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  13. data/app/models/raif/agents/re_act_agent.rb +6 -0
  14. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  15. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  16. data/app/models/raif/concerns/llm_response_parsing.rb +42 -14
  17. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  18. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  19. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
  20. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +70 -0
  21. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
  22. data/app/models/raif/concerns/llms/message_formatting.rb +42 -0
  23. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
  24. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +41 -0
  25. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
  26. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
  27. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
  28. data/app/models/raif/conversation.rb +28 -7
  29. data/app/models/raif/conversation_entry.rb +40 -8
  30. data/app/models/raif/embedding_model.rb +22 -0
  31. data/app/models/raif/embedding_models/bedrock.rb +34 -0
  32. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  33. data/app/models/raif/llm.rb +108 -9
  34. data/app/models/raif/llms/anthropic.rb +72 -57
  35. data/app/models/raif/llms/bedrock.rb +165 -0
  36. data/app/models/raif/llms/open_ai_base.rb +66 -0
  37. data/app/models/raif/llms/open_ai_completions.rb +100 -0
  38. data/app/models/raif/llms/open_ai_responses.rb +144 -0
  39. data/app/models/raif/llms/open_router.rb +88 -0
  40. data/app/models/raif/model_completion.rb +23 -2
  41. data/app/models/raif/model_file_input.rb +113 -0
  42. data/app/models/raif/model_image_input.rb +4 -0
  43. data/app/models/raif/model_tool.rb +82 -52
  44. data/app/models/raif/model_tool_invocation.rb +8 -6
  45. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  46. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  47. data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
  48. data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
  49. data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
  50. data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
  51. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  52. data/app/models/raif/streaming_responses/anthropic.rb +63 -0
  53. data/app/models/raif/streaming_responses/bedrock.rb +89 -0
  54. data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
  55. data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
  56. data/app/models/raif/task.rb +71 -16
  57. data/app/views/layouts/raif/admin.html.erb +10 -0
  58. data/app/views/raif/admin/agents/show.html.erb +3 -1
  59. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  60. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
  61. data/app/views/raif/admin/conversations/show.html.erb +4 -2
  62. data/app/views/raif/admin/model_completions/_model_completion.html.erb +8 -0
  63. data/app/views/raif/admin/model_completions/index.html.erb +2 -0
  64. data/app/views/raif/admin/model_completions/show.html.erb +58 -3
  65. data/app/views/raif/admin/stats/index.html.erb +128 -0
  66. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  67. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  68. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  69. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  70. data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
  71. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +22 -14
  72. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  73. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  74. data/app/views/raif/conversation_entries/_message.html.erb +14 -3
  75. data/config/locales/admin.en.yml +16 -0
  76. data/config/locales/en.yml +47 -3
  77. data/config/routes.rb +6 -0
  78. data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
  79. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  80. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  81. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  82. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  83. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  84. data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
  85. data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
  86. data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
  87. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  88. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  89. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  90. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  91. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  92. data/lib/generators/raif/conversation/templates/conversation.rb.tt +16 -14
  93. data/lib/generators/raif/install/templates/initializer.rb +62 -6
  94. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  95. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  96. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  97. data/lib/raif/configuration.rb +63 -4
  98. data/lib/raif/embedding_model_registry.rb +83 -0
  99. data/lib/raif/engine.rb +56 -7
  100. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  101. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  102. data/lib/raif/errors/streaming_error.rb +18 -0
  103. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  104. data/lib/raif/errors.rb +4 -2
  105. data/lib/raif/json_schema_builder.rb +104 -0
  106. data/lib/raif/llm_registry.rb +315 -0
  107. data/lib/raif/migration_checker.rb +74 -0
  108. data/lib/raif/utils/html_fragment_processor.rb +169 -0
  109. data/lib/raif/utils.rb +1 -0
  110. data/lib/raif/version.rb +1 -1
  111. data/lib/raif.rb +7 -32
  112. data/lib/tasks/raif_tasks.rake +9 -4
  113. metadata +62 -12
  114. data/app/models/raif/llms/bedrock_claude.rb +0 -134
  115. data/app/models/raif/llms/open_ai.rb +0 -259
  116. data/lib/raif/default_llms.rb +0 -37
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
4
+ extend ActiveSupport::Concern
5
+
6
+ def validate_json_schema!(schema)
7
+ return if schema.blank?
8
+
9
+ errors = []
10
+
11
+ # Check if schema is present
12
+ if schema.blank?
13
+ errors << "JSON schema must include a 'schema' property"
14
+ else
15
+ # Check root object type
16
+ if schema[:type] != "object" && !schema.key?(:properties)
17
+ errors << "Root schema must be of type 'object' with 'properties'"
18
+ end
19
+
20
+ # Check all objects in the schema recursively
21
+ validate_object_properties(schema, errors)
22
+
23
+ # Check properties count (max 100 total)
24
+ validate_properties_count(schema, errors)
25
+
26
+ # Check nesting depth (max 5 levels)
27
+ validate_nesting_depth(schema, errors)
28
+
29
+ # Check for unsupported anyOf at root level
30
+ if schema[:anyOf].present? && schema[:properties].blank?
31
+ errors << "Root objects cannot be of type 'anyOf'"
32
+ end
33
+ end
34
+
35
+ # Raise error if any validation issues found
36
+ if errors.any?
37
+ error_message = "Invalid JSON schema for OpenAI structured outputs: #{errors.join("; ")}\nSchema was: #{schema.inspect}"
38
+ raise Raif::Errors::OpenAi::JsonSchemaError, error_message
39
+ else
40
+ true
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def validate_object_properties(schema, errors)
47
+ return unless schema.is_a?(Hash)
48
+
49
+ # Check if the current schema is an object and validate additionalProperties and required fields
50
+ if schema[:type] == "object"
51
+ if schema[:additionalProperties] != false
52
+ errors << "All objects must have 'additionalProperties' set to false"
53
+ end
54
+
55
+ # Check that all properties are required
56
+ if schema[:properties].is_a?(Hash) && schema[:properties].any?
57
+ property_keys = schema[:properties].keys
58
+ required_fields = schema[:required] || []
59
+
60
+ if required_fields.sort != property_keys.map(&:to_s).sort
61
+ errors << "All object properties must be listed in the 'required' array"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Check if the current schema is an object and validate additionalProperties
67
+ if schema[:type] == "object"
68
+ if schema[:additionalProperties] != false
69
+ errors << "All objects must have 'additionalProperties' set to false"
70
+ end
71
+
72
+ # Check properties of the object recursively
73
+ if schema[:properties].is_a?(Hash)
74
+ schema[:properties].each_value do |property|
75
+ validate_object_properties(property, errors)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Check array items
81
+ if schema[:type] == "array" && schema[:items].is_a?(Hash)
82
+ validate_object_properties(schema[:items], errors)
83
+ end
84
+
85
+ # Check anyOf
86
+ if schema[:anyOf].is_a?(Array)
87
+ schema[:anyOf].each do |option|
88
+ validate_object_properties(option, errors)
89
+ end
90
+ end
91
+ end
92
+
93
+ def validate_properties_count(schema, errors, count = 0)
94
+ return count unless schema.is_a?(Hash)
95
+
96
+ if schema[:properties].is_a?(Hash)
97
+ count += schema[:properties].size
98
+
99
+ if count > 100
100
+ errors << "Schema exceeds maximum of 100 total object properties"
101
+ return count
102
+ end
103
+
104
+ # Check nested properties
105
+ schema[:properties].each_value do |property|
106
+ count = validate_properties_count(property, errors, count)
107
+ end
108
+ end
109
+
110
+ # Check array items
111
+ if schema[:type] == "array" && schema[:items].is_a?(Hash)
112
+ count = validate_properties_count(schema[:items], errors, count)
113
+ end
114
+
115
+ count
116
+ end
117
+
118
+ def validate_nesting_depth(schema, errors, depth = 1)
119
+ return unless schema.is_a?(Hash)
120
+
121
+ if depth > 5
122
+ errors << "Schema exceeds maximum nesting depth of 5 levels"
123
+ return
124
+ end
125
+
126
+ if schema[:properties].is_a?(Hash)
127
+ schema[:properties].each_value do |property|
128
+ validate_nesting_depth(property, errors, depth + 1)
129
+ end
130
+ end
131
+
132
+ # Check array items
133
+ if schema[:type] == "array" && schema[:items].is_a?(Hash)
134
+ validate_nesting_depth(schema[:items], errors, depth + 1)
135
+ end
136
+ end
137
+
138
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
4
+ extend ActiveSupport::Concern
5
+
6
+ def format_model_image_input_message(image_input)
7
+ if image_input.source_type == :url
8
+ {
9
+ "type" => "image_url",
10
+ "image_url" => { "url" => image_input.url }
11
+ }
12
+ elsif image_input.source_type == :file_content
13
+ {
14
+ "type" => "image_url",
15
+ "image_url" => {
16
+ "url" => "data:#{image_input.content_type};base64,#{image_input.base64_data}"
17
+ }
18
+ }
19
+ else
20
+ raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
21
+ end
22
+ end
23
+
24
+ def format_model_file_input_message(file_input)
25
+ if file_input.source_type == :url
26
+ raise Raif::Errors::UnsupportedFeatureError, "#{self.class.name} does not support providing a file by URL"
27
+ elsif file_input.source_type == :file_content
28
+ file_params = {
29
+ "filename" => file_input.filename,
30
+ "file_data" => "data:#{file_input.content_type};base64,#{file_input.base64_data}"
31
+ }.compact
32
+
33
+ {
34
+ "type" => "file",
35
+ "file" => file_params
36
+ }
37
+ else
38
+ raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
4
+ extend ActiveSupport::Concern
5
+
6
+ def build_tools_parameter(model_completion)
7
+ model_completion.available_model_tools_map.map do |_tool_name, tool|
8
+ if tool.provider_managed?
9
+ raise Raif::Errors::UnsupportedFeatureError,
10
+ "Raif doesn't yet support provider-managed tools for the OpenAI Completions API. Consider using the OpenAI Responses API instead."
11
+ else
12
+ # It's a developer-managed tool
13
+ validate_json_schema!(tool.tool_arguments_schema)
14
+
15
+ {
16
+ type: "function",
17
+ function: {
18
+ name: tool.tool_name,
19
+ description: tool.tool_description,
20
+ parameters: tool.tool_arguments_schema
21
+ }
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::OpenAiResponses::MessageFormatting
4
+ extend ActiveSupport::Concern
5
+
6
+ def format_string_message(content, role: nil)
7
+ if role == "assistant"
8
+ { "type" => "output_text", "text" => content }
9
+ else
10
+ { "type" => "input_text", "text" => content }
11
+ end
12
+ end
13
+
14
+ def format_model_image_input_message(image_input)
15
+ if image_input.source_type == :url
16
+ {
17
+ "type" => "input_image",
18
+ "image_url" => image_input.url
19
+ }
20
+ elsif image_input.source_type == :file_content
21
+ {
22
+ "type" => "input_image",
23
+ "image_url" => "data:#{image_input.content_type};base64,#{image_input.base64_data}"
24
+ }
25
+ else
26
+ raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
27
+ end
28
+ end
29
+
30
+ def format_model_file_input_message(file_input)
31
+ if file_input.source_type == :url
32
+ raise Raif::Errors::UnsupportedFeatureError, "#{self.class.name} does not support providing a file by URL"
33
+ elsif file_input.source_type == :file_content
34
+ {
35
+ "type" => "input_file",
36
+ "filename" => file_input.filename,
37
+ "file_data" => "data:#{file_input.content_type};base64,#{file_input.base64_data}"
38
+ }
39
+ else
40
+ raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif::Concerns::Llms::OpenAiResponses::ToolFormatting
4
+ extend ActiveSupport::Concern
5
+
6
+ def build_tools_parameter(model_completion)
7
+ model_completion.available_model_tools_map.map do |_tool_name, tool|
8
+ if tool.provider_managed?
9
+ format_provider_managed_tool(tool)
10
+ else
11
+ # It's a developer-managed tool
12
+ validate_json_schema!(tool.tool_arguments_schema)
13
+
14
+ {
15
+ type: "function",
16
+ name: tool.tool_name,
17
+ description: tool.tool_description,
18
+ parameters: tool.tool_arguments_schema
19
+ }
20
+ end
21
+ end
22
+ end
23
+
24
+ def format_provider_managed_tool(tool)
25
+ validate_provider_managed_tool_support!(tool)
26
+
27
+ case tool.name
28
+ when "Raif::ModelTools::ProviderManaged::WebSearch"
29
+ { type: "web_search_preview" }
30
+ when "Raif::ModelTools::ProviderManaged::CodeExecution"
31
+ {
32
+ type: "code_interpreter",
33
+ container: { "type": "auto" }
34
+ }
35
+ when "Raif::ModelTools::ProviderManaged::ImageGeneration"
36
+ { type: "image_generation" }
37
+ else
38
+ raise Raif::Errors::UnsupportedFeatureError,
39
+ "Invalid provider-managed tool: #{tool.name} for #{key}"
40
+ end
41
+ end
42
+ end
@@ -4,6 +4,7 @@ class Raif::Conversation < Raif::ApplicationRecord
4
4
  include Raif::Concerns::HasLlm
5
5
  include Raif::Concerns::HasRequestedLanguage
6
6
  include Raif::Concerns::HasAvailableModelTools
7
+ include Raif::Concerns::LlmResponseParsing
7
8
 
8
9
  belongs_to :creator, polymorphic: true
9
10
 
@@ -15,17 +16,17 @@ class Raif::Conversation < Raif::ApplicationRecord
15
16
  after_initialize -> { self.available_user_tools ||= [] }
16
17
 
17
18
  before_validation ->{ self.type ||= "Raif::Conversation" }, on: :create
18
- before_validation -> { self.system_prompt ||= build_system_prompt }, on: :create
19
19
 
20
20
  def build_system_prompt
21
- <<~PROMPT
21
+ <<~PROMPT.strip
22
22
  #{system_prompt_intro}
23
23
  #{system_prompt_language_preference}
24
24
  PROMPT
25
25
  end
26
26
 
27
27
  def system_prompt_intro
28
- Raif.config.conversation_system_prompt_intro
28
+ sp = Raif.config.conversation_system_prompt_intro
29
+ sp.respond_to?(:call) ? sp.call(self) : sp
29
30
  end
30
31
 
31
32
  # i18n-tasks-use t('raif.conversation.initial_chat_message')
@@ -33,21 +34,41 @@ class Raif::Conversation < Raif::ApplicationRecord
33
34
  I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
34
35
  end
35
36
 
36
- def prompt_model_for_entry_response(entry:)
37
+ def prompt_model_for_entry_response(entry:, &block)
38
+ update(system_prompt: build_system_prompt)
39
+
37
40
  llm.chat(
38
41
  messages: llm_messages,
39
42
  source: entry,
40
- response_format: :text,
43
+ response_format: response_format.to_sym,
41
44
  system_prompt: system_prompt,
42
- available_model_tools: available_model_tools
45
+ available_model_tools: available_model_tools,
46
+ &block
43
47
  )
48
+ rescue StandardError => e
49
+ Rails.logger.error("Error processing conversation entry ##{entry.id}. #{e.message}")
50
+ entry.failed!
51
+
52
+ if defined?(Airbrake)
53
+ notice = Airbrake.build_notice(e)
54
+ notice[:context][:component] = "raif_conversation"
55
+ notice[:context][:action] = "prompt_model_for_entry_response"
56
+
57
+ Airbrake.notify(notice)
58
+ end
59
+ end
60
+
61
+ def process_model_response_message(message:, entry:)
62
+ # no-op by default.
63
+ # Override in subclasses for type-specific processing of the model response message
64
+ message
44
65
  end
45
66
 
46
67
  def llm_messages
47
68
  messages = []
48
69
 
49
70
  entries.oldest_first.includes(:raif_model_tool_invocations).each do |entry|
50
- messages << { "role" => "user", "content" => entry.user_message }
71
+ messages << { "role" => "user", "content" => entry.user_message } unless entry.user_message.blank?
51
72
  next unless entry.completed?
52
73
 
53
74
  messages << { "role" => "assistant", "content" => entry.model_response_message } unless entry.model_response_message.blank?
@@ -16,7 +16,7 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
16
16
  has_one :raif_model_completion, as: :source, dependent: :destroy, class_name: "Raif::ModelCompletion"
17
17
 
18
18
  delegate :available_model_tools, to: :raif_conversation
19
- delegate :system_prompt, :llm_model_key, to: :raif_model_completion, allow_nil: true
19
+ delegate :system_prompt, :llm_model_key, :citations, to: :raif_model_completion, allow_nil: true
20
20
  delegate :json_response_schema, to: :class
21
21
 
22
22
  accepts_nested_attributes_for :raif_user_tool_invocation
@@ -33,11 +33,12 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
33
33
  def add_user_tool_invocation_to_user_message
34
34
  return unless raif_user_tool_invocation.present?
35
35
 
36
- self.user_message = <<~MESSAGE.strip
37
- #{raif_user_tool_invocation.as_user_message}
36
+ separator = response_format == "html" ? "<br>" : "\n\n"
37
+ self.user_message = [user_message, raif_user_tool_invocation.as_user_message].join(separator)
38
+ end
38
39
 
39
- #{user_message}
40
- MESSAGE
40
+ def response_format
41
+ raif_model_completion&.response_format.presence || raif_conversation.response_format
41
42
  end
42
43
 
43
44
  def generating_response?
@@ -45,10 +46,27 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
45
46
  end
46
47
 
47
48
  def process_entry!
48
- self.raif_model_completion = raif_conversation.prompt_model_for_entry_response(entry: self)
49
+ self.model_response_message = ""
50
+
51
+ self.raif_model_completion = raif_conversation.prompt_model_for_entry_response(entry: self) do |model_completion, _delta, _sse_event|
52
+ self.raw_response = model_completion.raw_response
53
+ self.model_response_message = raif_conversation.process_model_response_message(
54
+ message: model_completion.parsed_response(force_reparse: true),
55
+ entry: self
56
+ )
57
+
58
+ update_columns(
59
+ model_response_message: model_response_message,
60
+ raw_response: raw_response,
61
+ updated_at: Time.current
62
+ )
63
+
64
+ broadcast_replace_to raif_conversation
65
+ end
49
66
 
50
67
  if raif_model_completion.parsed_response.present? || raif_model_completion.response_tool_calls.present?
51
68
  extract_message_and_invoke_tools!
69
+ create_entry_for_observation! if triggers_observation_to_model?
52
70
  else
53
71
  logger.error "Error processing conversation entry ##{id}. No model response found."
54
72
  failed!
@@ -57,18 +75,32 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
57
75
  self
58
76
  end
59
77
 
78
+ def triggers_observation_to_model?
79
+ return false unless completed?
80
+
81
+ raif_model_tool_invocations.any?(&:triggers_observation_to_model?)
82
+ end
83
+
84
+ def create_entry_for_observation!
85
+ follow_up_entry = raif_conversation.entries.create!(creator: creator)
86
+ Raif::ConversationEntryJob.perform_later(conversation_entry: follow_up_entry)
87
+ follow_up_entry.broadcast_append_to raif_conversation, target: dom_id(raif_conversation, :entries)
88
+ end
89
+
60
90
  private
61
91
 
62
92
  def extract_message_and_invoke_tools!
63
93
  transaction do
64
94
  self.raw_response = raif_model_completion.raw_response
65
- self.model_response_message = raif_model_completion.parsed_response
95
+ self.model_response_message = raif_conversation.process_model_response_message(message: raif_model_completion.parsed_response, entry: self)
66
96
  save!
67
97
 
68
98
  if raif_model_completion.response_tool_calls.present?
69
99
  raif_model_completion.response_tool_calls.each do |tool_call|
70
100
  tool_klass = available_model_tools_map[tool_call["name"]]
71
- tool_klass&.invoke_tool(tool_arguments: tool_call["arguments"], source: self)
101
+ next if tool_klass.nil?
102
+
103
+ tool_klass.invoke_tool(tool_arguments: tool_call["arguments"], source: self)
72
104
  end
73
105
  end
74
106
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::EmbeddingModel
4
+ include ActiveModel::Model
5
+
6
+ attr_accessor :key,
7
+ :api_name,
8
+ :input_token_cost,
9
+ :default_output_vector_size
10
+
11
+ validates :default_output_vector_size, presence: true, numericality: { only_integer: true, greater_than: 0 }
12
+ validates :api_name, presence: true
13
+ validates :key, presence: true
14
+
15
+ def name
16
+ I18n.t("raif.embedding_model_names.#{key}")
17
+ end
18
+
19
+ def generate_embedding!(input, dimensions: nil)
20
+ raise NotImplementedError, "#{self.class.name} must implement #generate_embedding!"
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::EmbeddingModels::Bedrock < Raif::EmbeddingModel
4
+
5
+ def generate_embedding!(input, dimensions: nil)
6
+ unless input.is_a?(String)
7
+ raise ArgumentError, "Raif::EmbeddingModels::Bedrock#generate_embedding! input must be a string"
8
+ end
9
+
10
+ params = build_request_parameters(input, dimensions:)
11
+ response = bedrock_client.invoke_model(params)
12
+
13
+ response_body = JSON.parse(response.body.read)
14
+ response_body["embedding"]
15
+ rescue Aws::BedrockRuntime::Errors::ServiceError => e
16
+ raise "Bedrock API error: #{e.message}"
17
+ end
18
+
19
+ private
20
+
21
+ def build_request_parameters(input, dimensions: nil)
22
+ body_params = { inputText: input }
23
+ body_params[:dimensions] = dimensions if dimensions.present?
24
+
25
+ {
26
+ model_id: api_name,
27
+ body: body_params.to_json
28
+ }
29
+ end
30
+
31
+ def bedrock_client
32
+ @bedrock_client ||= Aws::BedrockRuntime::Client.new(region: Raif.config.aws_bedrock_region)
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::EmbeddingModels::OpenAi < Raif::EmbeddingModel
4
+ def generate_embedding!(input, dimensions: nil)
5
+ unless input.is_a?(String) || input.is_a?(Array)
6
+ raise ArgumentError, "Raif::EmbeddingModels::OpenAi#generate_embedding! input must be a string or an array of strings"
7
+ end
8
+
9
+ response = connection.post("embeddings") do |req|
10
+ req.body = build_request_parameters(input, dimensions:)
11
+ end
12
+
13
+ if input.is_a?(String)
14
+ response.body["data"][0]["embedding"]
15
+ else
16
+ response.body["data"].map{|v| v["embedding"] }
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def build_request_parameters(input, dimensions: nil)
23
+ params = {
24
+ model: api_name,
25
+ input: input
26
+ }
27
+
28
+ params[:dimensions] = dimensions if dimensions.present?
29
+ params
30
+ end
31
+
32
+ def connection
33
+ @connection ||= Faraday.new(url: "https://api.openai.com/v1") do |f|
34
+ f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
35
+ f.request :json
36
+ f.response :json
37
+ f.response :raise_error
38
+ end
39
+ end
40
+ end