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,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::Llms::OpenAiResponses < Raif::Llms::OpenAiBase
|
4
|
+
include Raif::Concerns::Llms::OpenAiResponses::MessageFormatting
|
5
|
+
include Raif::Concerns::Llms::OpenAiResponses::ToolFormatting
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def api_path
|
10
|
+
"responses"
|
11
|
+
end
|
12
|
+
|
13
|
+
def streaming_response_type
|
14
|
+
Raif::StreamingResponses::OpenAiResponses
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_model_completion(model_completion, response_json)
|
18
|
+
model_completion.update!(
|
19
|
+
response_id: response_json["id"],
|
20
|
+
response_tool_calls: extract_response_tool_calls(response_json),
|
21
|
+
raw_response: extract_raw_response(response_json),
|
22
|
+
response_array: response_json["output"],
|
23
|
+
citations: extract_citations(response_json),
|
24
|
+
completion_tokens: response_json.dig("usage", "output_tokens"),
|
25
|
+
prompt_tokens: response_json.dig("usage", "input_tokens"),
|
26
|
+
total_tokens: response_json.dig("usage", "total_tokens")
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_response_tool_calls(resp)
|
31
|
+
return if resp["output"].blank?
|
32
|
+
|
33
|
+
tool_calls = []
|
34
|
+
resp["output"].each do |output_item|
|
35
|
+
next unless output_item["type"] == "function_call"
|
36
|
+
|
37
|
+
tool_calls << {
|
38
|
+
"name" => output_item["name"],
|
39
|
+
"arguments" => JSON.parse(output_item["arguments"])
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
tool_calls.any? ? tool_calls : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def extract_raw_response(resp)
|
47
|
+
text_outputs = []
|
48
|
+
|
49
|
+
output_messages = resp["output"]&.select{ |output_item| output_item["type"] == "message" }
|
50
|
+
output_messages&.each do |output_message|
|
51
|
+
output_message["content"].each do |content_item|
|
52
|
+
text_outputs << content_item["text"] if content_item["type"] == "output_text"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
text_outputs.join("\n").presence
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_citations(resp)
|
60
|
+
return [] if resp["output"].blank?
|
61
|
+
|
62
|
+
citations = []
|
63
|
+
|
64
|
+
# Look through output messages for citations in annotations
|
65
|
+
output_messages = resp["output"].select{|output_item| output_item["type"] == "message" }
|
66
|
+
output_messages.each do |output_message|
|
67
|
+
next unless output_message["content"].present?
|
68
|
+
|
69
|
+
output_message["content"].each do |content_item|
|
70
|
+
next unless content_item["type"] == "output_text" && content_item["annotations"].present?
|
71
|
+
|
72
|
+
content_item["annotations"].each do |annotation|
|
73
|
+
next unless annotation["type"] == "url_citation"
|
74
|
+
|
75
|
+
citations << {
|
76
|
+
"url" => Raif::Utils::HtmlFragmentProcessor.strip_tracking_parameters(annotation["url"]),
|
77
|
+
"title" => annotation["title"]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
citations.uniq{|citation| citation["url"] }
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_request_parameters(model_completion)
|
87
|
+
parameters = {
|
88
|
+
model: api_name,
|
89
|
+
input: model_completion.messages,
|
90
|
+
}
|
91
|
+
|
92
|
+
if supports_temperature?
|
93
|
+
parameters[:temperature] = model_completion.temperature.to_f
|
94
|
+
end
|
95
|
+
|
96
|
+
parameters[:stream] = true if model_completion.stream_response?
|
97
|
+
|
98
|
+
# Add instructions (system prompt) if present
|
99
|
+
formatted_system_prompt = format_system_prompt(model_completion)
|
100
|
+
if formatted_system_prompt.present?
|
101
|
+
parameters[:instructions] = formatted_system_prompt
|
102
|
+
end
|
103
|
+
|
104
|
+
# Add max_output_tokens if specified
|
105
|
+
if model_completion.max_completion_tokens.present?
|
106
|
+
parameters[:max_output_tokens] = model_completion.max_completion_tokens
|
107
|
+
end
|
108
|
+
|
109
|
+
# If the LLM supports native tool use and there are available tools, add them to the parameters
|
110
|
+
if supports_native_tool_use?
|
111
|
+
tools = build_tools_parameter(model_completion)
|
112
|
+
parameters[:tools] = tools unless tools.blank?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Add response format if needed. Default will be { "type": "text" }
|
116
|
+
response_format = determine_response_format(model_completion)
|
117
|
+
if response_format.present?
|
118
|
+
parameters[:text] = { format: response_format }
|
119
|
+
model_completion.response_format_parameter = response_format[:type]
|
120
|
+
end
|
121
|
+
|
122
|
+
parameters
|
123
|
+
end
|
124
|
+
|
125
|
+
def determine_response_format(model_completion)
|
126
|
+
# Only configure response format for JSON outputs
|
127
|
+
return unless model_completion.response_format_json?
|
128
|
+
|
129
|
+
if model_completion.json_response_schema.present? && supports_structured_outputs?
|
130
|
+
validate_json_schema!(model_completion.json_response_schema)
|
131
|
+
|
132
|
+
{
|
133
|
+
type: "json_schema",
|
134
|
+
name: "json_response_schema",
|
135
|
+
strict: true,
|
136
|
+
schema: model_completion.json_response_schema
|
137
|
+
}
|
138
|
+
else
|
139
|
+
# Default JSON mode for OpenAI models that don't support structured outputs or no schema is provided
|
140
|
+
{ type: "json_object" }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::Llms::OpenRouter < Raif::Llm
|
4
|
+
include Raif::Concerns::Llms::OpenAiCompletions::MessageFormatting
|
5
|
+
include Raif::Concerns::Llms::OpenAiCompletions::ToolFormatting
|
6
|
+
include Raif::Concerns::Llms::OpenAi::JsonSchemaValidation
|
7
|
+
|
8
|
+
def perform_model_completion!(model_completion, &block)
|
9
|
+
model_completion.temperature ||= default_temperature
|
10
|
+
parameters = build_request_parameters(model_completion)
|
11
|
+
response = connection.post("chat/completions") do |req|
|
12
|
+
req.body = parameters
|
13
|
+
req.options.on_data = streaming_chunk_handler(model_completion, &block) if model_completion.stream_response?
|
14
|
+
end
|
15
|
+
|
16
|
+
unless model_completion.stream_response?
|
17
|
+
update_model_completion(model_completion, response.body)
|
18
|
+
end
|
19
|
+
|
20
|
+
model_completion
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def connection
|
26
|
+
@connection ||= Faraday.new(url: "https://openrouter.ai/api/v1") do |f|
|
27
|
+
f.headers["Authorization"] = "Bearer #{Raif.config.open_router_api_key}"
|
28
|
+
f.headers["HTTP-Referer"] = Raif.config.open_router_site_url if Raif.config.open_router_site_url.present?
|
29
|
+
f.headers["X-Title"] = Raif.config.open_router_app_name if Raif.config.open_router_app_name.present?
|
30
|
+
f.request :json
|
31
|
+
f.response :json
|
32
|
+
f.response :raise_error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def streaming_response_type
|
37
|
+
Raif::StreamingResponses::OpenAiCompletions
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_model_completion(model_completion, response_json)
|
41
|
+
model_completion.update!(
|
42
|
+
response_tool_calls: extract_response_tool_calls(response_json),
|
43
|
+
raw_response: response_json.dig("choices", 0, "message", "content"),
|
44
|
+
response_array: response_json["choices"],
|
45
|
+
completion_tokens: response_json.dig("usage", "completion_tokens"),
|
46
|
+
prompt_tokens: response_json.dig("usage", "prompt_tokens"),
|
47
|
+
total_tokens: response_json.dig("usage", "total_tokens")
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_request_parameters(model_completion)
|
52
|
+
params = {
|
53
|
+
model: model_completion.model_api_name,
|
54
|
+
messages: model_completion.messages,
|
55
|
+
temperature: model_completion.temperature.to_f,
|
56
|
+
max_tokens: model_completion.max_completion_tokens || default_max_completion_tokens,
|
57
|
+
}
|
58
|
+
|
59
|
+
# Add system message to the messages array if present
|
60
|
+
if model_completion.system_prompt.present?
|
61
|
+
params[:messages].unshift({ "role" => "system", "content" => model_completion.system_prompt })
|
62
|
+
end
|
63
|
+
|
64
|
+
if supports_native_tool_use?
|
65
|
+
tools = build_tools_parameter(model_completion)
|
66
|
+
params[:tools] = tools unless tools.blank?
|
67
|
+
end
|
68
|
+
|
69
|
+
if model_completion.stream_response?
|
70
|
+
# Ask for usage stats in the last chunk
|
71
|
+
params[:stream] = true
|
72
|
+
params[:stream_options] = { include_usage: true }
|
73
|
+
end
|
74
|
+
|
75
|
+
params
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_response_tool_calls(resp)
|
79
|
+
return if resp.dig("choices", 0, "message", "tool_calls").blank?
|
80
|
+
|
81
|
+
resp.dig("choices", 0, "message", "tool_calls").map do |tool_call|
|
82
|
+
{
|
83
|
+
"name" => tool_call["function"]["name"],
|
84
|
+
"arguments" => JSON.parse(tool_call["function"]["arguments"])
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -12,17 +12,38 @@ class Raif::ModelCompletion < Raif::ApplicationRecord
|
|
12
12
|
delegate :json_response_schema, to: :source, allow_nil: true
|
13
13
|
|
14
14
|
before_save :set_total_tokens
|
15
|
+
before_save :calculate_costs
|
15
16
|
|
16
17
|
after_initialize -> { self.messages ||= [] }
|
17
18
|
after_initialize -> { self.available_model_tools ||= [] }
|
19
|
+
after_initialize -> { self.response_array ||= [] }
|
20
|
+
after_initialize -> { self.citations ||= [] }
|
18
21
|
|
19
22
|
def json_response_schema
|
20
23
|
source.json_response_schema if source&.respond_to?(:json_response_schema)
|
21
24
|
end
|
22
25
|
|
23
|
-
protected
|
24
|
-
|
25
26
|
def set_total_tokens
|
26
27
|
self.total_tokens ||= completion_tokens.present? && prompt_tokens.present? ? completion_tokens + prompt_tokens : nil
|
27
28
|
end
|
29
|
+
|
30
|
+
def calculate_costs
|
31
|
+
if prompt_tokens.present? && llm_config[:input_token_cost].present?
|
32
|
+
self.prompt_token_cost = llm_config[:input_token_cost] * prompt_tokens
|
33
|
+
end
|
34
|
+
|
35
|
+
if completion_tokens.present? && llm_config[:output_token_cost].present?
|
36
|
+
self.output_token_cost = llm_config[:output_token_cost] * completion_tokens
|
37
|
+
end
|
38
|
+
|
39
|
+
if prompt_token_cost.present? || output_token_cost.present?
|
40
|
+
self.total_cost = (prompt_token_cost || 0) + (output_token_cost || 0)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def llm_config
|
47
|
+
@llm_config ||= Raif.llm_config(llm_model_key.to_sym)
|
48
|
+
end
|
28
49
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelFileInput
|
4
|
+
include ActiveModel::Model
|
5
|
+
|
6
|
+
attr_accessor :input, :url, :base64_data, :filename, :content_type, :source_type
|
7
|
+
|
8
|
+
validates :url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: "is not a valid URL" }, allow_nil: true
|
9
|
+
validates :base64_data, presence: { message: "could not be read from input" }, if: -> { input.present? }
|
10
|
+
validates :content_type, presence: { message: "could not be determined" }, if: -> { input.present? }
|
11
|
+
|
12
|
+
def initialize(input: nil, url: nil)
|
13
|
+
raise ArgumentError, "You must provide either an input or a URL" if input.blank? && url.blank?
|
14
|
+
raise ArgumentError, "Provide either input or URL, not both" if input.present? && url.present?
|
15
|
+
|
16
|
+
super(input: input, url: url)
|
17
|
+
|
18
|
+
if url.present?
|
19
|
+
@source_type = :url
|
20
|
+
elsif input.present?
|
21
|
+
@source_type = :file_content
|
22
|
+
process_input!
|
23
|
+
end
|
24
|
+
|
25
|
+
unless valid?
|
26
|
+
raise Raif::Errors::InvalidModelFileInputError, errors.full_messages.join(", ")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<#{self.class.name} input=#{input.inspect} url=#{url.inspect} base64_data=#{base64_data&.truncate(20)} filename=#{filename.inspect} content_type=#{content_type.inspect} source_type=#{source_type.inspect}>" # rubocop:disable Layout/LineLength
|
32
|
+
end
|
33
|
+
|
34
|
+
def file_bytes
|
35
|
+
Base64.strict_decode64(base64_data)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def process_input!
|
41
|
+
if input_is_active_storage_blob?
|
42
|
+
process_active_storage_blob(input)
|
43
|
+
return
|
44
|
+
elsif input_has_active_storage_blob?
|
45
|
+
process_active_storage_blob(input.blob)
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
case input
|
50
|
+
when String
|
51
|
+
process_string_input
|
52
|
+
when Pathname
|
53
|
+
process_from_path
|
54
|
+
when File, Tempfile, IO, StringIO
|
55
|
+
read_from_io
|
56
|
+
else
|
57
|
+
raise Raif::Errors::InvalidModelFileInputError, "Invalid input type for #{self.class.name} (got #{input.class})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def process_string_input
|
62
|
+
if File.exist?(input)
|
63
|
+
@input = Pathname.new(input)
|
64
|
+
process_from_path
|
65
|
+
else
|
66
|
+
raise Raif::Errors::InvalidModelFileInputError, "File does not exist: #{input}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_active_storage_blob(blob)
|
71
|
+
@filename = blob.filename.to_s
|
72
|
+
@content_type = blob.content_type
|
73
|
+
@base64_data = Base64.strict_encode64(blob.download)
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_from_path
|
77
|
+
@filename = input.basename.to_s
|
78
|
+
@content_type = Marcel::MimeType.for(input)
|
79
|
+
@base64_data = Base64.strict_encode64(input.binread)
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_from_io
|
83
|
+
@filename = input.respond_to?(:path) ? File.basename(input.path) : nil
|
84
|
+
@content_type = Marcel::MimeType.for(input)
|
85
|
+
try_rewind
|
86
|
+
@base64_data = Base64.strict_encode64(input.read)
|
87
|
+
end
|
88
|
+
|
89
|
+
def try_rewind
|
90
|
+
input.rewind if input.respond_to?(:rewind)
|
91
|
+
rescue IOError => e
|
92
|
+
logger.error "Failed to rewind IO: #{e.message}"
|
93
|
+
logger.error e.backtrace.join("\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
def input_looks_like_base64?
|
97
|
+
input.match?(%r{\A[a-zA-Z0-9+/\n\r]+={0,2}\z})
|
98
|
+
end
|
99
|
+
|
100
|
+
def input_has_active_storage_blob?
|
101
|
+
return false unless defined?(ActiveStorage)
|
102
|
+
return false unless input.respond_to?(:blob)
|
103
|
+
|
104
|
+
input.blob.is_a?(ActiveStorage::Blob)
|
105
|
+
end
|
106
|
+
|
107
|
+
def input_is_active_storage_blob?
|
108
|
+
return false unless defined?(ActiveStorage)
|
109
|
+
|
110
|
+
input.is_a?(ActiveStorage::Blob)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -1,69 +1,99 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTool
|
4
|
+
include Raif::Concerns::JsonSchemaDefinition
|
4
5
|
|
5
|
-
delegate :tool_name, :tool_description, :tool_arguments_schema, to: :class
|
6
|
-
|
7
|
-
# The description of the tool that will be provided to the model
|
8
|
-
# when giving it a list of available tools.
|
9
|
-
def self.description_for_llm
|
10
|
-
<<~DESCRIPTION
|
11
|
-
Name: #{tool_name}
|
12
|
-
Description: #{tool_description}
|
13
|
-
Arguments Schema:
|
14
|
-
#{JSON.pretty_generate(tool_arguments_schema)}
|
15
|
-
Example Usage:
|
16
|
-
#{JSON.pretty_generate(example_model_invocation)}
|
17
|
-
DESCRIPTION
|
18
|
-
end
|
6
|
+
delegate :tool_name, :tool_description, :tool_arguments_schema, :example_model_invocation, to: :class
|
19
7
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
8
|
+
class << self
|
9
|
+
# The description of the tool that will be provided to the model
|
10
|
+
# when giving it a list of available tools.
|
11
|
+
def description_for_llm
|
12
|
+
<<~DESCRIPTION
|
13
|
+
Name: #{tool_name}
|
14
|
+
Description: #{tool_description}
|
15
|
+
Arguments Schema:
|
16
|
+
#{JSON.pretty_generate(tool_arguments_schema)}
|
17
|
+
Example Usage:
|
18
|
+
#{JSON.pretty_generate(example_model_invocation)}
|
19
|
+
DESCRIPTION
|
20
|
+
end
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
# The name of the tool as it will be provided to the model & used in the model invocation.
|
23
|
+
# Default for something like Raif::ModelTools::WikipediaSearch would be "wikipedia_search"
|
24
|
+
def tool_name
|
25
|
+
name.split("::").last.underscore
|
26
|
+
end
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
def tool_description(&block)
|
29
|
+
if block_given?
|
30
|
+
@tool_description = block.call
|
31
|
+
elsif @tool_description.present?
|
32
|
+
@tool_description
|
33
|
+
else
|
34
|
+
raise NotImplementedError, "#{name}#tool_description is not implemented"
|
35
|
+
end
|
36
|
+
end
|
33
37
|
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
def example_model_invocation(&block)
|
39
|
+
if block_given?
|
40
|
+
@example_model_invocation = block.call
|
41
|
+
elsif @example_model_invocation.present?
|
42
|
+
@example_model_invocation
|
43
|
+
else
|
44
|
+
raise NotImplementedError, "#{name}#example_model_invocation is not implemented"
|
45
|
+
end
|
46
|
+
end
|
37
47
|
|
38
|
-
|
39
|
-
|
40
|
-
|
48
|
+
def process_invocation(invocation)
|
49
|
+
raise NotImplementedError, "#{name}#process_invocation is not implemented"
|
50
|
+
end
|
41
51
|
|
42
|
-
|
43
|
-
|
44
|
-
|
52
|
+
def invocation_partial_name
|
53
|
+
name.gsub("Raif::ModelTools::", "").underscore
|
54
|
+
end
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
|
56
|
+
def tool_arguments_schema(&block)
|
57
|
+
if block_given?
|
58
|
+
json_schema_definition(:tool_arguments, &block)
|
59
|
+
elsif schema_defined?(:tool_arguments)
|
60
|
+
schema_for(:tool_arguments)
|
61
|
+
else
|
62
|
+
raise NotImplementedError,
|
63
|
+
"#{name} must define tool arguments schema via tool_arguments_schema or override #{name}.tool_arguments_schema"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def provider_managed?
|
68
|
+
false
|
69
|
+
end
|
49
70
|
|
50
|
-
|
51
|
-
|
52
|
-
source: source,
|
53
|
-
tool_type: name,
|
54
|
-
tool_arguments: tool_arguments
|
55
|
-
)
|
56
|
-
|
57
|
-
ActiveRecord::Base.transaction do
|
58
|
-
tool_invocation.save!
|
59
|
-
process_invocation(tool_invocation)
|
60
|
-
tool_invocation.completed!
|
71
|
+
def renderable?
|
72
|
+
true
|
61
73
|
end
|
62
74
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
75
|
+
def triggers_observation_to_model?
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def invoke_tool(tool_arguments:, source:)
|
80
|
+
tool_invocation = Raif::ModelToolInvocation.new(
|
81
|
+
source: source,
|
82
|
+
tool_type: name,
|
83
|
+
tool_arguments: tool_arguments
|
84
|
+
)
|
85
|
+
|
86
|
+
ActiveRecord::Base.transaction do
|
87
|
+
tool_invocation.save!
|
88
|
+
process_invocation(tool_invocation)
|
89
|
+
tool_invocation.completed!
|
90
|
+
end
|
91
|
+
|
92
|
+
tool_invocation
|
93
|
+
rescue StandardError => e
|
94
|
+
tool_invocation.failed!
|
95
|
+
raise e
|
96
|
+
end
|
67
97
|
end
|
68
98
|
|
69
99
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json-schema"
|
4
|
-
|
5
3
|
class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
6
4
|
belongs_to :source, polymorphic: true
|
7
5
|
|
@@ -11,7 +9,11 @@ class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
11
9
|
validates :tool_type, presence: true
|
12
10
|
validate :ensure_valid_tool_argument_schema, if: -> { tool_type.present? && tool_arguments_schema.present? }
|
13
11
|
|
14
|
-
delegate :tool_arguments_schema,
|
12
|
+
delegate :tool_arguments_schema,
|
13
|
+
:renderable?,
|
14
|
+
:tool_name,
|
15
|
+
:triggers_observation_to_model?,
|
16
|
+
to: :tool
|
15
17
|
|
16
18
|
boolean_timestamp :completed_at
|
17
19
|
boolean_timestamp :failed_at
|
@@ -25,9 +27,9 @@ class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def result_llm_message
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
return unless tool.respond_to?(:observation_for_invocation)
|
31
|
+
|
32
|
+
tool.observation_for_invocation(self)
|
31
33
|
end
|
32
34
|
|
33
35
|
def to_partial_path
|
@@ -1,46 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTools::AgentFinalAnswer < Raif::ModelTool
|
4
|
+
tool_arguments_schema do
|
5
|
+
string "final_answer", description: "Your complete and final answer to the user's question or task"
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
example_model_invocation do
|
6
9
|
{
|
7
10
|
"name" => tool_name,
|
8
11
|
"arguments" => { "final_answer": "The answer to the user's question or task" }
|
9
12
|
}
|
10
13
|
end
|
11
14
|
|
12
|
-
|
13
|
-
{
|
14
|
-
type: "object",
|
15
|
-
additionalProperties: false,
|
16
|
-
required: ["final_answer"],
|
17
|
-
properties: {
|
18
|
-
final_answer: {
|
19
|
-
type: "string",
|
20
|
-
description: "Your complete and final answer to the user's question or task"
|
21
|
-
}
|
22
|
-
}
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.tool_description
|
15
|
+
tool_description do
|
27
16
|
"Provide your final answer to the user's question or task"
|
28
17
|
end
|
29
18
|
|
30
|
-
|
31
|
-
|
19
|
+
class << self
|
20
|
+
def observation_for_invocation(tool_invocation)
|
21
|
+
return "No answer provided" unless tool_invocation.result.present?
|
32
22
|
|
33
|
-
|
34
|
-
|
23
|
+
tool_invocation.result["final_answer"]
|
24
|
+
end
|
35
25
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
26
|
+
def process_invocation(tool_invocation)
|
27
|
+
tool_invocation.update!(
|
28
|
+
result: {
|
29
|
+
final_answer: tool_invocation.tool_arguments["final_answer"]
|
30
|
+
}
|
31
|
+
)
|
42
32
|
|
43
|
-
|
33
|
+
tool_invocation.result
|
34
|
+
end
|
44
35
|
end
|
45
36
|
|
46
37
|
end
|