raif 1.3.0 → 1.5.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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -7
  3. data/app/assets/builds/raif.css +4 -1
  4. data/app/assets/builds/raif_admin.css +52 -2
  5. data/app/assets/builds/raif_admin_sprockets.js +2709 -0
  6. data/app/assets/javascript/raif/admin/copy_to_clipboard_controller.js +132 -0
  7. data/app/assets/javascript/raif/admin/cost_estimate_controller.js +80 -0
  8. data/app/assets/javascript/raif/admin/judge_config_controller.js +23 -0
  9. data/app/assets/javascript/raif/admin/select_all_checkboxes_controller.js +33 -0
  10. data/app/assets/javascript/raif/admin/sortable_table_controller.js +51 -0
  11. data/app/assets/javascript/raif/admin/table_search_controller.js +15 -0
  12. data/app/assets/javascript/raif/admin/tom_select_controller.js +33 -0
  13. data/app/assets/javascript/raif/controllers/conversations_controller.js +1 -1
  14. data/app/assets/javascript/raif_admin.js +23 -0
  15. data/app/assets/javascript/raif_admin_sprockets.js +24 -0
  16. data/app/assets/stylesheets/raif/admin/conversation.scss +16 -0
  17. data/app/assets/stylesheets/raif/conversations.scss +3 -0
  18. data/app/assets/stylesheets/raif.scss +2 -1
  19. data/app/assets/stylesheets/raif_admin.scss +50 -1
  20. data/app/controllers/raif/admin/agents_controller.rb +27 -1
  21. data/app/controllers/raif/admin/application_controller.rb +16 -0
  22. data/app/controllers/raif/admin/configs_controller.rb +95 -0
  23. data/app/controllers/raif/admin/llms_controller.rb +27 -0
  24. data/app/controllers/raif/admin/model_completions_controller.rb +24 -1
  25. data/app/controllers/raif/admin/model_tool_invocations_controller.rb +7 -1
  26. data/app/controllers/raif/admin/prompt_studio/agents_controller.rb +25 -0
  27. data/app/controllers/raif/admin/prompt_studio/base_controller.rb +32 -0
  28. data/app/controllers/raif/admin/prompt_studio/batch_runs_controller.rb +102 -0
  29. data/app/controllers/raif/admin/prompt_studio/conversations_controller.rb +25 -0
  30. data/app/controllers/raif/admin/prompt_studio/tasks_controller.rb +64 -0
  31. data/app/controllers/raif/admin/stats/model_tool_invocations_controller.rb +21 -0
  32. data/app/controllers/raif/admin/stats/tasks_controller.rb +15 -6
  33. data/app/controllers/raif/admin/stats_controller.rb +32 -3
  34. data/app/controllers/raif/admin/tasks_controller.rb +5 -0
  35. data/app/controllers/raif/conversation_entries_controller.rb +1 -0
  36. data/app/controllers/raif/conversations_controller.rb +10 -2
  37. data/app/helpers/raif/application_helper.rb +40 -0
  38. data/app/jobs/raif/conversation_entry_job.rb +8 -6
  39. data/app/jobs/raif/prompt_studio_batch_run_item_job.rb +11 -0
  40. data/app/jobs/raif/prompt_studio_batch_run_job.rb +15 -0
  41. data/app/jobs/raif/prompt_studio_task_run_job.rb +36 -0
  42. data/app/models/raif/admin/task_stat.rb +7 -0
  43. data/app/models/raif/agent.rb +98 -6
  44. data/app/models/raif/agents/native_tool_calling_agent.rb +179 -52
  45. data/app/models/raif/application_record.rb +18 -0
  46. data/app/models/raif/concerns/agent_inference_stats.rb +35 -0
  47. data/app/models/raif/concerns/has_prompt_templates.rb +88 -0
  48. data/app/models/raif/concerns/has_runtime_duration.rb +41 -0
  49. data/app/models/raif/concerns/json_schema_definition.rb +54 -6
  50. data/app/models/raif/concerns/llm_prompt_caching.rb +20 -0
  51. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +34 -0
  52. data/app/models/raif/concerns/llms/anthropic/response_tool_calls.rb +24 -0
  53. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +8 -0
  54. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +43 -0
  55. data/app/models/raif/concerns/llms/bedrock/response_tool_calls.rb +26 -0
  56. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +8 -0
  57. data/app/models/raif/concerns/llms/google/message_formatting.rb +112 -0
  58. data/app/models/raif/concerns/llms/google/response_tool_calls.rb +32 -0
  59. data/app/models/raif/concerns/llms/google/tool_formatting.rb +76 -0
  60. data/app/models/raif/concerns/llms/message_formatting.rb +41 -5
  61. data/app/models/raif/concerns/llms/open_ai/json_schema_validation.rb +3 -3
  62. data/app/models/raif/concerns/llms/open_ai_completions/message_formatting.rb +22 -0
  63. data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +22 -0
  64. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +8 -0
  65. data/app/models/raif/concerns/llms/open_ai_responses/message_formatting.rb +17 -0
  66. data/app/models/raif/concerns/llms/open_ai_responses/response_tool_calls.rb +26 -0
  67. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +8 -0
  68. data/app/models/raif/concerns/provider_managed_tool_calls.rb +162 -0
  69. data/app/models/raif/concerns/run_with.rb +127 -0
  70. data/app/models/raif/conversation.rb +112 -8
  71. data/app/models/raif/conversation_entry.rb +38 -4
  72. data/app/models/raif/embedding_model.rb +2 -1
  73. data/app/models/raif/embedding_models/bedrock.rb +10 -1
  74. data/app/models/raif/embedding_models/google.rb +37 -0
  75. data/app/models/raif/embedding_models/open_ai.rb +1 -1
  76. data/app/models/raif/evals/llm_judge.rb +70 -0
  77. data/{lib → app/models}/raif/evals/llm_judges/binary.rb +41 -3
  78. data/{lib → app/models}/raif/evals/llm_judges/comparative.rb +41 -3
  79. data/{lib → app/models}/raif/evals/llm_judges/scored.rb +39 -1
  80. data/{lib → app/models}/raif/evals/llm_judges/summarization.rb +40 -2
  81. data/app/models/raif/llm.rb +104 -4
  82. data/app/models/raif/llms/anthropic.rb +32 -22
  83. data/app/models/raif/llms/bedrock.rb +64 -24
  84. data/app/models/raif/llms/google.rb +166 -0
  85. data/app/models/raif/llms/open_ai_base.rb +23 -5
  86. data/app/models/raif/llms/open_ai_completions.rb +14 -12
  87. data/app/models/raif/llms/open_ai_responses.rb +14 -17
  88. data/app/models/raif/llms/open_router.rb +16 -15
  89. data/app/models/raif/model_completion.rb +103 -1
  90. data/app/models/raif/model_tool.rb +55 -5
  91. data/app/models/raif/model_tool_invocation.rb +68 -6
  92. data/app/models/raif/model_tools/agent_final_answer.rb +2 -7
  93. data/app/models/raif/model_tools/provider_managed/code_execution.rb +4 -0
  94. data/app/models/raif/model_tools/provider_managed/image_generation.rb +4 -0
  95. data/app/models/raif/model_tools/provider_managed/web_search.rb +4 -0
  96. data/app/models/raif/prompt_studio_batch_run.rb +155 -0
  97. data/app/models/raif/prompt_studio_batch_run_item.rb +220 -0
  98. data/app/models/raif/streaming_responses/bedrock.rb +60 -1
  99. data/app/models/raif/streaming_responses/google.rb +71 -0
  100. data/app/models/raif/task.rb +85 -18
  101. data/app/models/raif/user_tool_invocation.rb +19 -0
  102. data/app/views/layouts/raif/admin.html.erb +43 -2
  103. data/app/views/raif/admin/agents/_agent.html.erb +9 -0
  104. data/app/views/raif/admin/agents/_conversation_message.html.erb +28 -6
  105. data/app/views/raif/admin/agents/index.html.erb +50 -0
  106. data/app/views/raif/admin/agents/show.html.erb +50 -1
  107. data/app/views/raif/admin/configs/show.html.erb +117 -0
  108. data/app/views/raif/admin/conversations/_conversation_entry.html.erb +29 -34
  109. data/app/views/raif/admin/conversations/show.html.erb +2 -0
  110. data/app/views/raif/admin/llms/index.html.erb +110 -0
  111. data/app/views/raif/admin/model_completions/_model_completion.html.erb +10 -5
  112. data/app/views/raif/admin/model_completions/index.html.erb +40 -1
  113. data/app/views/raif/admin/model_completions/show.html.erb +256 -84
  114. data/app/views/raif/admin/model_tool_invocations/index.html.erb +22 -1
  115. data/app/views/raif/admin/model_tool_invocations/show.html.erb +18 -0
  116. data/app/views/raif/admin/model_tools/_list.html.erb +16 -0
  117. data/app/views/raif/admin/model_tools/_model_tool.html.erb +36 -0
  118. data/app/views/raif/admin/prompt_studio/agents/index.html.erb +56 -0
  119. data/app/views/raif/admin/prompt_studio/agents/show.html.erb +57 -0
  120. data/app/views/raif/admin/prompt_studio/batch_runs/_batch_run_item.html.erb +54 -0
  121. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_config_fields.html.erb +76 -0
  122. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_detail_modal.html.erb +27 -0
  123. data/app/views/raif/admin/prompt_studio/batch_runs/_modal.html.erb +35 -0
  124. data/app/views/raif/admin/prompt_studio/batch_runs/_progress.html.erb +78 -0
  125. data/app/views/raif/admin/prompt_studio/batch_runs/show.html.erb +49 -0
  126. data/app/views/raif/admin/prompt_studio/conversations/index.html.erb +48 -0
  127. data/app/views/raif/admin/prompt_studio/conversations/show.html.erb +36 -0
  128. data/app/views/raif/admin/prompt_studio/shared/_nav_tabs.html.erb +17 -0
  129. data/app/views/raif/admin/prompt_studio/shared/_prompt_comparison.html.erb +87 -0
  130. data/app/views/raif/admin/prompt_studio/shared/_type_filter.html.erb +54 -0
  131. data/app/views/raif/admin/prompt_studio/tasks/_task_result.html.erb +145 -0
  132. data/app/views/raif/admin/prompt_studio/tasks/_task_row.html.erb +12 -0
  133. data/app/views/raif/admin/prompt_studio/tasks/_task_type_filter.html.erb +58 -0
  134. data/app/views/raif/admin/prompt_studio/tasks/_tasks_table.html.erb +22 -0
  135. data/app/views/raif/admin/prompt_studio/tasks/index.html.erb +35 -0
  136. data/app/views/raif/admin/prompt_studio/tasks/show.html.erb +19 -0
  137. data/app/views/raif/admin/stats/_stats_tile.html.erb +34 -0
  138. data/app/views/raif/admin/stats/index.html.erb +71 -88
  139. data/app/views/raif/admin/stats/model_tool_invocations/index.html.erb +43 -0
  140. data/app/views/raif/admin/stats/tasks/index.html.erb +20 -6
  141. data/app/views/raif/admin/tasks/_task.html.erb +1 -0
  142. data/app/views/raif/admin/tasks/index.html.erb +23 -6
  143. data/app/views/raif/admin/tasks/show.html.erb +56 -3
  144. data/app/views/raif/conversation_entries/_form.html.erb +3 -0
  145. data/app/views/raif/conversation_entries/_message.html.erb +10 -6
  146. data/app/views/raif/conversations/_conversation.html.erb +10 -0
  147. data/app/views/raif/conversations/_entry_processed.turbo_stream.erb +12 -0
  148. data/app/views/raif/conversations/index.html.erb +23 -0
  149. data/config/importmap.rb +8 -0
  150. data/config/locales/admin.en.yml +161 -1
  151. data/config/locales/en.yml +67 -4
  152. data/config/routes.rb +10 -0
  153. data/db/migrate/20250904194456_add_generating_entry_response_to_raif_conversations.rb +7 -0
  154. data/db/migrate/20250911125234_add_source_to_raif_tasks.rb +7 -0
  155. data/db/migrate/20251020005853_add_source_to_raif_agents.rb +7 -0
  156. data/db/migrate/20251020011346_rename_task_run_args_to_run_with.rb +7 -0
  157. data/db/migrate/20251020011405_add_run_with_to_raif_agents.rb +13 -0
  158. data/db/migrate/20251024160119_add_llm_messages_max_length_to_raif_conversations.rb +14 -0
  159. data/db/migrate/20251124185033_add_provider_tool_call_id_to_raif_model_tool_invocations.rb +7 -0
  160. data/db/migrate/20251128202941_add_tool_choice_to_raif_model_completions.rb +7 -0
  161. data/db/migrate/20260118144846_add_source_to_raif_conversations.rb +7 -0
  162. data/db/migrate/20260119000000_add_failure_tracking_to_raif_model_completions.rb +10 -0
  163. data/db/migrate/20260119000001_add_completed_at_to_raif_model_completions.rb +8 -0
  164. data/db/migrate/20260119000002_add_started_at_to_raif_model_completions.rb +8 -0
  165. data/db/migrate/20260307000000_add_prompt_studio_run_to_raif_tasks.rb +7 -0
  166. data/db/migrate/20260308000000_create_raif_prompt_studio_batch_runs.rb +27 -0
  167. data/db/migrate/20260308000001_create_raif_prompt_studio_batch_run_items.rb +24 -0
  168. data/db/migrate/20260407000000_add_cache_token_columns_to_raif_model_completions.rb +8 -0
  169. data/lib/generators/raif/agent/agent_generator.rb +18 -0
  170. data/lib/generators/raif/agent/templates/agent.rb.tt +7 -5
  171. data/lib/generators/raif/agent/templates/application_agent.rb.tt +1 -1
  172. data/lib/generators/raif/agent/templates/system_prompt.erb.tt +3 -0
  173. data/lib/generators/raif/conversation/conversation_generator.rb +19 -1
  174. data/lib/generators/raif/conversation/templates/conversation.rb.tt +6 -0
  175. data/lib/generators/raif/conversation/templates/system_prompt.erb.tt +4 -0
  176. data/lib/generators/raif/install/templates/initializer.rb +117 -8
  177. data/lib/generators/raif/task/task_generator.rb +18 -0
  178. data/lib/generators/raif/task/templates/prompt.erb.tt +4 -0
  179. data/lib/generators/raif/task/templates/task.rb.tt +10 -9
  180. data/lib/raif/configuration.rb +47 -2
  181. data/lib/raif/embedding_model_registry.rb +8 -0
  182. data/lib/raif/engine.rb +24 -1
  183. data/lib/raif/errors/blank_response_error.rb +8 -0
  184. data/lib/raif/errors/instance_dependent_schema_error.rb +8 -0
  185. data/lib/raif/errors/prompt_template_error.rb +15 -0
  186. data/lib/raif/errors/streaming_error.rb +6 -3
  187. data/lib/raif/errors.rb +3 -0
  188. data/lib/raif/evals/run.rb +1 -0
  189. data/lib/raif/evals.rb +0 -6
  190. data/lib/raif/json_schema_builder.rb +14 -0
  191. data/lib/raif/llm_registry.rb +433 -42
  192. data/lib/raif/messages.rb +180 -0
  193. data/lib/raif/prompt_studio_comparison_builder.rb +138 -0
  194. data/lib/raif/token_estimator.rb +28 -0
  195. data/lib/raif/version.rb +1 -1
  196. data/lib/raif.rb +11 -0
  197. data/lib/tasks/annotate_rb.rake +10 -0
  198. data/spec/support/rspec_helpers.rb +15 -9
  199. data/spec/support/test_task.rb +9 -0
  200. data/spec/support/test_template_task.rb +41 -0
  201. metadata +108 -15
  202. data/app/models/raif/agents/re_act_agent.rb +0 -127
  203. data/app/models/raif/agents/re_act_step.rb +0 -32
  204. data/app/models/raif/concerns/task_run_args.rb +0 -62
  205. data/lib/raif/evals/llm_judge.rb +0 -32
  206. /data/{lib → app/models}/raif/evals/scoring_rubric.rb +0 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ class ConfigsController < Raif::Admin::ApplicationController
