raif 1.0.0 → 1.1.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +200 -41
  3. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  4. data/app/controllers/raif/admin/application_controller.rb +14 -0
  5. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  6. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  7. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  8. data/app/controllers/raif/conversations_controller.rb +5 -1
  9. data/app/models/raif/agent.rb +11 -9
  10. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  11. data/app/models/raif/agents/re_act_agent.rb +6 -0
  12. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  13. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  14. data/app/models/raif/concerns/llm_response_parsing.rb +23 -1
  15. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  16. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  17. data/app/models/raif/concerns/llms/bedrock_claude/message_formatting.rb +70 -0
  18. data/app/models/raif/concerns/llms/message_formatting.rb +41 -0
  19. data/app/models/raif/concerns/llms/open_ai/message_formatting.rb +41 -0
  20. data/app/models/raif/conversation.rb +11 -3
  21. data/app/models/raif/conversation_entry.rb +22 -6
  22. data/app/models/raif/embedding_model.rb +22 -0
  23. data/app/models/raif/embedding_models/bedrock_titan.rb +34 -0
  24. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  25. data/app/models/raif/llm.rb +39 -6
  26. data/app/models/raif/llms/anthropic.rb +23 -28
  27. data/app/models/raif/llms/bedrock_claude.rb +33 -19
  28. data/app/models/raif/llms/open_ai.rb +14 -17
  29. data/app/models/raif/llms/open_router.rb +93 -0
  30. data/app/models/raif/model_completion.rb +21 -2
  31. data/app/models/raif/model_file_input.rb +113 -0
  32. data/app/models/raif/model_image_input.rb +4 -0
  33. data/app/models/raif/model_tool.rb +77 -51
  34. data/app/models/raif/model_tool_invocation.rb +8 -6
  35. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  36. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  37. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  38. data/app/models/raif/task.rb +71 -16
  39. data/app/views/layouts/raif/admin.html.erb +10 -0
  40. data/app/views/raif/admin/agents/show.html.erb +3 -1
  41. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  42. data/app/views/raif/admin/conversations/show.html.erb +3 -1
  43. data/app/views/raif/admin/model_completions/_model_completion.html.erb +1 -0
  44. data/app/views/raif/admin/model_completions/index.html.erb +1 -0
  45. data/app/views/raif/admin/model_completions/show.html.erb +30 -3
  46. data/app/views/raif/admin/stats/index.html.erb +128 -0
  47. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  48. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  49. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  50. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  51. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +18 -14
  52. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  53. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  54. data/app/views/raif/conversation_entries/_message.html.erb +10 -3
  55. data/config/locales/admin.en.yml +14 -0
  56. data/config/locales/en.yml +25 -3
  57. data/config/routes.rb +6 -0
  58. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  59. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  60. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  61. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  62. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  63. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  64. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  65. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  66. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  67. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  68. data/lib/generators/raif/conversation/templates/conversation.rb.tt +13 -11
  69. data/lib/generators/raif/install/templates/initializer.rb +50 -6
  70. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  71. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  72. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  73. data/lib/raif/configuration.rb +40 -3
  74. data/lib/raif/embedding_model_registry.rb +83 -0
  75. data/lib/raif/engine.rb +34 -1
  76. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  77. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  78. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  79. data/lib/raif/errors.rb +3 -2
  80. data/lib/raif/json_schema_builder.rb +104 -0
  81. data/lib/raif/llm_registry.rb +205 -0
  82. data/lib/raif/version.rb +1 -1
  83. data/lib/raif.rb +5 -32
  84. data/lib/tasks/raif_tasks.rake +9 -4
  85. metadata +32 -19
  86. data/lib/raif/default_llms.rb +0 -37
