raif 1.4.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/assets/builds/raif_admin.css +40 -2
  4. data/app/assets/builds/raif_admin_sprockets.js +2709 -0
  5. data/app/assets/javascript/raif/admin/copy_to_clipboard_controller.js +132 -0
  6. data/app/assets/javascript/raif/admin/cost_estimate_controller.js +80 -0
  7. data/app/assets/javascript/raif/admin/judge_config_controller.js +23 -0
  8. data/app/assets/javascript/raif/admin/select_all_checkboxes_controller.js +33 -0
  9. data/app/assets/javascript/raif/admin/sortable_table_controller.js +51 -0
  10. data/app/assets/javascript/raif/admin/table_search_controller.js +15 -0
  11. data/app/assets/javascript/raif/admin/tom_select_controller.js +33 -0
  12. data/app/assets/javascript/raif_admin.js +23 -0
  13. data/app/assets/javascript/raif_admin_sprockets.js +24 -0
  14. data/app/assets/stylesheets/raif_admin.scss +50 -1
  15. data/app/controllers/raif/admin/agents_controller.rb +27 -1
  16. data/app/controllers/raif/admin/configs_controller.rb +1 -0
  17. data/app/controllers/raif/admin/llms_controller.rb +27 -0
  18. data/app/controllers/raif/admin/model_completions_controller.rb +6 -0
  19. data/app/controllers/raif/admin/prompt_studio/agents_controller.rb +25 -0
  20. data/app/controllers/raif/admin/prompt_studio/base_controller.rb +32 -0
  21. data/app/controllers/raif/admin/prompt_studio/batch_runs_controller.rb +102 -0
  22. data/app/controllers/raif/admin/prompt_studio/conversations_controller.rb +25 -0
  23. data/app/controllers/raif/admin/prompt_studio/tasks_controller.rb +64 -0
  24. data/app/controllers/raif/admin/tasks_controller.rb +5 -0
  25. data/app/helpers/raif/application_helper.rb +40 -0
  26. data/app/jobs/raif/prompt_studio_batch_run_item_job.rb +11 -0
  27. data/app/jobs/raif/prompt_studio_batch_run_job.rb +15 -0
  28. data/app/jobs/raif/prompt_studio_task_run_job.rb +36 -0
  29. data/app/models/raif/agent.rb +36 -5
  30. data/app/models/raif/agents/native_tool_calling_agent.rb +101 -19
  31. data/app/models/raif/concerns/has_prompt_templates.rb +88 -0
  32. data/app/models/raif/concerns/has_runtime_duration.rb +41 -0
  33. data/app/models/raif/concerns/json_schema_definition.rb +16 -3
  34. data/app/models/raif/concerns/llm_prompt_caching.rb +20 -0
  35. data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +6 -0
  36. data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +5 -1
  37. data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +7 -0
  38. data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +4 -0
  39. data/app/models/raif/concerns/llms/google/message_formatting.rb +5 -2
  40. data/app/models/raif/concerns/llms/google/tool_formatting.rb +4 -0
  41. data/app/models/raif/concerns/llms/message_formatting.rb +30 -0
  42. data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +1 -1
  43. data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +4 -0
  44. data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +4 -0
  45. data/app/models/raif/concerns/provider_managed_tool_calls.rb +162 -0
  46. data/app/models/raif/conversation.rb +24 -3
  47. data/app/models/raif/conversation_entry.rb +6 -3
  48. data/app/models/raif/embedding_models/bedrock.rb +10 -1
  49. data/app/models/raif/embedding_models/google.rb +37 -0
  50. data/app/models/raif/evals/llm_judge.rb +70 -0
  51. data/{lib → app/models}/raif/evals/llm_judges/binary.rb +38 -0
  52. data/{lib → app/models}/raif/evals/llm_judges/comparative.rb +38 -0
  53. data/{lib → app/models}/raif/evals/llm_judges/scored.rb +38 -0
  54. data/{lib → app/models}/raif/evals/llm_judges/summarization.rb +38 -0
  55. data/app/models/raif/llm.rb +82 -7
  56. data/app/models/raif/llms/anthropic.rb +26 -4
  57. data/app/models/raif/llms/bedrock.rb +59 -5
  58. data/app/models/raif/llms/google.rb +28 -2
  59. data/app/models/raif/llms/open_ai_base.rb +4 -0
  60. data/app/models/raif/llms/open_ai_completions.rb +9 -2
  61. data/app/models/raif/llms/open_ai_responses.rb +9 -2
  62. data/app/models/raif/llms/open_router.rb +10 -3
  63. data/app/models/raif/model_completion.rb +75 -34
  64. data/app/models/raif/model_tool.rb +45 -3
  65. data/app/models/raif/model_tool_invocation.rb +31 -1
  66. data/app/models/raif/prompt_studio_batch_run.rb +155 -0
  67. data/app/models/raif/prompt_studio_batch_run_item.rb +220 -0
  68. data/app/models/raif/streaming_responses/bedrock.rb +60 -1
  69. data/app/models/raif/task.rb +30 -6
  70. data/app/views/layouts/raif/admin.html.erb +31 -1
  71. data/app/views/raif/admin/agents/_agent.html.erb +1 -0
  72. data/app/views/raif/admin/agents/index.html.erb +48 -0
  73. data/app/views/raif/admin/agents/show.html.erb +4 -0
  74. data/app/views/raif/admin/llms/index.html.erb +110 -0
  75. data/app/views/raif/admin/model_completions/_model_completion.html.erb +3 -7
  76. data/app/views/raif/admin/model_completions/index.html.erb +14 -1
  77. data/app/views/raif/admin/model_completions/show.html.erb +164 -55
  78. data/app/views/raif/admin/model_tool_invocations/index.html.erb +1 -1
  79. data/app/views/raif/admin/model_tool_invocations/show.html.erb +18 -0
  80. data/app/views/raif/admin/prompt_studio/agents/index.html.erb +56 -0
  81. data/app/views/raif/admin/prompt_studio/agents/show.html.erb +57 -0
  82. data/app/views/raif/admin/prompt_studio/batch_runs/_batch_run_item.html.erb +54 -0
  83. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_config_fields.html.erb +76 -0
  84. data/app/views/raif/admin/prompt_studio/batch_runs/_judge_detail_modal.html.erb +27 -0
  85. data/app/views/raif/admin/prompt_studio/batch_runs/_modal.html.erb +35 -0
  86. data/app/views/raif/admin/prompt_studio/batch_runs/_progress.html.erb +78 -0
  87. data/app/views/raif/admin/prompt_studio/batch_runs/show.html.erb +49 -0
  88. data/app/views/raif/admin/prompt_studio/conversations/index.html.erb +48 -0
  89. data/app/views/raif/admin/prompt_studio/conversations/show.html.erb +36 -0
  90. data/app/views/raif/admin/prompt_studio/shared/_nav_tabs.html.erb +17 -0
  91. data/app/views/raif/admin/prompt_studio/shared/_prompt_comparison.html.erb +87 -0
  92. data/app/views/raif/admin/prompt_studio/shared/_type_filter.html.erb +54 -0
  93. data/app/views/raif/admin/prompt_studio/tasks/_task_result.html.erb +145 -0
  94. data/app/views/raif/admin/prompt_studio/tasks/_task_row.html.erb +12 -0
  95. data/app/views/raif/admin/prompt_studio/tasks/_task_type_filter.html.erb +58 -0
  96. data/app/views/raif/admin/prompt_studio/tasks/_tasks_table.html.erb +22 -0
  97. data/app/views/raif/admin/prompt_studio/tasks/index.html.erb +35 -0
  98. data/app/views/raif/admin/prompt_studio/tasks/show.html.erb +19 -0
  99. data/app/views/raif/admin/tasks/_task.html.erb +1 -0
  100. data/app/views/raif/admin/tasks/index.html.erb +17 -5
  101. data/app/views/raif/admin/tasks/show.html.erb +20 -0
  102. data/app/views/raif/conversation_entries/_message.html.erb +10 -6
  103. data/config/importmap.rb +8 -0
  104. data/config/locales/admin.en.yml +128 -0
  105. data/config/locales/en.yml +36 -2
  106. data/config/routes.rb +8 -0
  107. data/db/migrate/20260307000000_add_prompt_studio_run_to_raif_tasks.rb +7 -0
  108. data/db/migrate/20260308000000_create_raif_prompt_studio_batch_runs.rb +27 -0
  109. data/db/migrate/20260308000001_create_raif_prompt_studio_batch_run_items.rb +24 -0
  110. data/db/migrate/20260407000000_add_cache_token_columns_to_raif_model_completions.rb +8 -0
  111. data/lib/generators/raif/agent/agent_generator.rb +18 -0
  112. data/lib/generators/raif/agent/templates/agent.rb.tt +7 -5
  113. data/lib/generators/raif/agent/templates/system_prompt.erb.tt +3 -0
  114. data/lib/generators/raif/conversation/conversation_generator.rb +19 -1
  115. data/lib/generators/raif/conversation/templates/system_prompt.erb.tt +4 -0
  116. data/lib/generators/raif/install/templates/initializer.rb +68 -27
  117. data/lib/generators/raif/task/task_generator.rb +18 -0
  118. data/lib/generators/raif/task/templates/prompt.erb.tt +4 -0
  119. data/lib/generators/raif/task/templates/task.rb.tt +9 -8
  120. data/lib/raif/configuration.rb +10 -0
  121. data/lib/raif/embedding_model_registry.rb +8 -0
  122. data/lib/raif/engine.rb +16 -1
  123. data/lib/raif/errors/blank_response_error.rb +8 -0
  124. data/lib/raif/errors/prompt_template_error.rb +15 -0
  125. data/lib/raif/errors.rb +2 -0
  126. data/lib/raif/evals.rb +0 -6
  127. data/lib/raif/llm_registry.rb +230 -9
  128. data/lib/raif/prompt_studio_comparison_builder.rb +138 -0
  129. data/lib/raif/token_estimator.rb +28 -0
  130. data/lib/raif/version.rb +1 -1
  131. data/lib/raif.rb +2 -0
  132. data/spec/support/rspec_helpers.rb +7 -1
  133. data/spec/support/test_task.rb +9 -0
  134. data/spec/support/test_template_task.rb +41 -0
  135. metadata +65 -7
  136. data/lib/raif/evals/llm_judge.rb +0 -32
  137. /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
+ }
@@ -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)
@@ -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
@@ -33,6 +33,7 @@ module Raif
33
33
  { key: "evals_default_llm_judge_model_key", value: @config.evals_default_llm_judge_model_key },
34
34
  { key: "evals_verbose_output", value: @config.evals_verbose_output },
35
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 },
36
37
  { key: "google_models_enabled", value: @config.google_models_enabled },
37
38
  { key: "llm_api_requests_enabled", value: @config.llm_api_requests_enabled },
38
39
  { key: "llm_request_max_retries", value: @config.llm_request_max_retries },
@@ -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
@@ -7,6 +7,8 @@ module Raif
7
7
 
8
8
  def index
9
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)
10
12
 
11
13
  model_completions = Raif::ModelCompletion.order(created_at: :desc)
12
14
 
@@ -23,6 +25,10 @@ module Raif
23
25
  end
24
26
  end
25
27
 
28
+ if @selected_llm_model_key.present?
29
+ model_completions = model_completions.where(llm_model_key: @selected_llm_model_key)
30
+ end
31
+
26
32
  @pagy, @model_completions = pagy(model_completions)
27
33
  end
28
34
 
@@ -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