Class: Aidp::Prompts::PromptTemplateManager

Inherits:
Object
  • Object
show all
Defined in:
lib/aidp/prompts/prompt_template_manager.rb

Overview

Manages dynamic prompt templates with support for:

  • Template loading from YAML files
  • Variable substitution with {placeholder} syntax
  • Template versioning and caching
  • User/project-level template customization
  • Per issue #402: Database-backed versioned templates with feedback-based selection

Template search order: 0. Database versioned templates (for versionable categories like work_loop)

  1. Project-level: .aidp/prompts//.yml
  2. User-level: ~/.aidp/prompts//.yml
  3. Built-in: lib/aidp/prompts/defaults//.yml

Examples:

Load and render a template

manager = PromptTemplateManager.new(project_dir: Dir.pwd)
prompt = manager.render("decision_engine/condition_detection",
  response: error_message)

Check if a template exists

manager.template_exists?("decision_engine/completion_detection")

Constant Summary collapse

TEMPLATE_EXT =

Template file extension

".yml"
CACHE_TTL =

Cache TTL for template metadata (5 minutes)

300
VERSIONED_CATEGORIES =

Categories that support database versioning

%w[work_loop].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project_dir: Dir.pwd, version_manager: nil) ⇒ PromptTemplateManager

Returns a new instance of PromptTemplateManager.



42
43
44
45
46
47
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 42

def initialize(project_dir: Dir.pwd, version_manager: nil)
  @project_dir = project_dir
  @cache = {}
  @cache_timestamps = {}
  @version_manager_instance = version_manager
end

Instance Attribute Details

#cacheObject (readonly)

Returns the value of attribute cache.



40
41
42
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 40

def cache
  @cache
end

#project_dirObject (readonly)

Returns the value of attribute project_dir.



40
41
42
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 40

def project_dir
  @project_dir
end

Instance Method Details

#clear_cacheObject

Clear all cached templates



298
299
300
301
302
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 298

def clear_cache
  @cache.clear
  @cache_timestamps.clear
  Aidp.log_debug("prompt_template_manager", "cache_cleared")
end

#customize_template(template_id) ⇒ String

Copy a template to project-level for customization

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (String)

    Path to the new template file

Raises:



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 255

def customize_template(template_id)
  source_path = find_template_path(template_id)
  raise TemplateNotFoundError, "Template not found: #{template_id}" unless source_path

  dest_path = project_template_path(template_id)
  return dest_path if File.exist?(dest_path)

  FileUtils.mkdir_p(File.dirname(dest_path))
  FileUtils.cp(source_path, dest_path)

  Aidp.log_info("prompt_template_manager", "template_customized",
    template_id: template_id,
    path: dest_path)

  # Clear cache for this template
  invalidate_cache("template:#{template_id}")

  dest_path
end

#determine_source(path) ⇒ Symbol

Determine the source of a template based on its path

Parameters:

  • path (String)

    Full path to the template file

Returns:

  • (Symbol)

    :project, :user, or :builtin



317
318
319
320
321
322
323
324
325
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 317

def determine_source(path)
  if path.start_with?(project_prompts_dir)
    :project
  elsif path.start_with?(user_prompts_dir)
    :user
  else
    :builtin
  end
end

#list_templatesArray<Hash>

List all available templates

Returns:

  • (Array<Hash>)

    List of template metadata



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 195

def list_templates
  templates = []

  search_paths.each do |base_path|
    next unless Dir.exist?(base_path)

    Dir.glob(File.join(base_path, "**", "*#{TEMPLATE_EXT}")).each do |path|
      relative_path = path.sub("#{base_path}/", "")
      template_id = relative_path.sub(TEMPLATE_EXT, "")

      # Only add if not already found (respects precedence)
      next if templates.any? { |t| t[:id] == template_id }

      template_data = load_yaml_template(path)
      next if template_data.nil?

      templates << {
        id: template_id,
        path: path,
        name: template_data["name"] || template_id,
        description: template_data["description"],
        version: template_data["version"],
        category: File.dirname(template_id)
      }
    end
  end

  templates.sort_by { |t| t[:id] }
end

#load_template(template_id, use_versioned: true) ⇒ Hash?

Load template metadata without rendering Per issue #402: Checks database versions first for versionable templates

Parameters:

  • template_id (String)

    Template identifier

  • use_versioned (Boolean) (defaults to: true)

    Whether to check versioned templates (default: true)

Returns:

  • (Hash, nil)

    Template data or nil if not found



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 130

def load_template(template_id, use_versioned: true)
  cache_key = "template:#{template_id}"

  # Check cache
  if cached_valid?(cache_key)
    Aidp.log_debug("prompt_template_manager", "cache_hit",
      template_id: template_id)
    return @cache[cache_key]
  end

  # Try versioned template first for versionable categories
  if use_versioned && versionable?(template_id)
    versioned = load_versioned_template(template_id)
    if versioned
      set_cache(cache_key, versioned)
      return versioned
    end
  end

  # Search for template in order of precedence (file-based)
  template_path = find_template_path(template_id)
  return nil unless template_path

  Aidp.log_debug("prompt_template_manager", "loading_template",
    template_id: template_id,
    path: template_path)

  template_data = load_yaml_template(template_path)
  set_cache(cache_key, template_data)
  template_data