@@ -1,74 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Raif::ModelTools::<%= class_name %> < Raif::ModelTool
4
- # For example tool implementations, see:
5
- # Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search_tool.rb
6
- # Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url_tool.rb
4
+ # For example tool implementations, see:
5
+ # Wikipedia Search Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/wikipedia_search.rb
6
+ # Fetch URL Tool: https://github.com/CultivateLabs/raif/blob/main/app/models/raif/model_tools/fetch_url.rb
7
+
8
+ tool_description do
9
+ "Description of your tool that will be provided to the LLM so it knows when to invoke it"
10
+ end
11
+
12
+ # Define the schema for the arguments that the LLM should use when invoking your tool.
13
+ # It should be a valid JSON schema. When the model invokes your tool,
14
+ # the arguments it provides will be validated against this schema using JSON::Validator from the json-schema gem.
15
+ #
16
+ # All attributes will be required and additionalProperties will be set to false.
17
+ tool_arguments_schema do
18
+ # string :title, description: "The title of the operation", minLength: 3
19
+ #
20
+ # object :widget, description: "A widget's description" do
21
+ # boolean :is_red, description: "Whether the widget is red"
22
+ # integer :rating, description: "A rating of the widget from 1 to 10", minimum: 1, maximum: 10
23
+ # array :tags, description: "Associated tags" do
24
+ # items type: "string"
25
+ # end
26
+ # end
27
+ #
28
+ # array :products, description: "List of products" do
29
+ # object do
30
+ # integer :id, description: "Product identifier"
31
+ # string :name, description: "Product name"
32
+ # number :price, description: "Product price", minimum: 0
33
+ # end
34
+ # end
35
+ end
7
36
 
8
37
  # An example of how the LLM should invoke your tool. This should return a hash with name and arguments keys.
9
38
  # `to_json` will be called on it and provided to the LLM as an example of how to invoke your tool.
10
- def self.example_model_invocation
39
+ example_model_invocation do
11
40
  {
12
41
  "name": tool_name,
13
42
  "arguments": { }
14
43
  }
15
44
  end
16
45
 
17
- # Define your tool's argument schema here. It should be a valid JSON schema.
18
- # When the model invokes your tool, the arguments it provides will be validated
19
- # against this schema using JSON::Validator from the json-schema gem.
20
- def self.tool_arguments_schema
21
- # For example:
22
- # {
23
- # type: "object",
24
- # additionalProperties: false,
25
- # required: ["query"],
26
- # properties: {
27
- # query: {
28
- # type: "string",
29
- # description: "The query to search for"
30
- # }
31
- # }
32
- # }
33
- # Would expect the model to invoke your tool with an arguments JSON object like:
34
- # { "query" : "some query here" }
35
- end
46
+ class << self
47
+ # When your tool is invoked by the LLM in a Raif::Agent loop,
48
+ # the results of the tool invocation are provided back to the LLM as an observation.
49
+ # This method should return whatever you want provided to the LLM.
50
+ # For example, if you were implementing a GoogleSearch tool, this might return a JSON
51
+ # object containing search results for the query.
52
+ def observation_for_invocation(tool_invocation)
53
+ return "No results found" unless tool_invocation.result.present?
36
54
 
37
- def self.tool_description
38
- "Description of your tool that will be provided to the LLM so it knows when to invoke it"
39
- end
55
+ JSON.pretty_generate(tool_invocation.result)
56
+ end
40
57
 
41
- # When your tool is invoked by the LLM in a Raif::Agent loop,
42
- # the results of the tool invocation are provided back to the LLM as an observation.
43
- # This method should return whatever you want provided to the LLM.
44
- # For example, if you were implementing a GoogleSearch tool, this might return a JSON
45
- # object containing search results for the query.
46
- def self.observation_for_invocation(tool_invocation)
47
- return "No results found" unless tool_invocation.result.present?
48
-
49
- JSON.pretty_generate(tool_invocation.result)
50
- end
58
+ # When your tool is invoked in a Raif::Conversation, should the result be automatically provided back to the model?
59
+ # When true, observation_for_invocation will be used to produce the observation provided to the model
60
+ def triggers_observation_to_model?
61
+ false
62
+ end
51
63
 
