Module: OllamaChat::PersonaeManagement

Included in:
Chat
Defined in:
lib/ollama_chat/personae_management.rb

Overview

Module for managing personas in Ollama chat application

This module provides functionality to manage persona files, including creating, reading, updating, and deleting persona definitions stored as Markdown files in the personae directory.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#default_persona_nameString? (readonly, private)

The default_persona_name method returns the name of the default persona.

Returns:

  • (String, nil)

    the name of the default persona or nil if not set



102
103
104
# File 'lib/ollama_chat/personae_management.rb', line 102

def default_persona_name
  @default_persona_name
end

Instance Method Details

#add_personaObject (private)

Creates a new persona file interactively.

The method prompts the user to enter a name for the persona, creates an empty Markdown file with that name in the personas directory (if it does not already exist), opens the file in the configured editor, and finally returns the result of calling #personae_result on the created file.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ollama_chat/personae_management.rb', line 178

def add_persona
  persona_name = ask?(
    prompt: "❓ Enter the name of the new persona (or press return to cancel): "
  ).full? or return

  pathname = personae_directory + "#{persona_name}.md"

  unless pathname.exist?
    File.write pathname, prompt(:persona).to_s
  end

  edit_file(pathname)
  nil
end

#ask_to_set_default_persona_name(persona_name) ⇒ Boolean (private)

Interactively asks the user if they want to set the specified persona as the current default for the session.

If the user confirms, the default persona is updated via set_default_persona_name.

Parameters:

  • persona_name (String)

    the name of the persona to potentially set as default

Returns:

  • (Boolean)

    true if the persona was set as the default, false otherwise



609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/ollama_chat/personae_management.rb', line 609

def ask_to_set_default_persona_name(persona_name)
  yes = confirm?(
    prompt: "🔔 Set the new persona promt as current default persona? (y/n) ",
    yes: /\Ay/i
  )
  if yes
    set_default_persona_name(persona_name)
    true
  else
    false
  end
end

#assistantString, ...

Returns the name of the current default persona.

If the default persona is set to :none or is not configured, it returns nil.

Returns:

  • (String, Symbol, nil)

    the name of the default persona, or nil if none is set.



31
32
33
34
35
# File 'lib/ollama_chat/personae_management.rb', line 31

def assistant
  if default_persona_name && default_persona_name != :none
    default_persona_name
  end
end

#available_personaeArray<String> (private)

Returns a sorted list of available persona file names.

This method scans the personae directory for Markdown files and returns their basenames sorted alphabetically.

Returns:

  • (Array<String>)

    Sorted array of persona filenames without extension



144
145
146
# File 'lib/ollama_chat/personae_management.rb', line 144

def available_personae
  personae_directory.glob('*.md').map { pathname_to_persona_name(_1) }
end

#available_personae_namesArray<SearchUI::Wrapper> (private)

Retrieves a list of available personas, decorated with their favourite status.

Returns:

  • (Array<SearchUI::Wrapper>)

    a list of wrappers containing the persona name and its decorated display string



164
165
166
167
168
169
170
# File 'lib/ollama_chat/personae_management.rb', line 164

def available_personae_names
  favs = all_favourited('persona')
  personae_directory.glob('*.md').map(&:basename).sort.map { |bn|
    persona_name = bn.sub_ext('').to_s
    persona_name_with_favourite(persona_name, favs[persona_name])
  }
end

#backup_personaObject (private)

Backs up the content of a selected persona file.

Prompts the user to select a persona from the available list. If a persona is selected, its current content is read and saved to a designated backup location using File.write. This ensures a safe copy is preserved before any modifications are made to the original file.



281
282
283
284
285
286
287
288
289
# File 'lib/ollama_chat/personae_management.rb', line 281

def backup_persona
  if persona = choose_persona
    pathname        = persona_name_to_pathname(persona)
    old_content     = pathname.read
    backup_pathname = persona_backup_pathname(persona)
    backup_pathname.write(old_content)
    STDOUT.puts "Wrote backup of #{persona.to_s} to #{backup_pathname.to_s.inspect}."
  end
