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,297 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1><%= @prompt.name %></h1>
4
+ <% if @prompt.description.present? %>
5
+ <p class="text-muted"><%= @prompt.description %></p>
6
+ <% end %>
7
+ </div>
8
+ <div class="btn-group">
9
+ <%= link_to "Try this Prompt", playground_prompt_path(@prompt), class: "btn btn--primary btn--medium" %>
10
+ <%= link_to "Test Run History", prompt_playground_run_results_path(@prompt), class: "btn btn--secondary btn--medium" %>
11
+ <%= link_to "Version History", prompt_versions_path(@prompt), class: "btn btn--secondary btn--medium" %>
12
+ <%= link_to "Evaluations", prompt_eval_sets_path(@prompt), class: "btn btn--secondary btn--medium" %>
13
+ <%= link_to "Edit", edit_prompt_path(@prompt), class: "btn btn--secondary btn--medium" %>
14
+ <%= button_to "Delete", prompt_path(@prompt), method: :delete,
15
+ data: { confirm: "Are you sure you want to delete this prompt?" },
16
+ class: "btn btn--danger btn--medium" %>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="prompt-details">
21
+ <div class="card mb-lg">
22
+ <div class="card__header">
23
+ <h3 class="card__title">Configuration</h3>
24
+ </div>
25
+ <div class="card__body">
26
+ <div class="detail-grid">
27
+ <div class="detail-item">
28
+ <label class="detail-label">Status</label>
29
+ <div class="detail-value">
30
+ <span class="table__badge table__badge--<%= @prompt.status %>">
31
+ <%= @prompt.status %>
32
+ </span>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="detail-item">
37
+ <label class="detail-label">Model</label>
38
+ <div class="detail-value"><%= @prompt.model || "Not configured" %></div>
39
+ </div>
40
+
41
+ <div class="detail-item">
42
+ <label class="detail-label">Temperature</label>
43
+ <div class="detail-value"><%= @prompt.temperature || "Default" %></div>
44
+ </div>
45
+
46
+ <div class="detail-item">
47
+ <label class="detail-label">Max Tokens</label>
48
+ <div class="detail-value"><%= @prompt.max_tokens || "Default" %></div>
49
+ </div>
50
+
51
+ <div class="detail-item">
52
+ <label class="detail-label">Created</label>
53
+ <div class="detail-value"><%= @prompt.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
54
+ </div>
55
+
56
+ <div class="detail-item">
57
+ <label class="detail-label">Last Updated</label>
58
+ <div class="detail-value"><%= @prompt.updated_at.strftime("%B %d, %Y at %I:%M %p") %></div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ <% if @prompt.parameters.any? %>
65
+ <div class="card mb-lg">
66
+ <div class="card__header">
67
+ <h3 class="card__title">Parameters</h3>
68
+ </div>
69
+ <div class="card__body">
70
+ <div class="parameters-table">
71
+ <% @prompt.parameters.ordered.each do |parameter| %>
72
+ <div class="parameter-row">
73
+ <div class="parameter-name"><%= parameter.name %></div>
74
+ <div class="parameter-details">
75
+ <span class="parameter-type"><%= parameter.parameter_type %></span>
76
+ <span class="parameter-required <%= parameter.required? ? 'required' : 'optional' %>">
77
+ <%= parameter.required? ? 'Required' : 'Optional' %>
78
+ </span>
79
+ <% if parameter.default_value.present? %>
80
+ <span class="parameter-default">Default: <%= parameter.default_value %></span>
81
+ <% end %>
82
+ </div>
83
+ <% if parameter.description.present? %>
84
+ <div class="parameter-description"><%= parameter.description %></div>
85
+ <% end %>
86
+ </div>
87
+ <% end %>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ <% end %>
92
+
93
+ <div class="card mb-lg">
94
+ <div class="card__header">
95
+ <h3 class="card__title">Usage Example</h3>
96
+ </div>
97
+ <div class="card__body">
98
+ <p class="text-muted mb-md">Use this prompt in your Rails application:</p>
99
+ <pre class="code-example"># In your controller or model
100
+ result = PromptEngine.render(:<%= @prompt.name %><%
101
+ if @prompt.parameters.any?
102
+ %>, variables: {<% @prompt.parameters.ordered.each_with_index do |param, index| %>
103
+ <%= param.name %>: <%= param.example_value.presence || "\"value#{index + 1}\"" %>,<% end %>
104
+ }<% else %> # No variables needed<% end %>)
105
+
106
+ # Access the rendered content
107
+ rendered_content = result[:content]
108
+
109
+ # Access other prompt settings
110
+ model = result[:model] # => "<%= @prompt.model || "gpt-4" %>"
111
+ temperature = result[:temperature] # => <%= @prompt.temperature || "0.7" %>
112
+ max_tokens = result[:max_tokens] # => <%= @prompt.max_tokens || "1000" %>
113
+
114
+ # Use with your AI service
115
+ response = OpenAI::Client.new.chat(
116
+ parameters: {
117
+ model: model,
118
+ messages: [
119
+ { role: "system", content: result[:system_message] },
120
+ { role: "user", content: rendered_content }
121
+ ],
122
+ temperature: temperature,
123
+ max_tokens: max_tokens
124
+ }
125
+ )</pre>
126
+ </div>
127
+ </div>
128
+
129
+ <% if @prompt.system_message.present? %>
130
+ <div class="card mb-lg">
131
+ <div class="card__header">
132
+ <h3 class="card__title">System Message</h3>
133
+ </div>
134
+ <div class="card__body">
135
+ <pre class="prompt-content"><%= @prompt.system_message %></pre>
136
+ </div>
137
+ </div>
138
+ <% end %>
139
+
140
+ <div class="card mb-lg">
141
+ <div class="card__header">
142
+ <h3 class="card__title">Prompt Content</h3>
143
+ </div>
144
+ <div class="card__body">
145
+ <pre class="prompt-content"><%= @prompt.content %></pre>
146
+ </div>
147
+ </div>
148
+
149
+ <div class="card mb-lg">
150
+ <div class="card__header">
151
+ <h3 class="card__title">Recent Test Runs</h3>
152
+ <div class="card__actions">
153
+ <%= link_to "View All", prompt_playground_run_results_path(@prompt), class: "btn btn--secondary btn--small" %>
154
+ </div>
155
+ </div>
156
+ <div class="card__body">
157
+ <% if @recent_test_runs.any? %>
158
+ <div class="table-container table-container--compact">
159
+ <table class="table table--simple">
160
+ <tbody>
161
+ <% @recent_test_runs.each do |run| %>
162
+ <tr>
163
+ <td>
164
+ <div class="table__primary"><%= run.created_at.strftime("%b %d, %Y %I:%M %p") %></div>
165
+ </td>
166
+ <td>
167
+ <span class="table__badge table__badge--info">v<%= run.prompt_version.version_number %></span>
168
+ </td>
169
+ <td>
170
+ <span class="table__secondary"><%= run.provider %></span>
171
+ </td>
172
+ <td>
173
+ <span class="table__secondary"><%= run.model %></span>
174
+ </td>
175
+ <td>
176
+ <div class="table__metric">
177
+ <span class="table__metric-value"><%= run.execution_time %></span>
178
+ <span class="table__metric-unit">s</span>
179
+ </div>
180
+ </td>
181
+ <td>
182
+ <% if run.token_count %>
183
+ <span class="table__secondary"><%= run.token_count %> tokens</span>
184
+ <% end %>
185
+ </td>
186
+ <td class="table__actions">
187
+ <%= link_to "View Details", playground_run_result_path(run), class: "table__action" %>
188
+ </td>
189
+ </tr>
190
+ <% end %>
191
+ </tbody>
192
+ </table>
193
+ </div>
194
+ <% else %>
195
+ <p class="text-muted">No test runs yet. <%= link_to "Test this prompt", playground_prompt_path(@prompt) %> in the playground.</p>
196
+ <% end %>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="card mb-lg">
201
+ <div class="card__header">
202
+ <h3 class="card__title">Evaluation Sets</h3>
203
+ <div class="card__actions">
204
+ <%= link_to "View All", prompt_eval_sets_path(@prompt), class: "btn btn--secondary btn--small" %>
205
+ <%= link_to "New Evaluation Set", new_prompt_eval_set_path(@prompt), class: "btn btn--primary btn--small" %>
206
+ </div>
207
+ </div>
208
+ <div class="card__body">
209
+ <% if @eval_sets.any? %>
210
+ <div class="eval-sets-summary mb-md">
211
+ <% @eval_sets.each do |eval_set| %>
212
+ <div class="eval-set-summary-item">
213
+ <div class="eval-set-summary__header">
214
+ <h4 class="eval-set-summary__title">
215
+ <%= link_to eval_set.name, prompt_eval_set_path(@prompt, eval_set), class: "table__link" %>
216
+ </h4>
217
+ <div class="eval-set-summary__stats">
218
+ <span class="table__badge table__badge--secondary">
219
+ <%= pluralize(eval_set.test_cases.count, 'test') %>
220
+ </span>
221
+ <% if eval_set.eval_runs.any? %>
222
+ <span class="table__badge table__badge--info">
223
+ <%= pluralize(eval_set.eval_runs.count, 'run') %>
224
+ </span>
225
+ <% end %>
226
+ </div>
227
+ </div>
228
+ <% if eval_set.description.present? %>
229
+ <p class="eval-set-summary__description"><%= truncate(eval_set.description, length: 100) %></p>
230
+ <% end %>
231
+ <% latest_run = eval_set.eval_runs.order(created_at: :desc).first %>
232
+ <% if latest_run && latest_run.status == 'completed' && latest_run.total_count > 0 %>
233
+ <div class="eval-set-summary__result">
234
+ Latest run:
235
+ <% success_rate = (latest_run.passed_count.to_f / latest_run.total_count * 100) %>
236
+ <span class="<%= success_rate >= 80 ? 'text-success' : success_rate >= 60 ? 'text-warning' : 'text-danger' %>">
237
+ <%= number_to_percentage(success_rate, precision: 1) %> passed
238
+ </span>
239
+ <span class="text-muted">(<%= time_ago_in_words(latest_run.created_at) %> ago)</span>
240
+ </div>
241
+ <% end %>
242
+ </div>
243
+ <% end %>
244
+ </div>
245
+ <% else %>
246
+ <p class="text-muted">No evaluation sets yet. <%= link_to "Create an evaluation set", new_prompt_eval_set_path(@prompt) %> to start testing prompt variations.</p>
247
+ <% end %>
248
+
249
+ <% if @recent_eval_runs.any? %>
250
+ <div class="mt-lg">
251
+ <h4 class="mb-md">Recent Evaluation Runs</h4>
252
+ <div class="table-container table-container--compact">
253
+ <table class="table table--simple">
254
+ <tbody>
255
+ <% @recent_eval_runs.each do |run| %>
256
+ <tr>
257
+ <td>
258
+ <div class="table__primary"><%= run.created_at.strftime("%b %d, %Y %I:%M %p") %></div>
259
+ </td>
260
+ <td>
261
+ <%= link_to run.eval_set.name, prompt_eval_set_path(@prompt, run.eval_set), class: "table__link" %>
262
+ </td>
263
+ <td>
264
+ <span class="table__badge table__badge--<%= run.status %>">
265
+ <%= run.status.humanize %>
266
+ </span>
267
+ </td>
268
+ <td>
269
+ <% if run.status == 'completed' && run.total_count > 0 %>
270
+ <% success_rate = (run.passed_count.to_f / run.total_count * 100) %>
271
+ <span class="<%= success_rate >= 80 ? 'text-success' : success_rate >= 60 ? 'text-warning' : 'text-danger' %>">
272
+ <%= number_to_percentage(success_rate, precision: 1) %>
273
+ </span>
274
+ <% else %>
275
+ <span class="text-muted">—</span>
276
+ <% end %>
277
+ </td>
278
+ <td>
279
+ <%= pluralize(run.total_count, 'test') %>
280
+ </td>
281
+ <td class="table__actions">
282
+ <%= link_to "View Details", prompt_eval_run_path(@prompt, run), class: "table__action" %>
283
+ </td>
284
+ </tr>
285
+ <% end %>
286
+ </tbody>
287
+ </table>
288
+ </div>
289
+ </div>
290
+ <% end %>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <div class="form__actions mt-lg">
296
+ <%= link_to "Back to Prompts", prompts_path, class: "btn btn--secondary btn--medium" %>
297
+ </div>
@@ -0,0 +1,93 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Settings</h1>
4
+ <p class="text-muted">Configure your AI provider API keys</p>
5
+ </div>
6
+ </div>
7
+
8
+ <div class="card">
9
+ <div class="card__header">
10
+ <h3 class="card__title">API Keys</h3>
11
+ </div>
12
+ <div class="card__body">
13
+ <%= form_with model: @settings, url: settings_path, method: :patch, local: true, html: { class: "form" } do |form| %>
14
+ <div class="form__section">
15
+ <h4 class="form__section-title">OpenAI Configuration</h4>
16
+
17
+ <div class="form__group">
18
+ <%= form.label :openai_api_key, "OpenAI API Key", class: "form__label" %>
19
+ <%= form.password_field :openai_api_key,
20
+ class: "form__input",
21
+ placeholder: @settings.openai_configured? ? "API key is saved (#{@settings.masked_openai_api_key})" : "Enter your OpenAI API key",
22
+ autocomplete: "off" %>
23
+ <div class="form__help">
24
+ Your API key will be encrypted and stored securely.
25
+ <%= link_to "Get your API key from OpenAI", "https://platform.openai.com/api-keys", target: "_blank", class: "link" %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="form__section">
31
+ <h4 class="form__section-title">Anthropic Configuration</h4>
32
+
33
+ <div class="form__group">
34
+ <%= form.label :anthropic_api_key, "Anthropic API Key", class: "form__label" %>
35
+ <%= form.password_field :anthropic_api_key,
36
+ class: "form__input",
37
+ placeholder: @settings.anthropic_configured? ? "API key is saved (#{@settings.masked_anthropic_api_key})" : "Enter your Anthropic API key",
38
+ autocomplete: "off" %>
39
+ <div class="form__help">
40
+ Your API key will be encrypted and stored securely.
41
+ <%= link_to "Get your API key from Anthropic", "https://console.anthropic.com/account/keys", target: "_blank", class: "link" %>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="form__section">
47
+ <div class="alert alert--info">
48
+ <div class="alert__content">
49
+ <strong>Security Note:</strong> API keys are encrypted at rest using Rails' built-in encryption.
50
+ Leave fields empty to keep existing values unchanged.
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="form__actions">
56
+ <%= form.submit "Save Settings", class: "btn btn--primary btn--medium" %>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+
62
+ <style>
63
+ .form__section {
64
+ margin-bottom: 2rem;
65
+ }
66
+
67
+ .form__section:last-child {
68
+ margin-bottom: 0;
69
+ }
70
+
71
+ .form__section-title {
72
+ font-size: 1.125rem;
73
+ font-weight: 600;
74
+ margin-bottom: 1rem;
75
+ color: var(--color-gray-900);
76
+ }
77
+
78
+ .alert {
79
+ padding: 1rem;
80
+ border-radius: 0.5rem;
81
+ border: 1px solid;
82
+ }
83
+
84
+ .alert--info {
85
+ background-color: #eff6ff;
86
+ border-color: #bfdbfe;
87
+ color: #1e40af;
88
+ }
89
+
90
+ .alert__content {
91
+ font-size: 0.875rem;
92
+ }
93
+ </style>
@@ -0,0 +1,16 @@
1
+ <% if object.errors.any? %>
2
+ <div class="form__errors" role="alert">
3
+ <h3 class="form__errors-title">
4
+ <% if object.errors.count == 1 %>
5
+ 1 error prevented this <%= object.class.name.demodulize.underscore.humanize.downcase %> from being saved:
6
+ <% else %>
7
+ <%= object.errors.count %> errors prevented this <%= object.class.name.demodulize.underscore.humanize.downcase %> from being saved:
8
+ <% end %>
9
+ </h3>
10
+ <ul class="form__errors-list">
11
+ <% object.errors.full_messages.each do |message| %>
12
+ <li><%= message %></li>
13
+ <% end %>
14
+ </ul>
15
+ </div>
16
+ <% end %>
@@ -0,0 +1,72 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Edit Test Case</h1>
4
+ <p class="text-muted">Update test case for <%= @eval_set.name %></p>
5
+ </div>
6
+ </div>
7
+
8
+ <div class="card">
9
+ <div class="card__body">
10
+ <%= form_with model: [@prompt, @eval_set, @test_case],
11
+ url: prompt_eval_set_test_case_path(@prompt, @eval_set, @test_case), local: true do |form| %>
12
+ <%= render 'prompt_engine/shared/form_errors', object: @test_case %>
13
+
14
+ <div class="form__group">
15
+ <%= form.label :description, class: "form__label" %>
16
+ <%= form.text_field :description, class: "form__input" %>
17
+ <p class="form__help">Brief description of what this test case validates</p>
18
+ </div>
19
+
20
+ <div class="form__section">
21
+ <h3 class="form__section-title">Input Variables</h3>
22
+ <p class="form__help mb-md">Set values for each variable in the prompt template</p>
23
+
24
+ <% @prompt.parameters.ordered.each do |parameter| %>
25
+ <div class="form__group">
26
+ <%= label_tag "test_case[input_variables][#{parameter.name}]", parameter.name, class: "form__label" %>
27
+
28
+ <% case parameter.parameter_type %>
29
+ <% when 'boolean' %>
30
+ <%= select_tag "test_case[input_variables][#{parameter.name}]",
31
+ options_for_select([['true', true], ['false', false]],
32
+ @test_case.input_variables&.dig(parameter.name)),
33
+ class: "form__input" %>
34
+ <% when 'integer', 'decimal' %>
35
+ <%= number_field_tag "test_case[input_variables][#{parameter.name}]",
36
+ @test_case.input_variables&.dig(parameter.name),
37
+ class: "form__input",
38
+ step: parameter.parameter_type == 'decimal' ? '0.01' : '1' %>
39
+ <% when 'json', 'array' %>
40
+ <%= text_area_tag "test_case[input_variables][#{parameter.name}]",
41
+ @test_case.input_variables&.dig(parameter.name)&.to_json,
42
+ class: "form__input",
43
+ rows: 3,
44
+ placeholder: parameter.parameter_type == 'array' ? '["item1", "item2"]' : '{"key": "value"}' %>
45
+ <% else %>
46
+ <%= text_area_tag "test_case[input_variables][#{parameter.name}]",
47
+ @test_case.input_variables&.dig(parameter.name),
48
+ class: "form__input",
49
+ rows: 2 %>
50
+ <% end %>
51
+
52
+ <% if parameter.description.present? %>
53
+ <p class="form__help"><%= parameter.description %></p>
54
+ <% end %>
55
+ </div>
56
+ <% end %>
57
+ </div>
58
+
59
+ <div class="form__group">
60
+ <%= form.label :expected_output, class: "form__label" %>
61
+ <%= form.text_area :expected_output, class: "form__input", rows: 5, required: true %>
62
+ <p class="form__help">The expected output will be compared exactly with the actual output</p>
63
+ </div>
64
+
65
+ <div class="form__actions">
66
+ <%= form.submit "Update Test Case", class: "btn btn--primary btn--medium",
67
+ data: { disable_with: "Updating..." } %>
68
+ <%= link_to "Cancel", prompt_eval_set_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
69
+ </div>
70
+ <% end %>
71
+ </div>
72
+ </div>
@@ -0,0 +1,92 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Import Test Cases</h1>
4
+ <p class="text-muted">Import multiple test cases from a CSV or JSON file</p>
5
+ </div>
6
+ <div class="btn-group">
7
+ <%= link_to "Back to Eval Set", prompt_eval_set_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
8
+ </div>
9
+ </div>
10
+
11
+ <% if flash[:alert] %>
12
+ <div class="alert alert--danger mb-lg">
13
+ <%= flash[:alert] %>
14
+ </div>
15
+ <% end %>
16
+
17
+ <div class="card mb-lg">
18
+ <div class="card__header">
19
+ <h3 class="card__title">File Format Requirements</h3>
20
+ </div>
21
+ <div class="card__body">
22
+ <div class="mb-md">
23
+ <h4 class="mb-sm">CSV Format</h4>
24
+ <p class="text-muted mb-sm">
25
+ Your CSV file should have columns for each prompt parameter plus an 'expected_output' column.
26
+ An optional 'description' column can be included.
27
+ </p>
28
+ <div class="code-block mb-md">
29
+ <pre><code><% if @prompt.parameters.any? %><%= @prompt.parameters.pluck(:name).join(',') %>,expected_output,description
30
+ <%= @prompt.parameters.map { |p| p.example_value || "value#{p.position}" }.join(',') %>,Expected response here,Test case description (optional)<% else %>parameter1,parameter2,expected_output,description
31
+ value1,value2,Expected response here,Test case description (optional)<% end %></code></pre>
32
+ </div>
33
+ </div>
34
+
35
+ <div>
36
+ <h4 class="mb-sm">JSON Format</h4>
37
+ <p class="text-muted mb-sm">
38
+ Your JSON file should contain an array of objects with 'input_variables' and 'expected_output' fields.
39
+ An optional 'description' field can be included.
40
+ </p>
41
+ <div class="code-block">
42
+ <pre><code>[
43
+ {
44
+ "input_variables": {<% if @prompt.parameters.any? %>
45
+ <% @prompt.parameters.each_with_index do |param, index| %> "<%= param.name %>": "<%= param.example_value || "value#{index + 1}" %>"<%= index < @prompt.parameters.count - 1 ? ',' : '' %>
46
+ <% end %><% else %>
47
+ "parameter1": "value1",
48
+ "parameter2": "value2"<% end %>
49
+ },
50
+ "expected_output": "Expected response here",
51
+ "description": "Test case description (optional)"
52
+ }
53
+ ]</code></pre>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <%= form_with url: import_preview_prompt_eval_set_test_cases_path(@prompt, @eval_set),
60
+ local: true,
61
+ html: { multipart: true } do |form| %>
62
+ <div class="card">
63
+ <div class="card__header">
64
+ <h3 class="card__title">Upload File</h3>
65
+ </div>
66
+ <div class="card__body">
67
+ <div class="form-group">
68
+ <%= form.label :file, "Select File", class: "form-label" %>
69
+ <%= form.file_field :file,
70
+ accept: ".csv,.json",
71
+ required: true,
72
+ class: "form-input",
73
+ data: {
74
+ controller: "file-input",
75
+ action: "change->file-input#displayFileName"
76
+ } %>
77
+ <span class="form-hint">Accepts CSV or JSON files</span>
78
+ </div>
79
+
80
+ <% if @prompt.parameters.any? %>
81
+ <div class="alert alert--info">
82
+ <strong>Required columns/fields:</strong>
83
+ <%= @prompt.parameters.pluck(:name).join(', ') %>, expected_output
84
+ </div>
85
+ <% end %>
86
+ </div>
87
+ <div class="card__footer">
88
+ <%= form.submit "Preview Import", class: "btn btn--primary btn--medium" %>
89
+ <%= link_to "Cancel", prompt_eval_set_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
90
+ </div>
91
+ </div>
92
+ <% end %>
@@ -0,0 +1,103 @@
1
+ <div class="admin-header">
2
+ <div>
3
+ <h1>Import Preview</h1>
4
+ <p class="text-muted">Review the test cases before importing</p>
5
+ </div>
6
+ <div class="btn-group">
7
+ <%= link_to "Back to Import", import_prompt_eval_set_test_cases_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="card mb-lg">
12
+ <div class="card__header">
13
+ <h3 class="card__title">Import Summary</h3>
14
+ </div>
15
+ <div class="card__body">
16
+ <div class="alert alert--info">
17
+ <strong><%= @imported_data.count %></strong> test cases will be imported
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="card mb-lg">
23
+ <div class="card__header">
24
+ <h3 class="card__title">Preview</h3>
25
+ </div>
26
+ <div class="card__body">
27
+ <% if @imported_data.any? %>
28
+ <div class="table-container">
29
+ <table class="table">
30
+ <thead>
31
+ <tr>
32
+ <th>#</th>
33
+ <th>Description</th>
34
+ <th>Input Variables</th>
35
+ <th>Expected Output</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ <% @imported_data.first(10).each_with_index do |data, index| %>
40
+ <tr>
41
+ <td><%= index + 1 %></td>
42
+ <td>
43
+ <div class="table__primary">
44
+ <%= data[:description].presence || "Test case ##{index + 1}" %>
45
+ </div>
46
+ </td>
47
+ <td>
48
+ <code class="code-inline">
49
+ <%= truncate(data[:input_variables].to_json, length: 100) %>
50
+ </code>
51
+ </td>
52
+ <td>
53
+ <code class="code-inline">
54
+ <%= truncate(data[:expected_output], length: 100) %>
55
+ </code>
56
+ </td>
57
+ </tr>
58
+ <% end %>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+
63
+ <% if @imported_data.count > 10 %>
64
+ <div class="text-center mt-md">
65
+ <p class="text-muted">
66
+ Showing first 10 of <%= @imported_data.count %> test cases
67
+ </p>
68
+ </div>
69
+ <% end %>
70
+ <% else %>
71
+ <div class="table-empty">
72
+ <p class="text-muted">No valid test cases found in the uploaded file.</p>
73
+ </div>
74
+ <% end %>
75
+ </div>
76
+ </div>
77
+
78
+ <% if @imported_data.any? %>
79
+ <%= form_with url: import_create_prompt_eval_set_test_cases_path(@prompt, @eval_set),
80
+ method: :post,
81
+ local: true do |form| %>
82
+ <div class="card">
83
+ <div class="card__header">
84
+ <h3 class="card__title">Confirm Import</h3>
85
+ </div>
86
+ <div class="card__body">
87
+ <p>
88
+ Are you sure you want to import <%= pluralize(@imported_data.count, 'test case') %>?
89
+ This action cannot be undone.
90
+ </p>
91
+ </div>
92
+ <div class="card__footer">
93
+ <%= form.submit "Import Test Cases",
94
+ class: "btn btn--primary btn--medium",
95
+ data: {
96
+ disable_with: "Importing...",
97
+ confirm: "Are you sure you want to import #{@imported_data.count} test cases?"
98
+ } %>
99
+ <%= link_to "Cancel", prompt_eval_set_path(@prompt, @eval_set), class: "btn btn--secondary btn--medium" %>
100
+ </div>
101
+ </div>
102
+ <% end %>
103
+ <% end %>