52
- # When the LLM invokes your tool, this method will be called with a `Raif::ModelToolInvocation` record as an argument.
53
- # It should handle the actual execution of the tool.
54
- # For example, if you are implementing a GoogleSearch tool, this method should run the actual search
55
- # and store the results in the tool_invocation's result JSON column.
56
- def self.process_invocation(tool_invocation)
57
- # Extract arguments from tool_invocation.tool_arguments
58
- # query = tool_invocation.tool_arguments["query"]
59
- #
60
- # Process the invocation and perform the desired action
61
- # ...
62
- #
63
- # Store the results in the tool_invocation
64
- # tool_invocation.update!(
65
- # result: {
66
- # # Your result data structure
67
- # }
68
- # )
69
- #
70
- # Return the result
71
- # tool_invocation.result
64
+ # When the LLM invokes your tool, this method will be called with a `Raif::ModelToolInvocation` record as an argument.
65
+ # It should handle the actual execution of the tool.
66
+ # For example, if you are implementing a GoogleSearch tool, this method should run the actual search
67
+ # and store the results in the tool_invocation's result JSON column.
68
+ def process_invocation(tool_invocation)
69
+ # Extract arguments from tool_invocation.tool_arguments
70
+ # query = tool_invocation.tool_arguments["query"]
71
+ #
72
+ # Process the invocation and perform the desired action
73
+ # ...
74
+ #
75
+ # Store the results in the tool_invocation
76
+ # tool_invocation.update!(
77
+ # result: {
78
+ # # Your result data structure
79
+ # }
80
+ # )
81
+ #
82
+ # Return the result
83
+ # tool_invocation.result
84
+ end
72
85
  end
73
86
 
74
87
  end
@@ -6,39 +6,50 @@ module Raif
6
6
  # Set the response format for the task. Options are :html, :text, or :json.
7
7
  llm_response_format :<%= options[:response_format] %>
8
8
 
9
+ # Set the temperature for the task
10
+ # llm_temperature 0.7
11
+
12
+ # Optional: Set the allowed tags for the task. Only relevant if response_format is :html.
13
+ # Defaults to Rails::HTML5::SafeListSanitizer.allowed_tags
14
+ # llm_response_allowed_tags %w[p b i div strong]
15
+
16
+ # Optional: Set the allowed attributes for the task. Only relevant if response_format is :html.
17
+ # Defaults to Rails::HTML5::SafeListSanitizer.allowed_attributes
18
+ # llm_response_allowed_attributes %w[style]
19
+
9
20
  # Define any attributes that are needed for the task.
10
21
  # You can then pass them when running the task and they will be available in build_prompt:
11
22
  # Raif::Tasks::<%= task_class_name %>.run(your_attribute: "some value")
12
23
  # attr_accessor :your_attribute
13
24
 
14
25
  <%- if options[:response_format] == "json" -%>
15
- # Define a JSON schema that the response should adhere to
16
- # def self.json_response_schema
17
- # {
18
- # type: "object",
19
- # additionalProperties: false,
20
- # required: ["your_key_here"],
21
- # properties: {
22
- # your_key_here: {
23
- # type: "array",
24
- # items: {
25
- # type: "object",
26
- # additionalProperties: false,
27
- # required: ["query", "rationale"],
28
- # properties: {
29
- # query: { type: "string" },
30
- # rationale: { type: "string" }
31
- # }
32
- # }
33
- # }
34
- # }
35
- # }
36
- # end
26
+ # Define a JSON schema that the model's response should adhere to
27
+ #
28
+ # All attributes will be required and additionalProperties will be set to false.
29
+ json_response_schema do
30
+ # string :title, description: "The title of the operation", minLength: 3
31
+ #
32
+ # object :widget, description: "A widget's description" do
33
+ # boolean :is_red, description: "Whether the widget is red"
34
+ # integer :rating, description: "A rating of the widget from 1 to 10", minimum: 1, maximum: 10
35
+ # array :tags, description: "Associated tags" do
36
+ # items type: "string"
37
+ # end
38
+ # end
39
+ #
40
+ # array :products, description: "List of products" do
41
+ # object do
42
+ # integer :id, description: "Product identifier"
43
+ # string :name, description: "Product name"
44
+ # number :price, description: "Product price", minimum: 0
45
+ # end
46
+ # end
47
+ end
37
48
  <%- end -%>
