Module: Kward::ConfigFiles::Prompts
Instance Method Summary collapse
-
#active_persona_label(workspace_root:, model: nil, config: read_config) ⇒ Object
Returns the label of the persona selected by default/workspace/model rules.
- #add_persona_entry(entries, layer, value, name: nil) ⇒ Object
- #agents_prompt(config: read_config) ⇒ Object
- #canonical_workspace_root(path) ⇒ Object
- #character_entries(raw) ⇒ Object
- #character_entry(entry) ⇒ Object
- #config_agents_path ⇒ Object
- #config_principles_path ⇒ Object
- #crew_character_labels(personas) ⇒ Object
- #crew_characters(personas) ⇒ Object
- #extract_character_instruction(definition) ⇒ Object
- #extract_character_label(definition) ⇒ Object
-
#include_config_principles?(config = read_config) ⇒ Boolean
Returns whether config-directory principles are included in normal system prompt assembly.
- #named_character_values(personas) ⇒ Object
- #persona_entries(workspace_root:, model: nil, reasoning_effort: nil, now: Time.now, config: read_config, include_reasoning: true) ⇒ Object
- #persona_label_for_key(value, labels) ⇒ Object
-
#persona_prompt(workspace_root, model: nil, reasoning_effort: nil, now: Time.now, config: read_config) ⇒ String?
Builds persona prompt text from default, workspace, model, reasoning, time-of-day, weekday, and suffix config entries.
- #prompt_source_signature(path) ⇒ Object
- #read_prompt_file(path, label) ⇒ Object
- #replacement_system_prompt?(config = read_config) ⇒ Boolean
- #resolved_persona_text(value, characters: {}) ⇒ Object
-
#system_prompt_file(config = read_config) ⇒ Object
Returns the replacement system prompt text when configured.
-
#system_prompt_file_path(config = read_config) ⇒ Object
Returns the configured replacement system prompt path, or nil when normal prompt assembly should be used.
-
#system_prompt_sources_fingerprint ⇒ Object
Returns a lightweight fingerprint for config-owned prompt sources.
- #time_of_day_bucket(now) ⇒ Object
- #weekday_name(now) ⇒ Object
- #workspace_agents_file?(workspace_root) ⇒ Boolean
- #workspace_agents_path(workspace_root) ⇒ Object
- #workspace_agents_prompt(workspace_root) ⇒ Object
Instance Method Details
#active_persona_label(workspace_root:, model: nil, config: read_config) ⇒ Object
Returns the label of the persona selected by default/workspace/model rules.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/kward/config/prompts.rb', line 93 def active_persona_label(workspace_root:, model: nil, config: read_config) personas = config["personas"] return nil unless personas.is_a?(Hash) labels = crew_character_labels(personas) active_label = persona_label_for_key(personas["default"], labels) unless personas["default"].nil? workspaces = personas["workspaces"] if workspaces.is_a?(Hash) root = canonical_workspace_root(workspace_root) workspaces.each do |path, key| next unless canonical_workspace_root(path) == root active_label = persona_label_for_key(key, labels) break end end models = personas["models"] if models.is_a?(Hash) && !model.to_s.empty? && models.key?(model.to_s) active_label = persona_label_for_key(models[model.to_s], labels) end active_label end |
#add_persona_entry(entries, layer, value, name: nil) ⇒ Object
212 213 214 215 216 217 |
# File 'lib/kward/config/prompts.rb', line 212 def add_persona_entry(entries, layer, value, name: nil) text = presence(value) return unless text entries << { layer: layer.to_s, name: name.to_s, prompt: text } end |
#agents_prompt(config: read_config) ⇒ Object
7 8 9 10 11 12 13 14 |
# File 'lib/kward/config/prompts.rb', line 7 def agents_prompt(config: read_config) return nil unless include_config_principles?(config) path = config_principles_path return read_prompt_file(path, "Kward principles file") if File.exist?(path) read_prompt_file(config_agents_path, "Kward AGENTS.md alias") end |
#canonical_workspace_root(path) ⇒ Object
207 208 209 210 |
# File 'lib/kward/config/prompts.rb', line 207 def canonical_workspace_root(path) = File.(path.to_s.empty? ? Dir.pwd : path.to_s) File.directory?() ? File.realpath() : end |
#character_entries(raw) ⇒ Object
259 260 261 262 263 264 265 266 267 268 |
# File 'lib/kward/config/prompts.rb', line 259 def character_entries(raw) case raw when Hash raw.map { |key, definition| [key, definition] } when Array raw.filter_map { |entry| character_entry(entry) } else [] end end |
#character_entry(entry) ⇒ Object
270 271 272 273 274 275 276 277 278 279 |
# File 'lib/kward/config/prompts.rb', line 270 def character_entry(entry) return nil unless entry.is_a?(Hash) if entry.length == 1 && entry.keys.first.is_a?(String) [entry.keys.first, entry.values.first] else key = entry["key"] || entry[:key] || entry["id"] || entry[:id] || entry["name"] || entry[:name] key.to_s.empty? ? nil : [key, entry] end end |
#config_agents_path ⇒ Object
66 67 68 |
# File 'lib/kward/config/prompts.rb', line 66 def config_agents_path File.join(config_dir, "AGENTS.md") end |
#config_principles_path ⇒ Object
62 63 64 |
# File 'lib/kward/config/prompts.rb', line 62 def config_principles_path File.join(config_dir, "PRINCIPLES.md") end |
#crew_character_labels(personas) ⇒ Object
225 226 227 228 229 |
# File 'lib/kward/config/prompts.rb', line 225 def crew_character_labels(personas) named_character_values(personas) do |_key, definition| extract_character_label(definition) end end |
#crew_characters(personas) ⇒ Object
219 220 221 222 223 |
# File 'lib/kward/config/prompts.rb', line 219 def crew_characters(personas) named_character_values(personas) do |_key, definition| extract_character_instruction(definition) end end |
#extract_character_instruction(definition) ⇒ Object
287 288 289 290 291 292 293 294 295 296 |
# File 'lib/kward/config/prompts.rb', line 287 def extract_character_instruction(definition) return nil if definition.nil? if definition.is_a?(Hash) value = definition["instruction"] || definition[:instruction] return presence(value) end presence(definition) end |
#extract_character_label(definition) ⇒ Object
281 282 283 284 285 |
# File 'lib/kward/config/prompts.rb', line 281 def extract_character_label(definition) return nil unless definition.is_a?(Hash) presence(definition["label"] || definition[:label]) end |
#include_config_principles?(config = read_config) ⇒ Boolean
Returns whether config-directory principles are included in normal system prompt assembly. Replacement prompt files bypass all assembled sections.
18 19 20 21 22 23 |
# File 'lib/kward/config/prompts.rb', line 18 def include_config_principles?(config = read_config) settings = config["system_prompt"] return true unless settings.is_a?(Hash) settings["include_principles"] != false end |
#named_character_values(personas) ⇒ Object
250 251 252 253 254 255 256 257 |
# File 'lib/kward/config/prompts.rb', line 250 def named_character_values(personas) character_entries(personas["characters"] || personas["crew"]).each_with_object({}) do |(key, definition), mapping| value = yield(key, definition) next if value.to_s.empty? mapping[key.to_s] = value end end |
#persona_entries(workspace_root:, model: nil, reasoning_effort: nil, now: Time.now, config: read_config, include_reasoning: true) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 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 161 162 163 164 165 166 167 168 169 |
# File 'lib/kward/config/prompts.rb', line 119 def persona_entries(workspace_root:, model: nil, reasoning_effort: nil, now: Time.now, config: read_config, include_reasoning: true) personas = config["personas"] return [] unless personas.is_a?(Hash) characters = crew_characters(personas) entries = [] active_persona = { layer: "default", value: personas["default"], name: nil } workspaces = personas["workspaces"] if workspaces.is_a?(Hash) root = canonical_workspace_root(workspace_root) workspaces.each do |path, key| if canonical_workspace_root(path) == root active_persona = { layer: "workspace", value: key, name: path } break end end end models = personas["models"] if models.is_a?(Hash) && !model.to_s.empty? && models.key?(model.to_s) active_persona = { layer: "model", value: models[model.to_s], name: model.to_s } end add_persona_entry( entries, active_persona.fetch(:layer), resolved_persona_text(active_persona.fetch(:value), characters: characters), name: active_persona[:name] ) modifiers = personas["persona_modifiers"] if modifiers.is_a?(Hash) if include_reasoning reasoning = modifiers["reasoning"] add_persona_entry(entries, "reasoning", reasoning[reasoning_effort.to_s]) if reasoning.is_a?(Hash) && !reasoning_effort.to_s.empty? end time_of_day = modifiers["time_of_day"] bucket = time_of_day_bucket(now) add_persona_entry(entries, "time_of_day", time_of_day[bucket], name: bucket) if time_of_day.is_a?(Hash) weekday = modifiers["weekday"] day = weekday_name(now) add_persona_entry(entries, "weekday", weekday[day], name: day) if weekday.is_a?(Hash) add_persona_entry(entries, "suffix", modifiers["suffix"]) end entries end |
#persona_label_for_key(value, labels) ⇒ Object
243 244 245 246 247 248 |
# File 'lib/kward/config/prompts.rb', line 243 def persona_label_for_key(value, labels) key = value.to_s.strip return nil if key.empty? presence(labels[key]) end |
#persona_prompt(workspace_root, model: nil, reasoning_effort: nil, now: Time.now, config: read_config) ⇒ String?
Builds persona prompt text from default, workspace, model, reasoning, time-of-day, weekday, and suffix config entries.
Persona resolution is intentionally data-driven so users can edit config without plugin code. Keep new persona selectors additive and deterministic; prompt construction depends on stable ordering.
83 84 85 86 87 88 89 90 |
# File 'lib/kward/config/prompts.rb', line 83 def persona_prompt(workspace_root, model: nil, reasoning_effort: nil, now: Time.now, config: read_config) text = persona_entries(workspace_root: workspace_root, model: model, reasoning_effort: reasoning_effort, now: now, config: config).map do |entry| entry[:prompt] end.join("\n\n") return nil if text.empty? text end |
#prompt_source_signature(path) ⇒ Object
183 184 185 186 187 188 189 190 |
# File 'lib/kward/config/prompts.rb', line 183 def prompt_source_signature(path) return "#{path}:missing" unless File.exist?(path) stat = File.stat(path) "#{path}:#{stat.size}:#{stat.mtime.to_f}" rescue StandardError "#{path}:unavailable" end |
#read_prompt_file(path, label) ⇒ Object
192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/kward/config/prompts.rb', line 192 def read_prompt_file(path, label) return nil unless File.exist?(path) size = File.size(path) if size > MAX_PROMPT_FILE_BYTES emit_warning "Warning: skipping #{label} #{path}: file too large (#{size} bytes; limit is #{MAX_PROMPT_FILE_BYTES} bytes)" return nil end File.read(path) rescue StandardError => e emit_warning "Warning: skipping #{label} #{path}: #{e.}" nil end |
#replacement_system_prompt?(config = read_config) ⇒ Boolean
46 47 48 |
# File 'lib/kward/config/prompts.rb', line 46 def replacement_system_prompt?(config = read_config) !system_prompt_file_path(config).nil? end |
#resolved_persona_text(value, characters: {}) ⇒ Object
231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/kward/config/prompts.rb', line 231 def resolved_persona_text(value, characters: {}) return nil if value.nil? key = value.to_s.strip return nil if key.empty? text = characters[key.to_s] return text unless text.to_s.empty? value end |
#system_prompt_file(config = read_config) ⇒ Object
Returns the replacement system prompt text when configured. A configured but unavailable file intentionally yields nil: callers must not fall back to Kward's larger assembled prompt in replacement mode.
39 40 41 42 43 44 |
# File 'lib/kward/config/prompts.rb', line 39 def system_prompt_file(config = read_config) path = system_prompt_file_path(config) return nil unless path read_prompt_file(path, "custom system prompt file") end |
#system_prompt_file_path(config = read_config) ⇒ Object
Returns the configured replacement system prompt path, or nil when normal prompt assembly should be used. Relative paths are anchored to the config directory so configuration remains portable with KWARD_CONFIG_PATH.
28 29 30 31 32 33 34 |
# File 'lib/kward/config/prompts.rb', line 28 def system_prompt_file_path(config = read_config) settings = config["system_prompt"] value = settings.is_a?(Hash) ? settings["file"].to_s.strip : "" return nil if value.empty? File.(value, config_dir) end |
#system_prompt_sources_fingerprint ⇒ Object
Returns a lightweight fingerprint for config-owned prompt sources. It is used by active conversations to pick up prompt edits without a restart.
52 53 54 55 56 57 58 59 60 |
# File 'lib/kward/config/prompts.rb', line 52 def system_prompt_sources_fingerprint config = read_config paths = [config_path, system_prompt_file_path(config)] if include_config_principles?(config) paths << (File.exist?(config_principles_path) ? config_principles_path : config_agents_path) end paths.compact.map { |path| prompt_source_signature(path) }.join("\0") end |
#time_of_day_bucket(now) ⇒ Object
298 299 300 301 302 303 304 305 |
# File 'lib/kward/config/prompts.rb', line 298 def time_of_day_bucket(now) hour = now.hour return "morning" if hour >= 5 && hour < 11 return "before_lunch" if hour == 11 return "late_evening" if hour >= 21 || hour < 5 nil end |
#weekday_name(now) ⇒ Object
307 308 309 |
# File 'lib/kward/config/prompts.rb', line 307 def weekday_name(now) %w[sunday monday tuesday wednesday thursday friday saturday][now.wday] end |
#workspace_agents_file?(workspace_root) ⇒ Boolean
175 176 177 |
# File 'lib/kward/config/prompts.rb', line 175 def workspace_agents_file?(workspace_root) File.exist?(workspace_agents_path(workspace_root)) end |
#workspace_agents_path(workspace_root) ⇒ Object
171 172 173 |
# File 'lib/kward/config/prompts.rb', line 171 def workspace_agents_path(workspace_root) File.join(canonical_workspace_root(workspace_root), "AGENTS.md") end |
#workspace_agents_prompt(workspace_root) ⇒ Object
179 180 181 |
# File 'lib/kward/config/prompts.rb', line 179 def workspace_agents_prompt(workspace_root) read_prompt_file(workspace_agents_path(workspace_root), "workspace AGENTS.md") end |