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,90 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class VersionsController < ApplicationController
|
3
|
+
layout "prompt_engine/admin"
|
4
|
+
|
5
|
+
before_action :set_prompt
|
6
|
+
before_action :set_version, only: [ :show, :restore ]
|
7
|
+
before_action :set_compare_versions, only: [ :compare ]
|
8
|
+
|
9
|
+
def index
|
10
|
+
@versions = @prompt.versions
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
end
|
15
|
+
|
16
|
+
def compare
|
17
|
+
if @version_a && @version_b
|
18
|
+
@changes = calculate_changes(@version_a, @version_b)
|
19
|
+
else
|
20
|
+
redirect_to prompt_versions_path(@prompt), alert: "Please select two versions to compare"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def restore
|
25
|
+
ActiveRecord::Base.transaction do
|
26
|
+
@prompt.update!(
|
27
|
+
content: @version.content,
|
28
|
+
system_message: @version.system_message,
|
29
|
+
model: @version.model,
|
30
|
+
temperature: @version.temperature,
|
31
|
+
max_tokens: @version.max_tokens
|
32
|
+
)
|
33
|
+
end
|
34
|
+
redirect_to @prompt, notice: "Prompt restored to version #{@version.version_number}"
|
35
|
+
rescue => e
|
36
|
+
redirect_to prompt_versions_path(@prompt), alert: "Failed to restore version: #{e.message}"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def set_prompt
|
42
|
+
@prompt = Prompt.find(params[:prompt_id])
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_version
|
46
|
+
@version = @prompt.versions.find(params[:id])
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_compare_versions
|
50
|
+
if params[:version_a_id].present? && params[:version_b_id].present?
|
51
|
+
@version_a = @prompt.versions.find_by(id: params[:version_a_id])
|
52
|
+
@version_b = @prompt.versions.find_by(id: params[:version_b_id])
|
53
|
+
elsif params[:id].present?
|
54
|
+
@version_b = @prompt.versions.find(params[:id])
|
55
|
+
@version_a = @prompt.versions.where("version_number < ?", @version_b.version_number).order(version_number: :desc).first
|
56
|
+
@version_a ||= @version_b
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def calculate_changes(version_a, version_b)
|
61
|
+
{
|
62
|
+
content: {
|
63
|
+
old: version_a.content,
|
64
|
+
new: version_b.content,
|
65
|
+
changed: version_a.content != version_b.content
|
66
|
+
},
|
67
|
+
system_message: {
|
68
|
+
old: version_a.system_message,
|
69
|
+
new: version_b.system_message,
|
70
|
+
changed: version_a.system_message != version_b.system_message
|
71
|
+
},
|
72
|
+
model: {
|
73
|
+
old: version_a.model,
|
74
|
+
new: version_b.model,
|
75
|
+
changed: version_a.model != version_b.model
|
76
|
+
},
|
77
|
+
temperature: {
|
78
|
+
old: version_a.temperature,
|
79
|
+
new: version_b.temperature,
|
80
|
+
changed: version_a.temperature != version_b.temperature
|
81
|
+
},
|
82
|
+
max_tokens: {
|
83
|
+
old: version_a.max_tokens,
|
84
|
+
new: version_b.max_tokens,
|
85
|
+
changed: version_a.max_tokens != version_b.max_tokens
|
86
|
+
}
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class EvalResult < ApplicationRecord
|
3
|
+
belongs_to :eval_run
|
4
|
+
belongs_to :test_case
|
5
|
+
|
6
|
+
scope :passed, -> { where(passed: true) }
|
7
|
+
scope :failed, -> { where(passed: false) }
|
8
|
+
scope :by_execution_time, -> { order(:execution_time_ms) }
|
9
|
+
|
10
|
+
def execution_time_seconds
|
11
|
+
return nil unless execution_time_ms
|
12
|
+
execution_time_ms / 1000.0
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
passed? ? "passed" : "failed"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class EvalRun < ApplicationRecord
|
3
|
+
belongs_to :eval_set
|
4
|
+
belongs_to :prompt_version
|
5
|
+
has_many :eval_results, dependent: :destroy
|
6
|
+
|
7
|
+
enum :status, { pending: 0, running: 1, completed: 2, failed: 3 }
|
8
|
+
|
9
|
+
scope :recent, -> { order(created_at: :desc) }
|
10
|
+
scope :by_status, ->(status) { where(status: status) }
|
11
|
+
scope :successful, -> { completed.where("passed_count > 0") }
|
12
|
+
|
13
|
+
def success_rate
|
14
|
+
return 0 if total_count.zero?
|
15
|
+
(passed_count.to_f / total_count * 100).round(1)
|
16
|
+
end
|
17
|
+
|
18
|
+
def duration
|
19
|
+
return nil unless started_at && completed_at
|
20
|
+
completed_at - started_at
|
21
|
+
end
|
22
|
+
|
23
|
+
def duration_in_words
|
24
|
+
return "Not started" unless started_at
|
25
|
+
return "Running" if running?
|
26
|
+
return "Failed" if failed? && !completed_at
|
27
|
+
|
28
|
+
seconds = duration
|
29
|
+
return nil unless seconds
|
30
|
+
|
31
|
+
if seconds < 60
|
32
|
+
"#{seconds.round} seconds"
|
33
|
+
elsif seconds < 3600
|
34
|
+
"#{(seconds / 60).round} minutes"
|
35
|
+
else
|
36
|
+
"#{(seconds / 3600).round(1)} hours"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class EvalSet < ApplicationRecord
|
3
|
+
belongs_to :prompt
|
4
|
+
has_many :test_cases, dependent: :destroy
|
5
|
+
has_many :eval_runs, dependent: :destroy
|
6
|
+
|
7
|
+
GRADER_TYPES = {
|
8
|
+
exact_match: "Exact Match",
|
9
|
+
regex: "Regular Expression",
|
10
|
+
contains: "Contains Text",
|
11
|
+
json_schema: "JSON Match (Exact)"
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
validates :name, presence: true
|
15
|
+
validates :name, uniqueness: { scope: :prompt_id }
|
16
|
+
validates :grader_type, presence: true, inclusion: { in: GRADER_TYPES.keys.map(&:to_s) }
|
17
|
+
|
18
|
+
validate :validate_grader_config
|
19
|
+
|
20
|
+
scope :by_name, -> { order(:name) }
|
21
|
+
scope :with_test_cases, -> { includes(:test_cases) }
|
22
|
+
|
23
|
+
def latest_run
|
24
|
+
eval_runs.recent.first
|
25
|
+
end
|
26
|
+
|
27
|
+
def average_success_rate
|
28
|
+
runs_with_results = eval_runs.completed.where("total_count > 0")
|
29
|
+
return 0 if runs_with_results.empty?
|
30
|
+
|
31
|
+
total_passed = runs_with_results.sum(:passed_count)
|
32
|
+
total_count = runs_with_results.sum(:total_count)
|
33
|
+
|
34
|
+
return 0 if total_count.zero?
|
35
|
+
(total_passed.to_f / total_count * 100).round(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ready_to_run?
|
39
|
+
test_cases.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def grader_type_display
|
43
|
+
GRADER_TYPES[grader_type.to_sym] || grader_type.humanize
|
44
|
+
end
|
45
|
+
|
46
|
+
def requires_grader_config?
|
47
|
+
%w[regex json_schema].include?(grader_type)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def validate_grader_config
|
53
|
+
return unless requires_grader_config?
|
54
|
+
|
55
|
+
case grader_type
|
56
|
+
when "regex"
|
57
|
+
validate_regex_config
|
58
|
+
when "json_schema"
|
59
|
+
validate_json_schema_config
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_regex_config
|
64
|
+
pattern = grader_config["pattern"]
|
65
|
+
|
66
|
+
if pattern.blank?
|
67
|
+
errors.add(:grader_config, "regex pattern is required")
|
68
|
+
return
|
69
|
+
end
|
70
|
+
|
71
|
+
begin
|
72
|
+
Regexp.new(pattern)
|
73
|
+
rescue RegexpError => e
|
74
|
+
errors.add(:grader_config, "invalid regex pattern: #{e.message}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_json_schema_config
|
79
|
+
schema = grader_config["schema"]
|
80
|
+
|
81
|
+
if schema.blank?
|
82
|
+
errors.add(:grader_config, "JSON schema is required")
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
begin
|
87
|
+
JSON.parse(schema.to_json) if schema.is_a?(Hash)
|
88
|
+
# Basic schema validation - check for required fields
|
89
|
+
unless schema.is_a?(Hash) && schema["type"].present?
|
90
|
+
errors.add(:grader_config, "JSON schema must include a 'type' field")
|
91
|
+
end
|
92
|
+
rescue JSON::ParserError => e
|
93
|
+
errors.add(:grader_config, "invalid JSON schema: #{e.message}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class Parameter < ApplicationRecord
|
3
|
+
self.table_name = "prompt_engine_parameters"
|
4
|
+
|
5
|
+
# Parameter types that can be used
|
6
|
+
TYPES = %w[string integer decimal boolean datetime date array json].freeze
|
7
|
+
|
8
|
+
belongs_to :prompt, class_name: "PromptEngine::Prompt"
|
9
|
+
|
10
|
+
validates :name, presence: true,
|
11
|
+
uniqueness: { scope: :prompt_id },
|
12
|
+
format: { with: /\A[a-zA-Z_][a-zA-Z0-9_]*\z/, message: "must start with a letter or underscore and contain only letters, numbers, and underscores" }
|
13
|
+
validates :parameter_type, presence: true, inclusion: { in: TYPES }
|
14
|
+
validates :required, inclusion: { in: [ true, false ] }
|
15
|
+
|
16
|
+
scope :required, -> { where(required: true) }
|
17
|
+
scope :optional, -> { where(required: false) }
|
18
|
+
scope :ordered, -> { order(position: :asc, created_at: :asc) }
|
19
|
+
|
20
|
+
before_validation :set_defaults
|
21
|
+
|
22
|
+
# Convert the parameter value based on its type
|
23
|
+
def cast_value(value)
|
24
|
+
return default_value if value.blank? && !required?
|
25
|
+
|
26
|
+
case parameter_type
|
27
|
+
when "integer"
|
28
|
+
value.to_i
|
29
|
+
when "decimal"
|
30
|
+
value.to_f
|
31
|
+
when "boolean"
|
32
|
+
ActiveModel::Type::Boolean.new.cast(value)
|
33
|
+
when "datetime"
|
34
|
+
DateTime.parse(value.to_s) rescue nil
|
35
|
+
when "date"
|
36
|
+
Date.parse(value.to_s) rescue nil
|
37
|
+
when "array"
|
38
|
+
value.is_a?(Array) ? value : value.to_s.split(",").map(&:strip)
|
39
|
+
when "json"
|
40
|
+
value.is_a?(String) ? JSON.parse(value) : value rescue {}
|
41
|
+
else
|
42
|
+
value.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Validate a value against this parameter's rules
|
47
|
+
def validate_value(value)
|
48
|
+
errors = []
|
49
|
+
|
50
|
+
if required? && value.blank?
|
51
|
+
errors << "#{name} is required"
|
52
|
+
end
|
53
|
+
|
54
|
+
if validation_rules.present?
|
55
|
+
# Apply custom validation rules
|
56
|
+
if validation_rules["min_length"] && value.to_s.length < validation_rules["min_length"]
|
57
|
+
errors << "#{name} must be at least #{validation_rules['min_length']} characters"
|
58
|
+
end
|
59
|
+
|
60
|
+
if validation_rules["max_length"] && value.to_s.length > validation_rules["max_length"]
|
61
|
+
errors << "#{name} must be at most #{validation_rules['max_length']} characters"
|
62
|
+
end
|
63
|
+
|
64
|
+
if validation_rules["pattern"] && !value.to_s.match?(Regexp.new(validation_rules["pattern"]))
|
65
|
+
errors << "#{name} must match pattern: #{validation_rules['pattern']}"
|
66
|
+
end
|
67
|
+
|
68
|
+
if validation_rules["min"] && cast_value(value) < validation_rules["min"]
|
69
|
+
errors << "#{name} must be at least #{validation_rules['min']}"
|
70
|
+
end
|
71
|
+
|
72
|
+
if validation_rules["max"] && cast_value(value) > validation_rules["max"]
|
73
|
+
errors << "#{name} must be at most #{validation_rules['max']}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
errors
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generate form input attributes for this parameter
|
81
|
+
def form_input_options
|
82
|
+
options = {
|
83
|
+
label: name.humanize,
|
84
|
+
required: required?,
|
85
|
+
placeholder: example_value,
|
86
|
+
hint: description
|
87
|
+
}
|
88
|
+
|
89
|
+
case parameter_type
|
90
|
+
when "integer", "decimal"
|
91
|
+
options[:type] = "number"
|
92
|
+
options[:step] = parameter_type == "decimal" ? "0.01" : "1"
|
93
|
+
options[:min] = validation_rules["min"] if validation_rules&.dig("min")
|
94
|
+
options[:max] = validation_rules["max"] if validation_rules&.dig("max")
|
95
|
+
when "boolean"
|
96
|
+
options[:type] = "checkbox"
|
97
|
+
when "datetime"
|
98
|
+
options[:type] = "datetime-local"
|
99
|
+
when "date"
|
100
|
+
options[:type] = "date"
|
101
|
+
when "array"
|
102
|
+
options[:type] = "text"
|
103
|
+
options[:hint] = "#{description} (comma-separated values)"
|
104
|
+
when "json"
|
105
|
+
options[:type] = "textarea"
|
106
|
+
options[:hint] = "#{description} (JSON format)"
|
107
|
+
else
|
108
|
+
options[:type] = "text"
|
109
|
+
options[:minlength] = validation_rules["min_length"] if validation_rules&.dig("min_length")
|
110
|
+
options[:maxlength] = validation_rules["max_length"] if validation_rules&.dig("max_length")
|
111
|
+
options[:pattern] = validation_rules["pattern"] if validation_rules&.dig("pattern")
|
112
|
+
end
|
113
|
+
|
114
|
+
options[:value] = default_value if default_value.present?
|
115
|
+
|
116
|
+
options
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def set_defaults
|
122
|
+
self.parameter_type ||= "string"
|
123
|
+
self.required = true if required.nil?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class ParameterParser
|
3
|
+
attr_reader :content
|
4
|
+
|
5
|
+
def initialize(content)
|
6
|
+
@content = content
|
7
|
+
end
|
8
|
+
|
9
|
+
def extract_parameters
|
10
|
+
return [] if content.blank?
|
11
|
+
|
12
|
+
parameter_names = content.scan(/\{\{([^}]+)\}\}/).flatten.map(&:strip).uniq
|
13
|
+
|
14
|
+
parameter_names.map do |name|
|
15
|
+
{
|
16
|
+
name: name,
|
17
|
+
placeholder: "{{#{name}}}",
|
18
|
+
required: true
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def replace_parameters(parameters = {})
|
24
|
+
result = content.dup
|
25
|
+
|
26
|
+
return result if parameters.nil?
|
27
|
+
|
28
|
+
parameters.each do |key, value|
|
29
|
+
result.gsub!("{{#{key}}}", value.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_parameters?
|
36
|
+
extract_parameters.any?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class PlaygroundRunResult < ApplicationRecord
|
3
|
+
self.table_name = "prompt_engine_playground_run_results"
|
4
|
+
|
5
|
+
belongs_to :prompt_version, class_name: "PromptEngine::PromptVersion"
|
6
|
+
|
7
|
+
validates :provider, presence: true
|
8
|
+
validates :model, presence: true
|
9
|
+
validates :rendered_prompt, presence: true
|
10
|
+
validates :response, presence: true
|
11
|
+
validates :execution_time, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
12
|
+
validates :token_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
13
|
+
|
14
|
+
serialize :parameters, coder: JSON
|
15
|
+
|
16
|
+
scope :recent, -> { order(created_at: :desc) }
|
17
|
+
scope :by_provider, ->(provider) { where(provider: provider) }
|
18
|
+
scope :successful, -> { where.not(response: nil) }
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module PromptEngine
|
2
|
+
class Prompt < ApplicationRecord
|
3
|
+
self.table_name = "prompt_engine_prompts"
|
4
|
+
|
5
|
+
has_many :versions, -> { order(version_number: :desc) },
|
6
|
+
class_name: "PromptEngine::PromptVersion",
|
7
|
+
dependent: :destroy
|
8
|
+
has_many :parameters, -> { ordered },
|
9
|
+
class_name: "PromptEngine::Parameter",
|
10
|
+
dependent: :destroy
|
11
|
+
has_many :eval_sets,
|
12
|
+
class_name: "PromptEngine::EvalSet",
|
13
|
+
dependent: :destroy
|
14
|
+
|
15
|
+
attr_accessor :change_summary
|
16
|
+
|
17
|
+
validates :name, presence: true, uniqueness: { scope: :status }
|
18
|
+
validates :content, presence: true
|
19
|
+
|
20
|
+
accepts_nested_attributes_for :parameters, allow_destroy: true
|
21
|
+
|
22
|
+
enum :status, {
|
23
|
+
draft: "draft",
|
24
|
+
active: "active",
|
25
|
+
archived: "archived"
|
26
|
+
}, default: "draft"
|
27
|
+
|
28
|
+
scope :active, -> { where(status: "active") }
|
29
|
+
scope :by_name, -> { order(:name) }
|
30
|
+
|
31
|
+
after_create :create_initial_version
|
32
|
+
after_create :sync_parameters!
|
33
|
+
after_update :create_version_if_changed
|
34
|
+
after_update :sync_parameters!, if: :saved_change_to_content?
|
35
|
+
before_save :clean_orphaned_parameters
|
36
|
+
|
37
|
+
VERSIONED_ATTRIBUTES = %w[content system_message model temperature max_tokens metadata].freeze
|
38
|
+
|
39
|
+
def current_version
|
40
|
+
versions.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def version_count
|
44
|
+
versions_count
|
45
|
+
end
|
46
|
+
|
47
|
+
def restore_version!(version_number)
|
48
|
+
version = versions.find_by!(version_number: version_number)
|
49
|
+
version.restore!
|
50
|
+
end
|
51
|
+
|
52
|
+
def version_at(version_number)
|
53
|
+
versions.find_by(version_number: version_number)
|
54
|
+
end
|
55
|
+
|
56
|
+
def versioned_attributes_changed?
|
57
|
+
# This method is for checking if versioned attributes have changed before save
|
58
|
+
(changed & VERSIONED_ATTRIBUTES).any?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Parameter management methods
|
62
|
+
def detect_variables
|
63
|
+
# Don't cache the detector as content can change
|
64
|
+
PromptEngine::VariableDetector.new(content).variable_names
|
65
|
+
end
|
66
|
+
|
67
|
+
def sync_parameters!
|
68
|
+
detected_vars = detect_variables
|
69
|
+
existing_names = parameters.pluck(:name)
|
70
|
+
|
71
|
+
# Add new parameters
|
72
|
+
new_vars = detected_vars - existing_names
|
73
|
+
if new_vars.any?
|
74
|
+
# Get max position once, before the loop
|
75
|
+
max_position = parameters.maximum(:position) || 0
|
76
|
+
detector = PromptEngine::VariableDetector.new(content)
|
77
|
+
|
78
|
+
new_vars.each_with_index do |var_name, index|
|
79
|
+
var_info = detector.extract_variables.find { |v| v[:name] == var_name }
|
80
|
+
|
81
|
+
# Skip if parameter already exists (race condition protection)
|
82
|
+
next if parameters.exists?(name: var_name)
|
83
|
+
|
84
|
+
parameters.create!(
|
85
|
+
name: var_name,
|
86
|
+
parameter_type: var_info[:type],
|
87
|
+
required: var_info[:required],
|
88
|
+
position: max_position + index + 1
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Remove parameters that no longer exist
|
94
|
+
removed_vars = existing_names - detected_vars
|
95
|
+
parameters.where(name: removed_vars).destroy_all if removed_vars.any?
|
96
|
+
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_with_params(provided_params = {})
|
101
|
+
detector = PromptEngine::VariableDetector.new(content)
|
102
|
+
|
103
|
+
# Validate all required parameters are provided
|
104
|
+
validation = validate_parameters(provided_params)
|
105
|
+
return { error: validation[:errors].join(", ") } unless validation[:valid]
|
106
|
+
|
107
|
+
# Cast parameters to their correct types, including defaults
|
108
|
+
casted_params = {}
|
109
|
+
parameters.each do |param|
|
110
|
+
value = provided_params[param.name] || provided_params[param.name.to_sym]
|
111
|
+
# Let cast_value handle the default value logic
|
112
|
+
casted_params[param.name] = param.cast_value(value)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Also include any parameters not defined in the database but present in the template
|
116
|
+
detected_vars = detect_variables
|
117
|
+
detected_vars.each do |var_name|
|
118
|
+
unless casted_params.key?(var_name)
|
119
|
+
value = provided_params[var_name] || provided_params[var_name.to_sym]
|
120
|
+
casted_params[var_name] = value.to_s if value.present?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
{
|
125
|
+
content: detector.render(casted_params),
|
126
|
+
system_message: system_message,
|
127
|
+
model: model,
|
128
|
+
temperature: temperature,
|
129
|
+
max_tokens: max_tokens,
|
130
|
+
parameters_used: casted_params
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate_parameters(provided_params = {})
|
135
|
+
errors = []
|
136
|
+
|
137
|
+
parameters.each do |param|
|
138
|
+
value = provided_params[param.name] || provided_params[param.name.to_sym]
|
139
|
+
# Use default value if not provided and parameter is optional
|
140
|
+
if value.blank? && !param.required? && param.default_value.present?
|
141
|
+
value = param.default_value
|
142
|
+
end
|
143
|
+
param_errors = param.validate_value(value)
|
144
|
+
errors.concat(param_errors)
|
145
|
+
end
|
146
|
+
|
147
|
+
{
|
148
|
+
valid: errors.empty?,
|
149
|
+
errors: errors
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def create_initial_version
|
156
|
+
versions.create!(
|
157
|
+
content: content,
|
158
|
+
system_message: system_message,
|
159
|
+
model: model,
|
160
|
+
temperature: temperature,
|
161
|
+
max_tokens: max_tokens,
|
162
|
+
metadata: metadata,
|
163
|
+
change_description: "Initial version"
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_version_if_changed
|
168
|
+
# Check saved_changes in the after_update callback
|
169
|
+
return unless (saved_changes.keys & VERSIONED_ATTRIBUTES).any?
|
170
|
+
|
171
|
+
versions.create!(
|
172
|
+
content: content,
|
173
|
+
system_message: system_message,
|
174
|
+
model: model,
|
175
|
+
temperature: temperature,
|
176
|
+
max_tokens: max_tokens,
|
177
|
+
metadata: metadata,
|
178
|
+
change_description: "Updated: #{(saved_changes.keys & VERSIONED_ATTRIBUTES).join(', ')}"
|
179
|
+
)
|
180
|
+
end
|
181
|
+
|
182
|
+
def clean_orphaned_parameters
|
183
|
+
return unless content_changed?
|
184
|
+
|
185
|
+
# Mark parameters for destruction if their names are not in the content
|
186
|
+
detected_vars = detect_variables
|
187
|
+
parameters.each do |param|
|
188
|
+
param.mark_for_destruction unless detected_vars.include?(param.name)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|