llm.rb 12.2.0 → 12.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +187 -0
- data/README.md +51 -10
- data/data/openai.json +422 -0
- data/lib/llm/active_record/acts_as_agent.rb +11 -0
- data/lib/llm/agent.rb +14 -6
- data/lib/llm/compactor.rb +2 -2
- data/lib/llm/context.rb +16 -24
- data/lib/llm/object.rb +13 -0
- data/lib/llm/provider.rb +10 -7
- data/lib/llm/providers/anthropic.rb +2 -3
- data/lib/llm/providers/bedrock.rb +4 -5
- data/lib/llm/providers/google.rb +4 -2
- data/lib/llm/providers/mistral.rb +49 -0
- data/lib/llm/providers/ollama/response_adapter/completion.rb +39 -0
- data/lib/llm/providers/ollama.rb +2 -3
- data/lib/llm/providers/openai/responses.rb +6 -3
- data/lib/llm/providers/openai.rb +2 -3
- data/lib/llm/repl/bar.rb +52 -0
- data/lib/llm/repl/input.rb +143 -30
- data/lib/llm/repl/markdown.rb +85 -0
- data/lib/llm/repl/status.rb +16 -5
- data/lib/llm/repl/stream.rb +15 -5
- data/lib/llm/repl/transcript.rb +107 -18
- data/lib/llm/repl/window.rb +49 -18
- data/lib/llm/repl.rb +99 -18
- data/lib/llm/sequel/agent.rb +11 -0
- data/lib/llm/skill.rb +1 -1
- data/lib/llm/stream/disabled.rb +23 -0
- data/lib/llm/stream/io.rb +43 -0
- data/lib/llm/stream.rb +34 -0
- data/lib/llm/tools/git.rb +2 -3
- data/lib/llm/tools/pwd.rb +0 -1
- data/lib/llm/tools/rg.rb +2 -1
- data/lib/llm/tools/swap_text.rb +6 -0
- data/lib/llm/transport/execution.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +10 -0
- data/llm.gemspec +7 -3
- data/resources/deepdive.md +170 -50
- metadata +10 -3
data/lib/llm/repl/transcript.rb
CHANGED
|
@@ -2,30 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Repl
|
|
4
4
|
##
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# This class maintains conversation state that includes
|
|
6
|
+
# the conversation itself, and metadata associated with
|
|
7
|
+
# the conversation.
|
|
8
|
+
#
|
|
9
|
+
# Internally it maintains an array where each element
|
|
10
|
+
# represents a row, and each element in a row is a Hash
|
|
11
|
+
# that describes a piece of text and any styles that might
|
|
12
|
+
# be applied to it by the UI thread.
|
|
13
|
+
#
|
|
14
|
+
# It also maintains a cursor that tracks the active row
|
|
15
|
+
# by its index number. The streaming path reuses a single
|
|
16
|
+
# row by overwriting its contents repeatedly.
|
|
8
17
|
class Transcript
|
|
9
18
|
WIDTH = 80
|
|
10
19
|
|
|
11
20
|
##
|
|
12
21
|
# @return [LLM::Repl::Transcript]
|
|
13
22
|
def initialize
|
|
14
|
-
@
|
|
23
|
+
@rows = [[]]
|
|
24
|
+
@cursor = nil
|
|
25
|
+
@snapshot = nil
|
|
15
26
|
@offset = 0
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
##
|
|
19
30
|
# @param [String] chars
|
|
31
|
+
# @param [Object] attrs
|
|
32
|
+
# @param [Symbol] method
|
|
20
33
|
# @return [void]
|
|
21
|
-
def write(chars)
|
|
22
|
-
|
|
34
|
+
def write(chars, attrs = nil, method: :append)
|
|
35
|
+
chunks = [{text: chars.to_s, attrs:}.compact]
|
|
36
|
+
self.method(method).call(chunks)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Appends Markdown to the transcript.
|
|
41
|
+
# @param [String] chars
|
|
42
|
+
# @param [Symbol] method
|
|
43
|
+
# @return [void]
|
|
44
|
+
def markdown(chars, method: :append)
|
|
45
|
+
chunks = LLM::Repl::Markdown.new(chars).ast
|
|
46
|
+
self.method(method).call(chunks)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Start the transcript.
|
|
51
|
+
# @return [void]
|
|
52
|
+
def start
|
|
53
|
+
@cursor = @rows.size - 1
|
|
54
|
+
@snapshot = @rows.map(&:dup)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Finish the transcript.
|
|
59
|
+
# @return [void]
|
|
60
|
+
def finish
|
|
61
|
+
@cursor = nil
|
|
62
|
+
@snapshot = nil
|
|
23
63
|
end
|
|
24
64
|
|
|
25
65
|
##
|
|
26
66
|
# @return [void]
|
|
27
67
|
def scroll_up(height)
|
|
28
|
-
max = [
|
|
68
|
+
max = [rows.size - height, 0].max
|
|
29
69
|
@offset = [@offset + 1, max].min
|
|
30
70
|
end
|
|
31
71
|
|
|
@@ -39,23 +79,72 @@ class LLM::Repl
|
|
|
39
79
|
# @param [Integer] height
|
|
40
80
|
# @return [Array<String>]
|
|
41
81
|
def visible(height)
|
|
42
|
-
|
|
82
|
+
all = rows
|
|
83
|
+
last = all.size - 1 - @offset
|
|
43
84
|
first = [last - height + 1, 0].max
|
|
44
|
-
|
|
85
|
+
all[first..last] || []
|
|
45
86
|
end
|
|
46
87
|
|
|
47
88
|
private
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
##
|
|
91
|
+
# Appends a new row
|
|
92
|
+
# @param [Array<{text: String, attrs?: Integer}>] chunks
|
|
93
|
+
# One or more chunks.
|
|
94
|
+
# @return [void]
|
|
95
|
+
def append(chunks)
|
|
96
|
+
chunks.each { wrap(_1, @rows) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Replaces the content of the active row
|
|
101
|
+
# @param [Array<{text: String, attrs?: Integer}>] chunks
|
|
102
|
+
# One or more chunks.
|
|
103
|
+
# @return [void]
|
|
104
|
+
def replace(chunks)
|
|
105
|
+
@rows = @snapshot.map(&:dup)
|
|
106
|
+
append(chunks)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Given a chunk this method wraps text at
|
|
111
|
+
# around 80 columns: a new row starts when
|
|
112
|
+
# the current character is " " and the sum
|
|
113
|
+
# of all characters in that row is greater
|
|
114
|
+
# than 80 columns.
|
|
115
|
+
def wrap(chunk, rows)
|
|
116
|
+
attrs = chunk[:attrs]
|
|
117
|
+
chunk[:text].to_s.each_char do |char|
|
|
118
|
+
if char == "\n"
|
|
119
|
+
rows << []
|
|
120
|
+
elsif char == " " and sum(rows.last) >= WIDTH
|
|
121
|
+
rows << []
|
|
122
|
+
else
|
|
123
|
+
rows.last << {text: char, attrs:}.compact
|
|
124
|
+
end
|
|
58
125
|
end
|
|
59
126
|
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# @api private
|
|
130
|
+
def rows
|
|
131
|
+
@rows.dup.tap do |rows|
|
|
132
|
+
##
|
|
133
|
+
# Discard empty rows that would otherwise
|
|
134
|
+
# be rendered as newlines by the UI thread.
|
|
135
|
+
# It's not the most elegant way to deal with
|
|
136
|
+
# this and we probably shouldn't allow it to
|
|
137
|
+
# happen in the first place.
|
|
138
|
+
while rows.size > 1 and rows.last.empty?
|
|
139
|
+
rows.pop
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
##
|
|
145
|
+
# @api private
|
|
146
|
+
def sum(row)
|
|
147
|
+
row.sum { _1[:text].to_s.length }
|
|
148
|
+
end
|
|
60
149
|
end
|
|
61
150
|
end
|
data/lib/llm/repl/window.rb
CHANGED
|
@@ -37,6 +37,7 @@ class LLM::Repl
|
|
|
37
37
|
Curses.cbreak
|
|
38
38
|
Curses.noecho
|
|
39
39
|
Curses.stdscr.keypad(true)
|
|
40
|
+
Curses.stdscr.nodelay = true
|
|
40
41
|
yield
|
|
41
42
|
ensure
|
|
42
43
|
Curses.close_screen
|
|
@@ -45,9 +46,10 @@ class LLM::Repl
|
|
|
45
46
|
##
|
|
46
47
|
# @return [void]
|
|
47
48
|
def redraw
|
|
48
|
-
Curses.
|
|
49
|
-
draw_status
|
|
50
|
-
|
|
49
|
+
Curses.curs_set(0)
|
|
50
|
+
draw_status(offset: input.height + 1)
|
|
51
|
+
draw_divider(offset: 5)
|
|
52
|
+
draw_transcript(offset: 0)
|
|
51
53
|
draw_input
|
|
52
54
|
Curses.refresh
|
|
53
55
|
end
|
|
@@ -55,7 +57,7 @@ class LLM::Repl
|
|
|
55
57
|
##
|
|
56
58
|
# @return [Integer]
|
|
57
59
|
def rows
|
|
58
|
-
[Curses.lines -
|
|
60
|
+
[Curses.lines - (input.height + 4), 1].max
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
##
|
|
@@ -78,28 +80,57 @@ class LLM::Repl
|
|
|
78
80
|
|
|
79
81
|
private
|
|
80
82
|
|
|
81
|
-
def draw_status
|
|
82
|
-
Curses.setpos(
|
|
83
|
+
def draw_status(offset:)
|
|
84
|
+
Curses.setpos(Curses.lines - offset, 0)
|
|
85
|
+
Curses.clrtoeol
|
|
83
86
|
Curses.addstr(status.to_s)
|
|
84
|
-
|
|
85
|
-
Curses.setpos(
|
|
86
|
-
Curses.addstr(
|
|
87
|
+
context = status.context_bar
|
|
88
|
+
Curses.setpos(Curses.lines - offset, [(columns - context.length) / 2, 0].max)
|
|
89
|
+
Curses.addstr(context)
|
|
90
|
+
cost = status.cost.to_s
|
|
91
|
+
Curses.setpos(Curses.lines - offset, [columns - cost.length, 0].max)
|
|
92
|
+
Curses.addstr(cost)
|
|
87
93
|
end
|
|
88
94
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Curses.addstr(row)
|
|
94
|
-
end
|
|
95
|
+
def draw_divider(offset:)
|
|
96
|
+
Curses.setpos(Curses.lines - offset, 0)
|
|
97
|
+
Curses.clrtoeol
|
|
98
|
+
Curses.addstr("─" * Curses.cols)
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def draw_input
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
cols = columns
|
|
103
|
+
rows = input.lines(cols)
|
|
104
|
+
rows.each.with_index do |line, idx|
|
|
105
|
+
Curses.setpos((Curses.lines - input.height) + idx, 0)
|
|
106
|
+
Curses.clrtoeol
|
|
107
|
+
Curses.addstr(line)
|
|
108
|
+
end
|
|
109
|
+
line, col = input.cursor_pos(cols)
|
|
110
|
+
Curses.setpos((Curses.lines - input.height) + line, col)
|
|
101
111
|
end
|
|
102
112
|
|
|
113
|
+
def draw_transcript(offset:)
|
|
114
|
+
rows = transcript.visible(self.rows)
|
|
115
|
+
rows.each.with_index(offset) do |row, index|
|
|
116
|
+
Curses.setpos(index, 0)
|
|
117
|
+
Curses.clrtoeol
|
|
118
|
+
row.each do |chunk|
|
|
119
|
+
text, attrs = chunk.values_at(:text, :attrs)
|
|
120
|
+
Curses.attron(attrs) if attrs
|
|
121
|
+
Curses.addstr(text)
|
|
122
|
+
Curses.attroff(attrs) if attrs
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
last_drawn = offset + rows.size
|
|
126
|
+
(last_drawn...self.rows).each do |line|
|
|
127
|
+
Curses.setpos(line, 0)
|
|
128
|
+
Curses.clrtoeol
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# @return [Integer]
|
|
103
134
|
def columns
|
|
104
135
|
Curses.cols
|
|
105
136
|
end
|
data/lib/llm/repl.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
LLM.require "curses"
|
|
4
|
+
LLM.require "kramdown"
|
|
4
5
|
|
|
5
6
|
module LLM
|
|
6
7
|
##
|
|
@@ -18,19 +19,37 @@ module LLM
|
|
|
18
19
|
require_relative "repl/status"
|
|
19
20
|
require_relative "repl/transcript"
|
|
20
21
|
require_relative "repl/input"
|
|
22
|
+
require_relative "repl/bar"
|
|
21
23
|
require_relative "repl/stream"
|
|
24
|
+
require_relative "repl/markdown"
|
|
22
25
|
|
|
23
26
|
##
|
|
24
27
|
# @param [LLM::Agent] agent
|
|
28
|
+
# @param [Array<LLM::Tool>] tools
|
|
29
|
+
# Zero or more tools
|
|
30
|
+
# @param [Array<String>] skills
|
|
31
|
+
# Zero or more skills
|
|
25
32
|
# @return [LLM::Repl]
|
|
26
|
-
def initialize(agent)
|
|
33
|
+
def initialize(agent:, tools:, skills:)
|
|
27
34
|
@agent = agent
|
|
28
35
|
@provider = agent.llm.name
|
|
29
|
-
@status = Status.new(@
|
|
36
|
+
@status = Status.new(@agent)
|
|
30
37
|
@transcript = Transcript.new
|
|
31
|
-
@input = Input.new(@
|
|
38
|
+
@input = Input.new(@agent, height: 3)
|
|
32
39
|
@window = Window.new(@status, @transcript, @input)
|
|
33
|
-
@
|
|
40
|
+
@thread = nil
|
|
41
|
+
@queue = Queue.new
|
|
42
|
+
@stream = Stream.new(self, @queue)
|
|
43
|
+
@skills = skills.map do |path|
|
|
44
|
+
##
|
|
45
|
+
# I'm not sure it would make sense to expose
|
|
46
|
+
# the underlying context or not. In the meantime,
|
|
47
|
+
# this works and meets the expectations of the
|
|
48
|
+
# LLM::Skill class.
|
|
49
|
+
ctx = agent.instance_variable_get(:@ctx)
|
|
50
|
+
LLM::Skill.load(path).to_tool(ctx)
|
|
51
|
+
end
|
|
52
|
+
@tools = [agent.params[:tools], @skills, tools].flatten.compact
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
##
|
|
@@ -38,26 +57,33 @@ module LLM
|
|
|
38
57
|
def start
|
|
39
58
|
window.open do
|
|
40
59
|
loop do
|
|
41
|
-
window.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
status.text = "idle"
|
|
51
|
-
write("\n\n")
|
|
60
|
+
case input.on_char(window, window.getch)
|
|
61
|
+
when :exit then break
|
|
62
|
+
when :submit then submit
|
|
63
|
+
when :up, :down, :backspace, :char then window.redraw
|
|
64
|
+
else
|
|
65
|
+
window.redraw
|
|
66
|
+
read!
|
|
67
|
+
sleep 0.01
|
|
68
|
+
end
|
|
52
69
|
end
|
|
53
70
|
end
|
|
54
71
|
end
|
|
55
72
|
|
|
73
|
+
##
|
|
74
|
+
# @param [String] chars
|
|
75
|
+
# @param [Object] attrs
|
|
76
|
+
# @return [void]
|
|
77
|
+
def write(chars, attrs = nil)
|
|
78
|
+
transcript.write(chars, attrs)
|
|
79
|
+
window.redraw
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
##
|
|
57
83
|
# @param [String] chars
|
|
58
84
|
# @return [void]
|
|
59
|
-
def
|
|
60
|
-
transcript.
|
|
85
|
+
def markdown(chars)
|
|
86
|
+
transcript.markdown(chars)
|
|
61
87
|
window.redraw
|
|
62
88
|
end
|
|
63
89
|
|
|
@@ -71,8 +97,63 @@ module LLM
|
|
|
71
97
|
|
|
72
98
|
private
|
|
73
99
|
|
|
100
|
+
##
|
|
101
|
+
# This method is called when the user submits their input.
|
|
102
|
+
# It spawns a second thread that maintains a line of
|
|
103
|
+
# communication with a model and the main thread - where
|
|
104
|
+
# the UI runs - remains responsive.
|
|
105
|
+
# @api private
|
|
106
|
+
def submit
|
|
107
|
+
return if thread&.alive?
|
|
108
|
+
text = input.take
|
|
109
|
+
return if text.empty?
|
|
110
|
+
status.text = "thinking"
|
|
111
|
+
write("user: ", Curses::A_BOLD)
|
|
112
|
+
markdown(text)
|
|
113
|
+
write("\nagent: ", Curses::A_BOLD)
|
|
114
|
+
@thread = Thread.new do
|
|
115
|
+
@queue << [:start]
|
|
116
|
+
agent.talk(text, tools:, stream:)
|
|
117
|
+
@queue << [:done]
|
|
118
|
+
rescue => e
|
|
119
|
+
@queue << [:error, e]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# This method reads from the queue that is written to
|
|
125
|
+
# by a second thread. The queue is managed or written
|
|
126
|
+
# to by a subclass of {LLM::Stream LLM::Stream}.
|
|
127
|
+
# @api private
|
|
128
|
+
def read!
|
|
129
|
+
loop do
|
|
130
|
+
type, value = @queue.pop(true)
|
|
131
|
+
case type
|
|
132
|
+
when :start
|
|
133
|
+
transcript.start
|
|
134
|
+
stream.empty!
|
|
135
|
+
when :stream
|
|
136
|
+
transcript.markdown(value, method: :replace)
|
|
137
|
+
when :status
|
|
138
|
+
self.status = value
|
|
139
|
+
when :done
|
|
140
|
+
status.text = "idle"
|
|
141
|
+
@thread = nil
|
|
142
|
+
transcript.finish
|
|
143
|
+
write("\n\n")
|
|
144
|
+
when :error
|
|
145
|
+
# Do this better
|
|
146
|
+
status.text = "error"
|
|
147
|
+
transcript.finish
|
|
148
|
+
write("\nerror: #{value.message}\n", Curses::A_BOLD)
|
|
149
|
+
@thread = nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
rescue ThreadError
|
|
153
|
+
end
|
|
154
|
+
|
|
74
155
|
attr_reader :agent, :provider, :stream,
|
|
75
156
|
:status, :transcript, :input,
|
|
76
|
-
:window
|
|
157
|
+
:window, :tools, :thread
|
|
77
158
|
end
|
|
78
159
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -38,6 +38,17 @@ module LLM::Sequel
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
module InstanceMethods
|
|
41
|
+
##
|
|
42
|
+
# @note
|
|
43
|
+
# This method does not persist to the database,
|
|
44
|
+
# but it can inspect and alter runtime state in
|
|
45
|
+
# a way that is temporary.
|
|
46
|
+
# @param (see LLM::Agent#repl)
|
|
47
|
+
# @return (see LLM::Agent#repl)
|
|
48
|
+
def repl(**params)
|
|
49
|
+
ctx.repl(**params)
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
private
|
|
42
53
|
|
|
43
54
|
def ctx
|
data/lib/llm/skill.rb
CHANGED
|
@@ -155,7 +155,7 @@ module LLM
|
|
|
155
155
|
def agent(ctx)
|
|
156
156
|
instructions, tools, tracer, inherit_tools = self.instructions, self.tools, ctx.llm.tracer, inherit_tools?
|
|
157
157
|
params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
|
|
158
|
-
concurrency = params[:stream].extra[:concurrency]
|
|
158
|
+
concurrency = params[:stream].extra[:concurrency]
|
|
159
159
|
params[:concurrency] = concurrency if concurrency
|
|
160
160
|
agent = Class.new(LLM::Agent) do
|
|
161
161
|
instructions(instructions)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Stream
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Stream::Disabled LLM::Stream::Disabled} represents a stream
|
|
6
|
+
# that is explicitly turned off.
|
|
7
|
+
#
|
|
8
|
+
# {LLM::Stream.try} returns an instance of this class when given `false`
|
|
9
|
+
# or `nil`. Providers check {#enabled?} to decide whether to request a
|
|
10
|
+
# streaming response from the API. When disabled, the stream is replaced
|
|
11
|
+
# with `nil` before the transport layer executes the request.
|
|
12
|
+
#
|
|
13
|
+
# All callback methods inherited from {LLM::Stream LLM::Stream} are no-ops,
|
|
14
|
+
# so a disabled stream can be used safely wherever a stream object is
|
|
15
|
+
# expected.
|
|
16
|
+
class Disabled < self
|
|
17
|
+
##
|
|
18
|
+
# @return [false]
|
|
19
|
+
def enabled?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Stream
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Stream::IO LLM::Stream::IO} wraps an object that responds to
|
|
6
|
+
# `#<<` and forwards streamed content to it.
|
|
7
|
+
#
|
|
8
|
+
# This enables any object that implements `#<<` - such as an IO, StringIO,
|
|
9
|
+
# or a custom logger - to be used as a stream target. {LLM::Stream.try}
|
|
10
|
+
# creates instances of this class automatically when given an IO-like
|
|
11
|
+
# object.
|
|
12
|
+
#
|
|
13
|
+
# @example Using an IO object as a stream
|
|
14
|
+
# File.open("output.txt", "w") do |file|
|
|
15
|
+
# ctx.ask("Tell me a story", stream: file)
|
|
16
|
+
# end
|
|
17
|
+
class IO < self
|
|
18
|
+
##
|
|
19
|
+
# @param [#<<] io
|
|
20
|
+
# Any object that implements `#<<`.
|
|
21
|
+
def initialize(io)
|
|
22
|
+
@io = io
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Writes a chunk of content to the wrapped IO object.
|
|
27
|
+
# @param [String] content
|
|
28
|
+
# @return [void]
|
|
29
|
+
def on_content(content)
|
|
30
|
+
@io << content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Forwards streamed content to the wrapped IO object.
|
|
35
|
+
# Redefined here because +alias_method+ in the parent class
|
|
36
|
+
# captures the base implementation, not the overridden one.
|
|
37
|
+
# @param [String] content
|
|
38
|
+
# @return [void]
|
|
39
|
+
def <<(content)
|
|
40
|
+
on_content(content)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/llm/stream.rb
CHANGED
|
@@ -21,6 +21,34 @@ module LLM
|
|
|
21
21
|
# also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
|
|
22
22
|
class Stream
|
|
23
23
|
require_relative "stream/queue"
|
|
24
|
+
require_relative "stream/io"
|
|
25
|
+
require_relative "stream/disabled"
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# This method will try to convert its argument into
|
|
29
|
+
# an instance of {LLM::Stream LLM::Stream} or a
|
|
30
|
+
# subclass of it.
|
|
31
|
+
#
|
|
32
|
+
# Acceptable inputs include: {LLM::Stream LLM::Stream}
|
|
33
|
+
# objects, IO objects who implement `#<<`, true, false,
|
|
34
|
+
# and nil. Anything else raises a TypeError.
|
|
35
|
+
#
|
|
36
|
+
# @raise [TypeError]
|
|
37
|
+
# @param [LLM::Stream, #<<, Boolean, NilClass] obj
|
|
38
|
+
# @return [LLM::Stream]
|
|
39
|
+
def self.try(obj, extra: {})
|
|
40
|
+
if LLM::Stream === obj
|
|
41
|
+
obj.tap { _1.extra.merge!(extra) }
|
|
42
|
+
elsif obj.respond_to?(:<<)
|
|
43
|
+
LLM::Stream::IO.new(obj).tap { _1.extra.merge!(extra) }
|
|
44
|
+
elsif obj == true
|
|
45
|
+
LLM::Stream.new.tap { _1.extra.merge!(extra) }
|
|
46
|
+
elsif obj.nil? || obj == false
|
|
47
|
+
LLM::Stream::Disabled.new.tap { _1.extra.merge!(extra) }
|
|
48
|
+
else
|
|
49
|
+
raise TypeError, "invalid stream object"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
24
52
|
|
|
25
53
|
##
|
|
26
54
|
# Returns extra context associated with the current streamed request.
|
|
@@ -52,6 +80,12 @@ module LLM
|
|
|
52
80
|
queue.wait
|
|
53
81
|
end
|
|
54
82
|
|
|
83
|
+
##
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def enabled?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
55
89
|
# @group Public callbacks
|
|
56
90
|
|
|
57
91
|
##
|
data/lib/llm/tools/git.rb
CHANGED
|
@@ -16,9 +16,8 @@ class LLM::Tool
|
|
|
16
16
|
required %i[action]
|
|
17
17
|
|
|
18
18
|
##
|
|
19
|
-
# @param [String]
|
|
20
|
-
# @param [
|
|
21
|
-
# @param [Integer] stop
|
|
19
|
+
# @param [String] action
|
|
20
|
+
# @param [Array<String>, nil] arguments
|
|
22
21
|
# @return [Hash]
|
|
23
22
|
def call(action:, arguments: nil)
|
|
24
23
|
command = spawn(action:, arguments:)
|
data/lib/llm/tools/pwd.rb
CHANGED
data/lib/llm/tools/rg.rb
CHANGED
data/lib/llm/tools/swap_text.rb
CHANGED
|
@@ -14,6 +14,12 @@ class LLM::Tool
|
|
|
14
14
|
parameter :expected_count, Integer, "How many matches should be replaced"
|
|
15
15
|
required %i[path before after]
|
|
16
16
|
|
|
17
|
+
##
|
|
18
|
+
# @param [String] path
|
|
19
|
+
# @param [String] before
|
|
20
|
+
# @param [String] after
|
|
21
|
+
# @param [Integer] expected_count
|
|
22
|
+
# @return [Hash]
|
|
17
23
|
def call(path:, before:, after:, expected_count: 1)
|
|
18
24
|
content = File.read(path)
|
|
19
25
|
count = content.scan(before).length
|
|
@@ -29,6 +29,7 @@ class LLM::Transport
|
|
|
29
29
|
# When there is a network error at the operating system level
|
|
30
30
|
# @return [LLM::Transport::Response]
|
|
31
31
|
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, inputs: nil, &b)
|
|
32
|
+
stream = nil if !stream&.enabled?
|
|
32
33
|
stream &&= LLM::Object.from(streamer: stream, parser: stream_parser, decoder: stream_decoder)
|
|
33
34
|
owner = transport.request_owner
|
|
34
35
|
tracer = self.tracer
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
|
@@ -252,6 +252,16 @@ module LLM
|
|
|
252
252
|
LLM::Function.new(key, &b)
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
+
##
|
|
256
|
+
# @param [LLM::Provider] llm
|
|
257
|
+
# A provider
|
|
258
|
+
# @param [Hash] params
|
|
259
|
+
# Forwarded to LLM::Tracer::Logger
|
|
260
|
+
# @return [LLM::Tracer::Logger]
|
|
261
|
+
def logger(llm, **params)
|
|
262
|
+
LLM::Tracer::Logger.new(llm, params)
|
|
263
|
+
end
|
|
264
|
+
|
|
255
265
|
##
|
|
256
266
|
# Provides a thread-safe lock
|
|
257
267
|
# @param [Symbol] name The name of the lock
|
data/llm.gemspec
CHANGED
|
@@ -9,9 +9,13 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["robert@r.uby.dev"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "Ruby's capable AI runtime"
|
|
12
|
-
spec.description =
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
spec.description = <<~DESCRIPTION
|
|
13
|
+
llm.rb is an advanced runtime for building capable AI applications
|
|
14
|
+
on CRuby. By default it has zero runtime dependencies although certain
|
|
15
|
+
functionality – such as ActiveRecord support – require
|
|
16
|
+
optional dependencies that are opt-in.
|
|
17
|
+
DESCRIPTION
|
|
18
|
+
|
|
15
19
|
spec.license = "BUSL-1.1"
|
|
16
20
|
spec.required_ruby_version = ">= 3.3.0"
|
|
17
21
|
|