end

#load_versioned_template(template_id) ⇒ Hash?

Load template data from versioned storage

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (Hash, nil)

    Template data or nil if no version exists



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 166

def load_versioned_template(template_id)
  return nil unless versionable?(template_id)

  active = version_manager.active_version(template_id: template_id)
  return nil unless active

  Aidp.log_debug("prompt_template_manager", "loading_versioned_template",
    template_id: template_id,
    version_id: active[:id])

  YAML.safe_load(active[:content], permitted_classes: [Symbol], aliases: true)
rescue => e
  Aidp.log_warn("prompt_template_manager", "versioned_load_failed",
    template_id: template_id,
    error: e.message)
  nil
end

#render(template_id, use_versioned: true, **variables) ⇒ String

Render a template with variable substitution Per issue #402: Checks database versions first for versionable templates

Parameters:

  • template_id (String)

    Template identifier (e.g., "decision_engine/condition_detection")

  • variables (Hash)

    Variables to substitute in the template

  • use_versioned (Boolean) (defaults to: true)

    Whether to check versioned templates (default: true)

Returns:

  • (String)

    Rendered prompt text

Raises:



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 74

def render(template_id, use_versioned: true, **variables)
  Aidp.log_debug("prompt_template_manager", "rendering_template",
    template_id: template_id,
    variables: variables.keys,
    use_versioned: use_versioned)

  # Try versioned template first for versionable categories
  if use_versioned && versionable?(template_id)
    versioned_result = render_versioned(template_id, **variables)
    return versioned_result if versioned_result
  end

  template_data = load_template(template_id, use_versioned: use_versioned)
  raise TemplateNotFoundError, "Template not found: #{template_id}" if template_data.nil?

  prompt_text = template_data["prompt"] || template_data[:prompt]
  raise TemplateNotFoundError, "Template has no prompt: #{template_id}" if prompt_text.nil?

  substitute_variables(prompt_text, variables)
end

#render_versioned(template_id, **variables) ⇒ String?

Render using a versioned template from the database

Parameters:

  • template_id (String)

    Template identifier

  • variables (Hash)

    Variables to substitute

Returns:

  • (String, nil)

    Rendered prompt or nil if no version exists



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 100

def render_versioned(template_id, **variables)
  return nil unless versionable?(template_id)

  active = version_manager.active_version(template_id: template_id)
  return nil unless active

  Aidp.log_debug("prompt_template_manager", "rendering_versioned_template",
    template_id: template_id,
    version_id: active[:id],
    version_number: active[:version_number])

  # Parse the stored YAML content
  template_data = YAML.safe_load(active[:content], permitted_classes: [Symbol], aliases: true)
  prompt_text = template_data["prompt"] || template_data[:prompt]
  return nil unless prompt_text

  substitute_variables(prompt_text, variables)
rescue => e
  Aidp.log_warn("prompt_template_manager", "versioned_render_failed",
    template_id: template_id,
    error: e.message)
  nil
end

#reset_template(template_id) ⇒ Boolean

Reset a customized template to default

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (Boolean)

    True if reset, false if no customization existed



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 279

def reset_template(template_id)
  project_path = project_template_path(template_id)

  unless File.exist?(project_path)
    Aidp.log_debug("prompt_template_manager", "no_customization_to_reset",
      template_id: template_id)
    return false
  end

  FileUtils.rm(project_path)

  Aidp.log_info("prompt_template_manager", "template_reset",
    template_id: template_id)

  invalidate_cache("template:#{template_id}")
  true
end

#search_pathsObject

Get search paths in order of precedence



305
306
307
308
309
310
311
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 305

def search_paths
  [
    project_prompts_dir,
    user_prompts_dir,
    builtin_prompts_dir
  ]
end

#template_exists?(template_id) ⇒ Boolean

Check if a template exists

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (Boolean)


188
189
190
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 188

def template_exists?(template_id)
  !find_template_path(template_id).nil?
end

#template_info(template_id) ⇒ Hash?

Get template info

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (Hash, nil)

    Template info including path and source



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 229

def template_info(template_id)
  path = find_template_path(template_id)
  return nil unless path

  template_data = load_yaml_template(path)
  return nil if template_data.nil?

  source = determine_source(path)

  {
    id: template_id,
    path: path,
    source: source,
    name: template_data["name"],
    description: template_data["description"],
    version: template_data["version"],
    variables: extract_variables(template_data["prompt"]),
    schema: template_data["schema"],
    prompt_preview: truncate(template_data["prompt"], 500)
  }
end

#version_managerObject

Lazily initialize version manager



50
51
52
53
54
55
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 50

def version_manager
  @version_manager_instance ||= begin
    require_relative "template_version_manager"
    TemplateVersionManager.new(project_dir: @project_dir)
  end
end

#versionable?(template_id) ⇒ Boolean

Check if a template category supports versioning

Parameters:

  • template_id (String)

    Template identifier

Returns:

  • (Boolean)


61
62
63
64
# File 'lib/aidp/prompts/prompt_template_manager.rb', line 61

def versionable?(template_id)
  category = template_id.split("/").first
  VERSIONED_CATEGORIES.include?(category)
end