6
+
7
+ def show
8
+ @config = Raif.config
9
+ @config_settings = build_config_settings
10
+ end
11
+
12
+ private
13
+
14
+ def build_config_settings
15
+ [
16
+ { key: "agent_types", value: format_array(@config.agent_types.to_a) },
17
+ { key: "anthropic_api_key", value: mask_sensitive_value(@config.anthropic_api_key) },
18
+ { key: "anthropic_models_enabled", value: @config.anthropic_models_enabled },
19
+ { key: "authorize_admin_controller_action", value: format_proc(@config.authorize_admin_controller_action) },
20
+ { key: "authorize_controller_action", value: format_proc(@config.authorize_controller_action) },
21
+ { key: "aws_bedrock_model_name_prefix", value: @config.aws_bedrock_model_name_prefix },
22
+ { key: "aws_bedrock_region", value: @config.aws_bedrock_region },
23
+ { key: "bedrock_embedding_models_enabled", value: @config.bedrock_embedding_models_enabled },
24
+ { key: "bedrock_models_enabled", value: @config.bedrock_models_enabled },
25
+ { key: "conversation_entries_controller", value: @config.conversation_entries_controller },
26
+ { key: "conversation_llm_messages_max_length_default", value: @config.conversation_llm_messages_max_length_default },
27
+ { key: "conversation_system_prompt_intro", value: truncate_text(@config.conversation_system_prompt_intro, 100) },
28
+ { key: "conversation_types", value: format_array(@config.conversation_types.to_a) },
29
+ { key: "conversations_controller", value: @config.conversations_controller },
30
+ { key: "current_user_method", value: @config.current_user_method },
31
+ { key: "default_embedding_model_key", value: @config.default_embedding_model_key },
32
+ { key: "default_llm_model_key", value: @config.default_llm_model_key },
33
+ { key: "evals_default_llm_judge_model_key", value: @config.evals_default_llm_judge_model_key },
34
+ { key: "evals_verbose_output", value: @config.evals_verbose_output },
35
+ { key: "google_api_key", value: mask_sensitive_value(@config.google_api_key) },
36
+ { key: "google_embedding_models_enabled", value: @config.google_embedding_models_enabled },
37
+ { key: "google_models_enabled", value: @config.google_models_enabled },
38
+ { key: "llm_api_requests_enabled", value: @config.llm_api_requests_enabled },
39
+ { key: "llm_request_max_retries", value: @config.llm_request_max_retries },
40
+ { key: "llm_request_retriable_exceptions", value: @config.llm_request_retriable_exceptions.map(&:name).join(", ") },
41
+ { key: "model_superclass", value: @config.model_superclass },
42
+ { key: "open_ai_api_key", value: mask_sensitive_value(@config.open_ai_api_key) },
43
+ { key: "open_ai_api_version", value: @config.open_ai_api_version },
44
+ { key: "open_ai_auth_header_style", value: @config.open_ai_auth_header_style },
45
+ { key: "open_ai_base_url", value: @config.open_ai_base_url },
46
+ { key: "open_ai_embedding_base_url", value: @config.open_ai_embedding_base_url },
47
+ { key: "open_ai_embedding_models_enabled", value: @config.open_ai_embedding_models_enabled },
48
+ { key: "open_ai_models_enabled", value: @config.open_ai_models_enabled },
49
+ { key: "open_router_api_key", value: mask_sensitive_value(@config.open_router_api_key) },
50
+ { key: "open_router_app_name", value: @config.open_router_app_name },
51
+ { key: "open_router_models_enabled", value: @config.open_router_models_enabled },
52
+ { key: "open_router_site_url", value: @config.open_router_site_url },
53
+ { key: "request_open_timeout", value: @config.request_open_timeout },
54
+ { key: "request_read_timeout", value: @config.request_read_timeout },
55
+ { key: "request_write_timeout", value: @config.request_write_timeout },
56
+ { key: "streaming_update_chunk_size_threshold", value: @config.streaming_update_chunk_size_threshold },
57
+ { key: "task_creator_optional", value: @config.task_creator_optional },
58
+ { key: "task_system_prompt_intro", value: truncate_text(@config.task_system_prompt_intro, 100) },
59
+ { key: "user_tool_types", value: format_array(@config.user_tool_types) }
60
+ ]
61
+ end
62
+
63
+ def mask_sensitive_value(value)
64
+ return "Not set" if value.blank?
65
+ return "Not set" if value.include?("placeholder")
66
+
67
+ "#{value[0...5]}#{"*" * 20}"
68
+ end
69
+
70
+ def format_proc(value)
71
+ return "Not set" unless value.respond_to?(:call)
72
+
73
+ source = value.source_location
74
+ if source
75
+ "Lambda/Proc defined at #{source[0]}:#{source[1]}"
76
+ else
77
+ "Lambda/Proc (source unavailable)"
78
+ end
79
+ end
80
+
81
+ def truncate_text(text, length)
82
+ return "Not set" if text.blank?
83
+ return format_proc(text) if text.respond_to?(:call)
84
+
85
+ text.length > length ? "#{text[0...length]}..." : text
86
+ end
87
+
88
+ def format_array(array)
89
+ return "None" if array.blank?
90
+
91
+ array.join(", ")
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ class LlmsController < Raif::Admin::ApplicationController
6
+
7
+ def index
8
+ @llms = Raif.llm_registry.map do |_key, config|
9
+ llm_class = config[:llm_class]
10
+ llm_class.new(**config.except(:llm_class))
11
+ end
12
+
13
+ @llms.sort_by!(&:name)
14
+
15
+ @provider_names = @llms.map { |llm| llm.class.name.demodulize }.uniq.sort
16
+ @llm_names = @llms.map(&:name).sort
17
+
18
+ @selected_providers = Array(params[:providers]).reject(&:blank?)
19
+ @selected_names = Array(params[:names]).reject(&:blank?)
20
+
21
+ @llms = @llms.select { |llm| @selected_providers.include?(llm.class.name.demodulize) } if @selected_providers.present?
22
+ @llms = @llms.select { |llm| @selected_names.include?(llm.name) } if @selected_names.present?
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -6,7 +6,30 @@ module Raif
6
6
  include Pagy::Backend