end

#choose_persona(chosen: nil, none: false) ⇒ String, ... (private)

Interactive method to select a persona from a list.

Allows the user to choose a persona from available options or exit. Selected persona is returned if successful, nil if user exits.

Parameters:

  • chosen (Set, nil) (defaults to: nil)

    Optional set of already selected personas

  • none (Boolean) (defaults to: false)

    whether to include a '[NONE]' option in the list

Returns:

  • (String, Symbol, nil)

    The selected persona name, :none, or nil if user exits



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/ollama_chat/personae_management.rb', line 379

def choose_persona(chosen: nil, none: false)
  personae_list = available_personae_names.
    reject { chosen&.member?(_1) }
  if personae_list.empty?
    STDERR.puts "No personae defined."
    return
  end
  personae_list.unshift('[NONE]') if none
  personae_list.unshift('[EXIT]')
  case persona = choose_entry(personae_list)
  when '[EXIT]', nil
    STDOUT.puts "Exiting chooser."
    return
  when '[NONE]'
    :none
  else
    persona.value
  end
end

#convert_json_character_to_markdown(character) ⇒ String (private)

Transforms raw character data (JSON or YAML) into a high-fidelity, structured Markdown persona profile using the persona architect prompt and the current persona template.

This method leverages the LLM to interpret raw attributes and expand them into evocative prose, ensuring the final output conforms to the system's standard persona structure. It also normalizes placeholder syntax to ensure compatibility with the internal persona system.

Parameters:

  • character (String)

    the raw character data in JSON format

Returns:

  • (String)

    the resulting structured Markdown persona profile



566
567
568
569
570
571
572
573
# File 'lib/ollama_chat/personae_management.rb', line 566

def convert_json_character_to_markdown(character)
  generate(
    prompt:  prompt(:persona_architect).to_s % {
      character:,
      persona_template: prompt(:persona).to_s
    }
  ).response.gsub('{{user}}', '%{user}')
end

#default_personaPathname? (private)

The default_persona method returns the path to the default persona file.

Returns:

  • (Pathname, nil)

    the Pathname object for the default persona file or nil if no default persona is set



108
109
110
111
112
# File 'lib/ollama_chat/personae_management.rb', line 108

def default_persona
  if default_persona_name && default_persona_name != :none
    personae_directory.join(default_persona_name).sub_ext('.md')
  end
end

#default_persona_profileString?

Retrieves the formatted roleplay prompt for the current default persona.

Returns:

  • (String, nil)

    The formatted persona profile or nil if none set



18
19
20
21
22
# File 'lib/ollama_chat/personae_management.rb', line 18

def default_persona_profile
  if persona = default_persona and persona.exist?
    play_persona(persona)
  end
end

#delete_personaString (private)

Interactive method to delete an existing persona with backup functionality.

Prompts the user to select a persona, asks for confirmation, and creates a timestamped backup of the persona file before deletion.

Returns:

  • (String)

    A JSON object with deletion status on success, or nil if persona was not selected or deletion was cancelled



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/ollama_chat/personae_management.rb', line 216

def delete_persona
  if persona = choose_persona
    pathname        = persona_name_to_pathname(persona)
    backup_pathname = persona_backup_pathname(persona)
    if pathname.exist?
      STDOUT.puts "Deleting '#{bold{persona}}'..."
      STDOUT.puts "Backup will be saved to: #{backup_pathname}"

      if confirm?(prompt: "🔔 Are you sure? (y/n) ", yes: /\Ay/i)
        FileUtils.mv pathname, backup_pathname
        default_persona_name == persona and
          set_default_persona_name(:none)
        STDOUT.puts "Persona #{bold{persona}} deleted successfully"
        self
      else
        STDOUT.puts "Deletion cancelled."
        return
      end
    else
      STDOUT.puts "Persona not found."
      return
    end
  end
