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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +346 -43
  3. data/app/assets/builds/raif.css +26 -1
  4. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  5. data/app/assets/stylesheets/raif/loader.scss +27 -1
  6. data/app/controllers/raif/admin/application_controller.rb +14 -0
  7. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  8. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  9. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  10. data/app/controllers/raif/conversations_controller.rb +5 -1
  11. data/app/models/raif/agent.rb +11 -9
  12. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  13. data/app/models/raif/agents/re_act_agent.rb +6 -0
  14. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  15. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  16. data/app/models/raif/concerns/llm_response_parsing.rb +42 -14
  17. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  18. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  19. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +56 -0
  20. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +70 -0
  21. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +37 -0
  22. data/app/models/raif/concerns/llms/message_formatting.rb +42 -0
  23. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +138 -0
  24. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +41 -0
  25. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +26 -0
  26. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +43 -0
  27. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +42 -0
  28. data/app/models/raif/conversation.rb +28 -7
  29. data/app/models/raif/conversation_entry.rb +40 -8
  30. data/app/models/raif/embedding_model.rb +22 -0
  31. data/app/models/raif/embedding_models/bedrock.rb +34 -0
  32. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  33. data/app/models/raif/llm.rb +108 -9
  34. data/app/models/raif/llms/anthropic.rb +72 -57
  35. data/app/models/raif/llms/bedrock.rb +165 -0
  36. data/app/models/raif/llms/open_ai_base.rb +66 -0
  37. data/app/models/raif/llms/open_ai_completions.rb +100 -0
  38. data/app/models/raif/llms/open_ai_responses.rb +144 -0
  39. data/app/models/raif/llms/open_router.rb +88 -0
  40. data/app/models/raif/model_completion.rb +23 -2
  41. data/app/models/raif/model_file_input.rb +113 -0
  42. data/app/models/raif/model_image_input.rb +4 -0
  43. data/app/models/raif/model_tool.rb +82 -52
  44. data/app/models/raif/model_tool_invocation.rb +8 -6
  45. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  46. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  47. data/app/models/raif/model_tools/provider_managed/base.rb +9 -0
  48. data/app/models/raif/model_tools/provider_managed/code_execution.rb +5 -0
  49. data/app/models/raif/model_tools/provider_managed/image_generation.rb +5 -0
  50. data/app/models/raif/model_tools/provider_managed/web_search.rb +5 -0
  51. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  52. data/app/models/raif/streaming_responses/anthropic.rb +63 -0
  53. data/app/models/raif/streaming_responses/bedrock.rb +89 -0
  54. data/app/models/raif/streaming_responses/open_ai_completions.rb +76 -0
  55. data/app/models/raif/streaming_responses/open_ai_responses.rb +54 -0
  56. data/app/models/raif/task.rb +71 -16
  57. data/app/views/layouts/raif/admin.html.erb +10 -0
  58. data/app/views/raif/admin/agents/show.html.erb +3 -1
  59. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  60. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +48 -0
  61. data/app/views/raif/admin/conversations/show.html.erb +4 -2
  62. data/app/views/raif/admin/model_completions/_model_completion.html.erb +8 -0
  63. data/app/views/raif/admin/model_completions/index.html.erb +2 -0
  64. data/app/views/raif/admin/model_completions/show.html.erb +58 -3
  65. data/app/views/raif/admin/stats/index.html.erb +128 -0
  66. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  67. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  68. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  69. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  70. data/app/views/raif/conversation_entries/_citations.html.erb +9 -0
  71. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +22 -14
  72. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  73. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  74. data/app/views/raif/conversation_entries/_message.html.erb +14 -3
  75. data/config/locales/admin.en.yml +16 -0
  76. data/config/locales/en.yml +47 -3
  77. data/config/routes.rb +6 -0
  78. data/db/migrate/20250224234252_create_raif_tables.rb +1 -1
  79. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  80. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  81. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  82. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  83. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  84. data/db/migrate/20250527213016_add_response_id_and_response_array_to_model_completions.rb +14 -0
  85. data/db/migrate/20250603140622_add_citations_to_raif_model_completions.rb +13 -0
  86. data/db/migrate/20250603202013_add_stream_response_to_raif_model_completions.rb +7 -0
  87. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  88. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  89. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  90. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  91. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  92. data/lib/generators/raif/conversation/templates/conversation.rb.tt +16 -14
  93. data/lib/generators/raif/install/templates/initializer.rb +62 -6
  94. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  95. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  96. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  97. data/lib/raif/configuration.rb +63 -4
  98. data/lib/raif/embedding_model_registry.rb +83 -0
  99. data/lib/raif/engine.rb +56 -7
  100. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  101. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  102. data/lib/raif/errors/streaming_error.rb +18 -0
  103. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  104. data/lib/raif/errors.rb +4 -2
  105. data/lib/raif/json_schema_builder.rb +104 -0
  106. data/lib/raif/llm_registry.rb +315 -0
  107. data/lib/raif/migration_checker.rb +74 -0
  108. data/lib/raif/utils/html_fragment_processor.rb +169 -0
  109. data/lib/raif/utils.rb +1 -0
  110. data/lib/raif/version.rb +1 -1
  111. data/lib/raif.rb +7 -32
  112. data/lib/tasks/raif_tasks.rake +9 -4
  113. metadata +62 -12
  114. data/app/models/raif/llms/bedrock_claude.rb +0 -134
  115. data/app/models/raif/llms/open_ai.rb +0 -259
  116. 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
