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,132 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
4
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
5
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
6
+ </svg>`;
7
+
8
+ const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
9
+ <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
10
+ </svg>`;
11
+
12
+ export default class extends Controller {
13
+ connect() {
14
+ this.attachAll();
15
+
16
+ this.observer = new MutationObserver((mutations) => {
17
+ for (const mutation of mutations) {
18
+ for (const node of mutation.addedNodes) {
19
+ if (!(node instanceof Element)) continue;
20
+ if (node.matches && node.matches("pre")) {
21
+ this.attach(node);
22
+ }
23
+ if (node.querySelectorAll) {
24
+ node.querySelectorAll("pre").forEach((pre) => this.attach(pre));
25
+ }
26
+ }
27
+ }
28
+ });
29
+
30
+ this.observer.observe(this.element, { childList: true, subtree: true });
31
+ }
32
+
33
+ disconnect() {
34
+ if (this.observer) {
35
+ this.observer.disconnect();
36
+ this.observer = null;
37
+ }
38
+ }
39
+
40
+ attachAll() {
41
+ this.element.querySelectorAll("pre").forEach((pre) => this.attach(pre));
42
+ }
43
+
44
+ attach(pre) {
45
+ if (pre.dataset.raifCopyAttached === "true") return;
46
+ if (pre.closest("[data-raif-copy-skip]")) return;
47
+ pre.dataset.raifCopyAttached = "true";
48
+
49
+ const wrapper = document.createElement("div");
50
+ wrapper.className = "raif-copyable-pre";
51
+ const classesToCopy = ["mb-0", "mb-2", "mb-3", "mt-1", "mt-2", "mt-3"];
52
+ classesToCopy.forEach((cls) => {
53
+ if (pre.classList.contains(cls)) {
54
+ wrapper.classList.add(cls);
55
+ pre.classList.remove(cls);
56
+ }
57
+ });
58
+
59
+ pre.parentNode.insertBefore(wrapper, pre);
60
+ wrapper.appendChild(pre);
61
+
62
+ const button = document.createElement("button");
63
+ button.type = "button";
64
+ button.className = "btn btn-sm btn-outline-secondary raif-copy-btn";
65
+ button.setAttribute("aria-label", "Copy to clipboard");
66
+ button.setAttribute("title", "Copy to clipboard");
67
+ button.innerHTML = COPY_ICON;
68
+ button.addEventListener("click", (event) => {
69
+ event.preventDefault();
70
+ this.copy(pre, button);
71
+ });
72
+
73
+ wrapper.appendChild(button);
74
+ }
75
+
76
+ async copy(pre, button) {
77
+ const text = pre.textContent;
78
+ let succeeded = false;
79
+
80
+ if (navigator.clipboard && window.isSecureContext) {
81
+ try {
82
+ await navigator.clipboard.writeText(text);
83
+ succeeded = true;
84
+ } catch (_err) {
85
+ succeeded = false;
86
+ }
87
+ }
88
+
89
+ if (!succeeded) {
90
+ succeeded = this.fallbackCopy(text);
91
+ }
92
+
93
+ this.showFeedback(button, succeeded);
94
+ }
95
+
96
+ fallbackCopy(text) {
97
+ const textarea = document.createElement("textarea");
98
+ textarea.value = text;
99
+ textarea.setAttribute("readonly", "");
100
+ textarea.style.position = "absolute";
101
+ textarea.style.left = "-9999px";
102
+ document.body.appendChild(textarea);
103
+ textarea.select();
104
+ let ok = false;
105
+ try {
106
+ ok = document.execCommand("copy");
107
+ } catch (_err) {
108
+ ok = false;
109
+ }
110
+ document.body.removeChild(textarea);
111
+ return ok;
112
+ }
113
+
114
+ showFeedback(button, succeeded) {
115
+ const originalHTML = button.innerHTML;
116
+ const originalTitle = button.getAttribute("title");
117
+ const originalAriaLabel = button.getAttribute("aria-label");
118
+ const message = succeeded ? "Copied" : "Copy failed";
119
+ button.innerHTML = succeeded ? CHECK_ICON : "!";
120
+ button.setAttribute("title", message);
121
+ button.setAttribute("aria-label", message);
122
+ button.classList.add(succeeded ? "raif-copy-btn-success" : "raif-copy-btn-error");
123
+
124
+ clearTimeout(button._raifCopyTimer);
125
+ button._raifCopyTimer = setTimeout(() => {
126
+ button.innerHTML = originalHTML;
127
+ button.setAttribute("title", originalTitle || "Copy to clipboard");
128
+ button.setAttribute("aria-label", originalAriaLabel || "Copy to clipboard");
129
+ button.classList.remove("raif-copy-btn-success", "raif-copy-btn-error");
130
+ }, 1500);
131
+ }
132
+ }
@@ -0,0 +1,80 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["checkbox", "modelSelect", "judgeModelSelect", "judgeTypeSelect", "estimate"];
5
+ static values = { pricing: Object };
6
+
7
+ calculate() {
8
+ if (!this.hasModelSelectTarget || !this.hasEstimateTarget) return;
9
+
10
+ const modelKey = this.modelSelectTarget.value;
11
+ const pricing = this.pricingValue[modelKey];
12
+
13
+ if (!pricing) {
14
+ this.hideEstimate();
15
+ return;
16
+ }
17
+
18
+ const selected = this.checkboxTargets.filter((cb) => cb.checked);
19
+
20
+ if (selected.length === 0) {
21
+ this.hideEstimate();
22
+ return;
23
+ }
24
+
25
+ let totalPromptTokens = 0;
26
+ let totalCompletionTokens = 0;
27
+
28
+ selected.forEach((cb) => {
29
+ totalPromptTokens += parseInt(cb.dataset.promptTokens || 0, 10);
30
+ totalCompletionTokens += parseInt(cb.dataset.completionTokens || 0, 10);
31
+ });
32
+
33
+ const taskInputCost = totalPromptTokens * pricing.input;
34
+ const taskOutputCost = totalCompletionTokens * pricing.output;
35
+ const taskTotalCost = taskInputCost + taskOutputCost;
36
+
37
+ let judgeCost = null;
38
+ const judgeType = this.hasJudgeTypeSelectTarget ? this.judgeTypeSelectTarget.value : "";
39
+ if (judgeType) {
40
+ const judgeModelKey = this.hasJudgeModelSelectTarget ? this.judgeModelSelectTarget.value : "";
41
+ const judgePricing = this.pricingValue[judgeModelKey];
42
+
43
+ if (judgePricing) {
44
+ // Rough estimate: judge input ≈ prompt + completion tokens from the task, judge output ≈ 500 tokens
45
+ const judgeInputTokens = totalPromptTokens + totalCompletionTokens;
46
+ const judgeOutputTokens = selected.length * 500;
47
+ judgeCost = judgeInputTokens * judgePricing.input + judgeOutputTokens * judgePricing.output;
48
+ }
49
+ }
50
+
51
+ const totalCost = taskTotalCost + (judgeCost || 0);
52
+ const avgTokens = Math.round((totalPromptTokens + totalCompletionTokens) / selected.length);
53
+
54
+ let html = `<strong>Estimated cost: ${this.formatCurrency(totalCost)}</strong>`;
55
+ html += `<br><small class="text-muted">Task runs: ${this.formatCurrency(taskTotalCost)} (${selected.length} tasks &times; ~${this.formatNumber(avgTokens)} tokens avg)`;
56
+ if (judgeCost !== null) {
57
+ html += `<br>Judge runs: ~${this.formatCurrency(judgeCost)}`;
58
+ }
59
+ html += "</small>";
60
+
61
+ this.estimateTarget.innerHTML = html;
62
+ this.estimateTarget.classList.remove("d-none");
63
+ }
64
+
65
+ hideEstimate() {
66
+ this.estimateTarget.innerHTML = "";
67
+ this.estimateTarget.classList.add("d-none");
68
+ }
69
+
70
+ formatCurrency(amount) {
71
+ if (amount < 0.01) {
72
+ return `$${amount.toFixed(4)}`;
73
+ }
74
+ return `$${amount.toFixed(2)}`;
75
+ }
76
+
77
+ formatNumber(num) {
78
+ return num.toLocaleString();
79
+ }
80
+ }
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["binary", "scored", "comparative", "summarization", "sharedOptions"];
5
+
6
+ toggle(event) {
7
+ const selectedType = event.target.value;
8
+ const typeMap = {
9
+ "Raif::Evals::LlmJudges::Binary": this.binaryTarget,
10
+ "Raif::Evals::LlmJudges::Scored": this.scoredTarget,
11
+ "Raif::Evals::LlmJudges::Comparative": this.comparativeTarget,
12
+ "Raif::Evals::LlmJudges::Summarization": this.summarizationTarget,
13
+ };
14
+
15
+ Object.values(typeMap).forEach((el) => el.classList.add("d-none"));
16
+ this.sharedOptionsTarget.classList.add("d-none");
17
+
18
+ if (typeMap[selectedType]) {
19
+ typeMap[selectedType].classList.remove("d-none");
20
+ this.sharedOptionsTarget.classList.remove("d-none");
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,33 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["selectAll", "checkbox", "selectedCount", "batchButton"];
5
+ static values = { batchButtonLabel: String };
6
+
7
+ connect() {
8
+ if (this.hasBatchButtonTarget) {
9
+ this.batchButtonLabelValue = this.batchButtonTarget.textContent.trim();
10
+ }
11
+ }
12
+
13
+ toggle() {
14
+ const checked = this.selectAllTarget.checked;
15
+ this.checkboxTargets.forEach((cb) => (cb.checked = checked));
16
+ this.updateCount();
17
+ this.dispatch("toggled");
18
+ }
19
+
20
+ updateCount() {
21
+ const count = this.checkboxTargets.filter((cb) => cb.checked).length;
22
+ this.selectedCountTargets.forEach((el) => {
23
+ el.textContent = count;
24
+ });
25
+ if (this.hasBatchButtonTarget) {
26
+ this.batchButtonTarget.disabled = count === 0;
27
+ this.batchButtonTarget.textContent =
28
+ count > 0
29
+ ? `${this.batchButtonLabelValue} (${count})`
30
+ : this.batchButtonLabelValue;
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,51 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["header", "body"];
5
+
6
+ sort(event) {
7
+ const th = event.currentTarget;
8
+ const colIndex = parseInt(th.dataset.colIndex, 10);
9
+ const type = th.dataset.sortType || "string";
10
+ const currentDir = th.dataset.sortDir;
11
+ const newDir = currentDir === "asc" ? "desc" : "asc";
12
+
13
+ // Reset all headers
14
+ this.headerTargets.forEach((header) => {
15
+ header.dataset.sortDir = "";
16
+ header.querySelector(".sort-indicator")?.remove();
17
+ });
18
+
19
+ th.dataset.sortDir = newDir;
20
+
21
+ // Add sort indicator
22
+ const indicator = document.createElement("span");
23
+ indicator.className = "sort-indicator ms-1";
24
+ indicator.textContent = newDir === "asc" ? "\u25B2" : "\u25BC";
25
+ th.appendChild(indicator);
26
+
27
+ const rows = Array.from(this.bodyTarget.querySelectorAll("tr"));
28
+ rows.sort((a, b) => {
29
+ const aCell = a.children[colIndex];
30
+ const bCell = b.children[colIndex];
31
+ if (!aCell || !bCell) return 0;
32
+
33
+ let aVal = (aCell.dataset.sortValue || aCell.textContent).trim();
34
+ let bVal = (bCell.dataset.sortValue || bCell.textContent).trim();
35
+
36
+ if (type === "number") {
37
+ aVal = parseFloat(aVal) || 0;
38
+ bVal = parseFloat(bVal) || 0;
39
+ } else {
40
+ aVal = aVal.toLowerCase();
41
+ bVal = bVal.toLowerCase();
42
+ }
43
+
44
+ if (aVal < bVal) return newDir === "asc" ? -1 : 1;
45
+ if (aVal > bVal) return newDir === "asc" ? 1 : -1;
46
+ return 0;
47
+ });
48
+
49
+ rows.forEach((row) => this.bodyTarget.appendChild(row));
50
+ }
51
+ }
@@ -0,0 +1,15 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input", "row"];
5
+
6
+ filter() {
7
+ const terms = this.inputTarget.value.toLowerCase().trim().split(/\s+/).filter(Boolean);
8
+
9
+ this.rowTargets.forEach((row) => {
10
+ const text = row.dataset.searchable || row.textContent.toLowerCase();
11
+ const visible = terms.length === 0 || terms.every((term) => text.includes(term));
12
+ row.style.display = visible ? "" : "none";
13
+ });
14
+ }
15
+ }
@@ -0,0 +1,33 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ if (typeof TomSelect === "undefined") return;
6
+
7
+ const isMultiple = this.element.hasAttribute("multiple");
8
+ const blankOption = this.element.querySelector('option[value=""]');
9
+ const placeholder = blankOption ? blankOption.textContent : "";
10
+
11
+ const options = {
12
+ allowEmptyOption: !isMultiple,
13
+ placeholder: placeholder,
14
+ plugins: {},
15
+ };
16
+
17
+ if (isMultiple) {
18
+ options.plugins.remove_button = { title: "Remove" };
19
+ }
20
+
21
+ this.tomSelect = new TomSelect(this.element, options);
22
+
23
+ if (blankOption && !isMultiple) {
24
+ blankOption.textContent = "";
25
+ }
26
+ }
27
+
28
+ disconnect() {
29
+ if (this.tomSelect) {
30
+ this.tomSelect.destroy();
31
+ }
32
+ }
33
+ }
@@ -6,6 +6,6 @@ export default class extends Controller {
6
6
  }
