prompt_engine 1.0.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 (97) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +67 -0
  4. data/Rakefile +22 -0
  5. data/app/assets/stylesheets/prompt_engine/application.css +22 -0
  6. data/app/assets/stylesheets/prompt_engine/buttons.css +124 -0
  7. data/app/assets/stylesheets/prompt_engine/cards.css +63 -0
  8. data/app/assets/stylesheets/prompt_engine/comparison.css +244 -0
  9. data/app/assets/stylesheets/prompt_engine/components/_test_runs.css +144 -0
  10. data/app/assets/stylesheets/prompt_engine/dashboard.css +343 -0
  11. data/app/assets/stylesheets/prompt_engine/evaluations.css +124 -0
  12. data/app/assets/stylesheets/prompt_engine/forms.css +198 -0
  13. data/app/assets/stylesheets/prompt_engine/foundation.css +182 -0
  14. data/app/assets/stylesheets/prompt_engine/layout.css +75 -0
  15. data/app/assets/stylesheets/prompt_engine/loading.css +229 -0
  16. data/app/assets/stylesheets/prompt_engine/notifications.css +78 -0
  17. data/app/assets/stylesheets/prompt_engine/overrides.css +42 -0
  18. data/app/assets/stylesheets/prompt_engine/prompts.css +237 -0
  19. data/app/assets/stylesheets/prompt_engine/sidebar.css +90 -0
  20. data/app/assets/stylesheets/prompt_engine/tables.css +250 -0
  21. data/app/assets/stylesheets/prompt_engine/utilities.css +52 -0
  22. data/app/assets/stylesheets/prompt_engine/versions.css +370 -0
  23. data/app/clients/prompt_engine/open_ai_evals_client.rb +135 -0
  24. data/app/controllers/prompt_engine/admin/base_controller.rb +7 -0
  25. data/app/controllers/prompt_engine/application_controller.rb +4 -0
  26. data/app/controllers/prompt_engine/dashboard_controller.rb +24 -0
  27. data/app/controllers/prompt_engine/eval_runs_controller.rb +23 -0
  28. data/app/controllers/prompt_engine/eval_sets_controller.rb +200 -0
  29. data/app/controllers/prompt_engine/evaluations_controller.rb +32 -0
  30. data/app/controllers/prompt_engine/playground_controller.rb +57 -0
  31. data/app/controllers/prompt_engine/playground_run_results_controller.rb +41 -0
  32. data/app/controllers/prompt_engine/prompts_controller.rb +70 -0
  33. data/app/controllers/prompt_engine/settings_controller.rb +28 -0
  34. data/app/controllers/prompt_engine/test_cases_controller.rb +231 -0
  35. data/app/controllers/prompt_engine/versions_controller.rb +90 -0
  36. data/app/helpers/prompt_engine/application_helper.rb +4 -0
  37. data/app/jobs/prompt_engine/application_job.rb +4 -0
  38. data/app/mailers/prompt_engine/application_mailer.rb +6 -0
  39. data/app/models/prompt_engine/application_record.rb +5 -0
  40. data/app/models/prompt_engine/eval_result.rb +19 -0
  41. data/app/models/prompt_engine/eval_run.rb +40 -0
  42. data/app/models/prompt_engine/eval_set.rb +97 -0
  43. data/app/models/prompt_engine/parameter.rb +126 -0
  44. data/app/models/prompt_engine/parameter_parser.rb +39 -0
  45. data/app/models/prompt_engine/playground_run_result.rb +20 -0
  46. data/app/models/prompt_engine/prompt.rb +192 -0
  47. data/app/models/prompt_engine/prompt_version.rb +72 -0
  48. data/app/models/prompt_engine/setting.rb +45 -0
  49. data/app/models/prompt_engine/test_case.rb +29 -0
  50. data/app/services/prompt_engine/evaluation_runner.rb +258 -0
  51. data/app/services/prompt_engine/playground_executor.rb +124 -0
  52. data/app/services/prompt_engine/variable_detector.rb +97 -0
  53. data/app/views/layouts/prompt_engine/admin.html.erb +65 -0
  54. data/app/views/layouts/prompt_engine/application.html.erb +17 -0
  55. data/app/views/prompt_engine/dashboard/index.html.erb +230 -0
  56. data/app/views/prompt_engine/eval_runs/show.html.erb +204 -0
  57. data/app/views/prompt_engine/eval_sets/compare.html.erb +229 -0
  58. data/app/views/prompt_engine/eval_sets/edit.html.erb +111 -0
  59. data/app/views/prompt_engine/eval_sets/index.html.erb +63 -0
  60. data/app/views/prompt_engine/eval_sets/metrics.html.erb +371 -0
  61. data/app/views/prompt_engine/eval_sets/new.html.erb +113 -0
  62. data/app/views/prompt_engine/eval_sets/show.html.erb +235 -0
  63. data/app/views/prompt_engine/evaluations/index.html.erb +194 -0
  64. data/app/views/prompt_engine/playground/result.html.erb +58 -0
  65. data/app/views/prompt_engine/playground/show.html.erb +129 -0
  66. data/app/views/prompt_engine/playground_run_results/index.html.erb +99 -0
  67. data/app/views/prompt_engine/playground_run_results/show.html.erb +123 -0
  68. data/app/views/prompt_engine/prompts/_form.html.erb +224 -0
  69. data/app/views/prompt_engine/prompts/edit.html.erb +9 -0
  70. data/app/views/prompt_engine/prompts/index.html.erb +80 -0
  71. data/app/views/prompt_engine/prompts/new.html.erb +9 -0
  72. data/app/views/prompt_engine/prompts/show.html.erb +297 -0
  73. data/app/views/prompt_engine/settings/edit.html.erb +93 -0
  74. data/app/views/prompt_engine/shared/_form_errors.html.erb +16 -0
  75. data/app/views/prompt_engine/test_cases/edit.html.erb +72 -0
  76. data/app/views/prompt_engine/test_cases/import.html.erb +92 -0
  77. data/app/views/prompt_engine/test_cases/import_preview.html.erb +103 -0
  78. data/app/views/prompt_engine/test_cases/new.html.erb +79 -0
  79. data/app/views/prompt_engine/versions/_version_card.html.erb +56 -0
  80. data/app/views/prompt_engine/versions/compare.html.erb +82 -0
  81. data/app/views/prompt_engine/versions/index.html.erb +96 -0
  82. data/app/views/prompt_engine/versions/show.html.erb +98 -0
  83. data/config/routes.rb +61 -0
  84. data/db/migrate/20250124000001_create_eval_tables.rb +43 -0
  85. data/db/migrate/20250124000002_add_open_ai_fields_to_evals.rb +11 -0
  86. data/db/migrate/20250125000001_add_grader_fields_to_eval_sets.rb +8 -0
  87. data/db/migrate/20250723161909_create_prompts.rb +17 -0
  88. data/db/migrate/20250723184757_create_prompt_engine_versions.rb +24 -0
  89. data/db/migrate/20250723203838_create_prompt_engine_parameters.rb +20 -0
  90. data/db/migrate/20250724160623_create_prompt_engine_playground_run_results.rb +30 -0
  91. data/db/migrate/20250724165118_create_prompt_engine_settings.rb +14 -0
  92. data/lib/prompt_engine/engine.rb +25 -0
  93. data/lib/prompt_engine/version.rb +3 -0
  94. data/lib/prompt_engine.rb +33 -0
  95. data/lib/tasks/active_prompt_tasks.rake +32 -0
  96. data/lib/tasks/eval_demo.rake +149 -0
  97. metadata +293 -0
