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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +150 -4
  3. data/app/assets/builds/raif.css +26 -1
  4. data/app/assets/stylesheets/raif/loader.scss +27 -1
  5. data/app/models/raif/concerns/llm_response_parsing.rb +22 -16
  6. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
  7. data/app/models/raif/concerns/llms/{bedrock_claude → bedrock}/message_formatting.rb +4 -4
  8. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
  9. data/app/models/raif/concerns/llms/message_formatting.rb +7 -6
  10. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
  11. data/app/models/raif/concerns/llms/{open_ai → open_ai_completions}/message_formatting.rb +1 -1
  12. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
  13. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
  14. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
  15. data/app/models/raif/conversation.rb +17 -4
  16. data/app/models/raif/conversation_entry.rb +18 -2
  17. data/app/models/raif/embedding_models/{bedrock_titan.rb → bedrock.rb} +2 -2
  18. data/app/models/raif/llm.rb +73 -7
  19. data/app/models/raif/llms/anthropic.rb +56 -36
  20. data/app/models/raif/llms/{bedrock_claude.rb → bedrock.rb} +62 -45
  21. data/app/models/raif/llms/open_ai_base.rb +66 -0
  22. data/app/models/raif/llms/open_ai_completions.rb +100 -0
  23. data/app/models/raif/llms/open_ai_responses.rb +144 -0
  24. data/app/models/raif/llms/open_router.rb +44 -44
  25. data/app/models/raif/model_completion.rb +2 -0
  26. data/app/models/raif/model_tool.rb +4 -0
  27. data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
  28. data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
  29. data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
  30. data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
  31. data/app/models/raif/streaming_responses/anthropic.rb +63 -0
  32. data/app/models/raif/streaming_responses/bedrock.rb +89 -0
  33. data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
  34. data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
  35. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
  36. data/app/views/raif/admin/conversations/show.html.erb +1 -1
  37. data/app/views/raif/admin/model_completions/_model_completion.html.erb +7 -0
  38. data/app/views/raif/admin/model_completions/index.html.erb +1 -0
  39. data/app/views/raif/admin/model_completions/show.html.erb +28 -0
  40. data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
  41. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +5 -1
  42. data/app/views/raif/conversation_entries/_message.html.erb +4 -0
  43. data/config/locales/admin.en.yml +2 -0
  44. data/config/locales/en.yml +24 -0
  45. data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
  46. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +1 -1
  47. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +1 -1
  48. data/db/migrate/20250424232946_add_created_at_indexes.rb +1 -1
  49. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +1 -1
  50. data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
  51. data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
  52. data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
  53. data/lib/generators/raif/conversation/templates/conversation.rb.tt +3 -3
  54. data/lib/generators/raif/install/templates/initializer.rb +14 -2
  55. data/lib/raif/configuration.rb +27 -5
  56. data/lib/raif/embedding_model_registry.rb +1 -1
  57. data/lib/raif/engine.rb +25 -9
  58. data/lib/raif/errors/streaming_error.rb +18 -0
  59. data/lib/raif/errors.rb +1 -0
  60. data/lib/raif/llm_registry.rb +169 -47
  61. data/lib/raif/migration_checker.rb +74 -0
  62. data/lib/raif/utils/html_fragment_processor.rb +170 -0
  63. data/lib/raif/utils.rb +1 -0
  64. data/lib/raif/version.rb +1 -1
  65. data/lib/raif.rb +2 -0
  66. data/spec/support/complex_test_tool.rb +65 -0
  67. data/spec/support/rspec_helpers.rb +66 -0
  68. data/spec/support/test_conversation.rb +18 -0
  69. data/spec/support/test_embedding_model.rb +27 -0
  70. data/spec/support/test_llm.rb +22 -0
  71. data/spec/support/test_model_tool.rb +32 -0
  72. data/spec/support/test_task.rb +45 -0
  73. metadata +52 -8
  74. 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