raif 1.0.0 → 1.1.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +200 -41
  3. data/app/assets/stylesheets/raif/admin/stats.scss +12 -0
  4. data/app/controllers/raif/admin/application_controller.rb +14 -0
  5. data/app/controllers/raif/admin/stats/tasks_controller.rb +25 -0
  6. data/app/controllers/raif/admin/stats_controller.rb +19 -0
  7. data/app/controllers/raif/admin/tasks_controller.rb +18 -2
  8. data/app/controllers/raif/conversations_controller.rb +5 -1
  9. data/app/models/raif/agent.rb +11 -9
  10. data/app/models/raif/agents/native_tool_calling_agent.rb +11 -1
  11. data/app/models/raif/agents/re_act_agent.rb +6 -0
  12. data/app/models/raif/concerns/has_available_model_tools.rb +1 -1
  13. data/app/models/raif/concerns/json_schema_definition.rb +28 -0
  14. data/app/models/raif/concerns/llm_response_parsing.rb +23 -1
  15. data/app/models/raif/concerns/llm_temperature.rb +17 -0
  16. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +51 -0
  17. data/app/models/raif/concerns/llms/bedrock_claude/message_formatting.rb +70 -0
  18. data/app/models/raif/concerns/llms/message_formatting.rb +41 -0
  19. data/app/models/raif/concerns/llms/open_ai/message_formatting.rb +41 -0
  20. data/app/models/raif/conversation.rb +11 -3
  21. data/app/models/raif/conversation_entry.rb +22 -6
  22. data/app/models/raif/embedding_model.rb +22 -0
  23. data/app/models/raif/embedding_models/bedrock_titan.rb +34 -0
  24. data/app/models/raif/embedding_models/open_ai.rb +40 -0
  25. data/app/models/raif/llm.rb +39 -6
  26. data/app/models/raif/llms/anthropic.rb +23 -28
  27. data/app/models/raif/llms/bedrock_claude.rb +33 -19
  28. data/app/models/raif/llms/open_ai.rb +14 -17
  29. data/app/models/raif/llms/open_router.rb +93 -0
  30. data/app/models/raif/model_completion.rb +21 -2
  31. data/app/models/raif/model_file_input.rb +113 -0
  32. data/app/models/raif/model_image_input.rb +4 -0
  33. data/app/models/raif/model_tool.rb +77 -51
  34. data/app/models/raif/model_tool_invocation.rb +8 -6
  35. data/app/models/raif/model_tools/agent_final_answer.rb +18 -27
  36. data/app/models/raif/model_tools/fetch_url.rb +27 -36
  37. data/app/models/raif/model_tools/wikipedia_search.rb +46 -55
  38. data/app/models/raif/task.rb +71 -16
  39. data/app/views/layouts/raif/admin.html.erb +10 -0
  40. data/app/views/raif/admin/agents/show.html.erb +3 -1
  41. data/app/views/raif/admin/conversations/_conversation.html.erb +1 -1
  42. data/app/views/raif/admin/conversations/show.html.erb +3 -1
  43. data/app/views/raif/admin/model_completions/_model_completion.html.erb +1 -0
  44. data/app/views/raif/admin/model_completions/index.html.erb +1 -0
  45. data/app/views/raif/admin/model_completions/show.html.erb +30 -3
  46. data/app/views/raif/admin/stats/index.html.erb +128 -0
  47. data/app/views/raif/admin/stats/tasks/index.html.erb +45 -0
  48. data/app/views/raif/admin/tasks/_task.html.erb +5 -4
  49. data/app/views/raif/admin/tasks/index.html.erb +20 -2
  50. data/app/views/raif/admin/tasks/show.html.erb +3 -1
  51. data/app/views/raif/conversation_entries/_conversation_entry.html.erb +18 -14
  52. data/app/views/raif/conversation_entries/_form.html.erb +1 -1
  53. data/app/views/raif/conversation_entries/_form_with_available_tools.html.erb +4 -4
  54. data/app/views/raif/conversation_entries/_message.html.erb +10 -3
  55. data/config/locales/admin.en.yml +14 -0
  56. data/config/locales/en.yml +25 -3
  57. data/config/routes.rb +6 -0
  58. data/db/migrate/20250421202149_add_response_format_to_raif_conversations.rb +7 -0
  59. data/db/migrate/20250424200755_add_cost_columns_to_raif_model_completions.rb +14 -0
  60. data/db/migrate/20250424232946_add_created_at_indexes.rb +11 -0
  61. data/db/migrate/20250502155330_add_status_indexes_to_raif_tasks.rb +14 -0
  62. data/db/migrate/20250507155314_add_retry_count_to_raif_model_completions.rb +7 -0
  63. data/lib/generators/raif/agent/agent_generator.rb +22 -12
  64. data/lib/generators/raif/agent/templates/agent.rb.tt +3 -3
  65. data/lib/generators/raif/agent/templates/application_agent.rb.tt +7 -0
  66. data/lib/generators/raif/conversation/conversation_generator.rb +10 -0
  67. data/lib/generators/raif/conversation/templates/application_conversation.rb.tt +7 -0
  68. data/lib/generators/raif/conversation/templates/conversation.rb.tt +13 -11
  69. data/lib/generators/raif/install/templates/initializer.rb +50 -6
  70. data/lib/generators/raif/model_tool/model_tool_generator.rb +0 -5
  71. data/lib/generators/raif/model_tool/templates/model_tool.rb.tt +69 -56
  72. data/lib/generators/raif/task/templates/task.rb.tt +34 -23
  73. data/lib/raif/configuration.rb +40 -3
  74. data/lib/raif/embedding_model_registry.rb +83 -0
  75. data/lib/raif/engine.rb +34 -1
  76. data/lib/raif/errors/{open_ai/api_error.rb → invalid_model_file_input_error.rb} +1 -3
  77. data/lib/raif/errors/{anthropic/api_error.rb → invalid_model_image_input_error.rb} +1 -3
  78. data/lib/raif/errors/unsupported_feature_error.rb +8 -0
  79. data/lib/raif/errors.rb +3 -2
  80. data/lib/raif/json_schema_builder.rb +104 -0
  81. data/lib/raif/llm_registry.rb +205 -0
  82. data/lib/raif/version.rb +1 -1
  83. data/lib/raif.rb +5 -32
  84. data/lib/tasks/raif_tasks.rake +9 -4
  85. metadata +32 -19
  86. 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