7
7
 
8
8
  def index
9
- @pagy, @model_completions = pagy(Raif::ModelCompletion.order(created_at: :desc))
9
+ @selected_status = params[:status].present? ? params[:status].to_sym : :all
10
+ @selected_llm_model_key = params[:llm_model_key].presence
11
+ @llm_model_keys = Raif::ModelCompletion.distinct.order(:llm_model_key).pluck(:llm_model_key)
12
+
13
+ model_completions = Raif::ModelCompletion.order(created_at: :desc)
14
+
15
+ if @selected_status.present? && @selected_status != :all
16
+ case @selected_status
17
+ when :completed
18
+ model_completions = model_completions.completed
19
+ when :failed
20
+ model_completions = model_completions.failed
21
+ when :started
22
+ model_completions = model_completions.started.where(completed_at: nil, failed_at: nil)
23
+ when :pending
24
+ model_completions = model_completions.where(started_at: nil)
25
+ end
26
+ end
27
+
28
+ if @selected_llm_model_key.present?
29
+ model_completions = model_completions.where(llm_model_key: @selected_llm_model_key)
30
+ end
31
+
32
+ @pagy, @model_completions = pagy(model_completions)
10
33
  end
11
34
 
12
35
  def show
@@ -6,7 +6,13 @@ module Raif
6
6
  include Pagy::Backend
