raif 1.1.0 → 1.2.1.pre
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 +150 -4
- data/app/assets/builds/raif.css +26 -1
- data/app/assets/stylesheets/raif/loader.scss +27 -1
- data/app/models/raif/concerns/llm_response_parsing.rb +22 -16
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
- data/app/models/raif/concerns/llms/{bedrock_claude → bedrock}/message_formatting.rb +4 -4
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +7 -6
- data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
- data/app/models/raif/concerns/llms/{open_ai → open_ai_completions}/message_formatting.rb +1 -1
- 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 +17 -4
- data/app/models/raif/conversation_entry.rb +18 -2
- data/app/models/raif/embedding_models/{bedrock_titan.rb → bedrock.rb} +2 -2
- data/app/models/raif/llm.rb +73 -7
- data/app/models/raif/llms/anthropic.rb +56 -36
- data/app/models/raif/llms/{bedrock_claude.rb → bedrock.rb} +62 -45
- 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 +44 -44
- data/app/models/raif/model_completion.rb +2 -0
- data/app/models/raif/model_tool.rb +4 -0
- 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/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/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
- data/app/views/raif/admin/conversations/show.html.erb +1 -1
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +7 -0
- data/app/views/raif/admin/model_completions/index.html.erb +1 -0
- data/app/views/raif/admin/model_completions/show.html.erb +28 -0
- data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +5 -1
- data/app/views/raif/conversation_entries/_message.html.erb +4 -0
- data/config/locales/admin.en.yml +2 -0
- data/config/locales/en.yml +24 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
- data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +1 -1
- data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +1 -1
- data/db/migrate/20250424232946_add_created_at_indexes.rb +1 -1
- data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +1 -1
- 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/conversation/templates/conversation.rb.tt +3 -3
- data/lib/generators/raif/install/templates/initializer.rb +14 -2
- data/lib/raif/configuration.rb +27 -5
- data/lib/raif/embedding_model_registry.rb +1 -1
- data/lib/raif/engine.rb +25 -9
- data/lib/raif/errors/streaming_error.rb +18 -0
- data/lib/raif/errors.rb +1 -0
- data/lib/raif/llm_registry.rb +169 -47
- data/lib/raif/migration_checker.rb +74 -0
- data/lib/raif/utils/html_fragment_processor.rb +170 -0
- data/lib/raif/utils.rb +1 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +2 -0
- data/spec/support/complex_test_tool.rb +65 -0
- data/spec/support/rspec_helpers.rb +66 -0
- data/spec/support/test_conversation.rb +18 -0
- data/spec/support/test_embedding_model.rb +27 -0
- data/spec/support/test_llm.rb +22 -0
- data/spec/support/test_model_tool.rb +32 -0
- data/spec/support/test_task.rb +45 -0
- metadata +52 -8
- data/app/models/raif/llms/open_ai.rb +0 -256
@@ -1,256 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Raif::Llms::OpenAi < Raif::Llm
|
4
|
-
include Raif::Concerns::Llms::OpenAi::MessageFormatting
|
5
|
-
|
6
|
-
def perform_model_completion!(model_completion)
|
7
|
-
model_completion.temperature ||= default_temperature
|
8
|
-
parameters = build_request_parameters(model_completion)
|
9
|
-
|
10
|
-
response = connection.post("chat/completions") do |req|
|
11
|
-
req.body = parameters
|
12
|
-
end
|
13
|
-
|
14
|
-
response_json = response.body
|
15
|
-
|
16
|
-
model_completion.update!(
|
17
|
-
response_tool_calls: extract_response_tool_calls(response_json),
|
18
|
-
raw_response: response_json.dig("choices", 0, "message", "content"),
|
19
|
-
completion_tokens: response_json.dig("usage", "completion_tokens"),
|
20
|
-
prompt_tokens: response_json.dig("usage", "prompt_tokens"),
|
21
|
-
total_tokens: response_json.dig("usage", "total_tokens"),
|
22
|
-
response_format_parameter: parameters.dig(:response_format, :type)
|
23
|
-
)
|
24
|
-
|
25
|
-
model_completion
|
26
|
-
end
|
27
|
-
|
28
|
-
def connection
|
29
|
-
@connection ||= Faraday.new(url: "https://api.openai.com/v1") do |f|
|
30
|
-
f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
|
31
|
-
f.request :json
|
32
|
-
f.response :json
|
33
|
-
f.response :raise_error
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def validate_json_schema!(schema)
|
38
|
-
return if schema.blank?
|
39
|
-
|
40
|
-
errors = []
|
41
|
-
|
42
|
-
# Check if schema is present
|
43
|
-
if schema.blank?
|
44
|
-
errors << "JSON schema must include a 'schema' property"
|
45
|
-
else
|
46
|
-
# Check root object type
|
47
|
-
if schema[:type] != "object" && !schema.key?(:properties)
|
48
|
-
errors << "Root schema must be of type 'object' with 'properties'"
|
49
|
-
end
|
50
|
-
|
51
|
-
# Check all objects in the schema recursively
|
52
|
-
validate_object_properties(schema, errors)
|
53
|
-
|
54
|
-
# Check properties count (max 100 total)
|
55
|
-
validate_properties_count(schema, errors)
|
56
|
-
|
57
|
-
# Check nesting depth (max 5 levels)
|
58
|
-
validate_nesting_depth(schema, errors)
|
59
|
-
|
60
|
-
# Check for unsupported anyOf at root level
|
61
|
-
if schema[:anyOf].present? && schema[:properties].blank?
|
62
|
-
errors << "Root objects cannot be of type 'anyOf'"
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# Raise error if any validation issues found
|
67
|
-
if errors.any?
|
68
|
-
error_message = "Invalid JSON schema for OpenAI structured outputs: #{errors.join("; ")}\nSchema was: #{schema.inspect}"
|
69
|
-
raise Raif::Errors::OpenAi::JsonSchemaError, error_message
|
70
|
-
else
|
71
|
-
true
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def extract_response_tool_calls(resp)
|
78
|
-
return if resp.dig("choices", 0, "message", "tool_calls").blank?
|
79
|
-
|
80
|
-
resp.dig("choices", 0, "message", "tool_calls").map do |tool_call|
|
81
|
-
{
|
82
|
-
"name" => tool_call["function"]["name"],
|
83
|
-
"arguments" => JSON.parse(tool_call["function"]["arguments"])
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def build_request_parameters(model_completion)
|
89
|
-
formatted_system_prompt = model_completion.system_prompt.to_s.strip
|
90
|
-
|
91
|
-
# If the response format is JSON, we need to include "as json" in the system prompt.
|
92
|
-
# OpenAI requires this and will throw an error if it's not included.
|
93
|
-
if model_completion.response_format_json?
|
94
|
-
# Ensure system prompt ends with a period if not empty
|
95
|
-
if formatted_system_prompt.present? && !formatted_system_prompt.end_with?(".", "?", "!")
|
96
|
-
formatted_system_prompt += "."
|
97
|
-
end
|
98
|
-
formatted_system_prompt += " Return your response as JSON."
|
99
|
-
formatted_system_prompt.strip!
|
100
|
-
end
|
101
|
-
|
102
|
-
messages = model_completion.messages
|
103
|
-
messages_with_system = if !formatted_system_prompt.empty?
|
104
|
-
[{ "role" => "system", "content" => formatted_system_prompt }] + messages
|
105
|
-
else
|
106
|
-
messages
|
107
|
-
end
|
108
|
-
|
109
|
-
parameters = {
|
110
|
-
model: api_name,
|
111
|
-
messages: messages_with_system,
|
112
|
-
temperature: model_completion.temperature.to_f
|
113
|
-
}
|
114
|
-
|
115
|
-
# If the LLM supports native tool use and there are available tools, add them to the parameters
|
116
|
-
if supports_native_tool_use? && model_completion.available_model_tools.any?
|
117
|
-
parameters[:tools] = model_completion.available_model_tools_map.map do |_tool_name, tool|
|
118
|
-
validate_json_schema!(tool.tool_arguments_schema)
|
119
|
-
|
120
|
-
{
|
121
|
-
type: "function",
|
122
|
-
function: {
|
123
|
-
name: tool.tool_name,
|
124
|
-
description: tool.tool_description,
|
125
|
-
parameters: tool.tool_arguments_schema
|
126
|
-
}
|
127
|
-
}
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Add response format if needed
|
132
|
-
response_format = determine_response_format(model_completion)
|
133
|
-
parameters[:response_format] = response_format if response_format
|
134
|
-
|
135
|
-
parameters
|
136
|
-
end
|
137
|
-
|
138
|
-
def determine_response_format(model_completion)
|
139
|
-
# Only configure response format for JSON outputs
|
140
|
-
return unless model_completion.response_format_json?
|
141
|
-
|
142
|
-
if model_completion.json_response_schema.present? && supports_structured_outputs?
|
143
|
-
validate_json_schema!(model_completion.json_response_schema)
|
144
|
-
|
145
|
-
{
|
146
|
-
type: "json_schema",
|
147
|
-
json_schema: {
|
148
|
-
name: "json_response_schema",
|
149
|
-
strict: true,
|
150
|
-
schema: model_completion.json_response_schema
|
151
|
-
}
|
152
|
-
}
|
153
|
-
else
|
154
|
-
# Default JSON mode for OpenAI models that don't support structured outputs or no schema is provided
|
155
|
-
{ type: "json_object" }
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def supports_structured_outputs?
|
160
|
-
# Not all OpenAI models support structured outputs:
|
161
|
-
# https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat#supported-models
|
162
|
-
provider_settings.key?(:supports_structured_outputs) ? provider_settings[:supports_structured_outputs] : true
|
163
|
-
end
|
164
|
-
|
165
|
-
def validate_object_properties(schema, errors)
|
166
|
-
return unless schema.is_a?(Hash)
|
167
|
-
|
168
|
-
# Check if the current schema is an object and validate additionalProperties and required fields
|
169
|
-
if schema[:type] == "object"
|
170
|
-
if schema[:additionalProperties] != false
|
171
|
-
errors << "All objects must have 'additionalProperties' set to false"
|
172
|
-
end
|
173
|
-
|
174
|
-
# Check that all properties are required
|
175
|
-
if schema[:properties].is_a?(Hash) && schema[:properties].any?
|
176
|
-
property_keys = schema[:properties].keys
|
177
|
-
required_fields = schema[:required] || []
|
178
|
-
|
179
|
-
if required_fields.sort != property_keys.map(&:to_s).sort
|
180
|
-
errors << "All object properties must be listed in the 'required' array"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# Check if the current schema is an object and validate additionalProperties
|
186
|
-
if schema[:type] == "object"
|
187
|
-
if schema[:additionalProperties] != false
|
188
|
-
errors << "All objects must have 'additionalProperties' set to false"
|
189
|
-
end
|
190
|
-
|
191
|
-
# Check properties of the object recursively
|
192
|
-
if schema[:properties].is_a?(Hash)
|
193
|
-
schema[:properties].each_value do |property|
|
194
|
-
validate_object_properties(property, errors)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
end
|
198
|
-
|
199
|
-
# Check array items
|
200
|
-
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
201
|
-
validate_object_properties(schema[:items], errors)
|
202
|
-
end
|
203
|
-
|
204
|
-
# Check anyOf
|
205
|
-
if schema[:anyOf].is_a?(Array)
|
206
|
-
schema[:anyOf].each do |option|
|
207
|
-
validate_object_properties(option, errors)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def validate_properties_count(schema, errors, count = 0)
|
213
|
-
return count unless schema.is_a?(Hash)
|
214
|
-
|
215
|
-
if schema[:properties].is_a?(Hash)
|
216
|
-
count += schema[:properties].size
|
217
|
-
|
218
|
-
if count > 100
|
219
|
-
errors << "Schema exceeds maximum of 100 total object properties"
|
220
|
-
return count
|
221
|
-
end
|
222
|
-
|
223
|
-
# Check nested properties
|
224
|
-
schema[:properties].each_value do |property|
|
225
|
-
count = validate_properties_count(property, errors, count)
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
# Check array items
|
230
|
-
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
231
|
-
count = validate_properties_count(schema[:items], errors, count)
|
232
|
-
end
|
233
|
-
|
234
|
-
count
|
235
|
-
end
|
236
|
-
|
237
|
-
def validate_nesting_depth(schema, errors, depth = 1)
|
238
|
-
return unless schema.is_a?(Hash)
|
239
|
-
|
240
|
-
if depth > 5
|
241
|
-
errors << "Schema exceeds maximum nesting depth of 5 levels"
|
242
|
-
return
|
243
|
-
end
|
244
|
-
|
245
|
-
if schema[:properties].is_a?(Hash)
|
246
|
-
schema[:properties].each_value do |property|
|
247
|
-
validate_nesting_depth(property, errors, depth + 1)
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
# Check array items
|
252
|
-
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
253
|
-
validate_nesting_depth(schema[:items], errors, depth + 1)
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|