7
7
 
8
8
  scrollToBottom() {
9
- this.element.scrollTo({ top: this.element.scrollHeight, behavior: "smooth" });
9
+ this.element.scrollTo({ top: this.element.scrollHeight });
10
10
  }
11
11
  }
@@ -0,0 +1,23 @@
1
+ import "@hotwired/turbo-rails"
2
+ import { application } from "controllers/application"
3
+
4
+ import JudgeConfigController from "raif/admin/judge_config_controller"
5
+ application.register("raif--judge-config", JudgeConfigController)
6
+
7
+ import SelectAllCheckboxesController from "raif/admin/select_all_checkboxes_controller"
8
+ application.register("raif--select-all-checkboxes", SelectAllCheckboxesController)
9
+
10
+ import CostEstimateController from "raif/admin/cost_estimate_controller"
11
+ application.register("raif--cost-estimate", CostEstimateController)
12
+
13
+ import TomSelectController from "raif/admin/tom_select_controller"
14
+ application.register("raif--tom-select", TomSelectController)
15
+
16
+ import TableSearchController from "raif/admin/table_search_controller"
17
+ application.register("raif--table-search", TableSearchController)
18
+
19
+ import SortableTableController from "raif/admin/sortable_table_controller"
20
+ application.register("raif--sortable-table", SortableTableController)
21
+
22
+ import CopyToClipboardController from "raif/admin/copy_to_clipboard_controller"
23
+ application.register("raif--copy-to-clipboard", CopyToClipboardController)
@@ -0,0 +1,24 @@
1
+ // Sprockets entry point for Raif Admin.
2
+ // This is bundled by esbuild into app/assets/builds/raif_admin_sprockets.js
3
+ // and provides a self-contained Stimulus setup for host apps that don't use importmaps.
4
+ import { Application, Controller } from "@hotwired/stimulus"
5
+
6
+ // Make Controller available globally so the imported controller files can extend it
7
+ window.Stimulus = { Controller: Controller }
8
+
9
+ const application = Application.start()
10
+
11
+ import JudgeConfigController from "./raif/admin/judge_config_controller"
12
+ application.register("raif--judge-config", JudgeConfigController)
13
+
14
+ import SelectAllCheckboxesController from "./raif/admin/select_all_checkboxes_controller"
15
+ application.register("raif--select-all-checkboxes", SelectAllCheckboxesController)
16
+
17
+ import CostEstimateController from "./raif/admin/cost_estimate_controller"
18
+ application.register("raif--cost-estimate", CostEstimateController)
19
+
20
+ import TomSelectController from "./raif/admin/tom_select_controller"
21
+ application.register("raif--tom-select", TomSelectController)
22
+
23
+ import CopyToClipboardController from "./raif/admin/copy_to_clipboard_controller"
24
+ application.register("raif--copy-to-clipboard", CopyToClipboardController)
@@ -29,6 +29,22 @@
29
29
  }
