llm-shell 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redcarpet"
4
- require "coderay"
5
-
6
- class LLM::Shell
7
- ##
8
- # @api private
9
- # @see redcarpet https://github.com/vmg/redcarpet/blob/master/ext/redcarpet/markdown.h#L69-L110
10
- class Markdown < Redcarpet::Render::Base
11
- ##
12
- # Renders markdown text to a terminal-friendly format.
13
- # @return [String
14
- def self.render(text)
15
- renderer = Redcarpet::Markdown.new(self, options)
16
- renderer.render(wrap(p: text)).strip
17
- end
18
-
19
- ##
20
- # @api private
21
- def self.wrap(p:, width: 80)
22
- in_code = false
23
- p.lines.map do |line|
24
- if line =~ /^(\s*)(```|~~~)/
25
- in_code = !in_code
26
- line
27
- elsif in_code || line =~ /^\s{4}/
28
- line
29
- else
30
- line.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
31
- end
32
- end.join.strip + "\n"
33
- end
34
-
35
- ##
36
- # @api private
37
- def self.options
38
- {
39
- autolink: false, no_intra_emphasis: true,
40
- fenced_code_blocks: true, lax_spacing: true,
41
- strikethrough: true, superscript: true,
42
- tables: true, with_toc_data: true
43
- }
44
- end
45
-
46
- def block_code(code, lang)
47
- ["\n", Paint["#{lang}:", :blue, :bold],
48
- "\n", coderay(code, lang),
49
- "\n"].join
50
- end
51
-
52
- def header(text, level)
53
- color = levels.fetch(level, :white)
54
- "\n" + Paint[("#" * level) + " " + text, color] + "\n"
55
- end
56
-
57
- def paragraph(p) = "#{p.strip}\n\n"
58
- def list(items, _type) = items
59
- def list_item(item, _type) = "\n• #{item.strip}\n"
60
- def emphasis(text) = Paint[text, :italic]
61
- def double_emphasis(text) = Paint[text, :bold]
62
- def codespan(code) = Paint[code, :yellow, :underline]
63
- def block_quote(quote) = Paint[quote, :italic]
64
- def normal_text(text) = text
65
- def linebreak = "\n"
66
-
67
- private
68
-
69
- def coderay(code, lang)
70
- CodeRay.scan(code, lang).terminal
71
- rescue ArgumentError
72
- lang = "text"
73
- retry
74
- end
75
-
76
- def levels
77
- {
78
- 1 => :green, 2 => :blue, 3 => :green,
79
- 4 => :yellow, 5 => :red, 6 => :purple
80
- }
81
- end
82
- end
83
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class LLM::Shell
4
- ##
5
- # The {LLM::Shell::Options LLM::Shell::Options} class represents
6
- # the options provided to the shell at the command line, and the
7
- # configuration file (if any). The command-line options take precedence
8
- # over the configuration file.
9
- class Options
10
- ##
11
- # @param [Hash] options
12
- # @param [LLM::Shell::Default] default
13
- # @return [LLM::Shell::Options]
14
- def initialize(options, default)
15
- @options = options.transform_keys(&:to_sym)
16
- @provider = @options.delete(:provider)
17
- @tools = @options.delete(:tools)
18
- @prompt = @options[:prompt] ? custom_prompt : default.prompt
19
- @files = Dir[*@options.delete(:files) || []].reject { File.directory?(_1) }
20
- @bot_options = {model: @options.delete(:model)}.compact
21
- @default = default
22
- end
23
-
24
- def provider = @provider
25
- def tools = @tools
26
- def files = @files
27
- def llm = @options
28
- def bot = @bot_options
29
- def default = @default
30
- def prompt = File.read(@prompt)
31
-
32
- private
33
-
34
- def custom_prompt
35
- prompt = @options.delete(:prompt)
36
- File.join(LLM::Shell.home, "prompts", prompt)
37
- end
38
- end
39
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class LLM::Shell
4
- class Renderer
5
- RenderError = Class.new(RuntimeError)
6
- include Command::Utils
7
-
8
- ##
9
- # @param [LLM::Message] message
10
- # The message to render
11
- # @return [LLM::Shell::MessageRenderer]
12
- # Returns an instance of the renderer
13
- def initialize(message)
14
- @message = message
15
- end
16
-
17
- def render
18
- if message.user?
19
- render_message(message, :yellow)
20
- elsif message.assistant?
21
- render_message(message, :green)
22
- elsif message.system?
23
- render_message(message, :red)
24
- else
25
- raise RenderError.new("no handler for message role '#{message.role}'")
26
- end
27
- end
28
-
29
- private
30
-
31
- attr_reader :message
32
-
33
- def render_message(message, color)
34
- role = Paint[message.role, :bold, color]
35
- title = "#{role} says: "
36
- content = message.content
37
- if message.tool_call?
38
- body = "Tool call(s) request"
39
- elsif message.tool_return?
40
- body = "Tool call(s) return"
41
- elsif Array === content
42
- body = content.map { |c| render_message(c, color) }.join("\n")
43
- elsif content =~ file_pattern
44
- path = content.match(file_pattern) ? Regexp.last_match[1] : nil
45
- body = "<file path=#{path} />"
46
- else
47
- body = Markdown.render(content)
48
- end
49
- [title, "\n", body, "\n"].join
50
- end
51
- end
52
- end
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class LLM::Shell
4
- ##
5
- # The {LLM::Shell::REPL LLM::Shell::REPL} class represents a loop
6
- # that accepts user input, evaluates it via the LLM, and prints the
7
- # response to stdout.
8
- class REPL
9
- ##
10
- # @param [LLM::Chat] bot
11
- # @param [LLM::Shell::Options] options
12
- # @return [LLM::Shell::REPL]
13
- def initialize(bot:, options:)
14
- @bot = bot
15
- @console = IO.console
16
- @options = options
17
- @io = IO::Line.new($stdout)
18
- end
19
-
20
- ##
21
- # Performs initial setup
22
- # @return [void]
23
- def setup
24
- LLM::Shell.commands.each { |file| require file }
25
- Readline.completion_proc = Completion.to_proc
26
- chat options.prompt, role: options.default.role
27
- files.each { chat ["--- START: #{_1} ---", File.read(_1), "--- END: #{_1} ---"].join("\n") }
28
- bot.messages.each(&:read!)
29
- clear_screen
30
- end
31
-
32
- ##
33
- # Enters the main loop
34
- # @return [void]
35
- def start
36
- loop do
37
- read
38
- eval while functions.any?
39
- emit
40
- rescue LLM::ResponseError => ex
41
- print Paint[ex.response.class, :red], "\n"
42
- print ex.response.body, "\n"
43
- rescue => ex
44
- print Paint[ex.class, :red], "\n"
45
- print ex.message, "\n"
46
- print ex.backtrace[0..5].join("\n")
47
- rescue Interrupt
48
- throw(:exit, 0)
49
- end
50
- end
51
-
52
- private
53
-
54
- def read
55
- input = Readline.readline("llm> ", true) || throw(:exit, 0)
56
- words = input.split(" ")
57
- if LLM.commands[words[0]]
58
- cmd = LLM.commands[words[0]]
59
- argv = words[1..]
60
- cmd.setup(bot, io)
61
- cmd.call(*argv)
62
- else
63
- chat input.tap { clear_screen }
64
- io.rewind.print(Paint["Thinking", :bold])
65
- unread.tap { io.rewind }
66
- end
67
- end
68
-
69
- def eval
70
- callables = []
71
- cancels = []
72
- results = []
73
- functions.each do |function|
74
- print Paint["system", :bold, :red], " says: ", "\n"
75
- print "function: ", function.name, "\n"
76
- print "arguments: ", function.arguments.to_h.size, "\n"
77
- input = Readline.readline("Do you want to call it ? ", true)
78
- puts
79
- if %w(y yes yep yeah ok).include?(input)
80
- callables << function
81
- else
82
- cancels << function
83
- end
84
- end
85
- results.concat callables.map(&:call)
86
- results.concat cancels.map(&:cancel)
87
- bot.chat(results)
88
- end
89
-
90
- def emit
91
- LLM::Shell.pager do |io|
92
- io.write formatter(unread).format!(:user), "\n"
93
- io.write formatter(unread).format!(:assistant), "\n"
94
- end unless unread.empty?
95
- end
96
-
97
- attr_reader :bot, :console, :io, :default, :options
98
-
99
- def formatter(messages) = Formatter.new(messages)
100
- def unread = bot.messages.unread
101
- def functions = bot.functions
102
- def files = @options.files
103
- def clear_screen = console.clear_screen
104
- def chat(...) = bot.chat(...)
105
- end
106
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- end unless defined?(LLM)
5
-
6
- class LLM::Shell
7
- VERSION = "0.9.2"
8
- end
data/lib/llm/shell.rb DELETED
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "optparse"
4
- require "readline"
5
- require "yaml"
6
- require "llm"
7
- require "paint"
8
-
9
- class LLM::Shell
10
- require_relative "function"
11
- require_relative "../io/line"
12
- require_relative "shell/command"
13
- require_relative "shell/command/extension"
14
- require_relative "shell/markdown"
15
- require_relative "shell/renderer"
16
- require_relative "shell/formatter"
17
- require_relative "shell/default"
18
- require_relative "shell/options"
19
- require_relative "shell/repl"
20
- require_relative "shell/config"
21
- require_relative "shell/completion"
22
- require_relative "shell/version"
23
-
24
- ##
25
- # Load all commands
26
- Dir[File.join(__dir__, "shell", "commands", "*.rb")].each { require(_1) }
27
-
28
- ##
29
- # Opens a pager
30
- # @return [void]
31
- def self.pager(...)
32
- IO.popen("less -FRX", "w", ...)
33
- end
34
-
35
- ##
36
- # @return [String]
37
- def self.home
38
- File.join Dir.home, ".llm-shell"
39
- end
40
-
41
- ##
42
- # @return [Array<String>]
43
- def self.tools
44
- Dir[*TOOLGLOBS]
45
- end
46
-
47
- ##
48
- # @return [Array<String>]
49
- def self.commands
50
- Dir[File.join(home, "commands", "*.rb")]
51
- end
52
-
53
- TOOLGLOBS = [
54
- File.join(home, "tools", "*.rb"),
55
- File.join(__dir__, "shell", "functions", "*.rb")
56
- ].freeze
57
- private_constant :TOOLGLOBS
58
-
59
- ##
60
- # @param [Hash] options
61
- # @return [LLM::Shell]
62
- def initialize(options)
63
- @config = Config.new(options[:provider])
64
- @options = Options.new @config.merge(options), Default.new(options[:provider])
65
- @bot = LLM::Bot.new(llm, {tools:}.merge(@options.bot))
66
- @repl = REPL.new(bot: @bot, options: @options)
67
- end
68
-
69
- ##
70
- # Start the shell
71
- # @return [void]
72
- def start
73
- repl.setup
74
- repl.start
75
- end
76
-
77
- private
78
-
79
- def tools
80
- LLM::Shell.tools.map do |path|
81
- eval File.read(path), TOPLEVEL_BINDING, path, 1
82
- end.grep(LLM::Function)
83
- end
84
-
85
- attr_reader :options, :bot, :repl
86
- def provider = LLM.method(options.provider)
87
- def llm = provider.call(**options.llm)
88
- end
data/lib/llm-shell.rb DELETED
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "llm/shell"
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative "../../lib/llm/shell"
5
-
6
- def main(argv)
7
- if argv.include?("-v") || argv.include?("--version")
8
- puts LLM::Shell::VERSION
9
- else
10
- options = {tools: []}
11
- option_parser.parse(argv, into: options)
12
- if argv.empty? || options[:provider].nil?
13
- warn option_parser.help
14
- throw(:exit, 1)
15
- else
16
- LLM::Shell.new(options).start
17
- end
18
- end
19
- end
20
-
21
- def option_parser
22
- OptionParser.new do |o|
23
- o.banner = "Usage: llm-shell [OPTIONS]"
24
- o.on("-p PROVIDER", "--provider NAME", "Required. Options: gemini, openai, anthropic, ollama or llamacpp.", String)
25
- o.on("-k [KEY]", "--key [KEY]", "Optional. Required by gemini, openai, and anthropic.", String)
26
- o.on("-m [MODEL]", "--model [MODEL]", "Optional. The name of a model.", Array)
27
- o.on("-h [HOST]", "--host [HOST]", "Optional. Sometimes required by ollama.", String)
28
- o.on("-o [PORT]", "--port [PORT]", "Optional. Sometimes required by ollama.", Integer)
29
- o.on("-f [GLOB]", "--files [GLOB]", "Optional. Glob pattern(s) separated by a comma.", Array)
30
- o.on("-r [PROMPT]", "--prompt [PROMPT]", "Optional. The prompt to use.", String)
31
- o.on("-v", "--version", "Optional. Print the version and exit.")
32
- end
33
- end
34
-
35
- excode = catch(:exit) do
36
- main(ARGV)
37
- 0
38
- rescue => ex
39
- print Paint[ex.class, :red, :bold], "\n"
40
- print ex.message, "\n\n"
41
- print Paint["Backtrace", :bold], "\n"
42
- print ex.backtrace[0..5].join("\n"), "\n"
43
- 1
44
- end
45
- exit excode
@@ -1,21 +0,0 @@
1
- ## Task
2
-
3
- Your task is to provide helpful assistance to the user.
4
-
5
- ## Context
6
-
7
- The user is talking to you from a terminal-based application.
8
-
9
- ## Inputs
10
-
11
- The user might send you the content of file(s) from their filesystem.
12
- When the user sends you a file, it will be in this format:
13
-
14
- <file path="/path/to/file">
15
- CONTENT
16
- </file>
17
-
18
- ## Outputs
19
-
20
- Always respond in markdown.
21
- When the user sends a file, reply with "Got it" and nothing else.