7
7
 
8
8
  def index
9
- @pagy, @model_tool_invocations = pagy(Raif::ModelToolInvocation.order(created_at: :desc))
9
+ @tool_types = Raif::ModelToolInvocation.distinct.pluck(:tool_type)
10
+ @selected_type = params[:tool_types].present? && @tool_types.include?(params[:tool_types]) ? params[:tool_types] : "all"
11
+
12
+ model_tool_invocations = Raif::ModelToolInvocation.newest_first
13
+ model_tool_invocations = model_tool_invocations.where(tool_type: @selected_type) if @selected_type.present? && @selected_type != "all"
14
+
15
+ @pagy, @model_tool_invocations = pagy(model_tool_invocations)
10
16
  end
11
17
 
12
18
  def show
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module PromptStudio
6
+ class AgentsController < BaseController
7
+ def index
8
+ @agent_types = Raif::Agent.distinct.pluck(:type).sort
9
+ @selected_type = params[:agent_type] if params[:agent_type].present?
10
+ @llm_model_keys = Raif::Agent.where(type: @selected_type).distinct.pluck(:llm_model_key).compact.sort if @selected_type.present?
11
+
12
+ if @selected_type.present?
13
+ agents = apply_filters(Raif::Agent.where(type: @selected_type)).order(created_at: :desc)
14
+ @pagy, @agents = pagy(agents)
15
+ end
16
+ end
17
+
18
+ def show
19
+ @agent = Raif::Agent.find(params[:id])
20
+ @comparison = build_prompt_comparison(@agent)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module PromptStudio
6
+ class BaseController < Raif::Admin::ApplicationController
7
+ include Pagy::Backend
8
+
9
+ private
10
+
11
+ def build_prompt_comparison(record)
12
+ Raif::PromptStudioComparisonBuilder.build(record)
13
+ end
14
+
15
+ def apply_filters(scope)
16
+ scope = scope.where("#{scope.table_name}.created_at >= ?", Time.zone.parse(params[:created_after])) if params[:created_after].present?
17
+ scope = scope.where(
18
+ "#{scope.table_name}.created_at <= ?",
19
+ Time.zone.parse(params[:created_before]).end_of_day
20
+ ) if params[:created_before].present?
21
+ scope = scope.where(llm_model_key: params[:llm_model_key]) if params[:llm_model_key].present?
22
+ scope
23
+ end
24
+
25
+ helper_method :prompt_studio_runs_enabled?
26
+ def prompt_studio_runs_enabled?
27
+ Raif.config.prompt_studio_runs_enabled
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module PromptStudio
6
+ class BatchRunsController < BaseController
7
+ def create
8
+ unless prompt_studio_runs_enabled?
9
+ redirect_to raif.admin_prompt_studio_tasks_path, alert: t("raif.admin.prompt_studio.common.runs_disabled")
10
+ return
11
+ end
12
+
13
+ source_tasks = resolve_source_tasks
14
+ if source_tasks.empty?
15
+ redirect_to raif.admin_prompt_studio_tasks_path(task_type: params[:task_type]),
16
+ alert: t("raif.admin.prompt_studio.batch_runs.create.no_tasks_selected")
17
+ return
18
+ end
19
+
20
+ available_keys = Raif.available_llm_keys.map(&:to_s)
21
+
22
+ unless params[:llm_model_key].present? && available_keys.include?(params[:llm_model_key])
23
+ redirect_to raif.admin_prompt_studio_tasks_path(task_type: params[:task_type]),
24
+ alert: t("raif.admin.prompt_studio.tasks.rerun.invalid_model")
25
+ return
26
+ end
27
+
28
+ if params[:judge_type].present? && params[:judge_llm_model_key].present? && !available_keys.include?(params[:judge_llm_model_key])
29
+ redirect_to raif.admin_prompt_studio_tasks_path(task_type: params[:task_type]),
30
+ alert: t("raif.admin.prompt_studio.tasks.rerun.invalid_model")
31
+ return
32
+ end
33
+
34
+ batch_run = Raif::PromptStudioBatchRun.new(
35
+ task_type: params[:task_type],
36
+ llm_model_key: params[:llm_model_key],
37
+ judge_type: params[:judge_type].presence,
38
+ judge_llm_model_key: params[:judge_llm_model_key].presence,
39
+ judge_config: build_judge_config,
40
+ total_count: source_tasks.size
41
+ )
42
+
43
+ batch_run.save!
44
+
45
+ source_tasks.each do |task|
46
+ batch_run.items.create!(source_task: task)
47
+ end
48
+
49
+ Raif::PromptStudioBatchRunJob.perform_later(batch_run: batch_run)
50
+
51
+ redirect_to raif.admin_prompt_studio_batch_run_path(batch_run)
52
+ rescue StandardError => e
53
+ redirect_to raif.admin_prompt_studio_tasks_path(task_type: params[:task_type]),
54
+ alert: t("raif.admin.prompt_studio.batch_runs.create.error", message: e.message)
55
+ end
56
+
57
+ def show
58
+ @batch_run = Raif::PromptStudioBatchRun.find(params[:id])
59
+ items = @batch_run.items.includes(:source_task, :result_task, :judge_task).order(:id)
60
+ @pagy, @items = pagy(items)
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_source_tasks
66
+ ids = Array(params[:source_task_ids]).map(&:to_i).reject(&:zero?)
67
+ scope = Raif::Task.where(id: ids).completed
68
+ scope = scope.where(type: params[:task_type]) if params[:task_type].present?
69
+ scope
70
+ end
71
+
72
+ def build_judge_config
73
+ config = case params[:judge_type]
74
+ when "Raif::Evals::LlmJudges::Binary"
75
+ {
76
+ "criteria" => params[:judge_criteria].presence || "",
77
+ "strict_mode" => params[:judge_strict_mode] == "1"
78
+ }
79
+ when "Raif::Evals::LlmJudges::Scored"
80
+ {
81
+ "scoring_rubric" => params[:judge_scoring_rubric].presence || "accuracy"
82
+ }
83
+ when "Raif::Evals::LlmJudges::Comparative"
84
+ {
85
+ "comparison_criteria" => params[:judge_comparison_criteria].presence || ""
86
+ }
87
+ when "Raif::Evals::LlmJudges::Summarization"
88
+ {}
89
+ else
90
+ {}
91
+ end
92
+
93
+ if params[:judge_type].present?
94
+ config["include_original_prompt_as_context"] = params[:judge_include_original_prompt_as_context] == "1"
95
+ end
96
+
97
+ config
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module PromptStudio
6
+ class ConversationsController < BaseController
7
+ def index
8
+ @conversation_types = Raif::Conversation.distinct.pluck(:type).sort
9
+ @selected_type = params[:conversation_type] if params[:conversation_type].present?
10
+ @llm_model_keys = Raif::Conversation.where(type: @selected_type).distinct.pluck(:llm_model_key).compact.sort if @selected_type.present?
11
+
12
+ if @selected_type.present?
13
+ conversations = apply_filters(Raif::Conversation.where(type: @selected_type)).order(created_at: :desc)
14
+ @pagy, @conversations = pagy(conversations)
15
+ end
16
+ end
17
+
18
+ def show
19
+ @conversation = Raif::Conversation.find(params[:id])
20
+ @comparison = build_prompt_comparison(@conversation)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module PromptStudio
6
+ class TasksController < BaseController
7
+ def index
8
+ @task_types = Raif::Task.distinct.pluck(:type).sort
9
+ @selected_type = params[:task_type] if params[:task_type].present?
10
+ @llm_model_keys = Raif::Task.where(type: @selected_type).distinct.pluck(:llm_model_key).compact.sort if @selected_type.present?
11
+
12
+ if @selected_type.present?
13
+ tasks = apply_filters(Raif::Task.where(type: @selected_type).completed).includes(:raif_model_completion).order(created_at: :desc)
14
+ @pagy, @tasks = pagy(tasks)
15
+ end
16
+
17
+ @show_batch_runs = prompt_studio_runs_enabled? && @selected_type.present? && @tasks.present?
18
+ end
19
+
20
+ def show
21
+ @task = Raif::Task.find(params[:id])
22
+ @comparison = build_prompt_comparison(@task)
23
+ @original_task = @task.source if @task.prompt_studio_run? && @task.source.is_a?(Raif::Task)
24
+ @available_llm_keys = Raif.available_llm_keys.map(&:to_s).sort
25
+ end
26
+
27
+ def create
28
+ original_task = Raif::Task.find(params[:source_task_id])
29
+
30
+ unless prompt_studio_runs_enabled?
31
+ redirect_to raif.admin_prompt_studio_task_path(original_task), alert: t("raif.admin.prompt_studio.common.runs_disabled")
32
+ return
33
+ end
34
+
35
+ llm_model_key = params[:llm_model_key]
36
+
37
+ unless llm_model_key.present? && Raif.available_llm_keys.map(&:to_s).include?(llm_model_key)
38
+ redirect_to raif.admin_prompt_studio_task_path(original_task), alert: t("raif.admin.prompt_studio.tasks.rerun.invalid_model")
39
+ return
40
+ end
41
+
42
+ new_task = original_task.class.new(
43
+ creator: original_task.creator,
44
+ source: original_task,
45
+ llm_model_key: llm_model_key,
46
+ available_model_tools: original_task.available_model_tools,
47
+ run_with: original_task.run_with,
48
+ prompt_studio_run: true,
49
+ started_at: Time.current
50
+ )
51
+ new_task.assign_attributes(original_task.prompt_studio_task_attributes)
52
+ new_task.save!
53
+ Raif::PromptStudioTaskRunJob.perform_later(task: new_task)
54
+
55
+ redirect_to raif.admin_prompt_studio_task_path(new_task)
56
+ rescue StandardError => e
57
+ new_task&.update(failed_at: Time.current) unless new_task&.failed_at?
58
+ redirect_to raif.admin_prompt_studio_task_path(original_task || params[:source_task_id]),
59
+ alert: t("raif.admin.prompt_studio.tasks.rerun.error", message: e.message)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ module Admin
5
+ module Stats
6
+ class ModelToolInvocationsController < Raif::Admin::ApplicationController
7
+ def index
8
+ @selected_period = params[:period] || "day"
9
+ @time_range = get_time_range(@selected_period)
10
+
11
+ @model_tool_invocation_count = Raif::ModelToolInvocation.where(created_at: @time_range).count
12
+
13
+ @model_tool_invocation_stats_by_type = Raif::ModelToolInvocation
14
+ .where(created_at: @time_range)
15
+ .group(:tool_type)
16
+ .count
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -7,17 +7,26 @@ module Raif
7
7
  def index
