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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +67 -0
- data/Rakefile +22 -0
- data/app/assets/stylesheets/prompt_engine/application.css +22 -0
- data/app/assets/stylesheets/prompt_engine/buttons.css +124 -0
- data/app/assets/stylesheets/prompt_engine/cards.css +63 -0
- data/app/assets/stylesheets/prompt_engine/comparison.css +244 -0
- data/app/assets/stylesheets/prompt_engine/components/_test_runs.css +144 -0
- data/app/assets/stylesheets/prompt_engine/dashboard.css +343 -0
- data/app/assets/stylesheets/prompt_engine/evaluations.css +124 -0
- data/app/assets/stylesheets/prompt_engine/forms.css +198 -0
- data/app/assets/stylesheets/prompt_engine/foundation.css +182 -0
- data/app/assets/stylesheets/prompt_engine/layout.css +75 -0
- data/app/assets/stylesheets/prompt_engine/loading.css +229 -0
- data/app/assets/stylesheets/prompt_engine/notifications.css +78 -0
- data/app/assets/stylesheets/prompt_engine/overrides.css +42 -0
- data/app/assets/stylesheets/prompt_engine/prompts.css +237 -0
- data/app/assets/stylesheets/prompt_engine/sidebar.css +90 -0
- data/app/assets/stylesheets/prompt_engine/tables.css +250 -0
- data/app/assets/stylesheets/prompt_engine/utilities.css +52 -0
- data/app/assets/stylesheets/prompt_engine/versions.css +370 -0
- data/app/clients/prompt_engine/open_ai_evals_client.rb +135 -0
- data/app/controllers/prompt_engine/admin/base_controller.rb +7 -0
- data/app/controllers/prompt_engine/application_controller.rb +4 -0
- data/app/controllers/prompt_engine/dashboard_controller.rb +24 -0
- data/app/controllers/prompt_engine/eval_runs_controller.rb +23 -0
- data/app/controllers/prompt_engine/eval_sets_controller.rb +200 -0
- data/app/controllers/prompt_engine/evaluations_controller.rb +32 -0
- data/app/controllers/prompt_engine/playground_controller.rb +57 -0
- data/app/controllers/prompt_engine/playground_run_results_controller.rb +41 -0
- data/app/controllers/prompt_engine/prompts_controller.rb +70 -0
- data/app/controllers/prompt_engine/settings_controller.rb +28 -0
- data/app/controllers/prompt_engine/test_cases_controller.rb +231 -0
- data/app/controllers/prompt_engine/versions_controller.rb +90 -0
- data/app/helpers/prompt_engine/application_helper.rb +4 -0
- data/app/jobs/prompt_engine/application_job.rb +4 -0
- data/app/mailers/prompt_engine/application_mailer.rb +6 -0
- data/app/models/prompt_engine/application_record.rb +5 -0
- data/app/models/prompt_engine/eval_result.rb +19 -0
- data/app/models/prompt_engine/eval_run.rb +40 -0
- data/app/models/prompt_engine/eval_set.rb +97 -0
- data/app/models/prompt_engine/parameter.rb +126 -0
- data/app/models/prompt_engine/parameter_parser.rb +39 -0
- data/app/models/prompt_engine/playground_run_result.rb +20 -0
- data/app/models/prompt_engine/prompt.rb +192 -0
- data/app/models/prompt_engine/prompt_version.rb +72 -0
- data/app/models/prompt_engine/setting.rb +45 -0
- data/app/models/prompt_engine/test_case.rb +29 -0
- data/app/services/prompt_engine/evaluation_runner.rb +258 -0
- data/app/services/prompt_engine/playground_executor.rb +124 -0
- data/app/services/prompt_engine/variable_detector.rb +97 -0
- data/app/views/layouts/prompt_engine/admin.html.erb +65 -0
- data/app/views/layouts/prompt_engine/application.html.erb +17 -0
- data/app/views/prompt_engine/dashboard/index.html.erb +230 -0
- data/app/views/prompt_engine/eval_runs/show.html.erb +204 -0
- data/app/views/prompt_engine/eval_sets/compare.html.erb +229 -0
- data/app/views/prompt_engine/eval_sets/edit.html.erb +111 -0
- data/app/views/prompt_engine/eval_sets/index.html.erb +63 -0
- data/app/views/prompt_engine/eval_sets/metrics.html.erb +371 -0
- data/app/views/prompt_engine/eval_sets/new.html.erb +113 -0
- data/app/views/prompt_engine/eval_sets/show.html.erb +235 -0
- data/app/views/prompt_engine/evaluations/index.html.erb +194 -0
- data/app/views/prompt_engine/playground/result.html.erb +58 -0
- data/app/views/prompt_engine/playground/show.html.erb +129 -0
- data/app/views/prompt_engine/playground_run_results/index.html.erb +99 -0
- data/app/views/prompt_engine/playground_run_results/show.html.erb +123 -0
- data/app/views/prompt_engine/prompts/_form.html.erb +224 -0
- data/app/views/prompt_engine/prompts/edit.html.erb +9 -0
- data/app/views/prompt_engine/prompts/index.html.erb +80 -0
- data/app/views/prompt_engine/prompts/new.html.erb +9 -0
- data/app/views/prompt_engine/prompts/show.html.erb +297 -0
- data/app/views/prompt_engine/settings/edit.html.erb +93 -0
- data/app/views/prompt_engine/shared/_form_errors.html.erb +16 -0
- data/app/views/prompt_engine/test_cases/edit.html.erb +72 -0
- data/app/views/prompt_engine/test_cases/import.html.erb +92 -0
- data/app/views/prompt_engine/test_cases/import_preview.html.erb +103 -0
- data/app/views/prompt_engine/test_cases/new.html.erb +79 -0
- data/app/views/prompt_engine/versions/_version_card.html.erb +56 -0
- data/app/views/prompt_engine/versions/compare.html.erb +82 -0
- data/app/views/prompt_engine/versions/index.html.erb +96 -0
- data/app/views/prompt_engine/versions/show.html.erb +98 -0
- data/config/routes.rb +61 -0
- data/db/migrate/20250124000001_create_eval_tables.rb +43 -0
- data/db/migrate/20250124000002_add_open_ai_fields_to_evals.rb +11 -0
- data/db/migrate/20250125000001_add_grader_fields_to_eval_sets.rb +8 -0
- data/db/migrate/20250723161909_create_prompts.rb +17 -0
- data/db/migrate/20250723184757_create_prompt_engine_versions.rb +24 -0
- data/db/migrate/20250723203838_create_prompt_engine_parameters.rb +20 -0
- data/db/migrate/20250724160623_create_prompt_engine_playground_run_results.rb +30 -0
- data/db/migrate/20250724165118_create_prompt_engine_settings.rb +14 -0
- data/lib/prompt_engine/engine.rb +25 -0
- data/lib/prompt_engine/version.rb +3 -0
- data/lib/prompt_engine.rb +33 -0
- data/lib/tasks/active_prompt_tasks.rake +32 -0
- data/lib/tasks/eval_demo.rake +149 -0
- metadata +293 -0
@@ -0,0 +1,200 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class EvalSetsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
|
5
|
+
before_action :set_prompt
|
6
|
+
before_action :set_eval_set, only: [ :show, :edit, :update, :destroy, :run, :compare, :metrics ]
|
7
|
+
|
8
|
+
def index
|
9
|
+
@eval_sets = @prompt.eval_sets
|
10
|
+
end
|
11
|
+
|
12
|
+
def show
|
13
|
+
@test_cases = @eval_set.test_cases
|
14
|
+
@recent_runs = @eval_set.eval_runs.order(created_at: :desc).limit(5)
|
15
|
+
end
|
16
|
+
|
17
|
+
def new
|
18
|
+
@eval_set = @prompt.eval_sets.build
|
19
|
+
end
|
20
|
+
|
21
|
+
def create
|
22
|
+
@eval_set = @prompt.eval_sets.build(eval_set_params)
|
23
|
+
|
24
|
+
if @eval_set.save
|
25
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), notice: "Evaluation set was successfully created."
|
26
|
+
else
|
27
|
+
flash.now[:alert] = "Please fix the errors below."
|
28
|
+
render :new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def edit
|
33
|
+
end
|
34
|
+
|
35
|
+
def update
|
36
|
+
if @eval_set.update(eval_set_params)
|
37
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), notice: "Evaluation set was successfully updated."
|
38
|
+
else
|
39
|
+
flash.now[:alert] = "Please fix the errors below."
|
40
|
+
render :edit
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def destroy
|
45
|
+
@eval_set.destroy
|
46
|
+
redirect_to prompt_eval_sets_path(@prompt), notice: "Evaluation set was successfully deleted."
|
47
|
+
end
|
48
|
+
|
49
|
+
def run
|
50
|
+
# Check if API key is available
|
51
|
+
unless api_key_configured?
|
52
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set),
|
53
|
+
alert: "OpenAI API key not configured. Please configure it in Settings or contact your administrator."
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create new eval run with current prompt version
|
58
|
+
@eval_run = @eval_set.eval_runs.create!(
|
59
|
+
prompt_version: @prompt.current_version
|
60
|
+
)
|
61
|
+
|
62
|
+
begin
|
63
|
+
# Run evaluation synchronously for MVP
|
64
|
+
PromptEngine::EvaluationRunner.new(@eval_run).execute
|
65
|
+
redirect_to prompt_eval_run_path(@prompt, @eval_run), notice: "Evaluation started successfully"
|
66
|
+
rescue PromptEngine::OpenAiEvalsClient::AuthenticationError => e
|
67
|
+
@eval_run.update!(status: :failed, error_message: e.message)
|
68
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set),
|
69
|
+
alert: "Authentication failed: Please check your OpenAI API key in Settings"
|
70
|
+
rescue PromptEngine::OpenAiEvalsClient::RateLimitError => e
|
71
|
+
@eval_run.update!(status: :failed, error_message: e.message)
|
72
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), alert: "Rate limit exceeded: Please try again later"
|
73
|
+
rescue PromptEngine::OpenAiEvalsClient::APIError => e
|
74
|
+
@eval_run.update!(status: :failed, error_message: e.message)
|
75
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), alert: "API error: #{e.message}"
|
76
|
+
rescue => e
|
77
|
+
@eval_run.update!(status: :failed, error_message: e.message)
|
78
|
+
Rails.logger.error "Evaluation error: #{e.class} - #{e.message}"
|
79
|
+
Rails.logger.error e.backtrace.join("\n")
|
80
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), alert: "Evaluation failed: #{e.message}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def compare
|
85
|
+
unless params[:run_ids].present? && params[:run_ids].is_a?(Array) && params[:run_ids].length == 2
|
86
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set),
|
87
|
+
alert: "Please select exactly two evaluation runs to compare."
|
88
|
+
return
|
89
|
+
end
|
90
|
+
|
91
|
+
@run1 = @eval_set.eval_runs.find(params[:run_ids][0])
|
92
|
+
@run2 = @eval_set.eval_runs.find(params[:run_ids][1])
|
93
|
+
|
94
|
+
# Ensure both runs are completed
|
95
|
+
unless @run1.status == "completed" && @run2.status == "completed"
|
96
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set),
|
97
|
+
alert: "Both evaluation runs must be completed to compare them."
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
# Calculate comparison metrics
|
102
|
+
@run1_success_rate = (@run1.total_count > 0) ? (@run1.passed_count.to_f / @run1.total_count * 100) : 0
|
103
|
+
@run2_success_rate = (@run2.total_count > 0) ? (@run2.passed_count.to_f / @run2.total_count * 100) : 0
|
104
|
+
@success_rate_diff = @run2_success_rate - @run1_success_rate
|
105
|
+
rescue ActiveRecord::RecordNotFound
|
106
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set),
|
107
|
+
alert: "One or both evaluation runs could not be found."
|
108
|
+
end
|
109
|
+
|
110
|
+
def metrics
|
111
|
+
# Get all completed runs for this eval set
|
112
|
+
@eval_runs = @eval_set.eval_runs.where(status: "completed").order(created_at: :asc)
|
113
|
+
|
114
|
+
# Calculate metrics data for charts
|
115
|
+
if @eval_runs.any?
|
116
|
+
# Success rate trend data (for line chart)
|
117
|
+
@success_rate_trend = @eval_runs.map do |run|
|
118
|
+
{
|
119
|
+
date: run.created_at.strftime("%b %d, %Y %I:%M %p"),
|
120
|
+
rate: (run.total_count > 0) ? (run.passed_count.to_f / run.total_count * 100).round(2) : 0,
|
121
|
+
version: "v#{run.prompt_version.version_number}"
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
# Success rate by version (for bar chart)
|
126
|
+
version_stats = @eval_runs.group_by { |r| r.prompt_version.version_number }
|
127
|
+
@success_rate_by_version = version_stats.map do |version, runs|
|
128
|
+
total_passed = runs.sum(&:passed_count)
|
129
|
+
total_count = runs.sum(&:total_count)
|
130
|
+
{
|
131
|
+
version: "v#{version}",
|
132
|
+
rate: (total_count > 0) ? (total_passed.to_f / total_count * 100).round(2) : 0,
|
133
|
+
runs: runs.count
|
134
|
+
}
|
135
|
+
end.sort_by { |v| v[:version] }
|
136
|
+
|
137
|
+
# Test case statistics
|
138
|
+
@total_test_cases = @eval_set.test_cases.count
|
139
|
+
@total_runs = @eval_runs.count
|
140
|
+
@overall_pass_rate = begin
|
141
|
+
total_passed = @eval_runs.sum(&:passed_count)
|
142
|
+
total_tests = @eval_runs.sum(&:total_count)
|
143
|
+
(total_tests > 0) ? (total_passed.to_f / total_tests * 100).round(2) : 0
|
144
|
+
end
|
145
|
+
|
146
|
+
# Average duration trend
|
147
|
+
@duration_trend = @eval_runs.map do |run|
|
148
|
+
duration = if run.completed_at && run.started_at
|
149
|
+
(run.completed_at - run.started_at).to_i
|
150
|
+
else
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
{
|
154
|
+
date: run.created_at.strftime("%b %d, %Y %I:%M %p"),
|
155
|
+
duration: duration,
|
156
|
+
version: "v#{run.prompt_version.version_number}"
|
157
|
+
}
|
158
|
+
end.compact
|
159
|
+
|
160
|
+
# Recent activity (last 10 runs)
|
161
|
+
@recent_activity = @eval_runs.last(10).reverse
|
162
|
+
else
|
163
|
+
@success_rate_trend = []
|
164
|
+
@success_rate_by_version = []
|
165
|
+
@total_test_cases = @eval_set.test_cases.count
|
166
|
+
@total_runs = 0
|
167
|
+
@overall_pass_rate = 0
|
168
|
+
@duration_trend = []
|
169
|
+
@recent_activity = []
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
protected
|
174
|
+
|
175
|
+
helper_method :api_key_configured?
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def set_prompt
|
180
|
+
@prompt = Prompt.find(params[:prompt_id])
|
181
|
+
end
|
182
|
+
|
183
|
+
def set_eval_set
|
184
|
+
@eval_set = @prompt.eval_sets.find(params[:id])
|
185
|
+
end
|
186
|
+
|
187
|
+
def eval_set_params
|
188
|
+
params.require(:eval_set).permit(:name, :description, :grader_type, grader_config: {})
|
189
|
+
end
|
190
|
+
|
191
|
+
def api_key_configured?
|
192
|
+
# Check if OpenAI API key is available from Settings or Rails credentials
|
193
|
+
settings = PromptEngine::Setting.instance
|
194
|
+
settings.openai_configured? || Rails.application.credentials.dig(:openai, :api_key).present?
|
195
|
+
rescue ActiveRecord::RecordNotFound
|
196
|
+
# If settings record doesn't exist, check Rails credentials
|
197
|
+
Rails.application.credentials.dig(:openai, :api_key).present?
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class EvaluationsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
|
5
|
+
def index
|
6
|
+
@prompts_with_eval_sets = Prompt.joins(:eval_sets)
|
7
|
+
.includes(eval_sets: [ :eval_runs ])
|
8
|
+
.distinct
|
9
|
+
.order(:name)
|
10
|
+
|
11
|
+
# Calculate overall statistics
|
12
|
+
@total_eval_sets = EvalSet.count
|
13
|
+
@total_eval_runs = EvalRun.count
|
14
|
+
@total_test_cases = TestCase.count
|
15
|
+
|
16
|
+
# Get recent evaluation activity
|
17
|
+
@recent_runs = EvalRun.includes(eval_set: :prompt, prompt_version: :prompt)
|
18
|
+
.order(created_at: :desc)
|
19
|
+
.limit(10)
|
20
|
+
|
21
|
+
# Calculate overall pass rate
|
22
|
+
completed_runs = EvalRun.where(status: "completed")
|
23
|
+
if completed_runs.any?
|
24
|
+
total_passed = completed_runs.sum(:passed_count)
|
25
|
+
total_tests = completed_runs.sum(:total_count)
|
26
|
+
@overall_pass_rate = (total_tests > 0) ? (total_passed.to_f / total_tests * 100).round(2) : 0
|
27
|
+
else
|
28
|
+
@overall_pass_rate = 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class PlaygroundController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
before_action :set_prompt
|
5
|
+
|
6
|
+
def show
|
7
|
+
@parameters = ParameterParser.new(@prompt.content).extract_parameters.map { |p| p[:name] }
|
8
|
+
@settings = Setting.instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
executor = PlaygroundExecutor.new(
|
13
|
+
prompt: @prompt,
|
14
|
+
provider: params[:provider],
|
15
|
+
api_key: params[:api_key],
|
16
|
+
parameters: params[:parameters]
|
17
|
+
)
|
18
|
+
|
19
|
+
begin
|
20
|
+
result = executor.execute
|
21
|
+
@response = result[:response]
|
22
|
+
@execution_time = result[:execution_time]
|
23
|
+
@token_count = result[:token_count]
|
24
|
+
@model = result[:model]
|
25
|
+
@provider = result[:provider]
|
26
|
+
|
27
|
+
# Store the rendered prompt for display
|
28
|
+
parser = ParameterParser.new(@prompt.content)
|
29
|
+
@rendered_prompt = parser.replace_parameters(params[:parameters])
|
30
|
+
|
31
|
+
# Save the playground run result
|
32
|
+
@prompt.current_version.playground_run_results.create!(
|
33
|
+
provider: @provider,
|
34
|
+
model: @model,
|
35
|
+
rendered_prompt: @rendered_prompt,
|
36
|
+
system_message: @prompt.system_message,
|
37
|
+
parameters: params[:parameters],
|
38
|
+
response: @response,
|
39
|
+
execution_time: @execution_time,
|
40
|
+
token_count: @token_count,
|
41
|
+
temperature: @prompt.temperature,
|
42
|
+
max_tokens: @prompt.max_tokens
|
43
|
+
)
|
44
|
+
rescue => e
|
45
|
+
@error = e.message
|
46
|
+
end
|
47
|
+
|
48
|
+
render :result
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def set_prompt
|
54
|
+
@prompt = Prompt.find(params[:id])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class PlaygroundRunResultsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
before_action :set_playground_run_result, only: [ :show ]
|
5
|
+
before_action :set_context, only: [ :index ]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@playground_run_results = scope.recent.includes(prompt_version: :prompt)
|
9
|
+
end
|
10
|
+
|
11
|
+
def show
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def set_playground_run_result
|
17
|
+
@playground_run_result = PlaygroundRunResult.find(params[:id])
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_context
|
21
|
+
if params[:prompt_id]
|
22
|
+
@prompt = Prompt.find(params[:prompt_id])
|
23
|
+
elsif params[:version_id]
|
24
|
+
@prompt_version = PromptVersion.find(params[:version_id])
|
25
|
+
@prompt = @prompt_version.prompt
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def scope
|
30
|
+
if params[:prompt_id]
|
31
|
+
# Get all playground run results for all versions of this prompt
|
32
|
+
PlaygroundRunResult.joins(:prompt_version).where(prompt_engine_prompt_versions: { prompt_id: params[:prompt_id] })
|
33
|
+
elsif params[:version_id]
|
34
|
+
# Get playground run results for a specific version
|
35
|
+
PromptVersion.find(params[:version_id]).playground_run_results
|
36
|
+
else
|
37
|
+
PlaygroundRunResult.all
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class PromptsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
before_action :set_prompt, only: [ :show, :edit, :update, :destroy ]
|
5
|
+
|
6
|
+
def index
|
7
|
+
@prompts = PromptEngine::Prompt.by_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def show
|
11
|
+
# Get recent test runs for this prompt across all versions
|
12
|
+
@recent_test_runs = PromptEngine::PlaygroundRunResult
|
13
|
+
.joins(:prompt_version)
|
14
|
+
.where(prompt_engine_prompt_versions: { prompt_id: @prompt.id })
|
15
|
+
.recent
|
16
|
+
.limit(5)
|
17
|
+
.includes(:prompt_version)
|
18
|
+
|
19
|
+
# Get evaluation data for this prompt
|
20
|
+
@eval_sets = @prompt.eval_sets.includes(:test_cases, :eval_runs)
|
21
|
+
@recent_eval_runs = PromptEngine::EvalRun
|
22
|
+
.joins(:eval_set)
|
23
|
+
.where(prompt_engine_eval_sets: { prompt_id: @prompt.id })
|
24
|
+
.order(created_at: :desc)
|
25
|
+
.limit(5)
|
26
|
+
.includes(:eval_set, :prompt_version)
|
27
|
+
end
|
28
|
+
|
29
|
+
def new
|
30
|
+
@prompt = PromptEngine::Prompt.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def create
|
34
|
+
@prompt = PromptEngine::Prompt.new(prompt_params)
|
35
|
+
|
36
|
+
if @prompt.save
|
37
|
+
redirect_to prompt_path(@prompt), notice: "Prompt was successfully created."
|
38
|
+
else
|
39
|
+
render :new, status: :unprocessable_entity
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def edit
|
44
|
+
end
|
45
|
+
|
46
|
+
def update
|
47
|
+
if @prompt.update(prompt_params)
|
48
|
+
redirect_to prompt_path(@prompt), notice: "Prompt was successfully updated."
|
49
|
+
else
|
50
|
+
render :edit, status: :unprocessable_entity
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def destroy
|
55
|
+
@prompt.destroy
|
56
|
+
redirect_to prompts_path, notice: "Prompt was successfully deleted."
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def set_prompt
|
62
|
+
@prompt = PromptEngine::Prompt.find(params[:id])
|
63
|
+
end
|
64
|
+
|
65
|
+
def prompt_params
|
66
|
+
params.require(:prompt).permit(:name, :description, :content, :system_message, :model, :temperature, :max_tokens, :status,
|
67
|
+
parameters_attributes: [ :id, :name, :description, :parameter_type, :required, :default_value, :_destroy ])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class SettingsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
before_action :load_settings
|
5
|
+
|
6
|
+
def edit
|
7
|
+
# Just display the form
|
8
|
+
end
|
9
|
+
|
10
|
+
def update
|
11
|
+
if @settings.update(settings_params)
|
12
|
+
redirect_to edit_settings_path, notice: "Settings have been updated successfully."
|
13
|
+
else
|
14
|
+
render :edit, status: :unprocessable_entity
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def load_settings
|
21
|
+
@settings = Setting.instance
|
22
|
+
end
|
23
|
+
|
24
|
+
def settings_params
|
25
|
+
params.require(:setting).permit(:openai_api_key, :anthropic_api_key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class TestCasesController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
|
5
|
+
before_action :set_prompt
|
6
|
+
before_action :set_eval_set
|
7
|
+
before_action :set_test_case, only: [ :edit, :update, :destroy ]
|
8
|
+
|
9
|
+
def new
|
10
|
+
@test_case = @eval_set.test_cases.build
|
11
|
+
# Pre-populate with prompt's parameters
|
12
|
+
@test_case.input_variables = @prompt.parameters.each_with_object({}) do |param, hash|
|
13
|
+
hash[param.name] = param.default_value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
@test_case = @eval_set.test_cases.build(test_case_params)
|
19
|
+
|
20
|
+
if @test_case.save
|
21
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), notice: "Test case was successfully created."
|
22
|
+
else
|
23
|
+
flash.now[:alert] = "Please fix the errors below."
|
24
|
+
render :new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def edit
|
29
|
+
end
|
30
|
+
|
31
|
+
def update
|
32
|
+
if @test_case.update(test_case_params)
|
33
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), notice: "Test case was successfully updated."
|
34
|
+
else
|
35
|
+
flash.now[:alert] = "Please fix the errors below."
|
36
|
+
render :edit
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def destroy
|
41
|
+
@test_case.destroy
|
42
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set), notice: "Test case was successfully deleted."
|
43
|
+
end
|
44
|
+
|
45
|
+
def import
|
46
|
+
# Display the import form
|
47
|
+
end
|
48
|
+
|
49
|
+
def import_preview
|
50
|
+
unless params[:file].present?
|
51
|
+
redirect_to import_prompt_eval_set_test_cases_path(@prompt, @eval_set),
|
52
|
+
alert: "Please select a file to import."
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
@imported_data = []
|
57
|
+
@errors = []
|
58
|
+
|
59
|
+
begin
|
60
|
+
file_content = params[:file].read
|
61
|
+
file_type = detect_file_type(params[:file])
|
62
|
+
|
63
|
+
if file_type == :csv
|
64
|
+
parse_csv(file_content)
|
65
|
+
elsif file_type == :json
|
66
|
+
parse_json(file_content)
|
67
|
+
else
|
68
|
+
@errors << "Unsupported file format. Please upload a CSV or JSON file."
|
69
|
+
end
|
70
|
+
rescue => e
|
71
|
+
@errors << "Error reading file: #{e.message}"
|
72
|
+
end
|
73
|
+
|
74
|
+
if @errors.any?
|
75
|
+
flash.now[:alert] = @errors.join(", ")
|
76
|
+
render :import
|
77
|
+
else
|
78
|
+
# Store the imported data in session for the create action
|
79
|
+
session[:imported_test_cases] = @imported_data
|
80
|
+
render :import_preview
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def import_create
|
85
|
+
imported_data = session[:imported_test_cases]
|
86
|
+
|
87
|
+
unless imported_data.present?
|
88
|
+
redirect_to import_prompt_eval_set_test_cases_path(@prompt, @eval_set),
|
89
|
+
alert: "No import data found. Please upload a file again."
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
success_count = 0
|
94
|
+
errors = []
|
95
|
+
|
96
|
+
imported_data.each_with_index do |data, index|
|
97
|
+
test_case = @eval_set.test_cases.build(
|
98
|
+
input_variables: data[:input_variables],
|
99
|
+
expected_output: data[:expected_output],
|
100
|
+
description: data[:description]
|
101
|
+
)
|
102
|
+
|
103
|
+
if test_case.save
|
104
|
+
success_count += 1
|
105
|
+
else
|
106
|
+
errors << "Row #{index + 1}: #{test_case.errors.full_messages.join(", ")}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Clear the session data
|
111
|
+
session.delete(:imported_test_cases)
|
112
|
+
|
113
|
+
if errors.any?
|
114
|
+
flash[:alert] = "Import completed with errors: #{errors.join("; ")}"
|
115
|
+
else
|
116
|
+
flash[:notice] = "Successfully imported #{success_count} test cases."
|
117
|
+
end
|
118
|
+
|
119
|
+
redirect_to prompt_eval_set_path(@prompt, @eval_set)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def set_prompt
|
125
|
+
@prompt = Prompt.find(params[:prompt_id])
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_eval_set
|
129
|
+
@eval_set = @prompt.eval_sets.find(params[:eval_set_id])
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_test_case
|
133
|
+
@test_case = @eval_set.test_cases.find(params[:id])
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_case_params
|
137
|
+
params.require(:test_case).permit(:description, :expected_output, input_variables: {})
|
138
|
+
end
|
139
|
+
|
140
|
+
def detect_file_type(file)
|
141
|
+
filename = file.original_filename.downcase
|
142
|
+
|
143
|
+
if filename.ends_with?(".csv")
|
144
|
+
:csv
|
145
|
+
elsif filename.ends_with?(".json")
|
146
|
+
:json
|
147
|
+
else
|
148
|
+
:unknown
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def parse_csv(content)
|
153
|
+
require "csv"
|
154
|
+
|
155
|
+
# Get prompt parameters for column validation
|
156
|
+
expected_params = @prompt.parameters.pluck(:name)
|
157
|
+
|
158
|
+
CSV.parse(content, headers: true) do |row|
|
159
|
+
# Extract input variables from prompt parameter columns
|
160
|
+
input_variables = {}
|
161
|
+
|
162
|
+
expected_params.each do |param_name|
|
163
|
+
if row.headers.include?(param_name)
|
164
|
+
input_variables[param_name] = row[param_name]
|
165
|
+
else
|
166
|
+
@errors << "Missing required column: #{param_name}"
|
167
|
+
return
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Check for expected_output column
|
172
|
+
unless row.headers.include?("expected_output")
|
173
|
+
@errors << "Missing required column: expected_output"
|
174
|
+
return
|
175
|
+
end
|
176
|
+
|
177
|
+
# Add to imported data
|
178
|
+
@imported_data << {
|
179
|
+
input_variables: input_variables,
|
180
|
+
expected_output: row["expected_output"],
|
181
|
+
description: row["description"] # Optional column
|
182
|
+
}
|
183
|
+
end
|
184
|
+
rescue CSV::MalformedCSVError => e
|
185
|
+
@errors << "Invalid CSV format: #{e.message}"
|
186
|
+
end
|
187
|
+
|
188
|
+
def parse_json(content)
|
189
|
+
data = JSON.parse(content)
|
190
|
+
|
191
|
+
unless data.is_a?(Array)
|
192
|
+
@errors << "JSON must be an array of objects"
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
expected_params = @prompt.parameters.pluck(:name)
|
197
|
+
|
198
|
+
data.each_with_index do |item, index|
|
199
|
+
unless item.is_a?(Hash)
|
200
|
+
@errors << "Item #{index + 1} must be an object"
|
201
|
+
next
|
202
|
+
end
|
203
|
+
|
204
|
+
unless item["input_variables"].is_a?(Hash)
|
205
|
+
@errors << "Item #{index + 1}: input_variables must be an object"
|
206
|
+
next
|
207
|
+
end
|
208
|
+
|
209
|
+
unless item["expected_output"].present?
|
210
|
+
@errors << "Item #{index + 1}: expected_output is required"
|
211
|
+
next
|
212
|
+
end
|
213
|
+
|
214
|
+
# Validate that all required parameters are present
|
215
|
+
missing_params = expected_params - item["input_variables"].keys
|
216
|
+
if missing_params.any?
|
217
|
+
@errors << "Item #{index + 1}: missing required parameters: #{missing_params.join(", ")}"
|
218
|
+
next
|
219
|
+
end
|
220
|
+
|
221
|
+
@imported_data << {
|
222
|
+
input_variables: item["input_variables"],
|
223
|
+
expected_output: item["expected_output"],
|
224
|
+
description: item["description"]
|
225
|
+
}
|
226
|
+
end
|
227
|
+
rescue JSON::ParserError => e
|
228
|
+
@errors << "Invalid JSON format: #{e.message}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|