@@ -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
@@ -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
@@ -32,6 +32,16 @@
32
32
  <div class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
33
33
  <div class="position-sticky pt-3">
34
34
  <ul class="nav flex-column">
35
+ <li class="nav-item">
36
+ <a class="nav-link <%= current_page?(raif.admin_stats_path) ? "active" : "" %>" href="<%= raif.admin_stats_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-bar-chart" viewBox="0 0 16 16">
39
+ <path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z" />
40
+ </svg>
41
+ </span>
42
+ <%= t("raif.admin.common.stats") %>
43
+ </a>
44
+ </li>
35
45
  <li class="nav-item">
36
46
  <a class="nav-link <%= current_page?(raif.admin_tasks_path) ? "active" : "" %>" href="<%= raif.admin_tasks_path %>">
37
47
  <span class="d-inline-block me-2">
@@ -18,7 +18,9 @@
18
18
  </div>
19
19
  <div class="row mb-3">
20
20
  <div class="col-md-3"><strong><%= t("raif.admin.common.creator") %>:</strong></div>
21
- <div class="col-md-9"><%= @agent.creator_type %> #<%= @agent.creator_id %></div>
21
+ <div class="col-md-9">
22
+ <%= @agent.creator.try(:raif_display_name) || "#{@agent.creator_type} ##{@agent.creator_id}" %>
23
+ </div>
22
24
  </div>
23
25
  <div class="row mb-3">
24
26
  <div class="col-md-3"><strong><%= t("raif.admin.common.status") %>:</strong></div>
@@ -1,7 +1,7 @@
1
1
  <tr id="<%= dom_id(conversation) %>" class="raif-conversation">
2
2
  <td><%= link_to "##{conversation.id}", raif.admin_conversation_path(conversation) %></td>
3
3
  <td><small class="text-muted"><%= conversation.created_at.rfc822 %></small></td>
4
- <td><%= conversation.creator_type %> #<%= conversation.creator_id %></td>
4
+ <td><%= conversation.creator.try(:raif_display_name) || "#{conversation.creator_type} ##{conversation.creator_id}" %></td>
5
5
  <td><%= conversation.type %></td>
6
6
  <td><%= conversation.conversation_entries_count %></td>
7
7
  </tr>
@@ -14,7 +14,9 @@
14
14
  </div>
15
15
  <div class="row mb-3">
16
16
  <div class="col-md-3"><strong><%= t("raif.admin.common.creator") %>:</strong></div>
17
- <div class="col-md-9"><%= @conversation.creator_type %> #<%= @conversation.creator_id %></div>
17
+ <div class="col-md-9">
18
+ <%= @conversation.creator.try(:raif_display_name) || "#{@conversation.creator_type} ##{@conversation.creator_id}" %>
19
+ </div>
18
20
  </div>
19
21
  <div class="row mb-3">
