Module: AiCli::Helpers::Completion

Defined in:
lib/aicli/helpers/completion.rb

Constant Summary collapse

SHELL_CODE_EXCLUSIONS =
[/```[a-zA-Z]*\n/i, /```[a-zA-Z]*/i, "\n"].freeze

Class Method Summary collapse

Class Method Details

.ask(prompt, config, exclusions) ⇒ Object



67
68
69
# File 'lib/aicli/helpers/completion.rb', line 67

def ask(prompt, config, exclusions)
  ask_stream(prompt, config, exclusions) { |_chunk| }
end

.chat_system_promptObject



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/aicli/helpers/completion.rb', line 30

def chat_system_prompt
  <<~PROMPT
    You are ai, a terminal assistant. The user is asking for shell commands.

    Target OS: #{OsDetect.operating_system_name}
    Target shell: #{OsDetect.detect_shell}

    Whenever you suggest a command the user can run, put the exact runnable command
    alone inside a markdown fenced code block. Prefer a single command.
  PROMPT
end

.explain_scriptObject



116
117
118
# File 'lib/aicli/helpers/completion.rb', line 116

def explain_script
  'In one or two short sentences, explain what the script does. Plain text only.'
end

.explanation_prompt(script) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/aicli/helpers/completion.rb', line 108

def explanation_prompt(script)
  <<~PROMPT
    #{explain_script} Please reply in #{I18n.current_language_name}

    The script: #{script}
  PROMPT
end

.extract_commands(text) ⇒ Object



42
43
44
45
46
47
48
49
# File 'lib/aicli/helpers/completion.rb', line 42

def extract_commands(text)
  return [] if text.nil? || text.empty?

  text.scan(/```(?:[a-zA-Z0-9_+-]*)\s*\n?(.*?)```/m)
      .flatten
      .map { |block| block.strip.gsub(/\A`+|`+\z/, '') }
      .reject(&:empty?)
end

.full_prompt(prompt) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
# File 'lib/aicli/helpers/completion.rb', line 128

def full_prompt(prompt)
  <<~PROMPT
    Create a single line command that one can enter in a terminal and run, based on what is specified in the prompt.

    The target shell is #{OsDetect.detect_shell}

    #{generation_details}

    The prompt is: #{prompt}
  PROMPT
end

.generation_detailsObject



120
121
122
123
124
125
126
# File 'lib/aicli/helpers/completion.rb', line 120

def generation_details
  <<~DETAILS.chomp
    Only reply with the single line command surrounded by three backticks. It must be able to be directly run in the target shell. Do not include any other text.

    Make sure the command runs on #{OsDetect.operating_system_name} operating system.
  DETAILS
end

.get_explanation(script:, config:) ⇒ Object



14
15
16
# File 'lib/aicli/helpers/completion.rb', line 14

def get_explanation(script:, config:)
  ask(explanation_prompt(script), config, [])
end

.get_models(provider) ⇒ Object



22
23
24
# File 'lib/aicli/helpers/completion.rb', line 22

def get_models(provider)
  Llm.list_chat_models(provider)
end

.get_revision(prompt:, code:, config:) ⇒ Object



18
19
20
# File 'lib/aicli/helpers/completion.rb', line 18

def get_revision(prompt:, code:, config:)
  ask(revision_prompt(prompt, code), config, SHELL_CODE_EXCLUSIONS)
end

.get_script(prompt:, config:) ⇒ Object



10
11
12
# File 'lib/aicli/helpers/completion.rb', line 10

def get_script(prompt:, config:)
  ask(full_prompt(prompt), config, SHELL_CODE_EXCLUSIONS)
end

.revision_prompt(prompt, code) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
# File 'lib/aicli/helpers/completion.rb', line 140

def revision_prompt(prompt, code)
  <<~PROMPT
    Update the following script based on what is asked in the following prompt.

    The script: #{code}

    The prompt: #{prompt}

    #{generation_details}
  PROMPT
end

.start_chat(config) ⇒ Object



26
27
28
# File 'lib/aicli/helpers/completion.rb', line 26

def start_chat(config)
  Llm.build_chat(config).tap { |chat| chat.with_instructions(chat_system_prompt) }
end

.stream_chat_message(chat, message, writer:) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/aicli/helpers/completion.rb', line 51

def stream_chat_message(chat, message, writer:)
  data = +''

  chat.ask(message) do |chunk|
    content = chunk.content.to_s
    next if content.empty?

    data << content
    writer.call(content)
  end

  data
rescue RubyLLM::Error, RubyLLM::ConfigurationError => e
  handle_ruby_llm_error(e)
end