38
49
 
39
50
  def build_prompt
40
51
  # Implement the LLM prompt for this task.
41
- raise NotImplementedError, "Implement #build_prompt in #{self.class}"
52
+ raise NotImplementedError, "Implement #build_prompt in #{self.class.name}"
42
53
  end
43
54
 
44
55
  # Optional: Override build_system_prompt if you need custom system instructions.
@@ -10,16 +10,25 @@ module Raif
10
10
  :authorize_controller_action,
11
11
  :aws_bedrock_model_name_prefix,
12
12
  :aws_bedrock_region,
13
+ :aws_bedrock_titan_embedding_models_enabled,
13
14
  :conversation_entries_controller,
14
15
  :conversation_system_prompt_intro,
15
16
  :conversation_types,
16
17
  :conversations_controller,
17
18
  :current_user_method,
19
+ :default_embedding_model_key,
18
20
  :default_llm_model_key,
19
21
  :llm_api_requests_enabled,
22
+ :llm_request_max_retries,
23
+ :llm_request_retriable_exceptions,
20
24
  :model_superclass,
21
25
  :open_ai_api_key,
26
+ :open_ai_embedding_models_enabled,
22
27
  :open_ai_models_enabled,
28
+ :open_router_api_key,
29
+ :open_router_models_enabled,
30
+ :open_router_app_name,
31
+ :open_router_site_url,
23
32
  :task_system_prompt_intro,
24
33
  :user_tool_types
25
34
 
@@ -27,23 +36,36 @@ module Raif
27
36
  # Set default config
28
37
  @agent_types = Set.new(["Raif::Agents::ReActAgent", "Raif::Agents::NativeToolCallingAgent"])
29
38
  @anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
30
- @anthropic_bedrock_models_enabled = true
31
- @anthropic_models_enabled = true
39
+ @anthropic_bedrock_models_enabled = false
40
+ @anthropic_models_enabled = ENV["ANTHROPIC_API_KEY"].present?
32
41
  @authorize_admin_controller_action = ->{ false }
33
42
  @authorize_controller_action = ->{ false }
34
43
  @aws_bedrock_region = "us-east-1"
35
44
  @aws_bedrock_model_name_prefix = "us"
45
+ @aws_bedrock_titan_embedding_models_enabled = false
36
46
  @task_system_prompt_intro = "You are a helpful assistant."
37
47
  @conversation_entries_controller = "Raif::ConversationEntriesController"
38
48
  @conversation_system_prompt_intro = "You are a helpful assistant who is collaborating with a teammate."
39
49
  @conversation_types = Set.new(["Raif::Conversation"])
40
50
  @conversations_controller = "Raif::ConversationsController"
41
51
  @current_user_method = :current_user
52
+ @default_embedding_model_key = "open_ai_text_embedding_3_small"
42
53
  @default_llm_model_key = "open_ai_gpt_4o"
43
54
  @llm_api_requests_enabled = true
55
+ @llm_request_max_retries = 2
56
+ @llm_request_retriable_exceptions = [
57
+ Faraday::ConnectionFailed,
58
+ Faraday::TimeoutError,
59
+ Faraday::ServerError,
60
+ ]
44
61
  @model_superclass = "ApplicationRecord"
45
62
  @open_ai_api_key = ENV["OPENAI_API_KEY"]
46
- @open_ai_models_enabled = true
63
+ @open_ai_embedding_models_enabled = ENV["OPENAI_API_KEY"].present?
64
+ @open_ai_models_enabled = ENV["OPENAI_API_KEY"].present?
65
+ @open_router_api_key = ENV["OPENROUTER_API_KEY"]
66
+ @open_router_models_enabled = ENV["OPENROUTER_API_KEY"].present?
67
+ @open_router_app_name = nil
68
+ @open_router_site_url = nil
47
69
  @user_tool_types = []
48
70
  end
49
71
 
@@ -53,6 +75,11 @@ module Raif
53
75
  "Raif.config.default_llm_model_key was set to #{default_llm_model_key}, but must be one of: #{Raif.available_llm_keys.join(", ")}"