end

#determine_valid_new_name_for_persona(action) ⇒ String? (private)

Interactively determines a valid, non-conflicting name for a new persona.

Parameters:

  • action (String)

    The action being performed (e.g., 'to import')

Returns:

  • (String, nil)

    The validated persona name or nil if cancelled



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/ollama_chat/personae_management.rb', line 505

def determine_valid_new_name_for_persona(action)
  persona_name = nil
  loop do
    persona_name = ask?(
      prompt: "❓ Enter new persona prompt name #{action}, C-c ⇒ cancel: "
    )
    if persona_name.nil?
      STDOUT.puts "Cancelled."
      return nil
    end
    if persona_name_to_pathname(persona_name).exist?
      STDOUT.puts "Persona prompt named #{bold{persona_name}} already exists."
    else
      break
    end
  end
  persona_name
end

#duplicate_personaself? (private)

Interactively duplicates an existing persona profile to a new name.

The process follows these steps:

  1. Prompts the user to select a source persona via choose_persona.
  2. Prompts the user to enter a unique name for the duplicate via determine_valid_new_name_for_persona.
  3. Copies the content from the source persona file to the new persona file.

Returns:

  • (self, nil)

    returns self on success, or nil if the operation was cancelled during persona selection or name entry.



534
535
536
537
538
539
540
541
# File 'lib/ollama_chat/personae_management.rb', line 534

def duplicate_persona
  persona          = choose_persona or return
  pathname         = persona_name_to_pathname(persona)
  new_persona_name = determine_valid_new_name_for_persona('to ducplicate as') or return
  new_pathname     = persona_name_to_pathname(new_persona_name)
  new_pathname.write(pathname.read)
  self
end

#edit_personaString? (private)

Interactive method to edit an existing persona file.

Prompts the user to select a persona, opens it for editing, backups the old content, and returns the result after editing.

Returns:

  • (String, nil)

    persona name or nil if cancelled



247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ollama_chat/personae_management.rb', line 247

def edit_persona
  if persona = choose_persona
    pathname = persona_name_to_pathname(persona)
    old_content = pathname.read
    if edit_file(pathname)
      changed = pathname.read != old_content
      if changed
        persona_backup_pathname(persona).write(old_content)
        ask_to_set_default_persona_name(persona)
      end
      persona
    end
  end
end

#export_personaself? (private)

Interactively exports a persona profile to a specified file.

The process follows these steps:

  1. Prompts the user to select a persona via choose_persona.
  2. Displays the persona's current content to the terminal.
  3. Prompts for a destination filename via determine_valid_output_filename.
  4. Writes the persona content to the chosen file.

Returns:

  • (self, nil)

    returns self if the export was successful, or nil if the process was cancelled during persona selection or filename entry.



586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/ollama_chat/personae_management.rb', line 586

def export_persona
  persona  = choose_persona or return
  pathname = persona_name_to_pathname(persona)
  content  = pathname.read
  STDOUT.puts kramdown_ansi_parse(
    content + "\n---"
  )
  filename = determine_valid_output_filename('to write to') or return
  filename.write(content)
  STDOUT.puts "Persona #{persona.inspect} was exported as #{filename.to_path.inspect}?"
  self
end

#import_persona(pathname) ⇒ String? (private)

Imports a persona from a Markdown file, prompting for a new name.

Parameters:

  • pathname (Pathname, String)

    The path to the file to import

Returns:

  • (String, nil)

    The name of the imported persona or nil if cancelled



547
548
549
550
551
552
553
# File 'lib/ollama_chat/personae_management.rb', line 547

def import_persona(pathname)
  content          = pathname.read
  persona_name     = determine_valid_new_name_for_persona('to import') or return
  persona_pathname = persona_name_to_pathname(persona_name)
  persona_pathname.write(content)
  persona_name
end

#info_personaObject (private)