8
8
  @selected_period = params[:period] || "day"
9
9
  @time_range = get_time_range(@selected_period)
10
+ @show_model_breakdown = params[:show_model_breakdown] == "1"
10
11
 
11
12
  @task_count = Raif::Task.where(created_at: @time_range).count
12
13
 
13
- # Get task counts by type
14
- @task_counts_by_type = Raif::Task.where(created_at: @time_range).group(:type).count
14
+ group_columns = @show_model_breakdown ? [:type, :llm_model_key] : [:type]
15
+ select_columns = ["raif_tasks.type"]
16
+ select_columns << "raif_tasks.llm_model_key" if @show_model_breakdown
17
+ select_columns << "COUNT(raif_tasks.id)"
18
+ select_columns << "SUM(raif_model_completions.prompt_token_cost)"
19
+ select_columns << "SUM(raif_model_completions.output_token_cost)"
20
+ select_columns << "SUM(raif_model_completions.total_cost)"
15
21
 
16
- # Get costs by task type
17
- @task_costs_by_type = Raif::Task.joins(:raif_model_completion)
22
+ @task_stats_by_type = Raif::Task.joins(:raif_model_completion)
18
23
  .where(created_at: @time_range)
19
- .group(:type)
20
- .sum("raif_model_completions.total_cost")
24
+ .group(*group_columns)
25
+ .pluck(*select_columns)
26
+ .map do |type, *rest|
27
+ llm_model_key = @show_model_breakdown ? rest.shift : nil
28
+ Raif::Admin::TaskStat.new(type, llm_model_key, *rest)
29
+ end
21
30
  end