54
76
  end
55
77
 
78
+ unless Raif.available_embedding_model_keys.include?(default_embedding_model_key.to_sym)
79
+ raise Raif::Errors::InvalidConfigError,
80
+ "Raif.config.default_embedding_model_key was set to #{default_embedding_model_key}, but must be one of: #{Raif.available_embedding_model_keys.join(", ")}" # rubocop:disable Layout/LineLength
81
+ end
82
+
56
83
  if authorize_controller_action.respond_to?(:call)
57
84
  authorize_controller_action.freeze
58
85
  else
@@ -72,10 +99,20 @@ module Raif
72
99
  "Raif.config.open_ai_api_key is required when Raif.config.open_ai_models_enabled is true. Set it via Raif.config.open_ai_api_key or ENV[\"OPENAI_API_KEY\"]" # rubocop:disable Layout/LineLength
73
100
  end
74
101
 
102
+ if open_ai_embedding_models_enabled && open_ai_api_key.blank?
103
+ raise Raif::Errors::InvalidConfigError,
104
+ "Raif.config.open_ai_api_key is required when Raif.config.open_ai_embedding_models_enabled is true. Set it via Raif.config.open_ai_api_key or ENV[\"OPENAI_API_KEY\"]" # rubocop:disable Layout/LineLength
105
+ end
106
+
75
107
  if anthropic_models_enabled && anthropic_api_key.blank?
76
108
  raise Raif::Errors::InvalidConfigError,
77
109
  "Raif.config.anthropic_api_key is required when Raif.config.anthropic_models_enabled is true. Set it via Raif.config.anthropic_api_key or ENV['ANTHROPIC_API_KEY']" # rubocop:disable Layout/LineLength
78
110
  end
111
+
112
+ if open_router_models_enabled && open_router_api_key.blank?
113
+ raise Raif::Errors::InvalidConfigError,
114
+ "Raif.config.open_router_api_key is required when Raif.config.open_router_models_enabled is true. Set it via Raif.config.open_router_api_key or ENV['OPENROUTER_API_KEY']" # rubocop:disable Layout/LineLength
115
+ end
79
116
  end
80
117
 
81
118
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ class << self
5
+ attr_accessor :embedding_model_registry
6
+ end
7
+
8
+ def self.generate_embedding!(input, dimensions: nil)
9
+ embedding_model = embedding_model(default_embedding_model_key.to_sym)
10
+ embedding_model.generate_embedding!(input, dimensions:)
11
+ end
12
+
13
+ def self.default_embedding_model_key
14
+ Rails.env.test? ? :raif_test_embedding_model : Raif.config.default_embedding_model_key
15
+ end
16
+
17
+ def self.register_embedding_model(embedding_model_class, embedding_model_config)
18
+ embedding_model = embedding_model_class.new(**embedding_model_config)
19
+
20
+ unless embedding_model.valid?
21
+ raise ArgumentError, "The embedding model you tried to register is invalid: #{embedding_model.errors.full_messages.join(", ")}"
22
+ end
23
+
24
+ @embedding_model_registry ||= {}
25
+ @embedding_model_registry[embedding_model.key] = embedding_model_config.merge(embedding_model_class: embedding_model_class)
26
+ end
27
+
28
+ def self.embedding_model(model_key)
29
+ embedding_model_config = embedding_model_registry[model_key]
30
+
31
+ if embedding_model_config.nil?
32
+ raise ArgumentError, "No embedding model found for model key: #{model_key}. Available models: #{available_embedding_model_keys.join(", ")}"
33
+ end
34
+
35
+ embedding_model_class = embedding_model_config[:embedding_model_class]
36
+ embedding_model_class.new(**embedding_model_config.except(:embedding_model_class))
37
+ end
38
+
39
+ def self.available_embedding_models
40
+ embedding_model_registry.values
41
+ end
42
+
43
+ def self.available_embedding_model_keys
44
+ embedding_model_registry.keys
45
+ end
46
+
47
+ def self.embedding_model_config(model_key)
48
+ embedding_model_registry[model_key]
49
+ end
50
+
51
+ def self.default_embedding_models
52
+ {
53
+ Raif::EmbeddingModels::OpenAi => [
54
+ {
55
+ key: :open_ai_text_embedding_3_large,
56
+ api_name: "text-embedding-3-large",
57
+ input_token_cost: 0.13 / 1_000_000,
58
+ default_output_vector_size: 3072,
59
+ },
60
+ {
61
+ key: :open_ai_text_embedding_3_small,
62
+ api_name: "text-embedding-3-small",
63
+ input_token_cost: 0.02 / 1_000_000,
64
+ default_output_vector_size: 1536,
65
+ },
66
+ {
67
+ key: :open_ai_text_embedding_ada_002,
68
+ api_name: "text-embedding-ada-002",
69
+ input_token_cost: 0.01 / 1_000_000,
70
+ default_output_vector_size: 1536,
71
+ },
72
+ ],
73
+ Raif::EmbeddingModels::BedrockTitan => [
74
+ {
75
+ key: :bedrock_titan_embed_text_v2,
76
+ api_name: "amazon.titan-embed-text-v2:0",
77
+ input_token_cost: 0.01 / 1_000_000,
78
+ default_output_vector_size: 1024,
79
+ },
80
+ ]
81
+ }
82
+ end
83
+ end
data/lib/raif/engine.rb CHANGED
@@ -34,6 +34,14 @@ module Raif
34
34
  end