30
30
  }
31
31
 
32
+ &.tool_call {
33
+ border-left: 3px solid #f59e0b;
34
+
35
+ .message-content {
36
+ background-color: #fffbeb;
37
+ }
38
+ }
39
+
40
+ &.tool_call_result {
41
+ border-left: 3px solid rgba(25, 135, 84, 1);
42
+
43
+ .message-content {
44
+ background-color: rgb(232, 255, 244);
45
+ }
46
+ }
47
+
32
48
  .message-content {
33
49
  border-radius: 4px;
34
50
  overflow: hidden;
@@ -0,0 +1,3 @@
1
+ .raif-conversation-entries-container {
2
+ scroll-behavior: smooth;
3
+ }
@@ -1 +1,2 @@
1
- @use "raif/loader";
1
+ @use "raif/loader";
2
+ @use "raif/conversations";
@@ -157,7 +157,7 @@ body.raif-admin {
157
157
  border-radius: $radius;
158
158
  box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
159
159
  background-color: white;
160
- overflow: hidden;
160
+ overflow-x: auto;
161
161
  }
162
162
 
163
163
  // Badges
@@ -220,6 +220,55 @@ body.raif-admin {
220
220
  white-space: pre-wrap;
221
221
  }
222
222
 
223
+ // Copy-to-clipboard wrapper around pre blocks
224
+ .raif-copyable-pre {
225
+ position: relative;
226
+
227
+ > pre {
228
+ margin-bottom: 0;
229
+ }
230
+
231
+ .raif-copy-btn {
232
+ position: absolute;
233
+ top: 0.5rem;
234
+ right: 0.5rem;
235
+ padding: 0.25rem 0.5rem;
236
+ line-height: 1;
237
+ background-color: white;
238
+ box-shadow: 0 1px 2px rgba(50, 50, 93, 0.12);
239
+ opacity: 0;
240
+ transition: opacity 0.15s ease, background-color 0.15s ease, color 0.15s ease;
241
+
242
+ svg {
243
+ display: block;
244
+ }
245
+
246
+ &:focus,
247
+ &:focus-visible {
248
+ opacity: 1;
249
+ }
250
+
251
+ &.raif-copy-btn-success {
252
+ opacity: 1;
253
+ color: white;
254
+ background-color: $accent-color;
255
+ border-color: $accent-color;
256
+ }
257
+
258
+ &.raif-copy-btn-error {
259
+ opacity: 1;
260
+ color: white;
261
+ background-color: $danger-color;
262
+ border-color: $danger-color;
263
+ }
264
+ }
265
+
266
+ &:hover .raif-copy-btn,
267
+ &:focus-within .raif-copy-btn {
268
+ opacity: 1;
269
+ }
270
+ }
271
+
223
272
  // List groups
224
273
  .list-group-item {
225
274
  border-color: $border-color;
@@ -6,7 +6,33 @@ module Raif
6
6
  include Pagy::Backend
7
7
 
8
8
  def index
9
- @pagy, @agents = pagy(Raif::Agent.order(created_at: :desc))
9
+ @agent_types = Raif::Agent.distinct.pluck(:type)
10
+ @selected_type = params[:agent_type].present? ? params[:agent_type] : "all"
11
+
12
+ @selected_status = params[:status].present? ? params[:status].to_sym : :all
13
+
14
+ @selected_llm_model_key = params[:llm_model_key].presence
15
+ @llm_model_keys = Raif::Agent.distinct.order(:llm_model_key).pluck(:llm_model_key)
16
+
17
+ agents = Raif::Agent.order(created_at: :desc)
18
+ agents = agents.where(type: @selected_type) if @selected_type.present? && @selected_type != "all"
19
+
20
+ if @selected_status.present? && @selected_status != :all
21
+ case @selected_status
22
+ when :completed
23
+ agents = agents.completed
24
+ when :failed
25
+ agents = agents.failed
26
+ when :running
27
+ agents = agents.started.where(completed_at: nil, failed_at: nil)
28
+ when :pending
29
+ agents = agents.where(started_at: nil)
30
+ end
31
+ end
32
+
33
+ agents = agents.where(llm_model_key: @selected_llm_model_key) if @selected_llm_model_key.present?
34
+
35
+ @pagy, @agents = pagy(agents)
10
36
  end
11
37
 
12
38
  def show
@@ -29,6 +29,22 @@ module Raif
29
29
  24.hours.ago..Time.current
30
30
  end
31
31
  end
32
+
33
+ helper_method :conversation_message_header_class
34
+ def conversation_message_header_class(message)
35
+ message_type = message["type"] || message["role"]
36
+
37
+ case message_type
38
+ when "user"
39
+ "text-primary"
40
+ when "tool_call"
41
+ "text-warning"
42
+ when "tool_call_result"
43
+ "text-success"
44
+ else
45
+ "text-success"
46
+ end
47
+ end
32
48
  end
33
49
  end
34
50
  end