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
data/lib/raif.rb CHANGED
@@ -6,8 +6,14 @@ require "raif/engine"
6
6
  require "raif/configuration"
7
7
  require "raif/errors"
8
8
  require "raif/utils"
9
- require "raif/default_llms"
9
+ require "raif/llm_registry"
10
+ require "raif/embedding_model_registry"
11
+ require "raif/json_schema_builder"
12
+ require "raif/migration_checker"
13
+
10
14
  require "faraday"
15
+ require "event_stream_parser"
16
+ require "json-schema"
11
17
  require "loofah"
12
18
  require "pagy"
13
19
  require "reverse_markdown"
@@ -16,7 +22,6 @@ require "turbo-rails"
16
22
  module Raif
17
23
  class << self
18
24
  attr_accessor :configuration
19
- attr_accessor :llm_registry
20
25
 
21
26
  attr_writer :logger
22
27
  end
@@ -32,34 +37,4 @@ module Raif
32
37
  def self.logger
33
38
  @logger ||= Rails.logger
34
39
  end
35
-
36
- def self.register_llm(llm_class, llm_config)
37
- llm = llm_class.new(**llm_config)
38
-
39
- unless llm.valid?
40
- raise ArgumentError, "The LLM you tried to register is invalid: #{llm.errors.full_messages.join(", ")}"
41
- end
42
-
43
- @llm_registry ||= {}
44
- @llm_registry[llm.key] = llm_config.merge(llm_class: llm_class)
45
- end
46
-
47
- def self.llm(model_key)
48
- llm_config = llm_registry[model_key]
49
-
50
- if llm_config.nil?
51
- raise ArgumentError, "No LLM found for model key: #{model_key}. Available models: #{available_llm_keys.join(", ")}"
52
- end
53
-
54
- llm_class = llm_config[:llm_class]
55
- llm_class.new(**llm_config.except(:llm_class))
56
- end
57
-
58
- def self.available_llms
59
- llm_registry.values
60
- end
61
-
62
- def self.available_llm_keys
63
- llm_registry.keys
64
- end
65
40
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # desc "Explaining what the task does"
4
- # task :raif do
5
- # # Task goes here
6
- # end
3
+ namespace :raif do
4
+ namespace :install do
5
+ desc "Copy migrations from Raif to host application"
6
+ task :migrations do
7
+ ENV["FROM"] = "raif"
8
+ Rake::Task["railties:install:migrations"].invoke
9
+ end
10
+ end
11
+ end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raif
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Roesch
8
8
  - Brian Leslie
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-02 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: aws-sdk-bedrock
14
+ name: aws-sdk-bedrockruntime
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: aws-sdk-bedrockruntime
28
+ name: event_stream_parser
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '1.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: faraday
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -157,6 +157,7 @@ files:
157
157
  - app/assets/javascript/raif/stream_actions/raif_scroll_to_bottom.js
158
158
  - app/assets/stylesheets/raif.scss
159
159
  - app/assets/stylesheets/raif/admin/conversation.scss
160
+ - app/assets/stylesheets/raif/admin/stats.scss
160
161
  - app/assets/stylesheets/raif/loader.scss
161
162
  - app/assets/stylesheets/raif_admin.scss
162
163
  - app/controllers/raif/admin/agents_controller.rb
@@ -164,6 +165,8 @@ files:
164
165
  - app/controllers/raif/admin/conversations_controller.rb
165
166
  - app/controllers/raif/admin/model_completions_controller.rb
166
167
  - app/controllers/raif/admin/model_tool_invocations_controller.rb
168
+ - app/controllers/raif/admin/stats/tasks_controller.rb
169
+ - app/controllers/raif/admin/stats_controller.rb
167
170
  - app/controllers/raif/admin/tasks_controller.rb
168
171
  - app/controllers/raif/application_controller.rb
169
172
  - app/controllers/raif/conversation_entries_controller.rb
@@ -182,19 +185,47 @@ files:
182
185
  - app/models/raif/concerns/has_llm.rb
183
186
  - app/models/raif/concerns/has_requested_language.rb
184
187
  - app/models/raif/concerns/invokes_model_tools.rb
188
+ - app/models/raif/concerns/json_schema_definition.rb
185
189
  - app/models/raif/concerns/llm_response_parsing.rb