- def self.example_model_invocation
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
- def self.tool_arguments_schema
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
- def self.observation_for_invocation(tool_invocation)
31
- return "No results found" unless tool_invocation.result.present?
19
+ class << self
20
+ def observation_for_invocation(tool_invocation)
21
+ return "No results found" unless tool_invocation.result.present?
32
22
 
33
- <<~OBSERVATION
34
- Result Status: #{tool_invocation.result["status"]}
35
- Result Content:
36
- #{tool_invocation.result["content"]}
37
- OBSERVATION
38
- end
23
+ <<~OBSERVATION
24
+ Result Status: #{tool_invocation.result["status"]}
25
+ Result Content:
26
+ #{tool_invocation.result["content"]}
27
+ OBSERVATION
28
+ end
39
29
 
40
- def self.process_invocation(tool_invocation)
41
- url = tool_invocation.tool_arguments["url"]
42
- response = Faraday.get(url)
30
+ def process_invocation(tool_invocation)
31
+ url = tool_invocation.tool_arguments["url"]
32
+ response = Faraday.get(url)
43
33
 
44
- readable_content = Raif::Utils::ReadableContentExtractor.new(response.body).extract_readable_content
45
- markdown_content = Raif::Utils::HtmlToMarkdownConverter.convert(readable_content)
34
+ readable_content = Raif::Utils::ReadableContentExtractor.new(response.body).extract_readable_content
35
+ markdown_content = Raif::Utils::HtmlToMarkdownConverter.convert(readable_content)
46
36
 
47
- tool_invocation.update!(
48
- result: {
49
- status: response.status,
50
- content: markdown_content
51
- }
52
- )
37
+ tool_invocation.update!(
38
+ result: {
39
+ status: response.status,
40
+ content: markdown_content
41
+ }
42
+ )
53
43
 
54
- tool_invocation.result
44
+ tool_invocation.result
45
+ end
55
46
  end
56
47
 
57
48
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::ModelTools::ProviderManaged::Base < Raif::ModelTool
4
+ class << self
5
+ def provider_managed?
6
+ true
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::ModelTools::ProviderManaged::CodeExecution < Raif::ModelTools::ProviderManaged::Base
4
+
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::ModelTools::ProviderManaged::ImageGeneration < Raif::ModelTools::ProviderManaged::Base
4
+
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Raif::ModelTools::ProviderManaged::WebSearch < Raif::ModelTools::ProviderManaged::Base
4
+
5
+ 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
- def self.example_model_invocation
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
- def self.tool_arguments_schema
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
- def self.observation_for_invocation(tool_invocation)
31
- return "No results found" unless tool_invocation.result.present?
19
+ class << self
20
+ def observation_for_invocation(tool_invocation)
21
+ return "No results found" unless tool_invocation.result.present?
32
22
 
33
- JSON.pretty_generate(tool_invocation.result)
34
- end
23
+ JSON.pretty_generate(tool_invocation.result)
24
+ end
35
25
 
36
- def self.process_invocation(tool_invocation)
37
- query = tool_invocation.tool_arguments["query"]
26
+ def process_invocation(tool_invocation)
27
+ query = tool_invocation.tool_arguments["query"]
38
28
 
39
- conn = Faraday.new(url: "https://en.wikipedia.org/w/api.php")
29
+ conn = Faraday.new(url: "https://en.wikipedia.org/w/api.php")
40
30
 
41
- response = conn.get do |req|
42
- req.params["action"] = "query"
43
- req.params["format"] = "json"
44
- req.params["list"] = "search"
45
- req.params["srsearch"] = query
46
- req.params["srlimit"] = 5 # Limit to 5 results
47
- req.params["srprop"] = "snippet"
48
- end
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
- if response.success?
51
- results = JSON.parse(response.body)
52
- search_results = results.dig("query", "search") || []
40
+ if response.success?
41
+ results = JSON.parse(response.body)
42
+ search_results = results.dig("query", "search") || []
53
43
 
54
- # Store the results in the tool_invocation
55
- tool_invocation.update!(
56
- result: {
57
- results: search_results.map do |result|
58
- {
59
- title: result["title"],
60
- snippet: result["snippet"],
61
- page_id: result["pageid"],
62
- url: "https://en.wikipedia.org/wiki/#{result["title"].gsub(" ", "_")}"
63
- }
64
- end
65
- }
66
- )
67
- else
68
- tool_invocation.update!(
69
- result: {
70
- error: "Failed to fetch results from Wikipedia API: #{response.status} #{response.reason_phrase}"
71
- }
72
- )
73
- end
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
- tool_invocation.result
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
@@ -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
- after_initialize -> { self.available_model_tools ||= [] }
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
- def self.llm_response_format(format)
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
- after_initialize -> { self.response_format = format }, if: :new_record?
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" => prompt }]
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).prompt
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).system_prompt
125
+ new(creator:, **args).build_system_prompt
99
126
  end
100
127
 
101
- def self.json_response_schema
102
- nil
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