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
@@ -1,57 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTools::FetchUrl < Raif::ModelTool
|
4
|
+
tool_arguments_schema do
|
5
|
+
string "url", description: "The URL to fetch content from"
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
example_model_invocation do
|
6
9
|
{
|
7
10
|
"name": tool_name,
|
8
11
|
"arguments": { "url": "https://en.wikipedia.org/wiki/NASA" }
|
9
12
|
}
|
10
13
|
end
|
11
14
|
|
12
|
-
|
13
|
-
{
|
14
|
-
type: "object",
|
15
|
-
additionalProperties: false,
|
16
|
-
required: ["url"],
|
17
|
-
properties: {
|
18
|
-
url: {
|
19
|
-
type: "string",
|
20
|
-
description: "The URL to fetch content from"
|
21
|
-
}
|
22
|
-
}
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.tool_description
|
15
|
+
tool_description do
|
27
16
|
"Fetch a URL and return the page content as markdown"
|
28
17
|
end
|
29
18
|
|
30
|
-
|
31
|
-
|
19
|
+
class << self
|
20
|
+
def observation_for_invocation(tool_invocation)
|
21
|
+
return "No results found" unless tool_invocation.result.present?
|
32
22
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
23
|
+
<<~OBSERVATION
|
24
|
+
Result Status: #{tool_invocation.result["status"]}
|
25
|
+
Result Content:
|
26
|
+
#{tool_invocation.result["content"]}
|
27
|
+
OBSERVATION
|
28
|
+
end
|
39
29
|
|
40
|
-
|
41
|
-
|
42
|
-
|
30
|
+
def process_invocation(tool_invocation)
|
31
|
+
url = tool_invocation.tool_arguments["url"]
|
32
|
+
response = Faraday.get(url)
|
43
33
|
|
44
|
-
|
45
|
-
|
34
|
+
readable_content = Raif::Utils::ReadableContentExtractor.new(response.body).extract_readable_content
|
35
|
+
markdown_content = Raif::Utils::HtmlToMarkdownConverter.convert(readable_content)
|
46
36
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
37
|
+
tool_invocation.update!(
|
38
|
+
result: {
|
39
|
+
status: response.status,
|
40
|
+
content: markdown_content
|
41
|
+
}
|
42
|
+
)
|
53
43
|
|
54
|
-
|
44
|
+
tool_invocation.result
|
45
|
+
end
|
55
46
|
end
|
56
47
|
|
57
48
|
end
|
@@ -1,78 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Raif::ModelTools::WikipediaSearch < Raif::ModelTool
|
4
|
+
tool_arguments_schema do
|
5
|
+
string "query", description: "The query to search Wikipedia for"
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
example_model_invocation do
|
6
9
|
{
|
7
10
|
"name" => tool_name,
|
8
11
|
"arguments" => { "query": "Jimmy Buffett" }
|
9
12
|
}
|
10
13
|
end
|
11
14
|
|
12
|
-
|
13
|
-
{
|
14
|
-
type: "object",
|
15
|
-
additionalProperties: false,
|
16
|
-
required: ["query"],
|
17
|
-
properties: {
|
18
|
-
query: {
|
19
|
-
type: "string",
|
20
|
-
description: "The query to search Wikipedia for"
|
21
|
-
}
|
22
|
-
}
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.tool_description
|
15
|
+
tool_description do
|
27
16
|
"Search Wikipedia for information"
|
28
17
|
end
|
29
18
|
|
30
|
-
|
31
|
-
|
19
|
+
class << self
|
20
|
+
def observation_for_invocation(tool_invocation)
|
21
|
+
return "No results found" unless tool_invocation.result.present?
|
32
22
|
|
33
|
-
|
34
|
-
|
23
|
+
JSON.pretty_generate(tool_invocation.result)
|
24
|
+
end
|
35
25
|
|
36
|
-
|
37
|
-
|
26
|
+
def process_invocation(tool_invocation)
|
27
|
+
query = tool_invocation.tool_arguments["query"]
|
38
28
|
|
39
|
-
|
29
|
+
conn = Faraday.new(url: "https://en.wikipedia.org/w/api.php")
|
40
30
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
31
|
+
response = conn.get do |req|
|
32
|
+
req.params["action"] = "query"
|
33
|
+
req.params["format"] = "json"
|
34
|
+
req.params["list"] = "search"
|
35
|
+
req.params["srsearch"] = query
|
36
|
+
req.params["srlimit"] = 5 # Limit to 5 results
|
37
|
+
req.params["srprop"] = "snippet"
|
38
|
+
end
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
|
40
|
+
if response.success?
|
41
|
+
results = JSON.parse(response.body)
|
42
|
+
search_results = results.dig("query", "search") || []
|
53
43
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
44
|
+
# Store the results in the tool_invocation
|
45
|
+
tool_invocation.update!(
|
46
|
+
result: {
|
47
|
+
results: search_results.map do |result|
|
48
|
+
{
|
49
|
+
title: result["title"],
|
50
|
+
snippet: result["snippet"],
|
51
|
+
page_id: result["pageid"],
|
52
|
+
url: "https://en.wikipedia.org/wiki/#{result["title"].gsub(" ", "_")}"
|
53
|
+
}
|
54
|
+
end
|
55
|
+
}
|
56
|
+
)
|
57
|
+
else
|
58
|
+
tool_invocation.update!(
|
59
|
+
result: {
|
60
|
+
error: "Failed to fetch results from Wikipedia API: #{response.status} #{response.reason_phrase}"
|
61
|
+
}
|
62
|
+
)
|
63
|
+
end
|
74
64
|
|
75
|
-
|
65
|
+
tool_invocation.result
|
66
|
+
end
|
76
67
|
end
|
77
68
|
|
78
69
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::StreamingResponses::Anthropic
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@response_json = { "content" => [], "usage" => {} }
|
7
|
+
@finish_reason = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def process_streaming_event(event_type, event)
|
11
|
+
delta = nil
|
12
|
+
index = event["index"]
|
13
|
+
|
14
|
+
case event_type
|
15
|
+
when "message_start"
|
16
|
+
@response_json = event["message"]
|
17
|
+
@response_json["content"] = []
|
18
|
+
@response_json["usage"] ||= {}
|
19
|
+
when "content_block_start"
|
20
|
+
@response_json["content"][index] = event["content_block"]
|
21
|
+
if event.dig("content_block", "type") == "tool_use"
|
22
|
+
@response_json["content"][index]["input"] = ""
|
23
|
+
end
|
24
|
+
when "content_block_delta"
|
25
|
+
delta_chunk = event["delta"]
|
26
|
+
if delta_chunk["type"] == "text_delta"
|
27
|
+
delta = delta_chunk["text"]
|
28
|
+
@response_json["content"][index]["text"] += delta if delta
|
29
|
+
elsif delta_chunk["type"] == "input_json_delta"
|
30
|
+
@response_json["content"][index]["input"] += delta_chunk["partial_json"]
|
31
|
+
end
|
32
|
+
when "content_block_stop"
|
33
|
+
content_block = @response_json["content"][index]
|
34
|
+
if content_block&.dig("type") == "tool_use"
|
35
|
+
begin
|
36
|
+
content_block["input"] = JSON.parse(content_block["input"])
|
37
|
+
rescue JSON::ParserError
|
38
|
+
# If parsing fails, leave as a string
|
39
|
+
end
|
40
|
+
end
|
41
|
+
when "message_delta"
|
42
|
+
@finish_reason = event.dig("delta", "stop_reason")
|
43
|
+
@response_json["usage"]["output_tokens"] = event.dig("usage", "output_tokens")
|
44
|
+
when "message_stop"
|
45
|
+
@finish_reason = "stop"
|
46
|
+
when "error"
|
47
|
+
error_details = event["error"]
|
48
|
+
raise Raif::Errors::StreamingError.new(
|
49
|
+
message: error_details["message"],
|
50
|
+
type: error_details["type"],
|
51
|
+
event: event
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
[delta, @finish_reason]
|
56
|
+
end
|
57
|
+
|
58
|
+
def current_response_json
|
59
|
+
@response_json["stop_reason"] = @finish_reason
|
60
|
+
@response_json
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::StreamingResponses::Bedrock
|
4
|
+
|
5
|
+
def initialize_new_message
|
6
|
+
# Initialize empty AWS response object
|
7
|
+
@message = Aws::BedrockRuntime::Types::Message.new(
|
8
|
+
role: "assistant",
|
9
|
+
content: []
|
10
|
+
)
|
11
|
+
|
12
|
+
@output = Aws::BedrockRuntime::Types::ConverseOutput::Message.new(message: @message)
|
13
|
+
|
14
|
+
@usage = Aws::BedrockRuntime::Types::TokenUsage.new(
|
15
|
+
input_tokens: 0,
|
16
|
+
output_tokens: 0,
|
17
|
+
total_tokens: 0
|
18
|
+
)
|
19
|
+
|
20
|
+
@response = Aws::BedrockRuntime::Types::ConverseResponse.new(
|
21
|
+
output: @output,
|
22
|
+
usage: @usage,
|
23
|
+
stop_reason: nil
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_streaming_event(event_type, event)
|
28
|
+
delta = nil
|
29
|
+
|
30
|
+
case event.event_type
|
31
|
+
when :message_start
|
32
|
+
initialize_new_message
|
33
|
+
when :content_block_start
|
34
|
+
index = event.content_block_index
|
35
|
+
|
36
|
+
if event.start.is_a?(Aws::BedrockRuntime::Types::ContentBlockStart::ToolUse)
|
37
|
+
tool_use = event.start.tool_use
|
38
|
+
@message.content[index] = Aws::BedrockRuntime::Types::ContentBlock.new(
|
39
|
+
tool_use: Aws::BedrockRuntime::Types::ToolUseBlock.new(
|
40
|
+
tool_use_id: tool_use.tool_use_id,
|
41
|
+
name: tool_use.name,
|
42
|
+
input: ""
|
43
|
+
)
|
44
|
+
)
|
45
|
+
else
|
46
|
+
@message.content[index] = Aws::BedrockRuntime::Types::ContentBlock::Text.new(text: "")
|
47
|
+
end
|
48
|
+
when :content_block_delta
|
49
|
+
index = event.content_block_index
|
50
|
+
|
51
|
+
if event.delta.is_a?(Aws::BedrockRuntime::Types::ContentBlockDelta::Text)
|
52
|
+
@message.content[index] ||= Aws::BedrockRuntime::Types::ContentBlock::Text.new(text: "")
|
53
|
+
delta = event.delta.text
|
54
|
+
@message.content[index].text += delta
|
55
|
+
elsif event.delta.is_a?(Aws::BedrockRuntime::Types::ContentBlockDelta::ToolUse)
|
56
|
+
tool_use = event.delta.tool_use
|
57
|
+
@message.content[index] ||= Aws::BedrockRuntime::Types::ContentBlock.new
|
58
|
+
@message.content[index].tool_use ||= Aws::BedrockRuntime::Types::ToolUseBlock.new(
|
59
|
+
tool_use_id: tool_use.tool_use_id,
|
60
|
+
name: tool_use.name,
|
61
|
+
input: ""
|
62
|
+
)
|
63
|
+
|
64
|
+
@message.content[index].tool_use.input += event.delta.tool_use.input
|
65
|
+
end
|
66
|
+
when :content_block_stop
|
67
|
+
content_block = @message.content[event.content_block_index]
|
68
|
+
|
69
|
+
if content_block&.tool_use&.input.is_a?(String)
|
70
|
+
begin
|
71
|
+
content_block.tool_use.input = JSON.parse(content_block.tool_use.input)
|
72
|
+
rescue JSON::ParserError
|
73
|
+
# If parsing fails, leave as a string
|
74
|
+
end
|
75
|
+
end
|
76
|
+
when :message_stop
|
77
|
+
@response.stop_reason = event.stop_reason
|
78
|
+
when :metadata
|
79
|
+
@response.usage = event.usage if event.respond_to?(:usage)
|
80
|
+
end
|
81
|
+
|
82
|
+
[delta, @response.stop_reason]
|
83
|
+
end
|
84
|
+
|
85
|
+
def current_response
|
86
|
+
@response
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::StreamingResponses::OpenAiCompletions
|
4
|
+
attr_reader :raw_response, :tool_calls
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@id = nil
|
8
|
+
@raw_response = ""
|
9
|
+
@tool_calls = []
|
10
|
+
@usage = {}
|
11
|
+
@finish_reason = nil
|
12
|
+
@response_json = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def process_streaming_event(event_type, event)
|
16
|
+
@id ||= event["id"]
|
17
|
+
delta_chunk = event.dig("choices", 0, "delta")
|
18
|
+
finish_reason = event.dig("choices", 0, "finish_reason")
|
19
|
+
@finish_reason ||= finish_reason
|
20
|
+
|
21
|
+
delta_content = delta_chunk&.dig("content")
|
22
|
+
@raw_response += delta_content if delta_content
|
23
|
+
|
24
|
+
if delta_chunk&.key?("tool_calls")
|
25
|
+
delta_chunk["tool_calls"].each do |tool_call_chunk|
|
26
|
+
index = tool_call_chunk["index"]
|
27
|
+
@tool_calls[index] ||= { "function" => {} }
|
28
|
+
@tool_calls[index]["id"] ||= tool_call_chunk["id"]
|
29
|
+
if tool_call_chunk.dig("function", "name")
|
30
|
+
@tool_calls[index]["function"]["name"] = tool_call_chunk.dig("function", "name")
|
31
|
+
end
|
32
|
+
if tool_call_chunk.dig("function", "arguments")
|
33
|
+
@tool_calls[index]["function"]["arguments"] ||= ""
|
34
|
+
@tool_calls[index]["function"]["arguments"] += tool_call_chunk.dig("function", "arguments")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@usage = event["usage"] if event["usage"]
|
40
|
+
|
41
|
+
[delta_content, finish_reason]
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_response_json
|
45
|
+
message = {
|
46
|
+
"role" => "assistant",
|
47
|
+
"content" => @raw_response
|
48
|
+
}
|
49
|
+
|
50
|
+
if @tool_calls.any?
|
51
|
+
message["content"] = nil # Per OpenAI spec, content is null if tool_calls are present
|
52
|
+
message["tool_calls"] = @tool_calls.map do |tc|
|
53
|
+
# The streaming format for tool calls is slightly different from the final format.
|
54
|
+
# We need to adjust it here.
|
55
|
+
{
|
56
|
+
"id" => tc["id"],
|
57
|
+
"type" => "function",
|
58
|
+
"function" => {
|
59
|
+
"name" => tc.dig("function", "name"),
|
60
|
+
"arguments" => tc.dig("function", "arguments")
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
{
|
67
|
+
"id" => @id,
|
68
|
+
"choices" => [{
|
69
|
+
"index" => 0,
|
70
|
+
"message" => message,
|
71
|
+
"finish_reason" => @finish_reason
|
72
|
+
}],
|
73
|
+
"usage" => @usage
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::StreamingResponses::OpenAiResponses
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@output_items = []
|
7
|
+
@finish_reason = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def process_streaming_event(event_type, event)
|
11
|
+
output_index = event["output_index"]
|
12
|
+
content_index = event["content_index"]
|
13
|
+
|
14
|
+
delta = nil
|
15
|
+
|
16
|
+
case event["type"]
|
17
|
+
when "response.created"
|
18
|
+
@id = event["response"]["id"]
|
19
|
+
when "response.output_item.added", "response.output_item.done"
|
20
|
+
@output_items[output_index] = event["item"]
|
21
|
+
when "response.content_part.added", "response.content_part.done"
|
22
|
+
@output_items[output_index]["content"] ||= []
|
23
|
+
@output_items[output_index]["content"][content_index] = event["part"]
|
24
|
+
when "response.output_text.delta"
|
25
|
+
delta = event["delta"]
|
26
|
+
@output_items[output_index]["content"][content_index]["text"] ||= ""
|
27
|
+
@output_items[output_index]["content"][content_index]["text"] += event["delta"]
|
28
|
+
when "response.output_text.done"
|
29
|
+
@output_items[output_index]["content"][content_index]["text"] = event["text"]
|
30
|
+
when "response.completed"
|
31
|
+
@usage = event["response"]["usage"]
|
32
|
+
@finish_reason = "stop"
|
33
|
+
when "error"
|
34
|
+
raise Raif::Errors::StreamingError.new(
|
35
|
+
message: event["message"],
|
36
|
+
type: event["type"],
|
37
|
+
code: event["code"],
|
38
|
+
event: event
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
[delta, @finish_reason]
|
43
|
+
end
|
44
|
+
|
45
|
+
# The response we've built up so far.
|
46
|
+
def current_response_json
|
47
|
+
{
|
48
|
+
"id" => @id,
|
49
|
+
"output" => @output_items,
|
50
|
+
"usage" => @usage
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/app/models/raif/task.rb
CHANGED
@@ -7,6 +7,10 @@ module Raif
|
|
7
7
|
include Raif::Concerns::HasAvailableModelTools
|
8
8
|
include Raif::Concerns::InvokesModelTools
|
9
9
|
include Raif::Concerns::LlmResponseParsing
|
10
|
+
include Raif::Concerns::LlmTemperature
|
11
|
+
include Raif::Concerns::JsonSchemaDefinition
|
12
|
+
|
13
|
+
llm_temperature 0.7
|
10
14
|
|
11
15
|
belongs_to :creator, polymorphic: true
|
12
16
|
|
@@ -20,12 +24,25 @@ module Raif
|
|
20
24
|
|
21
25
|
delegate :json_response_schema, to: :class
|
22
26
|
|
23
|
-
|
27
|
+
scope :completed, -> { where.not(completed_at: nil) }
|
28
|
+
scope :failed, -> { where.not(failed_at: nil) }
|
29
|
+
scope :in_progress, -> { where.not(started_at: nil).where(completed_at: nil, failed_at: nil) }
|
30
|
+
scope :pending, -> { where(started_at: nil, completed_at: nil, failed_at: nil) }
|
31
|
+
|
32
|
+
attr_accessor :files, :images
|
24
33
|
|
25
|
-
|
26
|
-
raise ArgumentError, "response_format must be one of: #{response_formats.keys.join(", ")}" unless response_formats.keys.include?(format.to_s)
|
34
|
+
after_initialize -> { self.available_model_tools ||= [] }
|
27
35
|
|
28
|
-
|
36
|
+
def status
|
37
|
+
if completed_at?
|
38
|
+
:completed
|
39
|
+
elsif failed_at?
|
40
|
+
:failed
|
41
|
+
elsif started_at?
|
42
|
+
:in_progress
|
43
|
+
else
|
44
|
+
:pending
|
45
|
+
end
|
29
46
|
end
|
30
47
|
|
31
48
|
# The primary interface for running a task. It will hit the LLM with the task's prompt and system prompt and return a Raif::Task object.
|
@@ -34,10 +51,13 @@ module Raif
|
|
34
51
|
# @param creator [Object] The creator of the task (polymorphic association)
|
35
52
|
# @param available_model_tools [Array<Class>] Optional array of model tool classes that will be provided to the LLM for it to invoke.
|
36
53
|
# @param llm_model_key [Symbol, String] Optional key for the LLM model to use. If blank, Raif.config.default_llm_model_key will be used.
|
54
|
+
# @param images [Array] Optional array of Raif::ModelImageInput objects to include with the prompt.
|
55
|
+
# @param files [Array] Optional array of Raif::ModelFileInput objects to include with the prompt.
|
37
56
|
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
38
57
|
# @return [Raif::Task, nil] The task instance that was created and run.
|
39
|
-
def self.run(creator:, available_model_tools: [], llm_model_key: nil, **args)
|
40
|
-
task = new(creator:, llm_model_key:, available_model_tools:, started_at: Time.current, **args)
|
58
|
+
def self.run(creator:, available_model_tools: [], llm_model_key: nil, images: [], files: [], **args)
|
59
|
+
task = new(creator:, llm_model_key:, available_model_tools:, started_at: Time.current, images: images, files: files, **args)
|
60
|
+
|
41
61
|
task.save!
|
42
62
|
task.run
|
43
63
|
task
|
@@ -58,17 +78,19 @@ module Raif
|
|
58
78
|
task
|
59
79
|
end
|
60
80
|
|
61
|
-
def run
|
81
|
+
def run(skip_prompt_population: false)
|
62
82
|
update_columns(started_at: Time.current) if started_at.nil?
|
63
83
|
|
64
|
-
populate_prompts
|
65
|
-
messages = [{ "role" => "user", "content" =>
|
84
|
+
populate_prompts unless skip_prompt_population
|
85
|
+
messages = [{ "role" => "user", "content" => message_content }]
|
86
|
+
|
66
87
|
mc = llm.chat(
|
67
88
|
messages: messages,
|
68
89
|
source: self,
|
69
90
|
system_prompt: system_prompt,
|
70
91
|
response_format: response_format.to_sym,
|
71
|
-
available_model_tools: available_model_tools
|
92
|
+
available_model_tools: available_model_tools,
|
93
|
+
temperature: self.class.temperature
|
72
94
|
)
|
73
95
|
|
74
96
|
self.raif_model_completion = mc.becomes(Raif::ModelCompletion)
|
@@ -80,13 +102,18 @@ module Raif
|
|
80
102
|
self
|
81
103
|
end
|
82
104
|
|
105
|
+
def re_run
|
106
|
+
update_columns(started_at: Time.current)
|
107
|
+
run(skip_prompt_population: true)
|
108
|
+
end
|
109
|
+
|
83
110
|
# Returns the LLM prompt for the task.
|
84
111
|
#
|
85
112
|
# @param creator [Object] The creator of the task (polymorphic association)
|
86
113
|
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
87
114
|
# @return [String] The LLM prompt for the task.
|
88
115
|
def self.prompt(creator:, **args)
|
89
|
-
new(creator:, **args).
|
116
|
+
new(creator:, **args).build_prompt
|
90
117
|
end
|
91
118
|
|
92
119
|
# Returns the LLM system prompt for the task.
|
@@ -95,25 +122,53 @@ module Raif
|
|
95
122
|
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
96
123
|
# @return [String] The LLM system prompt for the task.
|
97
124
|
def self.system_prompt(creator:, **args)
|
98
|
-
new(creator:, **args).
|
125
|
+
new(creator:, **args).build_system_prompt
|
99
126
|
end
|
100
127
|
|
101
|
-
def self.json_response_schema
|
102
|
-
|
128
|
+
def self.json_response_schema(&block)
|
129
|
+
if block_given?
|
130
|
+
json_schema_definition(:json_response, &block)
|
131
|
+
elsif schema_defined?(:json_response)
|
132
|
+
schema_for(:json_response)
|
133
|
+
end
|
103
134
|
end
|
104
135
|
|
105
|
-
private
|
106
|
-
|
107
136
|
def build_prompt
|
108
137
|
raise NotImplementedError, "Raif::Task subclasses must implement #build_prompt"
|
109
138
|
end
|
110
139
|
|
111
140
|
def build_system_prompt
|
112
141
|
sp = Raif.config.task_system_prompt_intro
|
142
|
+
sp = sp.call(self) if sp.respond_to?(:call)
|
113
143
|
sp += system_prompt_language_preference if requested_language_key.present?
|
114
144
|
sp
|
115
145
|
end
|
116
146
|
|
147
|
+
private
|
148
|
+
|
149
|
+
def message_content
|
150
|
+
# If there are no images or files, just return the message content can just be a string with the prompt
|
151
|
+
return prompt if images.blank? && files.blank?
|
152
|
+
|
153
|
+
content = [{ "type" => "text", "text" => prompt }]
|
154
|
+
|
155
|
+
images.each do |image|
|
156
|
+
raise Raif::Errors::InvalidModelImageInputError,
|
157
|
+
"Images must be a Raif::ModelImageInput: #{image.inspect}" unless image.is_a?(Raif::ModelImageInput)
|
158
|
+
|
159
|
+
content << image
|
160
|
+
end
|
161
|
+
|
162
|
+
files.each do |file|
|
163
|
+
raise Raif::Errors::InvalidFileInputError,
|
164
|
+
"Files must be a Raif::ModelFileInput: #{file.inspect}" unless file.is_a?(Raif::ModelFileInput)
|
165
|
+
|
166
|
+
content << file
|
167
|
+
end
|
168
|
+
|
169
|
+
content
|
170
|
+
end
|
171
|
+
|
117
172
|
def populate_prompts
|
118
173
|
self.requested_language_key ||= creator.preferred_language_key if creator.respond_to?(:preferred_language_key)
|
119
174
|
self.prompt = build_prompt
|