190
+ - app/models/raif/concerns/llm_temperature.rb
191
+ - app/models/raif/concerns/llms/anthropic/message_formatting.rb
192
+ - app/models/raif/concerns/llms/anthropic/tool_formatting.rb
193
+ - app/models/raif/concerns/llms/bedrock/message_formatting.rb
194
+ - app/models/raif/concerns/llms/bedrock/tool_formatting.rb
195
+ - app/models/raif/concerns/llms/message_formatting.rb
196
+ - app/models/raif/concerns/llms/open_ai/json_schema_validation.rb
197
+ - app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb
198
+ - app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb
199
+ - app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb
200
+ - app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb
186
201
  - app/models/raif/conversation.rb
187
202
  - app/models/raif/conversation_entry.rb
203
+ - app/models/raif/embedding_model.rb
204
+ - app/models/raif/embedding_models/bedrock.rb
205
+ - app/models/raif/embedding_models/open_ai.rb
188
206
  - app/models/raif/llm.rb
189
207
  - app/models/raif/llms/anthropic.rb
190
- - app/models/raif/llms/bedrock_claude.rb
191
- - app/models/raif/llms/open_ai.rb
208
+ - app/models/raif/llms/bedrock.rb
209
+ - app/models/raif/llms/open_ai_base.rb
210
+ - app/models/raif/llms/open_ai_completions.rb
211
+ - app/models/raif/llms/open_ai_responses.rb
212
+ - app/models/raif/llms/open_router.rb
192
213
  - app/models/raif/model_completion.rb
214
+ - app/models/raif/model_file_input.rb
215
+ - app/models/raif/model_image_input.rb
193
216
  - app/models/raif/model_tool.rb
194
217
  - app/models/raif/model_tool_invocation.rb
195
218
  - app/models/raif/model_tools/agent_final_answer.rb
196
219
  - app/models/raif/model_tools/fetch_url.rb
220
+ - app/models/raif/model_tools/provider_managed/base.rb
221
+ - app/models/raif/model_tools/provider_managed/code_execution.rb
222
+ - app/models/raif/model_tools/provider_managed/image_generation.rb
223
+ - app/models/raif/model_tools/provider_managed/web_search.rb
197
224
  - app/models/raif/model_tools/wikipedia_search.rb
225
+ - app/models/raif/streaming_responses/anthropic.rb
226
+ - app/models/raif/streaming_responses/bedrock.rb
227
+ - app/models/raif/streaming_responses/open_ai_completions.rb
228
+ - app/models/raif/streaming_responses/open_ai_responses.rb
198
229
  - app/models/raif/task.rb
199
230
  - app/models/raif/user_tool_invocation.rb
200
231
  - app/views/layouts/raif/admin.html.erb
@@ -212,9 +243,12 @@ files:
212
243
  - app/views/raif/admin/model_tool_invocations/_model_tool_invocation.html.erb
213
244
  - app/views/raif/admin/model_tool_invocations/index.html.erb
214
245
  - app/views/raif/admin/model_tool_invocations/show.html.erb
246
+ - app/views/raif/admin/stats/index.html.erb
247
+ - app/views/raif/admin/stats/tasks/index.html.erb
215
248
  - app/views/raif/admin/tasks/_task.html.erb
216
249
  - app/views/raif/admin/tasks/index.html.erb
217
250
  - app/views/raif/admin/tasks/show.html.erb
251
+ - app/views/raif/conversation_entries/_citations.html.erb
218
252
  - app/views/raif/conversation_entries/_conversation_entry.html.erb
219
253
  - app/views/raif/conversation_entries/_form.html.erb
220
254
  - app/views/raif/conversation_entries/_form_with_available_tools.html.erb
@@ -234,9 +268,19 @@ files:
234
268
  - config/locales/en.yml
235
269
  - config/routes.rb
236
270
  - db/migrate/20250224234252_create_raif_tables.rb
271
+ - db/migrate/20250421202149_add_response_format_to_raif_conversations.rb
272
+ - db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb
273
+ - db/migrate/20250424232946_add_created_at_indexes.rb
274
+ - db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb
275
+ - db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb
276
+ - db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb
277
+ - db/migrate/20250603140622_add_citations_to_raif_model_completions.rb
278
+ - db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb
237
279
  - lib/generators/raif/agent/agent_generator.rb
238
280
  - lib/generators/raif/agent/templates/agent.rb.tt
281
+ - lib/generators/raif/agent/templates/application_agent.rb.tt
239
282
  - lib/generators/raif/conversation/conversation_generator.rb
