Module: AiCli::Helpers::Completion

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

Constant Summary collapse

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

Class Method Summary collapse

Class Method Details

.chat_system_promptObject



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

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, for example:

    ```
    ls -la
    ```

    Prefer a single command. You may briefly explain outside the fence.
    Do not wrap non-runnable prose in fences.
  PROMPT
end

.explain_scriptObject



112
113
114
# File 'lib/aicli/helpers/completion.rb', line 112

def explain_script
  'Please provide a clear, concise description of the script, using minimal words. Outline the steps in a list format.'
end

.explanation_prompt(script) ⇒ Object



100
101
102
103
104
105
106
# File 'lib/aicli/helpers/completion.rb', line 100

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

    The script: #{script}
  PROMPT
end

.extract_commands(text) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/aicli/helpers/completion.rb', line 69

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

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

  # Prefer the last fenced command if several were suggested.
  blocks
end

.full_prompt(prompt) ⇒ Object



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

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

    #{shell_details}

    #{generation_details}

    #{explain}

    The prompt is: #{prompt}
  PROMPT
end

.generation_detailsObject



116
117
118
119
120
121
122
# File 'lib/aicli/helpers/completion.rb', line 116

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



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

def get_explanation(script:, config:)
  { read_explanation: stream_prompt(explanation_prompt(script), config: config) }
end

.get_models(provider) ⇒ Object



32
33
34
# File 'lib/aicli/helpers/completion.rb', line 32

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

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



22
23
24
25
26
27
28
29
30
# File 'lib/aicli/helpers/completion.rb', line 22

def get_revision(prompt:, code:, config:)
  {
    read_script: stream_prompt(
      revision_prompt(prompt, code),
      config: config,
      exclusions: SHELL_CODE_EXCLUSIONS
    )
  }
end

.get_script_and_info(prompt:, config:) ⇒ Object



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

def get_script_and_info(prompt:, config:)
  {
    read_script: stream_prompt(full_prompt(prompt), config: config, exclusions: SHELL_CODE_EXCLUSIONS),
    read_info: ->(_writer) { '' }
  }
end

.revision_prompt(prompt, code) ⇒ Object



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

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

.shell_detailsObject



108
109
110
# File 'lib/aicli/helpers/completion.rb', line 108

def shell_details
  "The target shell is #{OsDetect.detect_shell}"
end

.start_chat(config) ⇒ Object

Multi-turn chat helper used by ai chat.



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

def start_chat(config)
  chat = Llm.build_chat(config)
  chat.with_instructions(chat_system_prompt)
  chat
end

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



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/aicli/helpers/completion.rb', line 82

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

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

      data << content
      writer.call(content)
    end
  rescue RubyLLM::Error, RubyLLM::ConfigurationError => e
    handle_ruby_llm_error(e)
  end

  data
end

.stream_prompt(prompt, config:, exclusions: []) ⇒ Object

Streams a single-turn completion. Returns a callable that takes a writer.



37
38
39
40
41
# File 'lib/aicli/helpers/completion.rb', line 37

def stream_prompt(prompt, config:, exclusions: [])
  lambda do |writer|
    stream_to_writer(prompt, config: config, exclusions: exclusions, writer: writer)
  end
end