20
22
  <div class="col-md-3"><strong><%= t("raif.admin.common.type") %>:</strong></div>
@@ -5,5 +5,6 @@
5
5
  <td><%= model_completion.llm_model_key %></td>
6
6
  <td><%= model_completion.response_format %></td>
7
7
  <td><%= model_completion.total_tokens ? number_with_delimiter(model_completion.total_tokens) : "-" %></td>
8
+ <td><%= model_completion.total_cost ? number_to_currency(model_completion.total_cost, precision: 6) : "-" %></td>
8
9
  <td><small class="text-muted"><%= truncate(model_completion.raw_response, length: 100) %></small></td>
9
10
  </tr>
@@ -13,6 +13,7 @@
13
13
  <th><%= t("raif.admin.common.model") %></th>
14
14
  <th><%= t("raif.admin.common.response_format") %></th>
15
15
  <th><%= t("raif.admin.common.total_tokens") %></th>
16
+ <th><%= t("raif.admin.common.total_cost") %></th>
16
17
  <th><%= t("raif.admin.common.response") %></th>
17
18
  </tr>
18
19
  </thead>
@@ -34,15 +34,42 @@
34
34
  </div>
35
35
  <div class="row mb-3">
36
36
  <div class="col-md-3"><strong><%= t("raif.admin.common.prompt_tokens") %>:</strong></div>
37
- <div class="col-md-9"><%= @model_completion.prompt_tokens ? number_with_delimiter(@model_completion.prompt_tokens) : "-" %></div>
37
+ <div class="col-md-9">
38
+ <% if @model_completion.prompt_tokens %>
39
+ <%= number_with_delimiter(@model_completion.prompt_tokens) %>
40
+ <% if @model_completion.prompt_token_cost %>
41
+ <small>(<%= t("raif.admin.common.est_cost") %>: <%= "$" %><%= number_with_precision(@model_completion.prompt_token_cost, precision: 6) %>)</small>
42
+ <% end %>
43
+ <% else %>
44
+ -
45
+ <% end %>
46
+ </div>
38
47
  </div>
39
48
  <div class="row mb-3">
40
49
  <div class="col-md-3"><strong><%= t("raif.admin.common.completion_tokens") %>:</strong></div>
41
- <div class="col-md-9"><%= @model_completion.completion_tokens ? number_with_delimiter(@model_completion.completion_tokens) : "-" %></div>
50
+ <div class="col-md-9">
51
+ <% if @model_completion.completion_tokens %>
52
+ <%= number_with_delimiter(@model_completion.completion_tokens) %>
53
+ <% if @model_completion.output_token_cost %>
54
+ <small>(<%= t("raif.admin.common.est_cost") %>: <%= "$" %><%= number_with_precision(@model_completion.output_token_cost, precision: 6) %>)</small>
55
+ <% end %>
56
+ <% else %>
57
+ -
58
+ <% end %>
59
+ </div>
42
60
  </div>
43
61
  <div class="row mb-3">
44
62
  <div class="col-md-3"><strong><%= t("raif.admin.common.total_tokens") %>:</strong></div>
45
- <div class="col-md-9"><%= @model_completion.total_tokens ? number_with_delimiter(@model_completion.total_tokens) : "-" %></div>
63
+ <div class="col-md-9">
64
+ <% if @model_completion.total_tokens %>
65
+ <%= number_with_delimiter(@model_completion.total_tokens) %>
66
+ <% if @model_completion.total_cost %>
67
+ <small>(<%= t("raif.admin.common.est_cost") %>: <%= "$" %><%= number_with_precision(@model_completion.total_cost, precision: 6) %>)</small>
68
+ <% end %>
69
+ <% else %>
70
+ -
71
+ <% end %>
72
+ </div>
46
73
  </div>
47
74
  </div>
48
75
  </div>