35
35
  end
36
36
 
37
+ config.after_initialize do
38
+ next unless Raif.config.open_ai_embedding_models_enabled
39
+
40
+ Raif.default_embedding_models[Raif::EmbeddingModels::OpenAi].each do |embedding_model_config|
41
+ Raif.register_embedding_model(Raif::EmbeddingModels::OpenAi, **embedding_model_config)
42
+ end
43
+ end
44
+
37
45
  config.after_initialize do
38
46
  next unless Raif.config.anthropic_models_enabled
39
47
 
@@ -45,7 +53,6 @@ module Raif
45
53
  config.after_initialize do
46
54
  next unless Raif.config.anthropic_bedrock_models_enabled
47
55
 
48
- require "aws-sdk-bedrock"
49
56
  require "aws-sdk-bedrockruntime"
50
57
 
51
58
  Raif.default_llms[Raif::Llms::BedrockClaude].each do |llm_config|
@@ -53,6 +60,24 @@ module Raif
53
60
  end
54
61
  end
55
62
 
63
+ config.after_initialize do
64
+ next unless Raif.config.open_router_models_enabled
65
+
66
+ Raif.default_llms[Raif::Llms::OpenRouter].each do |llm_config|
67
+ Raif.register_llm(Raif::Llms::OpenRouter, **llm_config)
68
+ end
69
+ end
70
+
71
+ config.after_initialize do
72
+ next unless Raif.config.aws_bedrock_titan_embedding_models_enabled
73
+
74
+ require "aws-sdk-bedrockruntime"
75
+
76
+ Raif.default_embedding_models[Raif::EmbeddingModels::BedrockTitan].each do |embedding_model_config|
77
+ Raif.register_embedding_model(Raif::EmbeddingModels::BedrockTitan, **embedding_model_config)
78
+ end
79
+ end
80
+
56
81
  config.after_initialize do
57
82
  next unless Rails.env.test?
58
83
 
@@ -60,6 +85,14 @@ module Raif
60
85
 
61
86
  require "#{Raif::Engine.root}/spec/support/test_llm"
62
87
  Raif.register_llm(Raif::Llms::Test, key: :raif_test_llm, api_name: "raif-test-llm")
88
+
89
+ require "#{Raif::Engine.root}/spec/support/test_embedding_model"
90
+ Raif.register_embedding_model(
91
+ Raif::EmbeddingModels::Test,
92
+ key: :raif_test_embedding_model,
93
+ api_name: "raif-test-embedding-model",
94
+ default_output_vector_size: 1536
95
+ )
63
96
  end
64
97
 