283
+ - lib/generators/raif/conversation/templates/application_conversation.rb.tt
240
284
  - lib/generators/raif/conversation/templates/conversation.rb.tt
241
285
  - lib/generators/raif/install/install_generator.rb
242
286
  - lib/generators/raif/install/templates/initializer.rb
@@ -248,19 +292,25 @@ files:
248
292
  - lib/generators/raif/views_generator.rb
249
293
  - lib/raif.rb
250
294
  - lib/raif/configuration.rb
251
- - lib/raif/default_llms.rb
295
+ - lib/raif/embedding_model_registry.rb
252
296
  - lib/raif/engine.rb
253
297
  - lib/raif/errors.rb
254
298
  - lib/raif/errors/action_not_authorized_error.rb
255
- - lib/raif/errors/anthropic/api_error.rb
256
299
  - lib/raif/errors/invalid_config_error.rb
257
300
  - lib/raif/errors/invalid_conversation_type_error.rb
301
+ - lib/raif/errors/invalid_model_file_input_error.rb
302
+ - lib/raif/errors/invalid_model_image_input_error.rb
258
303
  - lib/raif/errors/invalid_user_tool_type_error.rb
259
- - lib/raif/errors/open_ai/api_error.rb
260
304
  - lib/raif/errors/open_ai/json_schema_error.rb
305
+ - lib/raif/errors/streaming_error.rb
306
+ - lib/raif/errors/unsupported_feature_error.rb
307
+ - lib/raif/json_schema_builder.rb
261
308
  - lib/raif/languages.rb
309
+ - lib/raif/llm_registry.rb
310
+ - lib/raif/migration_checker.rb
262
311
  - lib/raif/rspec.rb
263
312
  - lib/raif/utils.rb
313
+ - lib/raif/utils/html_fragment_processor.rb
264
314
  - lib/raif/utils/html_to_markdown_converter.rb
265
315
  - lib/raif/utils/readable_content_extractor.rb
266
316
  - lib/raif/version.rb
@@ -287,7 +337,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
337
  - !ruby/object:Gem::Version
288
338
  version: '0'
289
339
  requirements: []
290
- rubygems_version: 3.6.2
340
+ rubygems_version: 3.6.7
291
341
  specification_version: 4
292
342
  summary: Raif (Ruby AI Framework) is a Rails engine that helps you add AI-powered
293
343
  features to your Rails apps, such as tasks, conversations, and agents.