22
31
  end
23
32
  end
@@ -7,12 +7,41 @@ module Raif
7
7
  @selected_period = params[:period] || "day"
8
8
  @time_range = get_time_range(@selected_period)
9
9
 
10
- @model_completion_count = Raif::ModelCompletion.where(created_at: @time_range).count
11
- @model_completion_total_cost = Raif::ModelCompletion.where(created_at: @time_range).sum(:total_cost)
12
- @task_count = Raif::Task.where(created_at: @time_range).count
10
+ model_completions = Raif::ModelCompletion.where(created_at: @time_range)
11
+
12
+ @model_completion_count = model_completions.count
13
+ @model_completion_total_cost = model_completions.sum(:total_cost)
14
+ @model_completion_input_token_cost = model_completions.sum(:prompt_token_cost)
15
+ @model_completion_output_token_cost = model_completions.sum(:output_token_cost)
16
+
17
+ tasks = Raif::Task.where(created_at: @time_range)
18
+ @task_count = tasks.count
19
+ @task_total_cost = Raif::ModelCompletion.where(source_type: "Raif::Task", created_at: @time_range).sum(:total_cost)
20
+ @task_input_token_cost = Raif::ModelCompletion.where(source_type: "Raif::Task", created_at: @time_range).sum(:prompt_token_cost)
21
+ @task_output_token_cost = Raif::ModelCompletion.where(source_type: "Raif::Task", created_at: @time_range).sum(:output_token_cost)
22
+
13
23
  @conversation_count = Raif::Conversation.where(created_at: @time_range).count