65
98
  config.after_initialize do
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Raif
4
4
  module Errors
5
- module OpenAi
6
- class ApiError < StandardError
7
- end
5
+ class InvalidModelFileInputError < StandardError
8
6
  end
9
7
  end
10
8
  end
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Raif
4
4
  module Errors
5
- module Anthropic
6
- class ApiError < StandardError
7
- end
5
+ class InvalidModelImageInputError < StandardError
8
6
  end
9
7
  end
10
8
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Errors
5
+ class UnsupportedFeatureError < StandardError
6
+ end
7
+ end
8
+ end
data/lib/raif/errors.rb CHANGED
@@ -5,5 +5,6 @@ require "raif/errors/action_not_authorized_error"
5
5
  require "raif/errors/invalid_user_tool_type_error"
6
6
  require "raif/errors/invalid_conversation_type_error"
7
7
  require "raif/errors/open_ai/json_schema_error"
8
- require "raif/errors/open_ai/api_error"
9
- require "raif/errors/anthropic/api_error"
8
+ require "raif/errors/invalid_model_image_input_error"
9
+ require "raif/errors/invalid_model_file_input_error"
10
+ require "raif/errors/unsupported_feature_error"
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ class JsonSchemaBuilder
5
+ attr_reader :properties, :required_properties, :items_schema
6
+
7
+ def initialize
8
+ @properties = {}
9
+ @required_properties = []
10
+ @items_schema = nil
11
+ end
12
+
13
+ def string(name, options = {})
14
+ add_property(name, "string", options)
15
+ end
16
+
17
+ def integer(name, options = {})
18
+ add_property(name, "integer", options)
19
+ end
20
+
21
+ def number(name, options = {})
22
+ add_property(name, "number", options)
23
+ end
24
+
25
+ def boolean(name, options = {})
26
+ add_property(name, "boolean", options)
27
+ end
28
+
29
+ def object(name = nil, options = {}, &block)
30
+ schema = {}
31
+
32
+ if block_given?
33
+ nested_builder = self.class.new
34
+ nested_builder.instance_eval(&block)
35
+
36
+ schema[:properties] = nested_builder.properties
37
+ schema[:additionalProperties] = false
38
+
39
+ # We currently use strict mode, which means that all properties are required
40
+ schema[:required] = nested_builder.required_properties
41
+ end
42
+
43
+ # If name is nil, we're inside an array and should return the schema directly
44
+ if name.nil?
45
+ @items_schema = { type: "object" }.merge(schema)
46
+ else
47
+ add_property(name, "object", options.merge(schema))
48
+ end
49
+ end
50
+
51
+ def array(name, options = {}, &block)
52
+ items_schema = options.delete(:items) || {}
53
+
54
+ if block_given?
55
+ nested_builder = self.class.new
56
+ nested_builder.instance_eval(&block)
57
+
58
+ # If items were directly set using the items method
59
+ if nested_builder.items_schema.present?
60
+ items_schema = nested_builder.items_schema
61
+ # If there are properties defined, it's an object schema
62
+ elsif nested_builder.properties.any?
63
+ items_schema = {
64
+ type: "object",
65
+ properties: nested_builder.properties,
66
+ additionalProperties: false
67
+ }
68
+
69
+ # We currently use strict mode, which means that all properties are required
70
+ items_schema[:required] = nested_builder.required_properties
71
+ end
72
+ end
73
+
74
+ options[:items] = items_schema unless items_schema.empty?
75
+ add_property(name, "array", options)
76
+ end
77
+
78
+ # Allow setting array items directly
79
+ def items(options = {})
80
+ @items_schema = options
81
+ end
82
+
83
+ def to_schema
84
+ {
85
+ type: "object",
86
+ additionalProperties: false,
87
+ properties: @properties,
88
+ required: @required_properties
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ def add_property(name, type, options = {})
95
+ property = { type: type }
96
+
97
+ # We currently use strict mode, which means that all properties are required
98
+ @required_properties << name.to_s
99
+
100
+ property.merge!(options)
101
+ @properties[name] = property
102
+ end
103
+ end
104
+ end