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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +678 -0
  4. data/Rakefile +20 -0
  5. data/app/assets/builds/raif.css +74 -0
  6. data/app/assets/builds/raif_admin.css +266 -0
  7. data/app/assets/config/raif_manifest.js +1 -0
  8. data/app/assets/javascript/raif/controllers/conversations_controller.js +11 -0
  9. data/app/assets/javascript/raif/stream_actions/raif_scroll_to_bottom.js +12 -0
  10. data/app/assets/javascript/raif.js +10 -0
  11. data/app/assets/stylesheets/raif/admin/conversation.scss +64 -0
  12. data/app/assets/stylesheets/raif/loader.scss +85 -0
  13. data/app/assets/stylesheets/raif.scss +1 -0
  14. data/app/assets/stylesheets/raif_admin.scss +299 -0
  15. data/app/controllers/raif/admin/agents_controller.rb +17 -0
  16. data/app/controllers/raif/admin/application_controller.rb +20 -0
  17. data/app/controllers/raif/admin/conversations_controller.rb +17 -0
  18. data/app/controllers/raif/admin/model_completions_controller.rb +17 -0
  19. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +17 -0
  20. data/app/controllers/raif/admin/tasks_controller.rb +23 -0
  21. data/app/controllers/raif/application_controller.rb +20 -0
  22. data/app/controllers/raif/conversation_entries_controller.rb +60 -0
  23. data/app/controllers/raif/conversations_controller.rb +58 -0
  24. data/app/helpers/raif/application_helper.rb +7 -0
  25. data/app/helpers/raif/shared/conversations_helper.rb +13 -0
  26. data/app/jobs/raif/application_job.rb +8 -0
  27. data/app/jobs/raif/conversation_entry_job.rb +30 -0
  28. data/app/models/raif/agent.rb +133 -0
  29. data/app/models/raif/agents/native_tool_calling_agent.rb +127 -0
  30. data/app/models/raif/agents/re_act_agent.rb +121 -0
  31. data/app/models/raif/agents/re_act_step.rb +33 -0
  32. data/app/models/raif/application_record.rb +14 -0
  33. data/app/models/raif/concerns/boolean_timestamp.rb +69 -0
  34. data/app/models/raif/concerns/has_available_model_tools.rb +13 -0
  35. data/app/models/raif/concerns/has_llm.rb +19 -0
  36. data/app/models/raif/concerns/has_requested_language.rb +20 -0
  37. data/app/models/raif/concerns/invokes_model_tools.rb +13 -0
  38. data/app/models/raif/concerns/llm_response_parsing.rb +44 -0
  39. data/app/models/raif/conversation.rb +67 -0
  40. data/app/models/raif/conversation_entry.rb +85 -0
  41. data/app/models/raif/llm.rb +88 -0
  42. data/app/models/raif/llms/anthropic.rb +120 -0
  43. data/app/models/raif/llms/bedrock_claude.rb +134 -0
  44. data/app/models/raif/llms/open_ai.rb +259 -0
  45. data/app/models/raif/model_completion.rb +28 -0
  46. data/app/models/raif/model_tool.rb +69 -0
  47. data/app/models/raif/model_tool_invocation.rb +43 -0
  48. data/app/models/raif/model_tools/agent_final_answer.rb +46 -0
  49. data/app/models/raif/model_tools/fetch_url.rb +57 -0
  50. data/app/models/raif/model_tools/wikipedia_search.rb +78 -0
  51. data/app/models/raif/task.rb +137 -0
  52. data/app/models/raif/user_tool_invocation.rb +29 -0
  53. data/app/views/layouts/raif/admin.html.erb +98 -0
  54. data/app/views/raif/admin/agents/_agent.html.erb +18 -0
  55. data/app/views/raif/admin/agents/_conversation_message.html.erb +15 -0
  56. data/app/views/raif/admin/agents/index.html.erb +33 -0
  57. data/app/views/raif/admin/agents/show.html.erb +131 -0
  58. data/app/views/raif/admin/conversations/_conversation.html.erb +7 -0
  59. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +34 -0
  60. data/app/views/raif/admin/conversations/index.html.erb +32 -0
  61. data/app/views/raif/admin/conversations/show.html.erb +56 -0
  62. data/app/views/raif/admin/model_completions/_model_completion.html.erb +9 -0
  63. data/app/views/raif/admin/model_completions/index.html.erb +34 -0
  64. data/app/views/raif/admin/model_completions/show.html.erb +117 -0
  65. data/app/views/raif/admin/model_tool_invocations/_model_tool_invocation.html.erb +16 -0
  66. data/app/views/raif/admin/model_tool_invocations/index.html.erb +33 -0
  67. data/app/views/raif/admin/model_tool_invocations/show.html.erb +66 -0
  68. data/app/views/raif/admin/tasks/_task.html.erb +19 -0
  69. data/app/views/raif/admin/tasks/index.html.erb +49 -0
  70. data/app/views/raif/admin/tasks/show.html.erb +176 -0
  71. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +26 -0
  72. data/app/views/raif/conversation_entries/_form.html.erb +25 -0
  73. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -0
  74. data/app/views/raif/conversation_entries/_form_with_user_tool_invocation.html.erb +18 -0
  75. data/app/views/raif/conversation_entries/_message.html.erb +17 -0
  76. data/app/views/raif/conversation_entries/_model_response_avatar.html.erb +1 -0
  77. data/app/views/raif/conversation_entries/_user_avatar.html.erb +1 -0
  78. data/app/views/raif/conversation_entries/create.turbo_stream.erb +11 -0
  79. data/app/views/raif/conversation_entries/new.turbo_stream.erb +6 -0
  80. data/app/views/raif/conversations/_available_user_tools.html.erb +11 -0
  81. data/app/views/raif/conversations/_full_conversation.html.erb +15 -0
  82. data/app/views/raif/conversations/show.html.erb +1 -0
  83. data/config/i18n-tasks.yml +181 -0
  84. data/config/importmap.rb +6 -0
  85. data/config/initializers/pagy.rb +14 -0
  86. data/config/locales/admin.en.yml +91 -0
  87. data/config/locales/en.yml +50 -0
  88. data/config/routes.rb +22 -0
  89. data/db/migrate/20250224234252_create_raif_tables.rb +114 -0
  90. data/lib/generators/raif/agent/agent_generator.rb +22 -0
  91. data/lib/generators/raif/agent/templates/agent.rb.tt +28 -0
  92. data/lib/generators/raif/conversation/conversation_generator.rb +27 -0
  93. data/lib/generators/raif/conversation/templates/conversation.rb.tt +37 -0
  94. data/lib/generators/raif/install/install_generator.rb +31 -0
  95. data/lib/generators/raif/install/templates/initializer.rb +81 -0
  96. data/lib/generators/raif/model_tool/model_tool_generator.rb +27 -0
  97. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +74 -0
  98. data/lib/generators/raif/task/task_generator.rb +28 -0
  99. data/lib/generators/raif/task/templates/application_task.rb.tt +7 -0
  100. data/lib/generators/raif/task/templates/task.rb.tt +52 -0
  101. data/lib/generators/raif/views_generator.rb +22 -0
  102. data/lib/raif/configuration.rb +82 -0
  103. data/lib/raif/default_llms.rb +37 -0
  104. data/lib/raif/engine.rb +86 -0
  105. data/lib/raif/errors/action_not_authorized_error.rb +8 -0
  106. data/lib/raif/errors/anthropic/api_error.rb +10 -0
  107. data/lib/raif/errors/invalid_config_error.rb +8 -0
  108. data/lib/raif/errors/invalid_conversation_type_error.rb +8 -0
  109. data/lib/raif/errors/invalid_user_tool_type_error.rb +8 -0
  110. data/lib/raif/errors/open_ai/api_error.rb +10 -0
  111. data/lib/raif/errors/open_ai/json_schema_error.rb +10 -0
  112. data/lib/raif/errors.rb +9 -0
  113. data/lib/raif/languages.rb +33 -0
  114. data/lib/raif/rspec.rb +7 -0
  115. data/lib/raif/utils/html_to_markdown_converter.rb +7 -0
  116. data/lib/raif/utils/readable_content_extractor.rb +61 -0
  117. data/lib/raif/utils.rb +6 -0
  118. data/lib/raif/version.rb +5 -0
  119. data/lib/raif.rb +65 -0
  120. data/lib/tasks/raif_tasks.rake +6 -0
  121. 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>