@@ -0,0 +1,128 @@
1
+ <div class="d-flex justify-content-between align-items-center my-4">
2
+ <h1 class="mb-0"><%= t("raif.admin.common.stats") %></h1>
3
+
4
+ <div class="period-filter">
5
+ <%= form_tag raif.admin_stats_path, method: :get, id: "period_filter_form", class: "d-flex align-items-center" do %>
6
+ <%= select_tag :period,
7
+ options_for_select(
8
+ [
9
+ [t("raif.admin.common.period_day"), "day"],
10
+ [t("raif.admin.common.period_week"), "week"],
11
+ [t("raif.admin.common.period_month"), "month"],
12
+ [t("raif.admin.common.period_all"), "all"]
13
+ ],
14
+ @selected_period
15
+ ),
16
+ class: "form-select form-select-sm me-2" %>
17
+ <%= submit_tag t("raif.admin.common.update"), class: "btn btn-sm btn-primary" %>
18
+ <% end %>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="row">
23
+ <div class="col-12">
24
+ <div class="card shadow-sm mb-4">
25
+ <div class="card-body">
26
+ <div class="row g-4">
27
+ <!-- Model Completions -->
28
+ <div class="col-md-4 col-lg-4">
29
+ <div class="stats-card p-3 border rounded shadow-sm h-100">
30
+ <div class="d-flex align-items-center mb-2">
31
+ <span class="stats-icon bg-primary-subtle text-primary rounded p-2 me-3">
32
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-robot" viewBox="0 0 16 16">
33
+ <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" />
34
+ <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" />
35
+ </svg>
36
+ </span>
37
+ <div>
38
+ <h6 class="mb-0 text-muted"><%= t("raif.admin.common.model_completions") %></h6>
39
+ </div>
40
+ </div>
41
+ <div class="d-flex justify-content-between align-items-center">
42
+ <h3 class="fw-bold mb-0"><%= number_with_delimiter(@model_completion_count) %></h3>
43
+ <p class="text-muted mt-2 mb-0"><small><%= t("raif.admin.common.est_cost") %>: <%= number_to_currency(@model_completion_total_cost, precision: 6) %></small></p>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Tasks -->
49
+ <div class="col-md-4 col-lg-4">
50
+ <div class="stats-card p-3 border rounded shadow-sm h-100">
51
+ <div class="d-flex align-items-center mb-2">
52
+ <span class="stats-icon bg-success-subtle text-success rounded p-2 me-3">
53
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-list-check" viewBox="0 0 16 16">
54
+ <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" />
55
+ </svg>
56
+ </span>
57
+ <div>
58
+ <h6 class="mb-0 text-muted"><%= t("raif.admin.common.tasks") %></h6>
59
+ </div>
60
+ </div>
61
+ <div class="d-flex justify-content-between align-items-center">
62
+ <h3 class="fw-bold mb-0"><%= number_with_delimiter(@task_count) %></h3>
63
+ <div class="mt-2">
64
+ <%= link_to raif.admin_stats_tasks_path(period: @selected_period) do %>
65
+ <%= t("raif.admin.common.details") %> &raquo;
66
+ <% end %>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Agents -->
73
+ <div class="col-md-4 col-lg-4">
74
+ <div class="stats-card p-3 border rounded shadow-sm h-100">
75
+ <div class="d-flex align-items-center mb-2">
76
+ <span class="stats-icon bg-danger-subtle text-danger rounded p-2 me-3">
77
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-cpu" viewBox="0 0 16 16">
78
+ <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" />
79
+ </svg>
80
+ </span>
81
+ <div>
82
+ <h6 class="mb-0 text-muted"><%= t("raif.admin.common.agents") %></h6>
83
+ </div>
84
+ </div>
85
+ <h3 class="fw-bold mb-0"><%= number_with_delimiter(@agent_count) %></h3>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- Conversations -->
90
+ <div class="col-md-4 col-lg-4">
91
+ <div class="stats-card p-3 border rounded shadow-sm h-100">
92
+ <div class="d-flex align-items-center mb-2">
93
+ <span class="stats-icon bg-info-subtle text-info rounded p-2 me-3">
94
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-chat-dots" viewBox="0 0 16 16">
95
+ <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" />
96
+ <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-.272a1 1 0 0 0-.711.074c-.387.196-1.24.57-2.634.893a10.97 10.97 0 0 0 .398-2z" />
97
+ </svg>
98
+ </span>
99
+ <div>
100
+ <h6 class="mb-0 text-muted"><%= t("raif.admin.common.conversations") %></h6>
101
+ </div>
102
+ </div>
103
+ <h3 class="fw-bold mb-0"><%= number_with_delimiter(@conversation_count) %></h3>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Conversation Entries -->
108
+ <div class="col-md-4 col-lg-4">
109
+ <div class="stats-card p-3 border rounded shadow-sm h-100">
110
+ <div class="d-flex align-items-center mb-2">
111
+ <span class="stats-icon bg-warning-subtle text-warning rounded p-2 me-3">
112
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-chat-text" viewBox="0 0 16 16">
113
+ <path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z" />
114
+ <path d="M4 5.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zM4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8zm0 2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5z" />
115
+ </svg>
116
+ </span>
117
+ <div>
118
+ <h6 class="mb-0 text-muted"><%= t("raif.admin.common.conversation_entries") %></h6>
119
+ </div>
120
+ </div>
121
+ <h3 class="fw-bold mb-0"><%= number_with_delimiter(@conversation_entry_count) %></h3>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>