@@ -1,134 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Raif::Llms::BedrockClaude < Raif::Llm
4
-
5
- def perform_model_completion!(model_completion)
6
- if Raif.config.aws_bedrock_model_name_prefix.present?
7
- model_completion.model_api_name = "#{Raif.config.aws_bedrock_model_name_prefix}.#{model_completion.model_api_name}"
8
- end
9
-
10
- params = build_api_parameters(model_completion)
11
- resp = bedrock_client.converse(params)
12
-
13
- model_completion.raw_response = if model_completion.response_format_json?
14
- extract_json_response(resp)
15
- else
16
- extract_text_response(resp)
17
- end
18
-
19
- model_completion.completion_tokens = resp.usage.output_tokens
20
- model_completion.prompt_tokens = resp.usage.input_tokens
21
- model_completion.total_tokens = resp.usage.total_tokens
22
- model_completion.save!
23
-
24
- model_completion
25
- end
26
-
27
- protected
28
-
29
- def bedrock_client
30
- @bedrock_client ||= Aws::BedrockRuntime::Client.new(region: Raif.config.aws_bedrock_region)
31
- end
32
-
33
- def format_messages(messages)
34
- messages.map(&:symbolize_keys).map do |message|
35
- {
36
- role: message[:role],
37
- content: [{ text: message[:content] }]
38
- }
39
- end
40
- end
41
-
42
- def build_api_parameters(model_completion)
43
- params = {
44
- model_id: model_completion.model_api_name,
45
- inference_config: { max_tokens: model_completion.max_completion_tokens || 8192 },
46
- messages: format_messages(model_completion.messages)
47
- }
48
-
49
- params[:system] = [{ text: model_completion.system_prompt }] if model_completion.system_prompt.present?
50
-
51
- # Prepare tools configuration if needed
52
- tools = []
53
-
54
- # If we're looking for a JSON response, add a tool to the request that the model can use to provide a JSON response
55
- if model_completion.response_format_json? && model_completion.json_response_schema.present?
56
- tools << {
57
- name: "json_response",
58
- description: "Generate a structured JSON response based on the provided schema.",
59
- input_schema: { json: model_completion.json_response_schema }
60
- }
61
- end
62
-
63
- # If we support native tool use and have tools available, add them to the request
64
- if supports_native_tool_use? && model_completion.available_model_tools.any?
65
- model_completion.available_model_tools_map.each do |_tool_name, tool|
66
- tools << {
67
- name: tool.tool_name,
68
- description: tool.tool_description,
69
- input_schema: { json: tool.tool_arguments_schema }
70
- }
71
- end
72
- end
73
-
74
- # Add tool configuration if any tools are available
75
- if tools.any?
76
- params[:tool_config] = {
77
- tools: tools.map { |tool| { tool_spec: tool } }
78
- }
79
- end
80
-
81
- params
82
- end
83
-
84
- def extract_text_response(resp)
85
- # Get the message from the response object
86
- message = resp.output.message
87
-
88
- # Find the first text content block
89
- text_block = message.content&.find do |content|
90
- content.respond_to?(:text) && content.text.present?
91
- end
92
-
93
- text_block&.text
94
- end
95
-
96
- def extract_json_response(resp)
97
- # Get the message from the response object
98
- message = resp.output.message
99
-
100
- return extract_text_response(resp) if message.content.nil?
101
-
102
- # Look for tool_use blocks in the content array
103
- tool_response = message.content.find do |content|
104
- content.respond_to?(:tool_use) && content.tool_use.present? && content.tool_use.name == "json_response"
105
- end
106
-
107
- if tool_response&.tool_use
108
- JSON.generate(tool_response.tool_use.input)
109
- else
110
- extract_text_response(resp)
111
- end
112
- end
113
-
114
- def extract_response_tool_calls(resp)
115
- # Get the message from the response object
116
- message = resp.output.message
117
- return if message.content.nil?
118
-
119
- # Find any tool_use blocks in the content array
120
- tool_uses = message.content.select do |content|
121
- content.respond_to?(:tool_use) && content.tool_use.present?
122
- end
123
-
124
- return if tool_uses.blank?
125
-
126
- tool_uses.map do |content|
127
- {
128
- "name" => content.tool_use.name,
129
- "arguments" => content.tool_use.input
130
- }
131
- end
132
- end
133
-
134
- end
@@ -1,259 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Raif::Llms::OpenAi < Raif::Llm
4
-
5
- def perform_model_completion!(model_completion)
6
- model_completion.temperature ||= default_temperature
7
- parameters = build_chat_parameters(model_completion)
8
-
9
- response = connection.post("chat/completions") do |req|
10
- req.body = parameters.to_json
11
- end
12
-
13
- resp = JSON.parse(response.body)
14
-
15
- # Handle API errors
16
- unless response.success?
17
- error_message = resp["error"]&.dig("message") || "OpenAI API error: #{response.status}"
18
- raise Raif::Errors::OpenAi::ApiError, error_message
19
- end
20
-
21
- model_completion.update!(
22
- response_tool_calls: extract_response_tool_calls(resp),
23
- raw_response: resp.dig("choices", 0, "message", "content"),
24
- completion_tokens: resp["usage"]["completion_tokens"],
25
- prompt_tokens: resp["usage"]["prompt_tokens"],
26
- total_tokens: resp["usage"]["total_tokens"],
27
- response_format_parameter: parameters.dig(:response_format, :type)
28
- )
29
-
30
- model_completion
31
- end
32
-
33
- def connection
34
- @connection ||= Faraday.new(url: "https://api.openai.com/v1") do |f|
35
- f.headers["Content-Type"] = "application/json"
36
- f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
37
- end
38
- end
39
-
40
- def validate_json_schema!(schema)
41
- return if schema.blank?
42
-
43
- errors = []
44
-
45
- # Check if schema is present
46
- if schema.blank?
47
- errors << "JSON schema must include a 'schema' property"
48
- else
49
- # Check root object type
50
- if schema[:type] != "object" && !schema.key?(:properties)
51
- errors << "Root schema must be of type 'object' with 'properties'"
52
- end
53
-
54
- # Check all objects in the schema recursively
55
- validate_object_properties(schema, errors)
56
-
57
- # Check properties count (max 100 total)
58
- validate_properties_count(schema, errors)
59
-
60
- # Check nesting depth (max 5 levels)
61
- validate_nesting_depth(schema, errors)
62
-
63
- # Check for unsupported anyOf at root level
64
- if schema[:anyOf].present? && schema[:properties].blank?
65
- errors << "Root objects cannot be of type 'anyOf'"
66
- end
67
- end
68
-
69
- # Raise error if any validation issues found
70
- if errors.any?
71
- error_message = "Invalid JSON schema for OpenAI structured outputs: #{errors.join("; ")}\nSchema was: #{schema.inspect}"
72
- raise Raif::Errors::OpenAi::JsonSchemaError, error_message
73
- else
74
- true
75
- end
76
- end
77
-
78
- private
79
-
80
- def extract_response_tool_calls(resp)
81
- return if resp.dig("choices", 0, "message", "tool_calls").blank?
82
-
83
- resp.dig("choices", 0, "message", "tool_calls").map do |tool_call|
84
- {
85
- "name" => tool_call["function"]["name"],
86
- "arguments" => JSON.parse(tool_call["function"]["arguments"])
87
- }
88
- end
89
- end
90
-
91
- def build_chat_parameters(model_completion)
92
- formatted_system_prompt = model_completion.system_prompt.to_s.strip
93
-
94
- # If the response format is JSON, we need to include "as json" in the system prompt.
95
- # OpenAI requires this and will throw an error if it's not included.
96
- if model_completion.response_format_json?
97
- # Ensure system prompt ends with a period if not empty
98
- if formatted_system_prompt.present? && !formatted_system_prompt.end_with?(".", "?", "!")
99
- formatted_system_prompt += "."
100
- end
101
- formatted_system_prompt += " Return your response as JSON."
102
- formatted_system_prompt.strip!
103
- end
104
-
105
- messages = model_completion.messages
106
- messages_with_system = if !formatted_system_prompt.empty?
107
- [{ "role" => "system", "content" => formatted_system_prompt }] + messages
108
- else
109
- messages
110
- end
111
-
112
- parameters = {
113
- model: api_name,
114
- messages: messages_with_system,
115
- temperature: model_completion.temperature.to_f
116
- }
117
-
118
- # If the LLM supports native tool use and there are available tools, add them to the parameters
119
- if supports_native_tool_use? && model_completion.available_model_tools.any?
120
- parameters[:tools] = model_completion.available_model_tools_map.map do |_tool_name, tool|
121
- validate_json_schema!(tool.tool_arguments_schema)
122
-
123
- {
124
- type: "function",
125
- function: {
126
- name: tool.tool_name,
127
- description: tool.tool_description,
128
- parameters: tool.tool_arguments_schema
129
- }
130
- }
131
- end
132
- end
133
-
134
- # Add response format if needed
135
- response_format = determine_response_format(model_completion)
136
- parameters[:response_format] = response_format if response_format
137
-
138
- parameters
139
- end
140
-
141
- def determine_response_format(model_completion)
142
- # Only configure response format for JSON outputs
143
- return unless model_completion.response_format_json?
144
-
145
- if model_completion.json_response_schema.present? && supports_structured_outputs?
146
- validate_json_schema!(model_completion.json_response_schema)
147
-
148
- {
149
- type: "json_schema",
150
- json_schema: {
151
- name: "json_response_schema",
152
- strict: true,
153
- schema: model_completion.json_response_schema
154
- }
155
- }
156
- else
157
- # Default JSON mode for OpenAI models that don't support structured outputs or no schema is provided
158
- { type: "json_object" }
159
- end
160
- end
161
-
162
- def supports_structured_outputs?
163
- # Not all OpenAI models support structured outputs:
164
- # https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat#supported-models
165
- provider_settings[:supports_structured_outputs]
166
- end
167
-
168
- def validate_object_properties(schema, errors)
169
- return unless schema.is_a?(Hash)
170
-
171
- # Check if the current schema is an object and validate additionalProperties and required fields
172
- if schema[:type] == "object"
173
- if schema[:additionalProperties] != false
174
- errors << "All objects must have 'additionalProperties' set to false"
175
- end
176
-
177
- # Check that all properties are required
178
- if schema[:properties].is_a?(Hash) && schema[:properties].any?
179
- property_keys = schema[:properties].keys
180
- required_fields = schema[:required] || []
181
-
182
- if required_fields.sort != property_keys.map(&:to_s).sort
183
- errors << "All object properties must be listed in the 'required' array"
184
- end
185
- end
186
- end
187
-
188
- # Check if the current schema is an object and validate additionalProperties
189
- if schema[:type] == "object"
190
- if schema[:additionalProperties] != false
191
- errors << "All objects must have 'additionalProperties' set to false"
192
- end
193
-
194
- # Check properties of the object recursively
195
- if schema[:properties].is_a?(Hash)
196
- schema[:properties].each_value do |property|
197
- validate_object_properties(property, errors)
198
- end
199
- end
200
- end
201
-
202
- # Check array items
203
- if schema[:type] == "array" && schema[:items].is_a?(Hash)
204
- validate_object_properties(schema[:items], errors)
205
- end
206
-
207
- # Check anyOf
208
- if schema[:anyOf].is_a?(Array)
209
- schema[:anyOf].each do |option|
210
- validate_object_properties(option, errors)
211
- end
212
- end
213
- end
214
-
215
- def validate_properties_count(schema, errors, count = 0)
216
- return count unless schema.is_a?(Hash)
217
-
218
- if schema[:properties].is_a?(Hash)
219
- count += schema[:properties].size
220
-
221
- if count > 100
222
- errors << "Schema exceeds maximum of 100 total object properties"
223
- return count
224
- end
225
-
226
- # Check nested properties
227
- schema[:properties].each_value do |property|
228
- count = validate_properties_count(property, errors, count)
229
- end
230
- end
231
-
232
- # Check array items
233
- if schema[:type] == "array" && schema[:items].is_a?(Hash)
234
- count = validate_properties_count(schema[:items], errors, count)
235
- end
236
-
237
- count
238
- end
239
-
240
- def validate_nesting_depth(schema, errors, depth = 1)
241
- return unless schema.is_a?(Hash)
242
-
243
- if depth > 5
244
- errors << "Schema exceeds maximum nesting depth of 5 levels"
245
- return
246
- end
247
-
248
- if schema[:properties].is_a?(Hash)
249
- schema[:properties].each_value do |property|
250
- validate_nesting_depth(property, errors, depth + 1)
251
- end
252
- end
253
-
254
- # Check array items
255
- if schema[:type] == "array" && schema[:items].is_a?(Hash)
256
- validate_nesting_depth(schema[:items], errors, depth + 1)
257
- end
258
- end
259
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Raif
4
- def self.default_llms
5
- {
6
- Raif::Llms::OpenAi => [
7
- {
8
- key: :open_ai_gpt_4o_mini,
9
- api_name: "gpt-4o-mini",
10
- model_provider_settings: { supports_structured_outputs: true }
11
- },
12
- {
13
- key: :open_ai_gpt_4o,
14
- api_name: "gpt-4o",
15
- model_provider_settings: { supports_structured_outputs: true }
16
- },
17
- {
18
- key: :open_ai_gpt_3_5_turbo,
19
- api_name: "gpt-3.5-turbo",
20
- model_provider_settings: { supports_structured_outputs: false }
21
- },
22
- ],
23
- Raif::Llms::Anthropic => [
24
- { key: :anthropic_claude_3_7_sonnet, api_name: "claude-3-7-sonnet-latest", max_completion_tokens: 8192 },
25
- { key: :anthropic_claude_3_5_sonnet, api_name: "claude-3-5-sonnet-latest", max_completion_tokens: 8192 },
26
- { key: :anthropic_claude_3_5_haiku, api_name: "claude-3-5-haiku-latest", max_completion_tokens: 8192 },
27
- { key: :anthropic_claude_3_opus, api_name: "claude-3-opus-latest", max_completion_tokens: 4096 },
28
- ],
29
- Raif::Llms::BedrockClaude => [
30
- { key: :bedrock_claude_3_5_sonnet, api_name: "anthropic.claude-3-5-sonnet-20241022-v2:0", max_completion_tokens: 8192 },
31
- { key: :bedrock_claude_3_7_sonnet, api_name: "anthropic.claude-3-7-sonnet-20250219-v1:0", max_completion_tokens: 8192 },
32
- { key: :bedrock_claude_3_5_haiku, api_name: "anthropic.claude-3-5-haiku-20241022-v1:0", max_completion_tokens: 8192 },
33
- { key: :bedrock_claude_3_opus, api_name: "anthropic.claude-3-opus-20240229-v1:0", max_completion_tokens: 4096 },
34
- ]
35
- }
36
- end
37
- end