Class: Observ::Prompt

Inherits:
ApplicationRecord show all
Includes:
AASM
Defined in:
app/models/observ/prompt.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.cache_key_for(name:, version:, state:) ⇒ Object



98
99
100
# File 'app/models/observ/prompt.rb', line 98

def self.cache_key_for(name:, version:, state:)
  "observ:prompt:#{name}:#{version || state}"
end

.clear_cache(name:) ⇒ Object



102
103
104
105
106
107
# File 'app/models/observ/prompt.rb', line 102

def self.clear_cache(name:)
  # Clear all cache keys for this prompt
  [:draft, :production, :archived].each do |state|
    Rails.cache.delete(cache_key_for(name: name, version: nil, state: state))
  end
end

.create_version(name:, prompt:, config: {}, commit_message: nil, created_by: nil, promote_to_production: false) ⇒ Object

Create new version (auto-increment)



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'app/models/observ/prompt.rb', line 110

def self.create_version(name:, prompt:, config: {}, commit_message: nil, created_by: nil, promote_to_production: false)
  latest_version = where(name: name).maximum(:version) || 0
  new_version = latest_version + 1

  new_prompt = create!(
    name: name,
    prompt: prompt,
    version: new_version,
    config: config,
    commit_message: commit_message,
    created_by: created_by,
    state: :draft
  )

  new_prompt.promote! if promote_to_production
  new_prompt
end

.fetch(name:, version: nil, state: :production, fallback: nil) ⇒ Object

Fetch prompt by name, state, or version



74
75
76
77
78
79
80
81
82
# File 'app/models/observ/prompt.rb', line 74

def self.fetch(name:, version: nil, state: :production, fallback: nil)
  state ||= Observ.config.prompt_default_state
  cache_key = cache_key_for(name: name, version: version, state: state)
  cache_ttl = Observ.config.prompt_cache_ttl

  Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
    fetch_from_database(name: name, version: version, state: state, fallback: fallback)
  end
end

.fetch_from_database(name:, version:, state:, fallback:) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'app/models/observ/prompt.rb', line 84

def self.fetch_from_database(name:, version:, state:, fallback:)
  query = where(name: name)

  prompt = if version.present?
    query.find_by(version: version)
  else
    query.public_send(state).first
  end

  return prompt if prompt
  return fallback if fallback
  raise PromptNotFoundError, "Prompt '#{name}' not found"
end

Instance Method Details

#can_delete?Boolean

Returns:

  • (Boolean)


179
180
181
# File 'app/models/observ/prompt.rb', line 179

def can_delete?
  draft? || archived?
end

#clone_to_draftObject

Clone to new draft version



184
185
186
187
188
189
190
191
192
# File 'app/models/observ/prompt.rb', line 184

def clone_to_draft
  self.class.create_version(
    name: name,
    prompt: prompt,
    config: config,
    commit_message: "Cloned from v#{version} (#{state})",
    created_by: nil
  )
end

#compile(variables = {}) ⇒ Object

Compile prompt with Mustache templating Supports: variables {name}, loops Observ::Prompt.{{#items}...{/items}, conditionals Observ::Prompt.{{#flag}...{/flag}, inverted sections {^items}...{/items}



135
136
137
# File 'app/models/observ/prompt.rb', line 135

def compile(variables = {})
  Mustache.render(prompt, variables)
end

#compile_with_validation(variables = {}) ⇒ Object

Compile with validation (raises if missing top-level variables) Note: Variables inside sections (loops) are validated at render time by Mustache



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'app/models/observ/prompt.rb', line 141

def compile_with_validation(variables = {})
  # Extract top-level variables (outside of sections)
  # This is a simplified approach - we strip section content and check remaining vars
  template_without_sections = strip_sections(prompt)

  # Matches: {{name}}, {{user.name}} but not {{#section}}, {{/section}}, {{^section}}, {{!comment}}, {{>partial}}, {{{raw}}}
  required_vars = template_without_sections.scan(/\{\{([^#\^\/!>\{\s][^}\s]*)\}\}/).flatten.uniq

  # Check which variables are missing (convert all keys to strings for comparison)
  provided_keys = variables.keys.map(&:to_s)
  missing_vars = required_vars.reject do |var|
    # Handle dot notation (e.g., "user.name" - check if "user" key exists)
    root_key = var.split(".").first
    provided_keys.include?(var) || provided_keys.include?(root_key)
  end

  if missing_vars.any?
    raise VariableSubstitutionError, "Missing variables: #{missing_vars.join(', ')}"
  end

  compile(variables)
end

#editable?Boolean

Immutability checks

Returns:

  • (Boolean)


171
172
173
# File 'app/models/observ/prompt.rb', line 171

def editable?
  draft?
end

#immutable?Boolean

Returns:

  • (Boolean)


175
176
177
# File 'app/models/observ/prompt.rb', line 175

def immutable?
  production? || archived?
end

#latest_versionObject



203
204
205
# File 'app/models/observ/prompt.rb', line 203

def latest_version
  self.class.where(name: name).order(version: :desc).first
end

#next_versionObject



199
200
201
# File 'app/models/observ/prompt.rb', line 199

def next_version
  self.class.where(name: name).where("version > ?", version).order(version: :asc).first
end

#previous_versionObject

Version navigation



195
196
197
# File 'app/models/observ/prompt.rb', line 195

def previous_version
  self.class.where(name: name).where("version < ?", version).order(version: :desc).first
end

#required_variablesObject

Extract top-level variables from template (for validation purposes)



165
166
167
168
# File 'app/models/observ/prompt.rb', line 165

def required_variables
  template_without_sections = strip_sections(prompt)
  template_without_sections.scan(/\{\{([^#\^\/!>\{\s][^}\s]*)\}\}/).flatten.uniq
end

#to_json_exportObject

Export



208
209
210
# File 'app/models/observ/prompt.rb', line 208

def to_json_export
  as_json(except: [:id, :created_at, :updated_at])
end

#to_yaml_exportObject



212
213
214
# File 'app/models/observ/prompt.rb', line 212

def to_yaml_export
  to_json_export.to_yaml
end