Class: LlmLogs::PromptSyncer

Inherits:
Object
  • Object
show all
Defined in:
app/services/llm_logs/prompt_syncer.rb

Constant Summary collapse

REQUIRED_FIELDS =
%w[slug name].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path:, auto_tag:) ⇒ PromptSyncer

Returns a new instance of PromptSyncer.



38
39
40
41
# File 'app/services/llm_logs/prompt_syncer.rb', line 38

def initialize(path:, auto_tag:)
  @path     = Pathname.new(path)
  @auto_tag = auto_tag
end

Class Method Details

.body_file_only?(path) ⇒ Boolean

Returns:

  • (Boolean)


25
26
27
28
29
30
31
# File 'app/services/llm_logs/prompt_syncer.rb', line 25

def self.body_file_only?(path)
  raw = File.read(path)
  # Files referenced as body_file targets have no YAML front-matter.
  # Files with front-matter are prompts and will be validated (missing
  # slug/name raises a clear error rather than being silently skipped).
  !raw.start_with?("---")
end

.parse(raw) ⇒ Object



33
34
35
36
# File 'app/services/llm_logs/prompt_syncer.rb', line 33

def self.parse(raw)
  _, front_yaml, body = raw.split(/^---\s*$/, 3)
  [YAML.safe_load(front_yaml.to_s, permitted_classes: [Symbol], aliases: true) || {}, body.to_s.sub(/\A\n+/, "")]
end

.sync_all(root:, subfolders:) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'app/services/llm_logs/prompt_syncer.rb', line 8

def self.sync_all(root:, subfolders:)
  root = Pathname.new(root)
  subfolders.each do |sub|
    dir = root / sub
    next unless dir.directory?

    Dir.glob(dir / "*.md").sort.each do |path|
      # Skip files that are referenced as body_file targets rather than
      # top-level prompts — those have no top-level `slug:` and are pulled
      # in by the parent template instead.
      pathname = Pathname.new(path)
      next if body_file_only?(pathname)
      new(path: pathname, auto_tag: sub).call
    end
  end
end

Instance Method Details

#callObject



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/services/llm_logs/prompt_syncer.rb', line 43

def call
  raw = @path.read
  raise "Missing front-matter in #{@path}" unless raw.start_with?("---")

  front, body = self.class.parse(raw)
  REQUIRED_FIELDS.each do |field|
    raise "Missing required front-matter field '#{field}' in #{@path}" unless front[field].is_a?(String) && !front[field].strip.empty?
  end

  ActiveRecord::Base.transaction do
    tags = (Array(front["tags"]) + [@auto_tag]).map(&:to_s).uniq
    prompt = LlmLogs::Prompt.find_or_initialize_by(slug: front["slug"])
    created = prompt.new_record?

    prompt.name        = front["name"]
    prompt.description = front["description"]
    prompt.tags        = tags
    prompt.save!

    messages = build_messages(front, body)
    model    = front["model"]
    model_params = front["model_params"].is_a?(Hash) ? front["model_params"] : {}
    model_params = model_params.deep_stringify_keys

    if version_needs_update?(prompt, messages, model, model_params)
      prompt.update_content!(messages: messages, model: model, model_params: model_params, changelog: "Synced from #{@path.basename}")
      status = created ? "Created" : "Updated"
    else
      status = "Unchanged"
    end

    log "#{status}: #{prompt.slug}"
  end
end