24
+
14
25
  @conversation_entry_count = Raif::ConversationEntry.where(created_at: @time_range).count
26
+ @conversation_entry_total_cost = Raif::ModelCompletion.where(
27
+ source_type: "Raif::ConversationEntry",
28
+ created_at: @time_range,
29
+ ).sum(:total_cost)
30
+ @conversation_entry_input_token_cost = Raif::ModelCompletion.where(
31
+ source_type: "Raif::ConversationEntry",
32
+ created_at: @time_range,
33
+ ).sum(:prompt_token_cost)
34
+ @conversation_entry_output_token_cost = Raif::ModelCompletion.where(
35
+ source_type: "Raif::ConversationEntry",
36
+ created_at: @time_range,
37
+ ).sum(:output_token_cost)
38
+
15
39
  @agent_count = Raif::Agent.where(created_at: @time_range).count
40
+ @agent_total_cost = Raif::ModelCompletion.where(source_type: "Raif::Agent", created_at: @time_range).sum(:total_cost)
41
+ @agent_input_token_cost = Raif::ModelCompletion.where(source_type: "Raif::Agent", created_at: @time_range).sum(:prompt_token_cost)
42
+ @agent_output_token_cost = Raif::ModelCompletion.where(source_type: "Raif::Agent", created_at: @time_range).sum(:output_token_cost)
43
+
44
+ @model_tool_invocation_count = Raif::ModelToolInvocation.where(created_at: @time_range).count
16
45
  end