Displays detailed information about a selected persona.

Shows the persona's profile using kramdown formatting with ansi parsing.



319
320
321
322
323
324
325
326
# File 'lib/ollama_chat/personae_management.rb', line 319

def info_persona
  if persona = choose_persona
    description = persona_description(persona) or return
    use_pager do |output|
      output.puts kramdown_ansi_parse(description)
    end
  end
end

#initial_persona_nameString?

The initial_persona_name method retrieves the initial persona for the chat session.

Returns:

  • (String, nil)

    the persona name or nil if not set



11
12
13
# File 'lib/ollama_chat/personae_management.rb', line 11

def initial_persona_name
  session&.default_persona_name
end

#list_personae(output: STDOUT) ⇒ Object (private)

Lists all available persona names in a formatted table.

Parameters:

  • output (IO) (defaults to: STDOUT)

    the output stream to write the table to (default: STDOUT)



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/ollama_chat/personae_management.rb', line 331

def list_personae(output: STDOUT)
  use_pager do |output|
    personae = available_personae
    if personae.empty?
      STDOUT.puts "No personae defined."
      return
    end

    favs = all_favourited('persona')

    table = Terminal::Table.new
    table.style = {
      all_separators: true,
      border:         :unicode_round,
    }
    table.headings = %w[ NAME SIZE #TOK ].map { |header| bold { header } }

    personae.map do |persona_name|
      pathname = persona_name_to_pathname(persona_name)
      pathname.exist? or next
      [ pathname, pathname.size ]
    end.compact.sort_by(&:last).reverse_each do |pathname, size_bytes|
      persona_name = pathname.basename.sub_ext('').to_s
      size_bytes   = pathname.size
      size         = format_bytes(size_bytes)
      tokens       = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
      tokens_size  = format_tokens(tokens)
      is_default   = default_persona_name == persona_name
      display_name = prefix_favourite(is_default ? bold { persona_name } : persona_name, favs[persona_name])

      table << [ display_name, size, tokens_size, ]
    end

    table.align_column 1, :right
    table.align_column 2, :right
    output.puts table
  end
end

#load_persona_file(persona) ⇒ Array<Pathname, String> (private)

Loads a persona file from disk.

Parameters:

  • persona (String)

    The basename of the persona (without extension)

Returns:

  • (Array<Pathname, String>)

    Returns the pathname and its content as a string



446
447
448
449
450
451
# File 'lib/ollama_chat/personae_management.rb', line 446

def load_persona_file(persona)
  pathname = persona_name_to_pathname(persona)
  if pathname.exist?
    return pathname, pathname.read
  end
end

#load_personaeObject (private)

Interactive method to load multiple personae for use.

Allows sequential selection of multiple personae. Returns JSON results for each loaded persona.



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/ollama_chat/personae_management.rb', line 403

def load_personae
  chosen = Set[]
  choose_with_state do
    while persona = choose_persona(chosen: chosen)
      persona == :none and next
      chosen << persona
    end
  end

  if chosen.empty?
    STDOUT.puts "No persona loaded."
    return
  end

  personae_result(chosen)
end

#pathname_to_persona_name(pathname) ⇒ String (private)

Converts a persona pathname to its prompt name.

Parameters:

  • pathname (Pathname, String)

    The path to the persona file

Returns:

  • (String)

    The persona name without extension



497
498
499
# File 'lib/ollama_chat/personae_management.rb', line 497

def pathname_to_persona_name(pathname)
  pathname.basename.sub_ext('').to_s
end

#persona_backup_pathname(persona) ⇒ Pathname (private)

Note:

The timestamp ensures each backup has a unique filename when multiple backups of the same persona exist

Generates the backup pathname for a persona file with timestamp.

Creates a unique backup filename with the persona name, timestamp, and .md.bak extension. The timestamp format is YYYYMMDDHHMMSS for precise identification of backup versions.

Parameters:

  • persona (String)

    The persona name to create a backup for

Returns:

  • (Pathname)

    The full path to the backup file



204
205
206
207
# File 'lib/ollama_chat/personae_management.rb', line 204

def persona_backup_pathname(persona)
  timestamp = Time.now.strftime('%Y%m%d%H%M%S')
  personae_backup_directory + (persona + ?- + timestamp + '.md.bak')
end

#persona_description(persona, substitute_variables: false) ⇒ String? (private)

Generates a formatted description of a persona, including its path and profile.

Parameters:

  • persona (String)

    The persona name.

  • substitute_variables (Boolean) (defaults to: false)

    Whether to substitute variables in the profile.

Returns:

  • (String, nil)

    The formatted description or nil if the persona is not found.



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/ollama_chat/personae_management.rb', line 297

def persona_description(persona, substitute_variables: false)
  persona_path, persona_profile = load_persona_file(persona)
  if substitute_variables
    persona_profile = self.substitute_variables(persona_profile)
  end
  persona_profile or return
  <<~EOT
    # Persona #{persona}

    File #{persona_path.to_path}

    ---

    #{persona_profile}

    ---
  EOT
end

#persona_name_to_pathname(persona_name) ⇒ Pathname (private)

Converts a persona prompt name to its full filesystem pathname.

Parameters:

  • persona_name (String)

    The name of the persona (without extension)

Returns:

  • (Pathname)

    The full path to the .md persona file



489
490
491
# File 'lib/ollama_chat/personae_management.rb', line 489

def persona_name_to_pathname(persona_name)
  personae_directory.join(persona_name).sub_ext('.md')
end

#persona_name_with_favourite(name, favourited) ⇒ SearchUI::Wrapper (private)

Helper to wrap a persona name with its favourite status for the UI.

Parameters:

  • name (String)

    the name of the persona

  • favourited (Boolean)

    whether the persona is marked as a favourite

Returns:

  • (SearchUI::Wrapper)

    a wrapper containing the original name and the decorated display string



154
155
156
157
# File 'lib/ollama_chat/personae_management.rb', line 154

def persona_name_with_favourite(name, favourited)
  display = prefix_favourite(name, favourited)
  SearchUI::Wrapper.new(name, display:)
end

#personae_backup_directoryPathname (private)

Returns the directory path for persona backup files.

Returns:

  • (Pathname)

    Path to the backups subdirectory within the personae directory



55
56
57
# File 'lib/ollama_chat/personae_management.rb', line 55

def personae_backup_directory
  personae_directory + 'backups'
end

#personae_directoryPathname (private)

Note:

The directory is created automatically if it doesn't exist

Returns the directory path where persona files are stored.

The path is constructed using the XDG_CONFIG_HOME environment variable for platform-agnostic configuration storage.

Returns:

  • (Pathname)

    Path to the personae directory



47
48
49
# File 'lib/ollama_chat/personae_management.rb', line 47

def personae_directory
  OC::XDG_CONFIG_HOME + 'personae'
end

#personae_result(personae) ⇒ String? (private)

Compiles the descriptions for one or more personae into a single string.

Loads the profile for each persona and concatenates their descriptions.

Parameters:

  • personae (String, Array<String>)

    Persona name(s) to load.

Returns:

  • (String, nil)

    A string containing all descriptions, or nil if the result is blank.



427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/ollama_chat/personae_management.rb', line 427

def personae_result(personae)
  personae = Array(personae)

  result = +''

  personae.each do |persona|
    description = persona_description(persona, substitute_variables: true) or next
    result << description << "\n"
  end

  result.full?
end

#play_persona(persona) ⇒ String (private)

Generates the roleplay prompt string for a persona.

Creates a formatted prompt string that includes the persona name and profile.

Parameters:

  • persona (String, Pathname)

    The persona name or path to include in the prompt

Returns:

  • (String)

    Formatted roleplay prompt



472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/ollama_chat/personae_management.rb', line 472

def play_persona(persona)
  pathname, profile = load_persona_file(persona)
  profile = substitute_variables(profile)
  profile_intro = <<~EOT
    Roleplay as persona %{persona} (no nead to read the file) loaded from %{pathname}

    %{profile}
  EOT
  profile_intro % {
    persona:, pathname:, profile:
  }
end

#select_persona_pathString? (private)

Prompts the user to select a persona, copies its filesystem path to the clipboard, and sets it as the prefill prompt for the next interaction.

Returns:

  • (String, nil)

    the filesystem path of the selected persona, or nil if the selection was cancelled.



267
268
269
270
271
272
273
# File 'lib/ollama_chat/personae_management.rb', line 267

def select_persona_path
  persona = choose_persona or return
  path = persona_name_to_pathname(persona).to_s
  perform_copy_to_clipboard(text: path)
  @prefill_prompt = path
  path
end

#set_default_personaString? (private)

Interactively selects a persona and sets it as the default for the session.

This method prompts the user to choose a persona from the available list using choose_persona. If a valid persona is selected, it updates the session and the internal state via set_default_persona_name.

Returns:

  • (String, nil)

    The name of the persona that was set as default, or nil if the selection was cancelled or no persona was chosen.



93
94
95
96
97
# File 'lib/ollama_chat/personae_management.rb', line 93

def set_default_persona
  if persona = choose_persona(none: true)
    set_default_persona_name(persona)
  end
end

#set_default_persona_name(persona_name) ⇒ String? (private)

Sets the default persona name and updates the session.

Parameters:

  • persona_name (String, nil)

    the name of the persona to set as default

Returns:

  • (String, nil)

    the set default persona name or nil if not set



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ollama_chat/personae_management.rb', line 73

def set_default_persona_name(persona_name)
  if persona_name.present? && persona_name != :none
    @default_persona_name = Pathname.new(persona_name).basename.sub_ext('').to_path
    @session.update(default_persona_name: default_persona_name)
  else
    @session.update(default_persona_name: nil)
    @default_persona_name = nil
  end
  messages.set_system_prompt(session&.current_system_prompt.full?)
  default_persona_name
end

#setup_persona_from_sessionPathname, ... (private)

Initializes the default persona for the current chat session.

This method ensures that a default persona is configured at startup:

  1. If a default persona is already set, it returns immediately.
  2. Otherwise, it attempts to retrieve the initial persona prompt name from the session and verifies if the corresponding Markdown file exists.
  3. If a valid file is found, it sets it as the default persona.
  4. The ensure block guarantees that the session always has a default persona assigned, falling back to :none if no valid persona is found.

Returns:

  • (Pathname, String, nil)

    The resulting default persona path or name, or nil if no persona is set.



126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ollama_chat/personae_management.rb', line 126

def setup_persona_from_session
  default_persona and return
  if persona = initial_persona_name and
    persona_pathname = personae_directory + (persona + '.md') and
    persona_pathname.exist?
  then
    set_default_persona_name(persona_pathname)
  end
ensure
  default_persona or set_default_persona_name(:none)
end

#setup_personae_directoryObject (private)

Creates the personae directory structure if it doesn't already exist.

This method ensures both the main personae directory and the backups subdirectory exist for proper file organization.



63
64
65
# File 'lib/ollama_chat/personae_management.rb', line 63

def setup_personae_directory
  FileUtils.mkdir_p personae_backup_directory
end

#substitute_variables(profile) ⇒ String (private)

The substitute_variables method handles the substitution of variables in profiles. It replaces placeholders with actual values.

Parameters:

  • profile (String)

    the profile string to be processed

Returns:

  • (String)

    the processed string with variables substituted



459
460
461
462
463
# File 'lib/ollama_chat/personae_management.rb', line 459

def substitute_variables(profile)
  profile = profile.gsub(/%(?=[^{])/, '%%')
  profile = profile % { user: }
  profile
end