@@ -0,0 +1,229 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Compare Evaluation Runs</h1>
4
+ <p class="text-muted">
5
+ Comparing runs from <%= @run1.created_at.strftime("%b %d, %Y %I:%M %p") %>
6
+ and <%= @run2.created_at.strftime("%b %d, %Y %I:%M %p") %>
7
+ </p>
8
+ </div>
9
+ <div class="btn-group">
10
+ <%= link_to "Back to Eval Set", prompt_eval_set_path(@prompt, @eval_set),
11
+ class: "btn btn--secondary btn--medium" %>
12
+ </div>
13
+ </div>
14
+
15
+ <!-- Overall Comparison Summary -->
16
+ <div class="card mb-lg">
17
+ <div class="card__header">
18
+ <h3 class="card__title">Performance Comparison</h3>
19
+ </div>
20
+ <div class="card__body">
21
+ <div class="comparison-grid">
22
+ <div class="comparison-column">
23
+ <div class="comparison-header">
24
+ <h4>Run 1</h4>
25
+ <span class="table__badge table__badge--info">v<%= @run1.prompt_version.version_number %></span>
26
+ <span class="text-muted"><%= @run1.created_at.strftime("%b %d, %Y %I:%M %p") %></span>
27
+ </div>
28
+ <div class="comparison-metrics">
29
+ <div class="metric">
30
+ <div class="metric__label">Success Rate</div>
31
+ <div class="metric__value metric__value--large">
32
+ <%= number_to_percentage(@run1_success_rate, precision: 1) %>
33
+ </div>
34
+ </div>
35
+ <div class="metric">
36
+ <div class="metric__label">Passed Tests</div>
37
+ <div class="metric__value">
38
+ <%= @run1.passed_count %> / <%= @run1.total_count %>
39
+ </div>
40
+ </div>
41
+ <div class="metric">
42
+ <div class="metric__label">Run Time</div>
43
+ <div class="metric__value">
44
+ <% if @run1.completed_at && @run1.started_at %>
45
+ <%= distance_of_time_in_words(@run1.started_at, @run1.completed_at) %>
46
+ <% else %>
47
+
48
+ <% end %>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="comparison-divider">
55
+ <div class="comparison-indicator">
56
+ <% if @success_rate_diff > 0 %>
57
+ <span class="indicator indicator--success">
58
+ <svg class="indicator__icon" viewBox="0 0 20 20" fill="currentColor">
59
+ <path fill-rule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
60
+ </svg>
61
+ +<%= number_to_percentage(@success_rate_diff.abs, precision: 1) %>
62
+ </span>
63
+ <% elsif @success_rate_diff < 0 %>
64
+ <span class="indicator indicator--danger">
65
+ <svg class="indicator__icon" viewBox="0 0 20 20" fill="currentColor">
66
+ <path fill-rule="evenodd" d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z" clip-rule="evenodd" />
67
+ </svg>
68
+ <%= number_to_percentage(@success_rate_diff.abs, precision: 1) %>
69
+ </span>
70
+ <% else %>
71
+ <span class="indicator indicator--neutral">
72
+ <svg class="indicator__icon" viewBox="0 0 20 20" fill="currentColor">
73
+ <path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" />
74
+ </svg>
75
+ No change
76
+ </span>
77
+ <% end %>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="comparison-column">
82
+ <div class="comparison-header">
83
+ <h4>Run 2</h4>
84
+ <span class="table__badge table__badge--info">v<%= @run2.prompt_version.version_number %></span>
85
+ <span class="text-muted"><%= @run2.created_at.strftime("%b %d, %Y %I:%M %p") %></span>
86
+ </div>
87
+ <div class="comparison-metrics">
88
+ <div class="metric">
89
+ <div class="metric__label">Success Rate</div>
90
+ <div class="metric__value metric__value--large <%= @success_rate_diff > 0 ? 'text-success' : @success_rate_diff < 0 ? 'text-danger' : '' %>">
91
+ <%= number_to_percentage(@run2_success_rate, precision: 1) %>
92
+ </div>
93
+ </div>
94
+ <div class="metric">
95
+ <div class="metric__label">Passed Tests</div>
96
+ <div class="metric__value">
97
+ <%= @run2.passed_count %> / <%= @run2.total_count %>
98
+ </div>
99
+ </div>
100
+ <div class="metric">
101
+ <div class="metric__label">Run Time</div>
102
+ <div class="metric__value">
103
+ <% if @run2.completed_at && @run2.started_at %>
104
+ <%= distance_of_time_in_words(@run2.started_at, @run2.completed_at) %>
105
+ <% else %>
106
+
107
+ <% end %>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Prompt Content Comparison -->
117
+ <div class="card mb-lg">
118
+ <div class="card__header">
119
+ <h3 class="card__title">Prompt Content Comparison</h3>
120
+ </div>
121
+ <div class="card__body">
122
+ <div class="comparison-grid">
123
+ <div class="comparison-column">
124
+ <div class="comparison-content">
125
+ <% if @run1.prompt_version.system_message.present? %>
126
+ <div class="comparison-section">
127
+ <h5 class="comparison-section__title">System Message</h5>
128
+ <pre class="prompt-content prompt-content--comparison"><%= @run1.prompt_version.system_message %></pre>
129
+ </div>
130
+ <% end %>
131
+ <div class="comparison-section">
132
+ <h5 class="comparison-section__title">Prompt Content</h5>
133
+ <pre class="prompt-content prompt-content--comparison"><%= @run1.prompt_version.content %></pre>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="comparison-divider"></div>
139
+
140
+ <div class="comparison-column">
141
+ <div class="comparison-content">
142
+ <% if @run2.prompt_version.system_message.present? %>
143
+ <div class="comparison-section">
144
+ <h5 class="comparison-section__title">System Message</h5>
145
+ <pre class="prompt-content prompt-content--comparison"><%= @run2.prompt_version.system_message %></pre>
146
+ </div>
147
+ <% end %>
148
+ <div class="comparison-section">
149
+ <h5 class="comparison-section__title">Prompt Content</h5>
150
+ <pre class="prompt-content prompt-content--comparison"><%= @run2.prompt_version.content %></pre>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Model Configuration Comparison -->
159
+ <div class="card mb-lg">
160
+ <div class="card__header">
161
+ <h3 class="card__title">Model Configuration</h3>
162
+ </div>
163
+ <div class="card__body">
164
+ <div class="comparison-grid">
165
+ <div class="comparison-column">
166
+ <dl class="comparison-list">
167
+ <dt>Model</dt>
168
+ <dd><%= @run1.prompt_version.model || "Default" %></dd>
169
+
170
+ <dt>Temperature</dt>
171
+ <dd><%= @run1.prompt_version.temperature || "Default" %></dd>
172
+
173
+ <dt>Max Tokens</dt>
174
+ <dd><%= @run1.prompt_version.max_tokens || "Default" %></dd>
175
+ </dl>
176
+ </div>
177
+
178
+ <div class="comparison-divider"></div>
179
+
180
+ <div class="comparison-column">
181
+ <dl class="comparison-list">
182
+ <dt>Model</dt>
183
+ <dd class="<%= @run1.prompt_version.model != @run2.prompt_version.model ? 'text-warning' : '' %>">
184
+ <%= @run2.prompt_version.model || "Default" %>
185
+ </dd>
186
+
187
+ <dt>Temperature</dt>
188
+ <dd class="<%= @run1.prompt_version.temperature != @run2.prompt_version.temperature ? 'text-warning' : '' %>">
189
+ <%= @run2.prompt_version.temperature || "Default" %>
190
+ </dd>
191
+
192
+ <dt>Max Tokens</dt>
193
+ <dd class="<%= @run1.prompt_version.max_tokens != @run2.prompt_version.max_tokens ? 'text-warning' : '' %>">
194
+ <%= @run2.prompt_version.max_tokens || "Default" %>
195
+ </dd>
196
+ </dl>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+
202
+ <!-- Test Results Summary -->
203
+ <div class="card">
204
+ <div class="card__header">
205
+ <h3 class="card__title">Test Results Summary</h3>
206
+ </div>
207
+ <div class="card__body">
208
+ <div class="alert alert--info mb-md">
209
+ <p>
210
+ Individual test case results are available in the OpenAI evaluation reports:
211
+ </p>
212
+ <div class="mt-md">
213
+ <% if @run1.report_url.present? %>
214
+ <%= link_to "View Run 1 Report", @run1.report_url,
215
+ target: "_blank", rel: "noopener", class: "btn btn--secondary btn--small" %>
216
+ <% end %>
217
+ <% if @run2.report_url.present? %>
218
+ <%= link_to "View Run 2 Report", @run2.report_url,
219
+ target: "_blank", rel: "noopener", class: "btn btn--secondary btn--small" %>
220
+ <% end %>
221
+ </div>
222
+ </div>
223
+
224
+ <p class="text-muted">
225
+ The OpenAI evaluation reports provide detailed test-by-test comparisons, including actual outputs
226
+ and specific failure reasons for each test case.
227
+ </p>
228
+ </div>
229
+ </div>
@@ -0,0 +1,111 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Edit Evaluation Set</h1>
4
+ <p class="text-muted">Update evaluation set details</p>
5
+ </div>
6
+ </div>
7
+
8
+ <div class="card">
9
+ <div class="card__body">
10
+ <%= form_with model: [@prompt, @eval_set], url: prompt_eval_set_path(@prompt, @eval_set), local: true do |form| %>
11
+ <%= render 'prompt_engine/shared/form_errors', object: @eval_set %>
12
+
13
+ <div class="form__group">
14
+ <%= form.label :name, class: "form__label" %>
15
+ <%= form.text_field :name, class: "form__input", required: true %>
16
+ <p class="form__help">A descriptive name for this evaluation set</p>
17
+ </div>
18
+
19
+ <div class="form__group">
20
+ <%= form.label :description, class: "form__label" %>
21
+ <%= form.text_area :description, class: "form__input", rows: 3 %>
22
+ <p class="form__help">Optional description of what this evaluation set tests</p>
23
+ </div>
24
+
25
+ <div class="form__group">
26
+ <%= form.label :grader_type, "Grader Type", class: "form__label" %>
27
+ <span class="form__required">*</span>
28
+ <%= form.select :grader_type,
29
+ options_for_select(PromptEngine::EvalSet::GRADER_TYPES.map { |k, v| [v, k] }, @eval_set.grader_type),
30
+ { include_blank: false },
31
+ { class: "form__input", id: "grader-type-select",
32
+ data: { controller: "grader-config", action: "change->grader-config#toggleConfig" } } %>
33
+ <p class="form__help">Select how the expected output should be compared to the actual output</p>
34
+ </div>
35
+
36
+ <div id="grader-config-fields" class="form__group" style="display: none;">
37
+ <label class="form__label">Grader Configuration</label>
38
+
39
+ <div id="regex-config" class="grader-config-section" style="display: none;">
40
+ <%= form.label :grader_config_pattern, "Regular Expression Pattern", class: "form__label" %>
41
+ <span class="form__required">*</span>
42
+ <%= text_field_tag "eval_set[grader_config][pattern]",
43
+ @eval_set.grader_config&.dig('pattern'),
44
+ class: "form__input",
45
+ placeholder: "e.g., ^Hello.*world$",
46
+ data: { grader_config_field: "regex" } %>
47
+ <p class="form__help">Enter a valid regular expression pattern to match against the output</p>
48
+ </div>
49
+
50
+ <div id="json-schema-config" class="grader-config-section" style="display: none;">
51
+ <%= form.label :grader_config_schema, "JSON Schema", class: "form__label" %>
52
+ <span class="form__required">*</span>
53
+ <%= text_area_tag "eval_set[grader_config][schema]",
54
+ @eval_set.grader_config&.dig('schema')&.to_json,
55
+ rows: 8,
56
+ class: "form__input",
57
+ placeholder: '{ "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }',
58
+ data: { grader_config_field: "json_schema" } %>
59
+ <p class="form__help">Note: Currently validates exact JSON match. Full schema validation coming soon.</p>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="form__actions">
64
+ <%= form.submit "Update Evaluation Set", class: "btn btn--primary btn--medium",
65
+ data: { disable_with: "Updating..." } %>
66
+ <%= link_to "Cancel", prompt_eval_set_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
67
+ </div>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+
72
+ <div class="mt-lg">
73
+ <%= button_to "Delete Evaluation Set", prompt_eval_set_path(@prompt, @eval_set),
74
+ method: :delete,
75
+ data: { confirm: "Are you sure you want to delete this evaluation set? This will also delete all test cases and evaluation runs." },
76
+ class: "btn btn--danger btn--medium" %>
77
+ </div>
78
+
79
+ <script>
80
+ document.addEventListener('DOMContentLoaded', function() {
81
+ const graderTypeSelect = document.getElementById('grader-type-select');
82
+ const graderConfigFields = document.getElementById('grader-config-fields');
83
+ const regexConfig = document.getElementById('regex-config');
84
+ const jsonSchemaConfig = document.getElementById('json-schema-config');
85
+
86
+ function toggleGraderConfig() {
87
+ const selectedGrader = graderTypeSelect.value;
88
+
89
+ // Hide all config sections first
90
+ regexConfig.style.display = 'none';
91
+ jsonSchemaConfig.style.display = 'none';
92
+
93
+ // Show appropriate config section based on grader type
94
+ if (selectedGrader === 'regex') {
95
+ graderConfigFields.style.display = 'block';
96
+ regexConfig.style.display = 'block';
97
+ } else if (selectedGrader === 'json_schema') {
98
+ graderConfigFields.style.display = 'block';
99
+ jsonSchemaConfig.style.display = 'block';
100
+ } else {
101
+ graderConfigFields.style.display = 'none';
102
+ }
103
+ }
104
+
105
+ // Add event listener
106
+ graderTypeSelect.addEventListener('change', toggleGraderConfig);
107
+
108
+ // Initialize on page load
109
+ toggleGraderConfig();
110
+ });
111
+ </script>
@@ -0,0 +1,63 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Evaluation Sets for <%= @prompt.name %></h1>
4
+ <p class="text-muted">Manage test cases and run evaluations to measure prompt performance</p>
5
+ </div>
6
+ <div class="btn-group">
7
+ <%= link_to "Back to Prompt", prompt_path(@prompt), class: "btn btn--secondary btn--medium" %>
8
+ <%= link_to "New Evaluation Set", new_prompt_eval_set_path(@prompt), class: "btn btn--primary btn--medium" %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="card">
13
+ <div class="card__body">
14
+ <% if @eval_sets.any? %>
15
+ <div class="table-container">
16
+ <table class="table">
17
+ <thead>
18
+ <tr>
19
+ <th>Name</th>
20
+ <th>Description</th>
21
+ <th>Test Cases</th>
22
+ <th>Last Run</th>
23
+ <th class="table__actions">Actions</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ <% @eval_sets.each do |eval_set| %>
28
+ <tr>
29
+ <td>
30
+ <div class="table__primary">
31
+ <%= link_to eval_set.name, prompt_eval_set_path(@prompt, eval_set), class: "table__link" %>
32
+ </div>
33
+ </td>
34
+ <td>
35
+ <span class="table__secondary"><%= eval_set.description || "—" %></span>
36
+ </td>
37
+ <td>
38
+ <span class="table__secondary"><%= pluralize(eval_set.test_cases.count, 'test case') %></span>
39
+ </td>
40
+ <td>
41
+ <% if eval_set.eval_runs.any? %>
42
+ <span class="table__secondary"><%= time_ago_in_words(eval_set.eval_runs.last.created_at) %> ago</span>
43
+ <% else %>
44
+ <span class="table__secondary text-muted">Never run</span>
45
+ <% end %>
46
+ </td>
47
+ <td class="table__actions">
48
+ <%= link_to "View", prompt_eval_set_path(@prompt, eval_set), class: "btn btn--secondary btn--small" %>
49
+ <%= link_to "Edit", edit_prompt_eval_set_path(@prompt, eval_set), class: "btn btn--secondary btn--small" %>
50
+ </td>
51
+ </tr>
52
+ <% end %>
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ <% else %>
57
+ <div class="table-empty">
58
+ <p class="text-muted mb-md">No evaluation sets created yet.</p>
59
+ <%= link_to "Create Your First Evaluation Set", new_prompt_eval_set_path(@prompt), class: "btn btn--primary btn--medium" %>
60
+ </div>
61
+ <% end %>
62
+ </div>
63
+ </div>