raif 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +678 -0
- data/Rakefile +20 -0
- data/app/assets/builds/raif.css +74 -0
- data/app/assets/builds/raif_admin.css +266 -0
- data/app/assets/config/raif_manifest.js +1 -0
- data/app/assets/javascript/raif/controllers/conversations_controller.js +11 -0
- data/app/assets/javascript/raif/stream_actions/raif_scroll_to_bottom.js +12 -0
- data/app/assets/javascript/raif.js +10 -0
- data/app/assets/stylesheets/raif/admin/conversation.scss +64 -0
- data/app/assets/stylesheets/raif/loader.scss +85 -0
- data/app/assets/stylesheets/raif.scss +1 -0
- data/app/assets/stylesheets/raif_admin.scss +299 -0
- data/app/controllers/raif/admin/agents_controller.rb +17 -0
- data/app/controllers/raif/admin/application_controller.rb +20 -0
- data/app/controllers/raif/admin/conversations_controller.rb +17 -0
- data/app/controllers/raif/admin/model_completions_controller.rb +17 -0
- data/app/controllers/raif/admin/model_tool_invocations_controller.rb +17 -0
- data/app/controllers/raif/admin/tasks_controller.rb +23 -0
- data/app/controllers/raif/application_controller.rb +20 -0
- data/app/controllers/raif/conversation_entries_controller.rb +60 -0
- data/app/controllers/raif/conversations_controller.rb +58 -0
- data/app/helpers/raif/application_helper.rb +7 -0
- data/app/helpers/raif/shared/conversations_helper.rb +13 -0
- data/app/jobs/raif/application_job.rb +8 -0
- data/app/jobs/raif/conversation_entry_job.rb +30 -0
- data/app/models/raif/agent.rb +133 -0
- data/app/models/raif/agents/native_tool_calling_agent.rb +127 -0
- data/app/models/raif/agents/re_act_agent.rb +121 -0
- data/app/models/raif/agents/re_act_step.rb +33 -0
- data/app/models/raif/application_record.rb +14 -0
- data/app/models/raif/concerns/boolean_timestamp.rb +69 -0
- data/app/models/raif/concerns/has_available_model_tools.rb +13 -0
- data/app/models/raif/concerns/has_llm.rb +19 -0
- data/app/models/raif/concerns/has_requested_language.rb +20 -0
- data/app/models/raif/concerns/invokes_model_tools.rb +13 -0
- data/app/models/raif/concerns/llm_response_parsing.rb +44 -0
- data/app/models/raif/conversation.rb +67 -0
- data/app/models/raif/conversation_entry.rb +85 -0
- data/app/models/raif/llm.rb +88 -0
- data/app/models/raif/llms/anthropic.rb +120 -0
- data/app/models/raif/llms/bedrock_claude.rb +134 -0
- data/app/models/raif/llms/open_ai.rb +259 -0
- data/app/models/raif/model_completion.rb +28 -0
- data/app/models/raif/model_tool.rb +69 -0
- data/app/models/raif/model_tool_invocation.rb +43 -0
- data/app/models/raif/model_tools/agent_final_answer.rb +46 -0
- data/app/models/raif/model_tools/fetch_url.rb +57 -0
- data/app/models/raif/model_tools/wikipedia_search.rb +78 -0
- data/app/models/raif/task.rb +137 -0
- data/app/models/raif/user_tool_invocation.rb +29 -0
- data/app/views/layouts/raif/admin.html.erb +98 -0
- data/app/views/raif/admin/agents/_agent.html.erb +18 -0
- data/app/views/raif/admin/agents/_conversation_message.html.erb +15 -0
- data/app/views/raif/admin/agents/index.html.erb +33 -0
- data/app/views/raif/admin/agents/show.html.erb +131 -0
- data/app/views/raif/admin/conversations/_conversation.html.erb +7 -0
- data/app/views/raif/admin/conversations/_conversation_entry.html.erb +34 -0
- data/app/views/raif/admin/conversations/index.html.erb +32 -0
- data/app/views/raif/admin/conversations/show.html.erb +56 -0
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
- data/app/views/raif/admin/model_completions/index.html.erb +34 -0
- data/app/views/raif/admin/model_completions/show.html.erb +117 -0
- data/app/views/raif/admin/model_tool_invocations/_model_tool_invocation.html.erb +16 -0
- data/app/views/raif/admin/model_tool_invocations/index.html.erb +33 -0
- data/app/views/raif/admin/model_tool_invocations/show.html.erb +66 -0
- data/app/views/raif/admin/tasks/_task.html.erb +19 -0
- data/app/views/raif/admin/tasks/index.html.erb +49 -0
- data/app/views/raif/admin/tasks/show.html.erb +176 -0
- data/app/views/raif/conversation_entries/_conversation_entry.html.erb +26 -0
- data/app/views/raif/conversation_entries/_form.html.erb +25 -0
- data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -0
- data/app/views/raif/conversation_entries/_form_with_user_tool_invocation.html.erb +18 -0
- data/app/views/raif/conversation_entries/_message.html.erb +17 -0
- data/app/views/raif/conversation_entries/_model_response_avatar.html.erb +1 -0
- data/app/views/raif/conversation_entries/_user_avatar.html.erb +1 -0
- data/app/views/raif/conversation_entries/create.turbo_stream.erb +11 -0
- data/app/views/raif/conversation_entries/new.turbo_stream.erb +6 -0
- data/app/views/raif/conversations/_available_user_tools.html.erb +11 -0
- data/app/views/raif/conversations/_full_conversation.html.erb +15 -0
- data/app/views/raif/conversations/show.html.erb +1 -0
- data/config/i18n-tasks.yml +181 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/pagy.rb +14 -0
- data/config/locales/admin.en.yml +91 -0
- data/config/locales/en.yml +50 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20250224234252_create_raif_tables.rb +114 -0
- data/lib/generators/raif/agent/agent_generator.rb +22 -0
- data/lib/generators/raif/agent/templates/agent.rb.tt +28 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +27 -0
- data/lib/generators/raif/conversation/templates/conversation.rb.tt +37 -0
- data/lib/generators/raif/install/install_generator.rb +31 -0
- data/lib/generators/raif/install/templates/initializer.rb +81 -0
- data/lib/generators/raif/model_tool/model_tool_generator.rb +27 -0
- data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +74 -0
- data/lib/generators/raif/task/task_generator.rb +28 -0
- data/lib/generators/raif/task/templates/application_task.rb.tt +7 -0
- data/lib/generators/raif/task/templates/task.rb.tt +52 -0
- data/lib/generators/raif/views_generator.rb +22 -0
- data/lib/raif/configuration.rb +82 -0
- data/lib/raif/default_llms.rb +37 -0
- data/lib/raif/engine.rb +86 -0
- data/lib/raif/errors/action_not_authorized_error.rb +8 -0
- data/lib/raif/errors/anthropic/api_error.rb +10 -0
- data/lib/raif/errors/invalid_config_error.rb +8 -0
- data/lib/raif/errors/invalid_conversation_type_error.rb +8 -0
- data/lib/raif/errors/invalid_user_tool_type_error.rb +8 -0
- data/lib/raif/errors/open_ai/api_error.rb +10 -0
- data/lib/raif/errors/open_ai/json_schema_error.rb +10 -0
- data/lib/raif/errors.rb +9 -0
- data/lib/raif/languages.rb +33 -0
- data/lib/raif/rspec.rb +7 -0
- data/lib/raif/utils/html_to_markdown_converter.rb +7 -0
- data/lib/raif/utils/readable_content_extractor.rb +61 -0
- data/lib/raif/utils.rb +6 -0
- data/lib/raif/version.rb +5 -0
- data/lib/raif.rb +65 -0
- data/lib/tasks/raif_tasks.rake +6 -0
- metadata +294 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelCompletion < Raif::ApplicationRecord
|
4
|
+
include Raif::Concerns::LlmResponseParsing
|
5
|
+
include Raif::Concerns::HasAvailableModelTools
|
6
|
+
|
7
|
+
belongs_to :source, polymorphic: true, optional: true
|
8
|
+
|
9
|
+
validates :llm_model_key, presence: true, inclusion: { in: ->{ Raif.available_llm_keys.map(&:to_s) } }
|
10
|
+
validates :model_api_name, presence: true
|
11
|
+
|
12
|
+
delegate :json_response_schema, to: :source, allow_nil: true
|
13
|
+
|
14
|
+
before_save :set_total_tokens
|
15
|
+
|
16
|
+
after_initialize -> { self.messages ||= [] }
|
17
|
+
after_initialize -> { self.available_model_tools ||= [] }
|
18
|
+
|
19
|
+
def json_response_schema
|
20
|
+
source.json_response_schema if source&.respond_to?(:json_response_schema)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def set_total_tokens
|
26
|
+
self.total_tokens ||= completion_tokens.present? && prompt_tokens.present? ? completion_tokens + prompt_tokens : nil
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelTool
|
4
|
+
|
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
|
19
|
+
|
20
|
+
# The name of the tool as it will be provided to the model & used in the model invocation.
|
21
|
+
# Default for something like Raif::ModelTools::WikipediaSearch would be "wikipedia_search"
|
22
|
+
def self.tool_name
|
23
|
+
name.split("::").last.underscore
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.tool_description
|
27
|
+
raise NotImplementedError, "#{self.class.name}#tool_description is not implemented"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.example_model_invocation
|
31
|
+
raise NotImplementedError, "#{self.class.name}#example_model_invocation is not implemented"
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.process_invocation(invocation)
|
35
|
+
raise NotImplementedError, "#{self.class.name}#process_invocation is not implemented"
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.invocation_partial_name
|
39
|
+
name.gsub("Raif::ModelTools::", "").underscore
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.tool_arguments_schema
|
43
|
+
raise NotImplementedError, "#{self.class.name}#tool_arguments_schema is not implemented"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.renderable?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.invoke_tool(tool_arguments:, source:)
|
51
|
+
tool_invocation = Raif::ModelToolInvocation.new(
|
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!
|
61
|
+
end
|
62
|
+
|
63
|
+
tool_invocation
|
64
|
+
rescue StandardError => e
|
65
|
+
tool_invocation.failed!
|
66
|
+
raise e
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json-schema"
|
4
|
+
|
5
|
+
class Raif::ModelToolInvocation < Raif::ApplicationRecord
|
6
|
+
belongs_to :source, polymorphic: true
|
7
|
+
|
8
|
+
after_initialize -> { self.tool_arguments ||= {} }
|
9
|
+
after_initialize -> { self.result ||= {} }
|
10
|
+
|
11
|
+
validates :tool_type, presence: true
|
12
|
+
validate :ensure_valid_tool_argument_schema, if: -> { tool_type.present? && tool_arguments_schema.present? }
|
13
|
+
|
14
|
+
delegate :tool_arguments_schema, :renderable?, :tool_name, to: :tool
|
15
|
+
|
16
|
+
boolean_timestamp :completed_at
|
17
|
+
boolean_timestamp :failed_at
|
18
|
+
|
19
|
+
def tool
|
20
|
+
@tool ||= tool_type.constantize
|
21
|
+
end
|
22
|
+
|
23
|
+
def as_llm_message
|
24
|
+
"Invoking tool: #{tool_name} with arguments: #{tool_arguments.to_json}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def result_llm_message
|
28
|
+
if result.present?
|
29
|
+
"Result from #{tool_name}: #{result.to_json}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_partial_path
|
34
|
+
"raif/model_tool_invocations/#{tool.invocation_partial_name}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def ensure_valid_tool_argument_schema
|
38
|
+
unless JSON::Validator.validate(tool_arguments_schema, tool_arguments)
|
39
|
+
errors.add(:tool_arguments, "does not match schema")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelTools::AgentFinalAnswer < Raif::ModelTool
|
4
|
+
|
5
|
+
def self.example_model_invocation
|
6
|
+
{
|
7
|
+
"name" => tool_name,
|
8
|
+
"arguments" => { "final_answer": "The answer to the user's question or task" }
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.tool_arguments_schema
|
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
|
27
|
+
"Provide your final answer to the user's question or task"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.observation_for_invocation(tool_invocation)
|
31
|
+
return "No answer provided" unless tool_invocation.result.present?
|
32
|
+
|
33
|
+
tool_invocation.result["final_answer"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.process_invocation(tool_invocation)
|
37
|
+
tool_invocation.update!(
|
38
|
+
result: {
|
39
|
+
final_answer: tool_invocation.tool_arguments["final_answer"]
|
40
|
+
}
|
41
|
+
)
|
42
|
+
|
43
|
+
tool_invocation.result
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelTools::FetchUrl < Raif::ModelTool
|
4
|
+
|
5
|
+
def self.example_model_invocation
|
6
|
+
{
|
7
|
+
"name": tool_name,
|
8
|
+
"arguments": { "url": "https://en.wikipedia.org/wiki/NASA" }
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
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
|
27
|
+
"Fetch a URL and return the page content as markdown"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.observation_for_invocation(tool_invocation)
|
31
|
+
return "No results found" unless tool_invocation.result.present?
|
32
|
+
|
33
|
+
<<~OBSERVATION
|
34
|
+
Result Status: #{tool_invocation.result["status"]}
|
35
|
+
Result Content:
|
36
|
+
#{tool_invocation.result["content"]}
|
37
|
+
OBSERVATION
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.process_invocation(tool_invocation)
|
41
|
+
url = tool_invocation.tool_arguments["url"]
|
42
|
+
response = Faraday.get(url)
|
43
|
+
|
44
|
+
readable_content = Raif::Utils::ReadableContentExtractor.new(response.body).extract_readable_content
|
45
|
+
markdown_content = Raif::Utils::HtmlToMarkdownConverter.convert(readable_content)
|
46
|
+
|
47
|
+
tool_invocation.update!(
|
48
|
+
result: {
|
49
|
+
status: response.status,
|
50
|
+
content: markdown_content
|
51
|
+
}
|
52
|
+
)
|
53
|
+
|
54
|
+
tool_invocation.result
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::ModelTools::WikipediaSearch < Raif::ModelTool
|
4
|
+
|
5
|
+
def self.example_model_invocation
|
6
|
+
{
|
7
|
+
"name" => tool_name,
|
8
|
+
"arguments" => { "query": "Jimmy Buffett" }
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
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
|
27
|
+
"Search Wikipedia for information"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.observation_for_invocation(tool_invocation)
|
31
|
+
return "No results found" unless tool_invocation.result.present?
|
32
|
+
|
33
|
+
JSON.pretty_generate(tool_invocation.result)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.process_invocation(tool_invocation)
|
37
|
+
query = tool_invocation.tool_arguments["query"]
|
38
|
+
|
39
|
+
conn = Faraday.new(url: "https://en.wikipedia.org/w/api.php")
|
40
|
+
|
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
|
49
|
+
|
50
|
+
if response.success?
|
51
|
+
results = JSON.parse(response.body)
|
52
|
+
search_results = results.dig("query", "search") || []
|
53
|
+
|
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
|
74
|
+
|
75
|
+
tool_invocation.result
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Raif
|
4
|
+
class Task < Raif::ApplicationRecord
|
5
|
+
include Raif::Concerns::HasLlm
|
6
|
+
include Raif::Concerns::HasRequestedLanguage
|
7
|
+
include Raif::Concerns::HasAvailableModelTools
|
8
|
+
include Raif::Concerns::InvokesModelTools
|
9
|
+
include Raif::Concerns::LlmResponseParsing
|
10
|
+
|
11
|
+
belongs_to :creator, polymorphic: true
|
12
|
+
|
13
|
+
has_one :raif_model_completion, as: :source, dependent: :destroy, class_name: "Raif::ModelCompletion"
|
14
|
+
|
15
|
+
boolean_timestamp :started_at
|
16
|
+
boolean_timestamp :completed_at
|
17
|
+
boolean_timestamp :failed_at
|
18
|
+
|
19
|
+
normalizes :prompt, :system_prompt, with: ->(text){ text&.strip }
|
20
|
+
|
21
|
+
delegate :json_response_schema, to: :class
|
22
|
+
|
23
|
+
after_initialize -> { self.available_model_tools ||= [] }
|
24
|
+
|
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)
|
27
|
+
|
28
|
+
after_initialize -> { self.response_format = format }, if: :new_record?
|
29
|
+
end
|
30
|
+
|
31
|
+
# 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.
|
32
|
+
# It will also create a new Raif::ModelCompletion record.
|
33
|
+
#
|
34
|
+
# @param creator [Object] The creator of the task (polymorphic association)
|
35
|
+
# @param available_model_tools [Array<Class>] Optional array of model tool classes that will be provided to the LLM for it to invoke.
|
36
|
+
# @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.
|
37
|
+
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
38
|
+
# @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)
|
41
|
+
task.save!
|
42
|
+
task.run
|
43
|
+
task
|
44
|
+
rescue StandardError => e
|
45
|
+
task&.failed!
|
46
|
+
|
47
|
+
logger.error e.message
|
48
|
+
logger.error e.backtrace.join("\n")
|
49
|
+
|
50
|
+
if defined?(Airbrake)
|
51
|
+
notice = Airbrake.build_notice(e)
|
52
|
+
notice[:context][:component] = "raif_task"
|
53
|
+
notice[:context][:action] = name
|
54
|
+
|
55
|
+
Airbrake.notify(notice)
|
56
|
+
end
|
57
|
+
|
58
|
+
task
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
update_columns(started_at: Time.current) if started_at.nil?
|
63
|
+
|
64
|
+
populate_prompts
|
65
|
+
messages = [{ "role" => "user", "content" => prompt }]
|
66
|
+
mc = llm.chat(
|
67
|
+
messages: messages,
|
68
|
+
source: self,
|
69
|
+
system_prompt: system_prompt,
|
70
|
+
response_format: response_format.to_sym,
|
71
|
+
available_model_tools: available_model_tools
|
72
|
+
)
|
73
|
+
|
74
|
+
self.raif_model_completion = mc.becomes(Raif::ModelCompletion)
|
75
|
+
|
76
|
+
update(raw_response: raif_model_completion.raw_response)
|
77
|
+
|
78
|
+
process_model_tool_invocations
|
79
|
+
completed!
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the LLM prompt for the task.
|
84
|
+
#
|
85
|
+
# @param creator [Object] The creator of the task (polymorphic association)
|
86
|
+
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
87
|
+
# @return [String] The LLM prompt for the task.
|
88
|
+
def self.prompt(creator:, **args)
|
89
|
+
new(creator:, **args).prompt
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns the LLM system prompt for the task.
|
93
|
+
#
|
94
|
+
# @param creator [Object] The creator of the task (polymorphic association)
|
95
|
+
# @param args [Hash] Additional arguments to pass to the instance of the task that is created.
|
96
|
+
# @return [String] The LLM system prompt for the task.
|
97
|
+
def self.system_prompt(creator:, **args)
|
98
|
+
new(creator:, **args).system_prompt
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.json_response_schema
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def build_prompt
|
108
|
+
raise NotImplementedError, "Raif::Task subclasses must implement #build_prompt"
|
109
|
+
end
|
110
|
+
|
111
|
+
def build_system_prompt
|
112
|
+
sp = Raif.config.task_system_prompt_intro
|
113
|
+
sp += system_prompt_language_preference if requested_language_key.present?
|
114
|
+
sp
|
115
|
+
end
|
116
|
+
|
117
|
+
def populate_prompts
|
118
|
+
self.requested_language_key ||= creator.preferred_language_key if creator.respond_to?(:preferred_language_key)
|
119
|
+
self.prompt = build_prompt
|
120
|
+
self.system_prompt = build_system_prompt
|
121
|
+
end
|
122
|
+
|
123
|
+
def process_model_tool_invocations
|
124
|
+
return unless response_format_json?
|
125
|
+
return unless parsed_response.is_a?(Hash)
|
126
|
+
return unless parsed_response["tools"].present? && parsed_response["tools"].is_a?(Array)
|
127
|
+
|
128
|
+
parsed_response["tools"].each do |t|
|
129
|
+
tool_klass = available_model_tools_map[t["name"]]
|
130
|
+
next unless tool_klass
|
131
|
+
|
132
|
+
tool_klass.invoke_tool(tool_arguments: t["arguments"], source: self)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Raif::UserToolInvocation < Raif::ApplicationRecord
|
4
|
+
belongs_to :raif_conversation_entry, class_name: "Raif::ConversationEntry"
|
5
|
+
|
6
|
+
after_initialize -> { self.tool_settings ||= {} }
|
7
|
+
|
8
|
+
delegate :tool_name, :tool_key, to: :class
|
9
|
+
|
10
|
+
def message_input_placeholder
|
11
|
+
I18n.t("#{self.class.name.underscore.gsub("/", ".")}.message_input_placeholder", default: nil)
|
12
|
+
end
|
13
|
+
|
14
|
+
def as_user_message
|
15
|
+
# implement in subclasses
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.tool_name
|
19
|
+
I18n.t("#{name.underscore.gsub("/", ".")}.name")
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.tool_key
|
23
|
+
model_name.element
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.tool_params
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%= t("raif.admin.layouts.admin.title") %></title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<%= csp_meta_tag %>
|
8
|
+
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" nonce="<%= request.content_security_policy_nonce %>"></script>
|
11
|
+
|
12
|
+
<%= stylesheet_link_tag "raif_admin" %>
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body class="raif-admin">
|
16
|
+
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
17
|
+
<div class="container-fluid">
|
18
|
+
<a class="navbar-brand fw-bold" href="<%= raif.admin_tasks_path %>"><%= t("raif.admin.layouts.admin.title") %></a>
|
19
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
20
|
+
<span class="navbar-toggler-icon"></span>
|
21
|
+
</button>
|
22
|
+
<div class="collapse navbar-collapse" id="navbarCollapse">
|
23
|
+
<ul class="navbar-nav ms-auto mb-2 mb-md-0">
|
24
|
+
</ul>
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
</nav>
|
28
|
+
|
29
|
+
<div class="container-fluid">
|
30
|
+
<div class="row">
|
31
|
+
<!-- Left sidebar navigation -->
|
32
|
+
<div class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
33
|
+
<div class="position-sticky pt-3">
|
34
|
+
<ul class="nav flex-column">
|
35
|
+
<li class="nav-item">
|
36
|
+
<a class="nav-link <%= current_page?(raif.admin_tasks_path) ? "active" : "" %>" href="<%= raif.admin_tasks_path %>">
|
37
|
+
<span class="d-inline-block me-2">
|
38
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-check" viewBox="0 0 16 16">
|
39
|
+
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3.854 2.146a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 1 1 .708-.708L2 3.293l1.146-1.147a.5.5 0 0 1 .708 0zm0 4a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 1 1 .708-.708L2 7.293l1.146-1.147a.5.5 0 0 1 .708 0zm0 4a.5.5 0 0 1 0 .708l-1.5 1.5a.5.5 0 0 1-.708 0l-.5-.5a.5.5 0 0 1 .708-.708l.146.147 1.146-1.147a.5.5 0 0 1 .708 0z" />
|
40
|
+
</svg>
|
41
|
+
</span>
|
42
|
+
<%= t("raif.admin.common.tasks") %>
|
43
|
+
</a>
|
44
|
+
</li>
|
45
|
+
<li class="nav-item">
|
46
|
+
<a class="nav-link <%= current_page?(raif.admin_conversations_path) ? "active" : "" %>" href="<%= raif.admin_conversations_path %>">
|
47
|
+
<span class="d-inline-block me-2">
|
48
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-dots" viewBox="0 0 16 16">
|
49
|
+
<path d="M5 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" />
|
50
|
+
<path d="m2.165 15.803.02-.004c1.83-.363 2.948-.842 3.468-1.105A9.06 9.06 0 0 0 8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6a10.437 10.437 0 0 1-.524 2.318l-.003.011a10.722 10.722 0 0 1-.244.637c-.079.186.074.394.273.362a21.673 21.673 0 0 0 .693-.125zm.8-3.108a1 1 0 0 0-.287-.801C1.618 10.83 1 9.468 1 8c0-3.192 3.004-6 7-6s7 2.808 7 6c0 3.193-3.004 6-7 6a8.06 8.06 0 0 1-2.088-.272 1 1 0 0 0-.711.074c-.387.196-1.24.57-2.634.893a10.97 10.97 0 0 0 .398-2z" />
|
51
|
+
</svg>
|
52
|
+
</span>
|
53
|
+
<%= t("raif.admin.common.conversations") %>
|
54
|
+
</a>
|
55
|
+
</li>
|
56
|
+
<li class="nav-item">
|
57
|
+
<a class="nav-link <%= current_page?(raif.admin_model_completions_path) ? "active" : "" %>" href="<%= raif.admin_model_completions_path %>">
|
58
|
+
<span class="d-inline-block me-2">
|
59
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-robot" viewBox="0 0 16 16">
|
60
|
+
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a24.767 24.767 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25.286 25.286 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135Z" />
|
61
|
+
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2V1.866ZM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5Z" />
|
62
|
+
</svg>
|
63
|
+
</span>
|
64
|
+
<%= t("raif.admin.common.model_completions") %>
|
65
|
+
</a>
|
66
|
+
</li>
|
67
|
+
<li class="nav-item">
|
68
|
+
<a class="nav-link <%= current_page?(raif.admin_model_tool_invocations_path) ? "active" : "" %>" href="<%= raif.admin_model_tool_invocations_path %>">
|
69
|
+
<span class="d-inline-block me-2">
|
70
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-tools" viewBox="0 0 16 16">
|
71
|
+
<path d="M1 0 0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.27 3.27a.997.997 0 0 0 1.414 0l1.586-1.586a.997.997 0 0 0 0-1.414l-3.27-3.27a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3c0-.269-.035-.53-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814L1 0Zm9.646 10.646a.5.5 0 0 1 .708 0l2.914 2.915a.5.5 0 0 1-.707.707l-2.915-2.914a.5.5 0 0 1 0-.708ZM3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026L3 11Z" />
|
72
|
+
</svg>
|
73
|
+
</span>
|
74
|
+
<%= t("raif.admin.common.model_tool_invocations") %>
|
75
|
+
</a>
|
76
|
+
</li>
|
77
|
+
<li class="nav-item">
|
78
|
+
<a class="nav-link <%= current_page?(raif.admin_agents_path) ? "active" : "" %>" href="<%= raif.admin_agents_path %>">
|
79
|
+
<span class="d-inline-block me-2">
|
80
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cpu" viewBox="0 0 16 16">
|
81
|
+
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
|
82
|
+
</svg>
|
83
|
+
</span>
|
84
|
+
<%= t("raif.admin.common.agents") %>
|
85
|
+
</a>
|
86
|
+
</li>
|
87
|
+
</ul>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
|
91
|
+
<!-- Main content area -->
|
92
|
+
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
93
|
+
<%= yield %>
|
94
|
+
</main>
|
95
|
+
</div>
|
96
|
+
</div>
|
97
|
+
</body>
|
98
|
+
</html>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<tr id="<%= dom_id(agent) %>" class="raif-agent">
|
2
|
+
<td><%= link_to "##{agent.id}", raif.admin_agent_path(agent) %></td>
|
3
|
+
<td><small class="text-muted"><%= agent.created_at.rfc822 %></small></td>
|
4
|
+
<td><small class="text-muted"><%= truncate(agent.task, length: 100) %></small></td>
|
5
|
+
<td>
|
6
|
+
<% if agent.completed_at? %>
|
7
|
+
<span class="badge bg-success"><%= t("raif.admin.common.completed") %></span>
|
8
|
+
<% elsif agent.failed_at? %>
|
9
|
+
<span class="badge bg-danger"><%= t("raif.admin.common.failed") %></span>
|
10
|
+
<% elsif agent.started_at? %>
|
11
|
+
<span class="badge bg-warning"><%= t("raif.admin.common.running") %></span>
|
12
|
+
<% else %>
|
13
|
+
<span class="badge bg-secondary"><%= t("raif.admin.common.pending") %></span>
|
14
|
+
<% end %>
|
15
|
+
</td>
|
16
|
+
<td><%= agent.iteration_count %> / <%= agent.max_iterations %></td>
|
17
|
+
<td><small class="text-muted"><%= truncate(agent.final_answer, length: 100) if agent.final_answer %></small></td>
|
18
|
+
</tr>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<div class="message <%= message["role"] %> mb-3">
|
2
|
+
<div class="message-header d-flex justify-content-between align-items-center mb-2 px-2">
|
3
|
+
<strong class="<%= message["role"] == "user" ? "text-primary" : "text-success" %>">
|
4
|
+
<%= message["role"].capitalize %>
|
5
|
+
<% if message[:counter] == 0 && message["role"] == 'user' %>
|
6
|
+
(<%= t("raif.admin.common.initial_task") %>)
|
7
|
+
<% end %>
|
8
|
+
</strong>
|
9
|
+
<span class="badge bg-secondary">#<%= message_count %></span>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="message-content px-3 py-2">
|
13
|
+
<code><%= message["content"] %></code>
|
14
|
+
</div>
|
15
|
+
</div>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<h1 class="my-4"><%= t("raif.admin.common.agents") %></h1>
|
2
|
+
|
3
|
+
<div class="row">
|
4
|
+
<div class="col-12">
|
5
|
+
<% if @agents.any? %>
|
6
|
+
<div class="table-responsive">
|
7
|
+
<table class="table table-striped table-hover">
|
8
|
+
<thead class="table-light">
|
9
|
+
<tr>
|
10
|
+
<th><%= t("raif.admin.common.id") %></th>
|
11
|
+
<th><%= t("raif.admin.common.created_at") %></th>
|
12
|
+
<th><%= t("raif.admin.common.task") %></th>
|
13
|
+
<th><%= t("raif.admin.common.status") %></th>
|
14
|
+
<th><%= t("raif.admin.common.iterations") %></th>
|
15
|
+
<th><%= t("raif.admin.common.final_answer") %></th>
|
16
|
+
</tr>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<%= render partial: "raif/admin/agents/agent", collection: @agents %>
|
20
|
+
</tbody>
|
21
|
+
</table>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div class="mt-4">
|
25
|
+
<%== pagy_bootstrap_nav(@pagy) %>
|
26
|
+
</div>
|
27
|
+
<% else %>
|
28
|
+
<div class="alert alert-info">
|
29
|
+
<%= t("raif.admin.common.no_agents") %>
|
30
|
+
</div>
|
31
|
+
<% end %>
|
32
|
+
</div>
|
33
|
+
</div>
|