17
46
  end
18
47
  end
@@ -12,6 +12,9 @@ module Raif
12
12
  @task_statuses = [:all, :completed, :failed, :in_progress, :pending]
13
13
  @selected_statuses = params[:task_statuses].present? ? params[:task_statuses].to_sym : :all
14
14
 
15
+ @selected_llm_model_key = params[:llm_model_key].presence
16
+ @llm_model_keys = Raif::Task.distinct.order(:llm_model_key).pluck(:llm_model_key)
17
+
15
18
  tasks = Raif::Task.order(created_at: :desc)
16
19
  tasks = tasks.where(type: @selected_type) if @selected_type.present? && @selected_type != "all"
17
20
 
@@ -28,6 +31,8 @@ module Raif
28
31
  end
29
32
  end
30
33
 
34
+ tasks = tasks.where(llm_model_key: @selected_llm_model_key) if @selected_llm_model_key.present?
35
+
31
36
  @pagy, @tasks = pagy(tasks)
32
37
  end
33
38
 
@@ -25,6 +25,7 @@ class Raif::ConversationEntriesController < Raif::ApplicationController
25
25
  @conversation_entry.creator = current_user
26
26
 
27
27
  if @conversation_entry.save
28
+ @conversation.update_columns(generating_entry_response: true)
28
29
  Raif::ConversationEntryJob.perform_later(conversation_entry: @conversation_entry)
29
30
  end
30
31
  end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Raif::ConversationsController < Raif::ApplicationController
4
- before_action :validate_conversation_type
4
+ before_action :validate_conversation_type, unless: ->{ params[:action] == "index" && params[:conversation_type].blank? }
5
+
6
+ def index
7
+ @conversations = conversations_scope
8
+ end
5
9
 
6
10
  def show
7
11
  @conversations = conversations_scope
@@ -26,7 +30,11 @@ private
26
30
  end
27
31
 
28
32
  def conversations_scope
29
- raif_conversation_type.newest_first.where(creator: raif_current_user)
33
+ if params[:conversation_type].present?
34
+ raif_conversation_type
35
+ else
36
+ Raif::Conversation
37
+ end.newest_first.where(creator: raif_current_user)
30
38
  end
31
39
 
32
40
  def conversation_type_param
@@ -3,5 +3,45 @@
3
3
  module Raif
4
4
  module ApplicationHelper
5
5
  include Pagy::Frontend
6
+
7
+ def format_task_response(task)
8
+ if task.response_format_json? && task.raw_response.present?
9
+ JSON.pretty_generate(JSON.parse(task.raw_response))
10
+ else
11
+ task.raw_response
12
+ end
13
+ rescue JSON::ParserError
14
+ task.raw_response
15
+ end
16
+
17
+ def pretty_json(value)
18
+ JSON.pretty_generate(JSON.parse(value))
19
+ rescue StandardError
20
+ value
21
+ end
22
+
23
+ def llm_model_options(selected: nil)
24
+ options = Raif.available_llm_keys.map do |key|
25
+ label = I18n.t("raif.model_names.#{key}", default: key.to_s)
26
+ [label, key.to_s]
27
+ end.sort_by(&:first)
28
+
29
+ options_for_select(options, selected&.to_s)
30
+ end
31
+
32
+ def llm_pricing_json
33
+ pricing = {}
34
+ Raif.available_llm_keys.each do |key|
35
+ config = Raif.llm_config(key)
36
+ next unless config
37
+
38
+ pricing[key.to_s] = {
39
+ input: config[:input_token_cost] || 0,
40
+ output: config[:output_token_cost] || 0
41
+ }
42
+ end
43
+
44
+ pricing.to_json
45
+ end
6
46
  end
7
47
  end
@@ -6,22 +6,24 @@ module Raif
6
6
  before_enqueue do |job|
7
7
  conversation_entry = job.arguments.first[:conversation_entry]
8
8
  conversation_entry.update_columns(started_at: Time.current)
9
+
10
+ unless conversation_entry.raif_conversation.generating_entry_response?
11
+ conversation_entry.raif_conversation.update_columns(generating_entry_response: true)
12
+ end
9
13
  end
10
14
 
11
15
  def perform(conversation_entry:)
12
16
  conversation = conversation_entry.raif_conversation
13
17
  conversation_entry.process_entry!
14
- conversation_entry.broadcast_replace_to conversation
15
18
 
16
- Turbo::StreamsChannel.broadcast_action_to(
17
- conversation,
18
- action: :raif_scroll_to_bottom,
19
- target: ActionView::RecordIdentifier.dom_id(conversation, :entries)
20
- )
19
+ Turbo::StreamsChannel.broadcast_render_to conversation,
20
+ partial: "raif/conversations/entry_processed",
21
+ locals: { conversation: conversation, conversation_entry: conversation_entry }
21
22
  rescue StandardError => e
22
23
  logger.error "Error processing conversation entry: #{e.message}"
23
24
  logger.error e.backtrace.join("\n")
24
25
 
26
+ conversation_entry.raif_conversation.update_columns(generating_entry_response: false)
25
27
  conversation_entry.failed!
26
28
  conversation_entry.broadcast_replace_to conversation
27
29
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raif
4
+ class PromptStudioBatchRunItemJob < ApplicationJob
5
+
6
+ def perform(item:)
7
+ item.execute!
8
+ end
9
+
10
+ end
11
+ end