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,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def validate_json_schema!(schema)
|
7
|
+
return if schema.blank?
|
8
|
+
|
9
|
+
errors = []
|
10
|
+
|
11
|
+
# Check if schema is present
|
12
|
+
if schema.blank?
|
13
|
+
errors << "JSON schema must include a 'schema' property"
|
14
|
+
else
|
15
|
+
# Check root object type
|
16
|
+
if schema[:type] != "object" && !schema.key?(:properties)
|
17
|
+
errors << "Root schema must be of type 'object' with 'properties'"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check all objects in the schema recursively
|
21
|
+
validate_object_properties(schema, errors)
|
22
|
+
|
23
|
+
# Check properties count (max 100 total)
|
24
|
+
validate_properties_count(schema, errors)
|
25
|
+
|
26
|
+
# Check nesting depth (max 5 levels)
|
27
|
+
validate_nesting_depth(schema, errors)
|
28
|
+
|
29
|
+
# Check for unsupported anyOf at root level
|
30
|
+
if schema[:anyOf].present? && schema[:properties].blank?
|
31
|
+
errors << "Root objects cannot be of type 'anyOf'"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Raise error if any validation issues found
|
36
|
+
if errors.any?
|
37
|
+
error_message = "Invalid JSON schema for OpenAI structured outputs: #{errors.join("; ")}\nSchema was: #{schema.inspect}"
|
38
|
+
raise Raif::Errors::OpenAi::JsonSchemaError, error_message
|
39
|
+
else
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def validate_object_properties(schema, errors)
|
47
|
+
return unless schema.is_a?(Hash)
|
48
|
+
|
49
|
+
# Check if the current schema is an object and validate additionalProperties and required fields
|
50
|
+
if schema[:type] == "object"
|
51
|
+
if schema[:additionalProperties] != false
|
52
|
+
errors << "All objects must have 'additionalProperties' set to false"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check that all properties are required
|
56
|
+
if schema[:properties].is_a?(Hash) && schema[:properties].any?
|
57
|
+
property_keys = schema[:properties].keys
|
58
|
+
required_fields = schema[:required] || []
|
59
|
+
|
60
|
+
if required_fields.sort != property_keys.map(&:to_s).sort
|
61
|
+
errors << "All object properties must be listed in the 'required' array"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if the current schema is an object and validate additionalProperties
|
67
|
+
if schema[:type] == "object"
|
68
|
+
if schema[:additionalProperties] != false
|
69
|
+
errors << "All objects must have 'additionalProperties' set to false"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check properties of the object recursively
|
73
|
+
if schema[:properties].is_a?(Hash)
|
74
|
+
schema[:properties].each_value do |property|
|
75
|
+
validate_object_properties(property, errors)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check array items
|
81
|
+
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
82
|
+
validate_object_properties(schema[:items], errors)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check anyOf
|
86
|
+
if schema[:anyOf].is_a?(Array)
|
87
|
+
schema[:anyOf].each do |option|
|
88
|
+
validate_object_properties(option, errors)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_properties_count(schema, errors, count = 0)
|
94
|
+
return count unless schema.is_a?(Hash)
|
95
|
+
|
96
|
+
if schema[:properties].is_a?(Hash)
|
97
|
+
count += schema[:properties].size
|
98
|
+
|
99
|
+
if count > 100
|
100
|
+
errors << "Schema exceeds maximum of 100 total object properties"
|
101
|
+
return count
|
102
|
+
end
|
103
|
+
|
104
|
+
# Check nested properties
|
105
|
+
schema[:properties].each_value do |property|
|
106
|
+
count = validate_properties_count(property, errors, count)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check array items
|
111
|
+
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
112
|
+
count = validate_properties_count(schema[:items], errors, count)
|
113
|
+
end
|
114
|
+
|
115
|
+
count
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_nesting_depth(schema, errors, depth = 1)
|
119
|
+
return unless schema.is_a?(Hash)
|
120
|
+
|
121
|
+
if depth > 5
|
122
|
+
errors << "Schema exceeds maximum nesting depth of 5 levels"
|
123
|
+
return
|
124
|
+
end
|
125
|
+
|
126
|
+
if schema[:properties].is_a?(Hash)
|
127
|
+
schema[:properties].each_value do |property|
|
128
|
+
validate_nesting_depth(property, errors, depth + 1)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Check array items
|
133
|
+
if schema[:type] == "array" && schema[:items].is_a?(Hash)
|
134
|
+
validate_nesting_depth(schema[:items], errors, depth + 1)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def format_model_image_input_message(image_input)
|
7
|
+
if image_input.source_type == :url
|
8
|
+
{
|
9
|
+
"type" => "image_url",
|
10
|
+
"image_url" => { "url" => image_input.url }
|
11
|
+
}
|
12
|
+
elsif image_input.source_type == :file_content
|
13
|
+
{
|
14
|
+
"type" => "image_url",
|
15
|
+
"image_url" => {
|
16
|
+
"url" => "data:#{image_input.content_type};base64,#{image_input.base64_data}"
|
17
|
+
}
|
18
|
+
}
|
19
|
+
else
|
20
|
+
raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_model_file_input_message(file_input)
|
25
|
+
if file_input.source_type == :url
|
26
|
+
raise Raif::Errors::UnsupportedFeatureError, "#{self.class.name} does not support providing a file by URL"
|
27
|
+
elsif file_input.source_type == :file_content
|
28
|
+
file_params = {
|
29
|
+
"filename" => file_input.filename,
|
30
|
+
"file_data" => "data:#{file_input.content_type};base64,#{file_input.base64_data}"
|
31
|
+
}.compact
|
32
|
+
|
33
|
+
{
|
34
|
+
"type" => "file",
|
35
|
+
"file" => file_params
|
36
|
+
}
|
37
|
+
else
|
38
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
model_completion.available_model_tools_map.map do |_tool_name, tool|
|
8
|
+
if tool.provider_managed?
|
9
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
10
|
+
"Raif doesn't yet support provider-managed tools for the OpenAI Completions API. Consider using the OpenAI Responses API instead."
|
11
|
+
else
|
12
|
+
# It's a developer-managed tool
|
13
|
+
validate_json_schema!(tool.tool_arguments_schema)
|
14
|
+
|
15
|
+
{
|
16
|
+
type: "function",
|
17
|
+
function: {
|
18
|
+
name: tool.tool_name,
|
19
|
+
description: tool.tool_description,
|
20
|
+
parameters: tool.tool_arguments_schema
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::OpenAiResponses::MessageFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def format_string_message(content, role: nil)
|
7
|
+
if role == "assistant"
|
8
|
+
{ "type" => "output_text", "text" => content }
|
9
|
+
else
|
10
|
+
{ "type" => "input_text", "text" => content }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_model_image_input_message(image_input)
|
15
|
+
if image_input.source_type == :url
|
16
|
+
{
|
17
|
+
"type" => "input_image",
|
18
|
+
"image_url" => image_input.url
|
19
|
+
}
|
20
|
+
elsif image_input.source_type == :file_content
|
21
|
+
{
|
22
|
+
"type" => "input_image",
|
23
|
+
"image_url" => "data:#{image_input.content_type};base64,#{image_input.base64_data}"
|
24
|
+
}
|
25
|
+
else
|
26
|
+
raise Raif::Errors::InvalidModelImageInputError, "Invalid model image input source type: #{image_input.source_type}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_model_file_input_message(file_input)
|
31
|
+
if file_input.source_type == :url
|
32
|
+
raise Raif::Errors::UnsupportedFeatureError, "#{self.class.name} does not support providing a file by URL"
|
33
|
+
elsif file_input.source_type == :file_content
|
34
|
+
{
|
35
|
+
"type" => "input_file",
|
36
|
+
"filename" => file_input.filename,
|
37
|
+
"file_data" => "data:#{file_input.content_type};base64,#{file_input.base64_data}"
|
38
|
+
}
|
39
|
+
else
|
40
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid model image input source type: #{file_input.source_type}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif::Concerns::Llms::OpenAiResponses::ToolFormatting
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def build_tools_parameter(model_completion)
|
7
|
+
model_completion.available_model_tools_map.map do |_tool_name, tool|
|
8
|
+
if tool.provider_managed?
|
9
|
+
format_provider_managed_tool(tool)
|
10
|
+
else
|
11
|
+
# It's a developer-managed tool
|
12
|
+
validate_json_schema!(tool.tool_arguments_schema)
|
13
|
+
|
14
|
+
{
|
15
|
+
type: "function",
|
16
|
+
name: tool.tool_name,
|
17
|
+
description: tool.tool_description,
|
18
|
+
parameters: tool.tool_arguments_schema
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_provider_managed_tool(tool)
|
25
|
+
validate_provider_managed_tool_support!(tool)
|
26
|
+
|
27
|
+
case tool.name
|
28
|
+
when "Raif::ModelTools::ProviderManaged::WebSearch"
|
29
|
+
{ type: "web_search_preview" }
|
30
|
+
when "Raif::ModelTools::ProviderManaged::CodeExecution"
|
31
|
+
{
|
32
|
+
type: "code_interpreter",
|
33
|
+
container: { "type": "auto" }
|
34
|
+
}
|
35
|
+
when "Raif::ModelTools::ProviderManaged::ImageGeneration"
|
36
|
+
{ type: "image_generation" }
|
37
|
+
else
|
38
|
+
raise Raif::Errors::UnsupportedFeatureError,
|
39
|
+
"Invalid provider-managed tool: #{tool.name} for #{key}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -4,6 +4,7 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
4
4
|
include Raif::Concerns::HasLlm
|
5
5
|
include Raif::Concerns::HasRequestedLanguage
|
6
6
|
include Raif::Concerns::HasAvailableModelTools
|
7
|
+
include Raif::Concerns::LlmResponseParsing
|
7
8
|
|
8
9
|
belongs_to :creator, polymorphic: true
|
9
10
|
|
@@ -15,17 +16,17 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
15
16
|
after_initialize -> { self.available_user_tools ||= [] }
|
16
17
|
|
17
18
|
before_validation ->{ self.type ||= "Raif::Conversation" }, on: :create
|
18
|
-
before_validation -> { self.system_prompt ||= build_system_prompt }, on: :create
|
19
19
|
|
20
20
|
def build_system_prompt
|
21
|
-
<<~PROMPT
|
21
|
+
<<~PROMPT.strip
|
22
22
|
#{system_prompt_intro}
|
23
23
|
#{system_prompt_language_preference}
|
24
24
|
PROMPT
|
25
25
|
end
|
26
26
|
|
27
27
|
def system_prompt_intro
|
28
|
-
Raif.config.conversation_system_prompt_intro
|
28
|
+
sp = Raif.config.conversation_system_prompt_intro
|
29
|
+
sp.respond_to?(:call) ? sp.call(self) : sp
|
29
30
|
end
|
30
31
|
|
31
32
|
# i18n-tasks-use t('raif.conversation.initial_chat_message')
|
@@ -33,21 +34,41 @@ class Raif::Conversation < Raif::ApplicationRecord
|
|
33
34
|
I18n.t("#{self.class.name.underscore.gsub("/", ".")}.initial_chat_message")
|
34
35
|
end
|
35
36
|
|
36
|
-
def prompt_model_for_entry_response(entry
|
37
|
+
def prompt_model_for_entry_response(entry:, &block)
|
38
|
+
update(system_prompt: build_system_prompt)
|
39
|
+
|
37
40
|
llm.chat(
|
38
41
|
messages: llm_messages,
|
39
42
|
source: entry,
|
40
|
-
response_format:
|
43
|
+
response_format: response_format.to_sym,
|
41
44
|
system_prompt: system_prompt,
|
42
|
-
available_model_tools: available_model_tools
|
45
|
+
available_model_tools: available_model_tools,
|
46
|
+
&block
|
43
47
|
)
|
48
|
+
rescue StandardError => e
|
49
|
+
Rails.logger.error("Error processing conversation entry ##{entry.id}. #{e.message}")
|
50
|
+
entry.failed!
|
51
|
+
|
52
|
+
if defined?(Airbrake)
|
53
|
+
notice = Airbrake.build_notice(e)
|
54
|
+
notice[:context][:component] = "raif_conversation"
|
55
|
+
notice[:context][:action] = "prompt_model_for_entry_response"
|
56
|
+
|
57
|
+
Airbrake.notify(notice)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_model_response_message(message:, entry:)
|
62
|
+
# no-op by default.
|
63
|
+
# Override in subclasses for type-specific processing of the model response message
|
64
|
+
message
|
44
65
|
end
|
45
66
|
|
46
67
|
def llm_messages
|
47
68
|
messages = []
|
48
69
|
|
49
70
|
entries.oldest_first.includes(:raif_model_tool_invocations).each do |entry|
|
50
|
-
messages << { "role" => "user", "content" => entry.user_message }
|
71
|
+
messages << { "role" => "user", "content" => entry.user_message } unless entry.user_message.blank?
|
51
72
|
next unless entry.completed?
|
52
73
|
|
53
74
|
messages << { "role" => "assistant", "content" => entry.model_response_message } unless entry.model_response_message.blank?
|
@@ -16,7 +16,7 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
|
|
16
16
|
has_one :raif_model_completion, as: :source, dependent: :destroy, class_name: "Raif::ModelCompletion"
|
17
17
|
|
18
18
|
delegate :available_model_tools, to: :raif_conversation
|
19
|
-
delegate :system_prompt, :llm_model_key, to: :raif_model_completion, allow_nil: true
|
19
|
+
delegate :system_prompt, :llm_model_key, :citations, to: :raif_model_completion, allow_nil: true
|
20
20
|
delegate :json_response_schema, to: :class
|
21
21
|
|
22
22
|
accepts_nested_attributes_for :raif_user_tool_invocation
|
@@ -33,11 +33,12 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
|
|
33
33
|
def add_user_tool_invocation_to_user_message
|
34
34
|
return unless raif_user_tool_invocation.present?
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
separator = response_format == "html" ? "<br>" : "\n\n"
|
37
|
+
self.user_message = [user_message, raif_user_tool_invocation.as_user_message].join(separator)
|
38
|
+
end
|
38
39
|
|
39
|
-
|
40
|
-
|
40
|
+
def response_format
|
41
|
+
raif_model_completion&.response_format.presence || raif_conversation.response_format
|
41
42
|
end
|
42
43
|
|
43
44
|
def generating_response?
|
@@ -45,10 +46,27 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def process_entry!
|
48
|
-
self.
|
49
|
+
self.model_response_message = ""
|
50
|
+
|
51
|
+
self.raif_model_completion = raif_conversation.prompt_model_for_entry_response(entry: self) do |model_completion, _delta, _sse_event|
|
52
|
+
self.raw_response = model_completion.raw_response
|
53
|
+
self.model_response_message = raif_conversation.process_model_response_message(
|
54
|
+
message: model_completion.parsed_response(force_reparse: true),
|
55
|
+
entry: self
|
56
|
+
)
|
57
|
+
|
58
|
+
update_columns(
|
59
|
+
model_response_message: model_response_message,
|
60
|
+
raw_response: raw_response,
|
61
|
+
updated_at: Time.current
|
62
|
+
)
|
63
|
+
|
64
|
+
broadcast_replace_to raif_conversation
|
65
|
+
end
|
49
66
|
|
50
67
|
if raif_model_completion.parsed_response.present? || raif_model_completion.response_tool_calls.present?
|
51
68
|
extract_message_and_invoke_tools!
|
69
|
+
create_entry_for_observation! if triggers_observation_to_model?
|
52
70
|
else
|
53
71
|
logger.error "Error processing conversation entry ##{id}. No model response found."
|
54
72
|
failed!
|
@@ -57,18 +75,32 @@ class Raif::ConversationEntry < Raif::ApplicationRecord
|
|
57
75
|
self
|
58
76
|
end
|
59
77
|
|
78
|
+
def triggers_observation_to_model?
|
79
|
+
return false unless completed?
|
80
|
+
|
81
|
+
raif_model_tool_invocations.any?(&:triggers_observation_to_model?)
|
82
|
+
end
|
83
|
+
|
84
|
+
def create_entry_for_observation!
|
85
|
+
follow_up_entry = raif_conversation.entries.create!(creator: creator)
|
86
|
+
Raif::ConversationEntryJob.perform_later(conversation_entry: follow_up_entry)
|
87
|
+
follow_up_entry.broadcast_append_to raif_conversation, target: dom_id(raif_conversation, :entries)
|
88
|
+
end
|
89
|
+
|
60
90
|
private
|
61
91
|
|
62
92
|
def extract_message_and_invoke_tools!
|
63
93
|
transaction do
|
64
94
|
self.raw_response = raif_model_completion.raw_response
|
65
|
-
self.model_response_message = raif_model_completion.parsed_response
|
95
|
+
self.model_response_message = raif_conversation.process_model_response_message(message: raif_model_completion.parsed_response, entry: self)
|
66
96
|
save!
|
67
97
|
|
68
98
|
if raif_model_completion.response_tool_calls.present?
|
69
99
|
raif_model_completion.response_tool_calls.each do |tool_call|
|
70
100
|
tool_klass = available_model_tools_map[tool_call["name"]]
|
71
|
-
|
101
|
+
next if tool_klass.nil?
|
102
|
+
|
103
|
+
tool_klass.invoke_tool(tool_arguments: tool_call["arguments"], source: self)
|
72
104
|
end
|
73
105
|
end
|
74
106
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::EmbeddingModel
|
4
|
+
include ActiveModel::Model
|
5
|
+
|
6
|
+
attr_accessor :key,
|
7
|
+
:api_name,
|
8
|
+
:input_token_cost,
|
9
|
+
:default_output_vector_size
|
10
|
+
|
11
|
+
validates :default_output_vector_size, presence: true, numericality: { only_integer: true, greater_than: 0 }
|
12
|
+
validates :api_name, presence: true
|
13
|
+
validates :key, presence: true
|
14
|
+
|
15
|
+
def name
|
16
|
+
I18n.t("raif.embedding_model_names.#{key}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_embedding!(input, dimensions: nil)
|
20
|
+
raise NotImplementedError, "#{self.class.name} must implement #generate_embedding!"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::EmbeddingModels::Bedrock < Raif::EmbeddingModel
|
4
|
+
|
5
|
+
def generate_embedding!(input, dimensions: nil)
|
6
|
+
unless input.is_a?(String)
|
7
|
+
raise ArgumentError, "Raif::EmbeddingModels::Bedrock#generate_embedding! input must be a string"
|
8
|
+
end
|
9
|
+
|
10
|
+
params = build_request_parameters(input, dimensions:)
|
11
|
+
response = bedrock_client.invoke_model(params)
|
12
|
+
|
13
|
+
response_body = JSON.parse(response.body.read)
|
14
|
+
response_body["embedding"]
|
15
|
+
rescue Aws::BedrockRuntime::Errors::ServiceError => e
|
16
|
+
raise "Bedrock API error: #{e.message}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def build_request_parameters(input, dimensions: nil)
|
22
|
+
body_params = { inputText: input }
|
23
|
+
body_params[:dimensions] = dimensions if dimensions.present?
|
24
|
+
|
25
|
+
{
|
26
|
+
model_id: api_name,
|
27
|
+
body: body_params.to_json
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def bedrock_client
|
32
|
+
@bedrock_client ||= Aws::BedrockRuntime::Client.new(region: Raif.config.aws_bedrock_region)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::EmbeddingModels::OpenAi < Raif::EmbeddingModel
|
4
|
+
def generate_embedding!(input, dimensions: nil)
|
5
|
+
unless input.is_a?(String) || input.is_a?(Array)
|
6
|
+
raise ArgumentError, "Raif::EmbeddingModels::OpenAi#generate_embedding! input must be a string or an array of strings"
|
7
|
+
end
|
8
|
+
|
9
|
+
response = connection.post("embeddings") do |req|
|
10
|
+
req.body = build_request_parameters(input, dimensions:)
|
11
|
+
end
|
12
|
+
|
13
|
+
if input.is_a?(String)
|
14
|
+
response.body["data"][0]["embedding"]
|
15
|
+
else
|
16
|
+
response.body["data"].map{|v| v["embedding"] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def build_request_parameters(input, dimensions: nil)
|
23
|
+
params = {
|
24
|
+
model: api_name,
|
25
|
+
input: input
|
26
|
+
}
|
27
|
+
|
28
|
+
params[:dimensions] = dimensions if dimensions.present?
|
29
|
+
params
|
30
|
+
end
|
31
|
+
|
32
|
+
def connection
|
33
|
+
@connection ||= Faraday.new(url: "https://api.openai.com/v1") do |f|
|
34
|
+
f.headers["Authorization"] = "Bearer #{Raif.config.open_ai_api_key}"
|
35
|
+
f.request :json
|
36
|
+
f.response :json
|
37
|
+
f.response :raise_error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|