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.
- checksums.yaml +4 -4
- data/README.md +346 -43
- data/app/assets/builds/raif.css +26 -1
- data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
- data/app/assets/stylesheets/raif/loader.scss +27 -1
- data/app/controllers/raif/admin/application_controller.rb +14 -0
- data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
- data/app/controllers/raif/admin/stats_controller.rb +19 -0
- data/app/controllers/raif/admin/tasks_controller.rb +18 -2
- data/app/controllers/raif/conversations_controller.rb +5 -1
- data/app/models/raif/agent.rb +11 -9
- data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
- data/app/models/raif/agents/re_act_agent.rb +6 -0
- data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
- data/app/models/raif/concerns/json_schema_definition.rb +28 -0
- data/app/models/raif/concerns/llm_response_parsing.rb +42 -14
- data/app/models/raif/concerns/llm_temperature.rb +17 -0
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
- data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +70 -0
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +42 -0
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
- data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +41 -0
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
- data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
- data/app/models/raif/conversation.rb +28 -7
- data/app/models/raif/conversation_entry.rb +40 -8
- data/app/models/raif/embedding_model.rb +22 -0
- data/app/models/raif/embedding_models/bedrock.rb +34 -0
- data/app/models/raif/embedding_models/open_ai.rb +40 -0
- data/app/models/raif/llm.rb +108 -9
- data/app/models/raif/llms/anthropic.rb +72 -57
- data/app/models/raif/llms/bedrock.rb +165 -0
- data/app/models/raif/llms/open_ai_base.rb +66 -0
- data/app/models/raif/llms/open_ai_completions.rb +100 -0
- data/app/models/raif/llms/open_ai_responses.rb +144 -0
- data/app/models/raif/llms/open_router.rb +88 -0
- data/app/models/raif/model_completion.rb +23 -2
- data/app/models/raif/model_file_input.rb +113 -0
- data/app/models/raif/model_image_input.rb +4 -0
- data/app/models/raif/model_tool.rb +82 -52
- data/app/models/raif/model_tool_invocation.rb +8 -6
- data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
- data/app/models/raif/model_tools/fetch_url.rb +27 -36
- data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
- data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
- data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
- data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
- data/app/models/raif/streaming_responses/anthropic.rb +63 -0
- data/app/models/raif/streaming_responses/bedrock.rb +89 -0
- data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
- data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
- data/app/models/raif/task.rb +71 -16
- data/app/views/layouts/raif/admin.html.erb +10 -0
- data/app/views/raif/admin/agents/show.html.erb +3 -1
- data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
- data/app/views/raif/admin/conversations/show.html.erb +4 -2
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +8 -0
- data/app/views/raif/admin/model_completions/index.html.erb +2 -0
- data/app/views/raif/admin/model_completions/show.html.erb +58 -3
- data/app/views/raif/admin/stats/index.html.erb +128 -0
- data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
- data/app/views/raif/admin/tasks/_task.html.erb +5 -4
- data/app/views/raif/admin/tasks/index.html.erb +20 -2
- data/app/views/raif/admin/tasks/show.html.erb +3 -1
- data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +22 -14
- data/app/views/raif/conversation_entries/_form.html.erb +1 -1
- data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
- data/app/views/raif/conversation_entries/_message.html.erb +14 -3
- data/config/locales/admin.en.yml +16 -0
- data/config/locales/en.yml +47 -3
- data/config/routes.rb +6 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
- data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
- data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
- data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
- data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
- data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
- data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
- data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
- data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -12
- data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
- data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
- data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +16 -14
- data/lib/generators/raif/install/templates/initializer.rb +62 -6
- data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
- data/lib/generators/raif/task/templates/task.rb.tt +34 -23
- data/lib/raif/configuration.rb +63 -4
- data/lib/raif/embedding_model_registry.rb +83 -0
- data/lib/raif/engine.rb +56 -7
- data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
- data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
- data/lib/raif/errors/streaming_error.rb +18 -0
- data/lib/raif/errors/unsupported_feature_error.rb +8 -0
- data/lib/raif/errors.rb +4 -2
- data/lib/raif/json_schema_builder.rb +104 -0
- data/lib/raif/llm_registry.rb +315 -0
- data/lib/raif/migration_checker.rb +74 -0
- data/lib/raif/utils/html_fragment_processor.rb +169 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +7 -32
- data/lib/tasks/raif_tasks.rake +9 -4
- metadata +62 -12
- data/app/models/raif/llms/bedrock_claude.rb +0 -134
- data/app/models/raif/llms/open_ai.rb +0 -259
- data/lib/raif/default_llms.rb +0 -37
@@ -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
|
@@ -0,0 +1,315 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
def self.llm_registry
|
5
|
+
@llm_registry ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.register_llm(llm_class, llm_config)
|
9
|
+
llm = llm_class.new(**llm_config)
|
10
|
+
|
11
|
+
unless llm.valid?
|
12
|
+
raise ArgumentError, "The LLM you tried to register is invalid: #{llm.errors.full_messages.join(", ")}"
|
13
|
+
end
|
14
|
+
|
15
|
+
@llm_registry ||= {}
|
16
|
+
@llm_registry[llm.key] = llm_config.merge(llm_class: llm_class)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.llm(model_key)
|
20
|
+
llm_config = llm_registry[model_key]
|
21
|
+
|
22
|
+
if llm_config.nil?
|
23
|
+
raise ArgumentError, "No LLM found for model key: #{model_key}. Available models: #{available_llm_keys.join(", ")}"
|
24
|
+
end
|
25
|
+
|
26
|
+
llm_class = llm_config[:llm_class]
|
27
|
+
llm_class.new(**llm_config.except(:llm_class))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.available_llms
|
31
|
+
llm_registry.values
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.available_llm_keys
|
35
|
+
llm_registry.keys
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.llm_config(model_key)
|
39
|
+
llm_registry[model_key]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.default_llms
|
43
|
+
open_ai_models = [
|
44
|
+
{
|
45
|
+
key: :open_ai_gpt_4o_mini,
|
46
|
+
api_name: "gpt-4o-mini",
|
47
|
+
input_token_cost: 0.15 / 1_000_000,
|
48
|
+
output_token_cost: 0.6 / 1_000_000,
|
49
|
+
},
|
50
|
+
{
|
51
|
+
key: :open_ai_gpt_4o,
|
52
|
+
api_name: "gpt-4o",
|
53
|
+
input_token_cost: 2.5 / 1_000_000,
|
54
|
+
output_token_cost: 10.0 / 1_000_000,
|
55
|
+
},
|
56
|
+
{
|
57
|
+
key: :open_ai_gpt_3_5_turbo,
|
58
|
+
api_name: "gpt-3.5-turbo",
|
59
|
+
input_token_cost: 0.5 / 1_000_000,
|
60
|
+
output_token_cost: 1.5 / 1_000_000,
|
61
|
+
model_provider_settings: { supports_structured_outputs: false }
|
62
|
+
},
|
63
|
+
{
|
64
|
+
key: :open_ai_gpt_4_1,
|
65
|
+
api_name: "gpt-4.1",
|
66
|
+
input_token_cost: 2.0 / 1_000_000,
|
67
|
+
output_token_cost: 8.0 / 1_000_000,
|
68
|
+
},
|
69
|
+
{
|
70
|
+
key: :open_ai_gpt_4_1_mini,
|
71
|
+
api_name: "gpt-4.1-mini",
|
72
|
+
input_token_cost: 0.4 / 1_000_000,
|
73
|
+
output_token_cost: 1.6 / 1_000_000,
|
74
|
+
},
|
75
|
+
{
|
76
|
+
key: :open_ai_gpt_4_1_nano,
|
77
|
+
api_name: "gpt-4.1-nano",
|
78
|
+
input_token_cost: 0.1 / 1_000_000,
|
79
|
+
output_token_cost: 0.4 / 1_000_000,
|
80
|
+
},
|
81
|
+
{
|
82
|
+
key: :open_ai_o1,
|
83
|
+
api_name: "o1",
|
84
|
+
input_token_cost: 15.0 / 1_000_000,
|
85
|
+
output_token_cost: 60.0 / 1_000_000,
|
86
|
+
model_provider_settings: { supports_temperature: false },
|
87
|
+
},
|
88
|
+
{
|
89
|
+
key: :open_ai_o1_mini,
|
90
|
+
api_name: "o1-mini",
|
91
|
+
input_token_cost: 1.5 / 1_000_000,
|
92
|
+
output_token_cost: 6.0 / 1_000_000,
|
93
|
+
model_provider_settings: { supports_temperature: false },
|
94
|
+
},
|
95
|
+
{
|
96
|
+
key: :open_ai_o3,
|
97
|
+
api_name: "o3",
|
98
|
+
input_token_cost: 2.0 / 1_000_000,
|
99
|
+
output_token_cost: 8.0 / 1_000_000,
|
100
|
+
model_provider_settings: { supports_temperature: false },
|
101
|
+
},
|
102
|
+
{
|
103
|
+
key: :open_ai_o3_mini,
|
104
|
+
api_name: "o3-mini",
|
105
|
+
input_token_cost: 1.1 / 1_000_000,
|
106
|
+
output_token_cost: 4.4 / 1_000_000,
|
107
|
+
model_provider_settings: { supports_temperature: false },
|
108
|
+
},
|
109
|
+
{
|
110
|
+
key: :open_ai_o4_mini,
|
111
|
+
api_name: "o4-mini",
|
112
|
+
input_token_cost: 1.1 / 1_000_000,
|
113
|
+
output_token_cost: 4.4 / 1_000_000,
|
114
|
+
model_provider_settings: { supports_temperature: false },
|
115
|
+
},
|
116
|
+
]
|
117
|
+
|
118
|
+
open_ai_responses_models = open_ai_models.dup.map.with_index do |model, _index|
|
119
|
+
model.merge(
|
120
|
+
key: model[:key].to_s.gsub("open_ai_", "open_ai_responses_").to_sym,
|
121
|
+
supported_provider_managed_tools: [
|
122
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
123
|
+
Raif::ModelTools::ProviderManaged::CodeExecution,
|
124
|
+
Raif::ModelTools::ProviderManaged::ImageGeneration
|
125
|
+
]
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
# o1-mini is not supported by the OpenAI Responses API.
|
130
|
+
open_ai_responses_models.delete_if{|model| model[:key] == :open_ai_o1_mini }
|
131
|
+
|
132
|
+
# o1-pro and o3-pro are not supported by the OpenAI Completions API, but it is supported by the OpenAI Responses API.
|
133
|
+
open_ai_responses_models << {
|
134
|
+
key: :open_ai_responses_o1_pro,
|
135
|
+
api_name: "o1-pro",
|
136
|
+
input_token_cost: 150.0 / 1_000_000,
|
137
|
+
output_token_cost: 600.0 / 1_000_000,
|
138
|
+
model_provider_settings: { supports_temperature: false },
|
139
|
+
}
|
140
|
+
|
141
|
+
open_ai_responses_models << {
|
142
|
+
key: :open_ai_responses_o3_pro,
|
143
|
+
api_name: "o3-pro",
|
144
|
+
input_token_cost: 20.0 / 1_000_000,
|
145
|
+
output_token_cost: 80.0 / 1_000_000,
|
146
|
+
model_provider_settings: { supports_temperature: false },
|
147
|
+
}
|
148
|
+
|
149
|
+
{
|
150
|
+
Raif::Llms::OpenAiCompletions => open_ai_models,
|
151
|
+
Raif::Llms::OpenAiResponses => open_ai_responses_models,
|
152
|
+
Raif::Llms::Anthropic => [
|
153
|
+
{
|
154
|
+
key: :anthropic_claude_4_sonnet,
|
155
|
+
api_name: "claude-sonnet-4-20250514",
|
156
|
+
input_token_cost: 3.0 / 1_000_000,
|
157
|
+
output_token_cost: 15.0 / 1_000_000,
|
158
|
+
max_completion_tokens: 8192,
|
159
|
+
supported_provider_managed_tools: [
|
160
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
161
|
+
Raif::ModelTools::ProviderManaged::CodeExecution
|
162
|
+
]
|
163
|
+
},
|
164
|
+
{
|
165
|
+
key: :anthropic_claude_4_opus,
|
166
|
+
api_name: "claude-opus-4-20250514",
|
167
|
+
input_token_cost: 15.0 / 1_000_000,
|
168
|
+
output_token_cost: 75.0 / 1_000_000,
|
169
|
+
max_completion_tokens: 8192,
|
170
|
+
supported_provider_managed_tools: [
|
171
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
172
|
+
Raif::ModelTools::ProviderManaged::CodeExecution
|
173
|
+
]
|
174
|
+
},
|
175
|
+
{
|
176
|
+
key: :anthropic_claude_3_7_sonnet,
|
177
|
+
api_name: "claude-3-7-sonnet-latest",
|
178
|
+
input_token_cost: 3.0 / 1_000_000,
|
179
|
+
output_token_cost: 15.0 / 1_000_000,
|
180
|
+
max_completion_tokens: 8192,
|
181
|
+
supported_provider_managed_tools: [
|
182
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
183
|
+
Raif::ModelTools::ProviderManaged::CodeExecution
|
184
|
+
]
|
185
|
+
},
|
186
|
+
{
|
187
|
+
key: :anthropic_claude_3_5_sonnet,
|
188
|
+
api_name: "claude-3-5-sonnet-latest",
|
189
|
+
input_token_cost: 3.0 / 1_000_000,
|
190
|
+
output_token_cost: 15.0 / 1_000_000,
|
191
|
+
max_completion_tokens: 8192,
|
192
|
+
supported_provider_managed_tools: [
|
193
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
194
|
+
Raif::ModelTools::ProviderManaged::CodeExecution
|
195
|
+
]
|
196
|
+
},
|
197
|
+
{
|
198
|
+
key: :anthropic_claude_3_5_haiku,
|
199
|
+
api_name: "claude-3-5-haiku-latest",
|
200
|
+
input_token_cost: 0.8 / 1_000_000,
|
201
|
+
output_token_cost: 4.0 / 1_000_000,
|
202
|
+
max_completion_tokens: 8192,
|
203
|
+
supported_provider_managed_tools: [
|
204
|
+
Raif::ModelTools::ProviderManaged::WebSearch,
|
205
|
+
Raif::ModelTools::ProviderManaged::CodeExecution
|
206
|
+
]
|
207
|
+
},
|
208
|
+
{
|
209
|
+
key: :anthropic_claude_3_opus,
|
210
|
+
api_name: "claude-3-opus-latest",
|
211
|
+
input_token_cost: 15.0 / 1_000_000,
|
212
|
+
output_token_cost: 75.0 / 1_000_000,
|
213
|
+
max_completion_tokens: 4096
|
214
|
+
},
|
215
|
+
],
|
216
|
+
Raif::Llms::Bedrock => [
|
217
|
+
{
|
218
|
+
key: :bedrock_claude_4_sonnet,
|
219
|
+
api_name: "anthropic.claude-sonnet-4-20250514-v1:0",
|
220
|
+
input_token_cost: 0.003 / 1000,
|
221
|
+
output_token_cost: 0.015 / 1000,
|
222
|
+
max_completion_tokens: 8192
|
223
|
+
},
|
224
|
+
{
|
225
|
+
key: :bedrock_claude_4_opus,
|
226
|
+
api_name: "anthropic.claude-opus-4-20250514-v1:0",
|
227
|
+
input_token_cost: 0.015 / 1000,
|
228
|
+
output_token_cost: 0.075 / 1000,
|
229
|
+
max_completion_tokens: 8192
|
230
|
+
},
|
231
|
+
{
|
232
|
+
key: :bedrock_claude_3_5_sonnet,
|
233
|
+
api_name: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
234
|
+
input_token_cost: 0.003 / 1000,
|
235
|
+
output_token_cost: 0.015 / 1000,
|
236
|
+
max_completion_tokens: 8192
|
237
|
+
},
|
238
|
+
{
|
239
|
+
key: :bedrock_claude_3_7_sonnet,
|
240
|
+
api_name: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
241
|
+
input_token_cost: 0.003 / 1000,
|
242
|
+
output_token_cost: 0.015 / 1000,
|
243
|
+
max_completion_tokens: 8192
|
244
|
+
},
|
245
|
+
{
|
246
|
+
key: :bedrock_claude_3_5_haiku,
|
247
|
+
api_name: "anthropic.claude-3-5-haiku-20241022-v1:0",
|
248
|
+
input_token_cost: 0.0008 / 1000,
|
249
|
+
output_token_cost: 0.004 / 1000,
|
250
|
+
max_completion_tokens: 8192
|
251
|
+
},
|
252
|
+
{
|
253
|
+
key: :bedrock_claude_3_opus,
|
254
|
+
api_name: "anthropic.claude-3-opus-20240229-v1:0",
|
255
|
+
input_token_cost: 0.015 / 1000,
|
256
|
+
output_token_cost: 0.075 / 1000,
|
257
|
+
max_completion_tokens: 4096
|
258
|
+
},
|
259
|
+
{
|
260
|
+
key: :bedrock_amazon_nova_micro,
|
261
|
+
api_name: "amazon.nova-micro-v1:0",
|
262
|
+
input_token_cost: 0.0000115 / 1000,
|
263
|
+
output_token_cost: 0.000184 / 1000,
|
264
|
+
max_completion_tokens: 4096
|
265
|
+
},
|
266
|
+
{
|
267
|
+
key: :bedrock_amazon_nova_lite,
|
268
|
+
api_name: "amazon.nova-lite-v1:0",
|
269
|
+
input_token_cost: 0.0000195 / 1000,
|
270
|
+
output_token_cost: 0.000312 / 1000,
|
271
|
+
max_completion_tokens: 4096
|
272
|
+
},
|
273
|
+
{
|
274
|
+
key: :bedrock_amazon_nova_pro,
|
275
|
+
api_name: "amazon.nova-pro-v1:0",
|
276
|
+
input_token_cost: 0.0002625 / 1000,
|
277
|
+
output_token_cost: 0.0042 / 1000,
|
278
|
+
max_completion_tokens: 4096
|
279
|
+
}
|
280
|
+
],
|
281
|
+
Raif::Llms::OpenRouter => [
|
282
|
+
{
|
283
|
+
key: :open_router_claude_3_7_sonnet,
|
284
|
+
api_name: "anthropic/claude-3.7-sonnet",
|
285
|
+
input_token_cost: 3.0 / 1_000_000,
|
286
|
+
output_token_cost: 15.0 / 1_000_000,
|
287
|
+
},
|
288
|
+
{
|
289
|
+
key: :open_router_llama_3_3_70b_instruct,
|
290
|
+
api_name: "meta-llama/llama-3.3-70b-instruct",
|
291
|
+
input_token_cost: 0.10 / 1_000_000,
|
292
|
+
output_token_cost: 0.25 / 1_000_000,
|
293
|
+
},
|
294
|
+
{
|
295
|
+
key: :open_router_llama_3_1_8b_instruct,
|
296
|
+
api_name: "meta-llama/llama-3.1-8b-instruct",
|
297
|
+
input_token_cost: 0.02 / 1_000_000,
|
298
|
+
output_token_cost: 0.03 / 1_000_000,
|
299
|
+
},
|
300
|
+
{
|
301
|
+
key: :open_router_gemini_2_0_flash,
|
302
|
+
api_name: "google/gemini-2.0-flash-001",
|
303
|
+
input_token_cost: 0.1 / 1_000_000,
|
304
|
+
output_token_cost: 0.4 / 1_000_000,
|
305
|
+
},
|
306
|
+
{
|
307
|
+
key: :open_router_deepseek_chat_v3,
|
308
|
+
api_name: "deepseek/deepseek-chat-v3-0324",
|
309
|
+
input_token_cost: 0.27 / 1_000_000,
|
310
|
+
output_token_cost: 1.1 / 1_000_000,
|
311
|
+
},
|
312
|
+
]
|
313
|
+
}
|
314
|
+
end
|
315
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
module MigrationChecker
|
5
|
+
class << self
|
6
|
+
def uninstalled_migrations
|
7
|
+
engine_migration_names = engine_migration_names_from_context
|
8
|
+
ran_migration_names = ran_migration_names_from_host
|
9
|
+
|
10
|
+
engine_migration_names - ran_migration_names
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_and_warn!
|
14
|
+
return unless defined?(Rails) && Rails.application
|
15
|
+
|
16
|
+
uninstalled = uninstalled_migrations
|
17
|
+
return if uninstalled.empty?
|
18
|
+
|
19
|
+
warning_message = build_warning_message(uninstalled)
|
20
|
+
|
21
|
+
# Output to both logger and STDOUT to ensure visibility
|
22
|
+
Rails.logger&.warn(warning_message)
|
23
|
+
warn warning_message
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def engine_migration_names_from_context
|
29
|
+
engine_paths = Raif::Engine.paths["db/migrate"].existent
|
30
|
+
return [] if engine_paths.empty?
|
31
|
+
|
32
|
+
ActiveRecord::MigrationContext.new(engine_paths).migrations.map(&:name)
|
33
|
+
rescue => e
|
34
|
+
Rails.logger&.debug("Raif: Could not load engine migrations: #{e.message}")
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
def ran_migration_names_from_host
|
39
|
+
return [] unless defined?(Rails) && Rails.application
|
40
|
+
|
41
|
+
app_paths = Rails.application.paths["db/migrate"].expanded
|
42
|
+
return [] if app_paths.empty?
|
43
|
+
|
44
|
+
ctx = ActiveRecord::MigrationContext.new(app_paths)
|
45
|
+
ran_versions = ctx.get_all_versions
|
46
|
+
ctx.migrations.select{|m| ran_versions.include?(m.version) }.map(&:name)
|
47
|
+
rescue ActiveRecord::NoDatabaseError
|
48
|
+
# Database doesn't exist yet, so no migrations have been run
|
49
|
+
[]
|
50
|
+
rescue => e
|
51
|
+
Rails.logger&.debug("Raif: Could not load migration status: #{e.message}")
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_warning_message(uninstalled_migration_names)
|
56
|
+
<<~WARNING
|
57
|
+
\e[33m
|
58
|
+
⚠️ RAIF MIGRATION WARNING ⚠️
|
59
|
+
|
60
|
+
The following Raif migrations have not been run in your application:
|
61
|
+
|
62
|
+
#{uninstalled_migration_names.map { |name| " • #{name}" }.join("\n")}
|
63
|
+
|
64
|
+
To install and run these migrations:
|
65
|
+
|
66
|
+
rails raif:install:migrations
|
67
|
+
rails db:migrate
|
68
|
+
|
69
|
+
\e[0m
|
70
|
+
WARNING
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Utility class for processing HTML fragments with various cleaning and transformation operations.
|
4
|
+
#
|
5
|
+
# This class provides methods for sanitizing HTML content, converting markdown links to HTML,
|
6
|
+
# processing existing HTML links (adding target="_blank", stripping tracking parameters),
|
7
|
+
# and removing tracking parameters from URLs.
|
8
|
+
class Raif::Utils::HtmlFragmentProcessor
|
9
|
+
# List of common tracking parameters to remove from URLs
|
10
|
+
TRACKING_PARAMS = %w[
|
11
|
+
utm_source
|
12
|
+
utm_medium
|
13
|
+
utm_campaign
|
14
|
+
utm_term
|
15
|
+
utm_content
|
16
|
+
utm_id
|
17
|
+
]
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Cleans and sanitizes an HTML fragment by removing empty text nodes and dangerous content.
|
21
|
+
#
|
22
|
+
# @param html [String, Nokogiri::HTML::DocumentFragment] The HTML content to clean
|
23
|
+
# @param allowed_tags [Array<String>, nil] Array of allowed HTML tags. Defaults to Rails HTML5 safe list
|
24
|
+
# @param allowed_attributes [Array<String>, nil] Array of allowed HTML attributes. Defaults to Rails HTML5 safe list
|
25
|
+
# @return [String] Cleaned and sanitized HTML string
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# clean_html_fragment("<script>alert('xss')</script><p>Safe content</p>")
|
29
|
+
# # => "<p>Safe content</p>"
|
30
|
+
#
|
31
|
+
# @example With custom allowed tags
|
32
|
+
# clean_html_fragment("<p>Para</p><div>Div</div>", allowed_tags: %w[p])
|
33
|
+
# # => "<p>Para</p>Div"
|
34
|
+
def clean_html_fragment(html, allowed_tags: nil, allowed_attributes: nil)
|
35
|
+
fragment = html.is_a?(Nokogiri::HTML::DocumentFragment) ? html : Nokogiri::HTML.fragment(html)
|
36
|
+
|
37
|
+
fragment.traverse do |node|
|
38
|
+
if node.text? && node.text.strip.empty?
|
39
|
+
node.remove
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
allowed_tags = allowed_tags.presence || Rails::HTML5::SafeListSanitizer.allowed_tags
|
44
|
+
allowed_attributes = allowed_attributes.presence || Rails::HTML5::SafeListSanitizer.allowed_attributes
|
45
|
+
|
46
|
+
ActionController::Base.helpers.sanitize(fragment.to_html, tags: allowed_tags, attributes: allowed_attributes).strip
|
47
|
+
end
|
48
|
+
|
49
|
+
# Converts markdown-style links to HTML anchor tags with target="_blank" and rel="noopener".
|
50
|
+
#
|
51
|
+
# Converts [text](url) format to <a href="url" target="_blank" rel="noopener">text</a>.
|
52
|
+
# Also strips tracking parameters from the URLs.
|
53
|
+
#
|
54
|
+
# @param text [String] The text content that may contain markdown links
|
55
|
+
# @return [String] HTML with markdown links converted to anchor tags
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# convert_markdown_links_to_html("Check out [Google](https://google.com) for search.")
|
59
|
+
# # => 'Check out <a href="https://google.com" target="_blank" rel="noopener">Google</a> for search.'
|
60
|
+
#
|
61
|
+
# @example With tracking parameters
|
62
|
+
# convert_markdown_links_to_html("[Example](https://example.com?utm_source=test¶m=keep)")
|
63
|
+
# # => '<a href="https://example.com?param=keep" target="_blank" rel="noopener">Example</a>'
|
64
|
+
def convert_markdown_links_to_html(text)
|
65
|
+
# Convert markdown links [text](url) to HTML links <a href="url" target="_blank" rel="noopener">text</a>
|
66
|
+
text.gsub(/\[([^\]]*)\]\(([^)]+)\)/) do |_match|
|
67
|
+
text = ::Regexp.last_match(1)
|
68
|
+
url = ::Regexp.last_match(2)
|
69
|
+
clean_url = strip_tracking_parameters(url)
|
70
|
+
%(<a href="#{CGI.escapeHTML(clean_url)}" target="_blank" rel="noopener">#{CGI.escapeHTML(text)}</a>)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Processes existing HTML links by optionally adding target="_blank" and stripping tracking parameters.
|
75
|
+
#
|
76
|
+
# This method provides fine-grained control over link processing with configurable options
|
77
|
+
# for both target="_blank" addition and tracking parameter removal.
|
78
|
+
#
|
79
|
+
# @param html [String, Nokogiri::HTML::DocumentFragment] The HTML content containing links to process
|
80
|
+
# @param add_target_blank [Boolean] Whether to add target="_blank" and rel="noopener" to links (required)
|
81
|
+
# @param strip_tracking_parameters [Boolean] Whether to remove tracking parameters from URLs (required)
|
82
|
+
# @return [String] Processed HTML with modified links
|
83
|
+
#
|
84
|
+
# @example Default behavior (adds target="_blank" and strips tracking params)
|
85
|
+
# process_links('<a href="https://example.com?utm_source=test">Link</a>', add_target_blank: true, strip_tracking_parameters: true)
|
86
|
+
# # => '<a href="https://example.com" target="_blank" rel="noopener">Link</a>'
|
87
|
+
#
|
88
|
+
# @example Only strip tracking parameters
|
89
|
+
# process_links(html, add_target_blank: false, strip_tracking_parameters: true)
|
90
|
+
# # => '<a href="https://example.com">Link</a>'
|
91
|
+
#
|
92
|
+
# @example Only add target="_blank"
|
93
|
+
# process_links(html, add_target_blank: true, strip_tracking_parameters: false)
|
94
|
+
# # => '<a href="https://example.com?utm_source=test" target="_blank" rel="noopener">Link</a>'
|
95
|
+
#
|
96
|
+
# @example No processing
|
97
|
+
# process_links(html, add_target_blank: false, strip_tracking_parameters: false)
|
98
|
+
# # => Original HTML unchanged
|
99
|
+
def process_links(html, add_target_blank:, strip_tracking_parameters:)
|
100
|
+
fragment = html.is_a?(Nokogiri::HTML::DocumentFragment) ? html : Nokogiri::HTML.fragment(html)
|
101
|
+
|
102
|
+
fragment.css("a").each do |link|
|
103
|
+
if add_target_blank
|
104
|
+
link["target"] = "_blank"
|
105
|
+
link["rel"] = "noopener"
|
106
|
+
end
|
107
|
+
|
108
|
+
if strip_tracking_parameters
|
109
|
+
link["href"] = strip_tracking_parameters(link["href"])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
fragment.to_html
|
114
|
+
end
|
115
|
+
|
116
|
+
# Removes tracking parameters (UTM parameters) from a URL.
|
117
|
+
#
|
118
|
+
# Preserves all non-tracking query parameters and handles various URL formats including
|
119
|
+
# relative URLs, absolute URLs, and malformed URLs gracefully.
|
120
|
+
#
|
121
|
+
# @param url [String] The URL to clean
|
122
|
+
# @return [String] URL with tracking parameters removed, or original URL if parsing fails
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# strip_tracking_parameters("https://example.com?utm_source=google&page=1")
|
126
|
+
# # => "https://example.com?page=1"
|
127
|
+
#
|
128
|
+
# @example Removes all tracking parameters
|
129
|
+
# strip_tracking_parameters("https://example.com?utm_source=test&utm_medium=cpc")
|
130
|
+
# # => "https://example.com"
|
131
|
+
#
|
132
|
+
# @example Preserves fragments
|
133
|
+
# strip_tracking_parameters("https://example.com?utm_source=test&page=1#section")
|
134
|
+
# # => "https://example.com?page=1#section"
|
135
|
+
#
|
136
|
+
# @example Handles relative URLs
|
137
|
+
# strip_tracking_parameters("/path?utm_source=test¶m=keep")
|
138
|
+
# # => "/path?param=keep"
|
139
|
+
def strip_tracking_parameters(url)
|
140
|
+
return url unless url.include?("?")
|
141
|
+
|
142
|
+
begin
|
143
|
+
uri = URI.parse(url)
|
144
|
+
return url unless uri.query
|
145
|
+
|
146
|
+
# Only process URLs that have a valid scheme and host, or are relative URLs
|
147
|
+
unless uri.scheme || url.start_with?("/", "#")
|
148
|
+
return url
|
149
|
+
end
|
150
|
+
|
151
|
+
# Parse query parameters and filter out tracking ones
|
152
|
+
params = URI.decode_www_form(uri.query)
|
153
|
+
clean_params = params.reject { |param, _| TRACKING_PARAMS.include?(param.downcase) }
|
154
|
+
|
155
|
+
# Rebuild the URL
|
156
|
+
uri.query = if clean_params.empty?
|
157
|
+
nil
|
158
|
+
else
|
159
|
+
URI.encode_www_form(clean_params)
|
160
|
+
end
|
161
|
+
|
162
|
+
uri.to_s
|
163
|
+
rescue URI::InvalidURIError
|
164
|
+
# If URL parsing fails, return the original URL
|
165
|
+
url
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/raif/utils.rb
CHANGED
data/lib/raif/version.rb
CHANGED