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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/app/assets/builds/raif_admin.css +40 -2
- data/app/assets/builds/raif_admin_sprockets.js +2709 -0
- data/app/assets/javascript/raif/admin/copy_to_clipboard_controller.js +132 -0
- data/app/assets/javascript/raif/admin/cost_estimate_controller.js +80 -0
- data/app/assets/javascript/raif/admin/judge_config_controller.js +23 -0
- data/app/assets/javascript/raif/admin/select_all_checkboxes_controller.js +33 -0
- data/app/assets/javascript/raif/admin/sortable_table_controller.js +51 -0
- data/app/assets/javascript/raif/admin/table_search_controller.js +15 -0
- data/app/assets/javascript/raif/admin/tom_select_controller.js +33 -0
- data/app/assets/javascript/raif_admin.js +23 -0
- data/app/assets/javascript/raif_admin_sprockets.js +24 -0
- data/app/assets/stylesheets/raif_admin.scss +50 -1
- data/app/controllers/raif/admin/agents_controller.rb +27 -1
- data/app/controllers/raif/admin/configs_controller.rb +1 -0
- data/app/controllers/raif/admin/llms_controller.rb +27 -0
- data/app/controllers/raif/admin/model_completions_controller.rb +6 -0
- data/app/controllers/raif/admin/prompt_studio/agents_controller.rb +25 -0
- data/app/controllers/raif/admin/prompt_studio/base_controller.rb +32 -0
- data/app/controllers/raif/admin/prompt_studio/batch_runs_controller.rb +102 -0
- data/app/controllers/raif/admin/prompt_studio/conversations_controller.rb +25 -0
- data/app/controllers/raif/admin/prompt_studio/tasks_controller.rb +64 -0
- data/app/controllers/raif/admin/tasks_controller.rb +5 -0
- data/app/helpers/raif/application_helper.rb +40 -0
- data/app/jobs/raif/prompt_studio_batch_run_item_job.rb +11 -0
- data/app/jobs/raif/prompt_studio_batch_run_job.rb +15 -0
- data/app/jobs/raif/prompt_studio_task_run_job.rb +36 -0
- data/app/models/raif/agent.rb +36 -5
- data/app/models/raif/agents/native_tool_calling_agent.rb +101 -19
- data/app/models/raif/concerns/has_prompt_templates.rb +88 -0
- data/app/models/raif/concerns/has_runtime_duration.rb +41 -0
- data/app/models/raif/concerns/json_schema_definition.rb +16 -3
- data/app/models/raif/concerns/llm_prompt_caching.rb +20 -0
- data/app/models/raif/concerns/llms/anthropic/message_formatting.rb +6 -0
- data/app/models/raif/concerns/llms/anthropic/tool_formatting.rb +5 -1
- data/app/models/raif/concerns/llms/bedrock/message_formatting.rb +7 -0
- data/app/models/raif/concerns/llms/bedrock/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/google/message_formatting.rb +5 -2
- data/app/models/raif/concerns/llms/google/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/message_formatting.rb +30 -0
- data/app/models/raif/concerns/llms/open_ai_completions/response_tool_calls.rb +1 -1
- data/app/models/raif/concerns/llms/open_ai_completions/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/llms/open_ai_responses/tool_formatting.rb +4 -0
- data/app/models/raif/concerns/provider_managed_tool_calls.rb +162 -0
- data/app/models/raif/conversation.rb +24 -3
- data/app/models/raif/conversation_entry.rb +6 -3
- data/app/models/raif/embedding_models/bedrock.rb +10 -1
- data/app/models/raif/embedding_models/google.rb +37 -0
- data/app/models/raif/evals/llm_judge.rb +70 -0
- data/{lib → app/models}/raif/evals/llm_judges/binary.rb +38 -0
- data/{lib → app/models}/raif/evals/llm_judges/comparative.rb +38 -0
- data/{lib → app/models}/raif/evals/llm_judges/scored.rb +38 -0
- data/{lib → app/models}/raif/evals/llm_judges/summarization.rb +38 -0
- data/app/models/raif/llm.rb +82 -7
- data/app/models/raif/llms/anthropic.rb +26 -4
- data/app/models/raif/llms/bedrock.rb +59 -5
- data/app/models/raif/llms/google.rb +28 -2
- data/app/models/raif/llms/open_ai_base.rb +4 -0
- data/app/models/raif/llms/open_ai_completions.rb +9 -2
- data/app/models/raif/llms/open_ai_responses.rb +9 -2
- data/app/models/raif/llms/open_router.rb +10 -3
- data/app/models/raif/model_completion.rb +75 -34
- data/app/models/raif/model_tool.rb +45 -3
- data/app/models/raif/model_tool_invocation.rb +31 -1
- data/app/models/raif/prompt_studio_batch_run.rb +155 -0
- data/app/models/raif/prompt_studio_batch_run_item.rb +220 -0
- data/app/models/raif/streaming_responses/bedrock.rb +60 -1
- data/app/models/raif/task.rb +30 -6
- data/app/views/layouts/raif/admin.html.erb +31 -1
- data/app/views/raif/admin/agents/_agent.html.erb +1 -0
- data/app/views/raif/admin/agents/index.html.erb +48 -0
- data/app/views/raif/admin/agents/show.html.erb +4 -0
- data/app/views/raif/admin/llms/index.html.erb +110 -0
- data/app/views/raif/admin/model_completions/_model_completion.html.erb +3 -7
- data/app/views/raif/admin/model_completions/index.html.erb +14 -1
- data/app/views/raif/admin/model_completions/show.html.erb +164 -55
- data/app/views/raif/admin/model_tool_invocations/index.html.erb +1 -1
- data/app/views/raif/admin/model_tool_invocations/show.html.erb +18 -0
- data/app/views/raif/admin/prompt_studio/agents/index.html.erb +56 -0
- data/app/views/raif/admin/prompt_studio/agents/show.html.erb +57 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_batch_run_item.html.erb +54 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_judge_config_fields.html.erb +76 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_judge_detail_modal.html.erb +27 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_modal.html.erb +35 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/_progress.html.erb +78 -0
- data/app/views/raif/admin/prompt_studio/batch_runs/show.html.erb +49 -0
- data/app/views/raif/admin/prompt_studio/conversations/index.html.erb +48 -0
- data/app/views/raif/admin/prompt_studio/conversations/show.html.erb +36 -0
- data/app/views/raif/admin/prompt_studio/shared/_nav_tabs.html.erb +17 -0
- data/app/views/raif/admin/prompt_studio/shared/_prompt_comparison.html.erb +87 -0
- data/app/views/raif/admin/prompt_studio/shared/_type_filter.html.erb +54 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_result.html.erb +145 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_row.html.erb +12 -0
- data/app/views/raif/admin/prompt_studio/tasks/_task_type_filter.html.erb +58 -0
- data/app/views/raif/admin/prompt_studio/tasks/_tasks_table.html.erb +22 -0
- data/app/views/raif/admin/prompt_studio/tasks/index.html.erb +35 -0
- data/app/views/raif/admin/prompt_studio/tasks/show.html.erb +19 -0
- data/app/views/raif/admin/tasks/_task.html.erb +1 -0
- data/app/views/raif/admin/tasks/index.html.erb +17 -5
- data/app/views/raif/admin/tasks/show.html.erb +20 -0
- data/app/views/raif/conversation_entries/_message.html.erb +10 -6
- data/config/importmap.rb +8 -0
- data/config/locales/admin.en.yml +128 -0
- data/config/locales/en.yml +36 -2
- data/config/routes.rb +8 -0
- data/db/migrate/20260307000000_add_prompt_studio_run_to_raif_tasks.rb +7 -0
- data/db/migrate/20260308000000_create_raif_prompt_studio_batch_runs.rb +27 -0
- data/db/migrate/20260308000001_create_raif_prompt_studio_batch_run_items.rb +24 -0
- data/db/migrate/20260407000000_add_cache_token_columns_to_raif_model_completions.rb +8 -0
- data/lib/generators/raif/agent/agent_generator.rb +18 -0
- data/lib/generators/raif/agent/templates/agent.rb.tt +7 -5
- data/lib/generators/raif/agent/templates/system_prompt.erb.tt +3 -0
- data/lib/generators/raif/conversation/conversation_generator.rb +19 -1
- data/lib/generators/raif/conversation/templates/system_prompt.erb.tt +4 -0
- data/lib/generators/raif/install/templates/initializer.rb +68 -27
- data/lib/generators/raif/task/task_generator.rb +18 -0
- data/lib/generators/raif/task/templates/prompt.erb.tt +4 -0
- data/lib/generators/raif/task/templates/task.rb.tt +9 -8
- data/lib/raif/configuration.rb +10 -0
- data/lib/raif/embedding_model_registry.rb +8 -0
- data/lib/raif/engine.rb +16 -1
- data/lib/raif/errors/blank_response_error.rb +8 -0
- data/lib/raif/errors/prompt_template_error.rb +15 -0
- data/lib/raif/errors.rb +2 -0
- data/lib/raif/evals.rb +0 -6
- data/lib/raif/llm_registry.rb +230 -9
- data/lib/raif/prompt_studio_comparison_builder.rb +138 -0
- data/lib/raif/token_estimator.rb +28 -0
- data/lib/raif/version.rb +1 -1
- data/lib/raif.rb +2 -0
- data/spec/support/rspec_helpers.rb +7 -1
- data/spec/support/test_task.rb +9 -0
- data/spec/support/test_template_task.rb +41 -0
- metadata +65 -7
- data/lib/raif/evals/llm_judge.rb +0 -32
- /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 × ~${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:
|
|
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
|
-
@
|
|
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
|