llm.rb 13.0.0 → 13.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +185 -14
- data/README.md +155 -30
- data/bin/llm.rb +124 -0
- data/lib/llm/agent.rb +78 -8
- data/lib/llm/function/array.rb +3 -3
- data/lib/llm/function.rb +1 -2
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/mistral.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/repl/{transcript.rb → buffer.rb} +34 -21
- data/lib/llm/repl/command.rb +12 -5
- data/lib/llm/repl/commands/compact.rb +2 -2
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/markdown/table.rb +7 -3
- data/lib/llm/repl/markdown.rb +3 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +2 -2
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +56 -30
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +3 -0
- data/lib/llm/tools/mkdir.rb +3 -0
- data/lib/llm/tools/rg.rb +3 -0
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +3 -0
- data/lib/llm/tracer/pretty_logger.rb +127 -0
- data/lib/llm/tracer.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +8 -2
- data/resources/deepdive.md +45 -1785
- metadata +16 -5
data/lib/llm/repl/stream.rb
CHANGED
data/lib/llm/repl/window.rb
CHANGED
|
@@ -11,22 +11,21 @@ class LLM::Repl
|
|
|
11
11
|
attr_reader :status
|
|
12
12
|
|
|
13
13
|
##
|
|
14
|
-
# @return [LLM::Repl::
|
|
15
|
-
attr_reader :
|
|
14
|
+
# @return [LLM::Repl::Buffer]
|
|
15
|
+
attr_reader :buffer
|
|
16
16
|
|
|
17
17
|
##
|
|
18
18
|
# @return [LLM::Repl::Input]
|
|
19
19
|
attr_reader :input
|
|
20
20
|
|
|
21
21
|
##
|
|
22
|
-
# @param [LLM::Repl
|
|
23
|
-
#
|
|
24
|
-
# @param [LLM::Repl::Input] input
|
|
22
|
+
# @param [LLM::Repl] repl
|
|
23
|
+
# A read-eval-print loop.
|
|
25
24
|
# @return [LLM::Repl::Window]
|
|
26
|
-
def initialize(
|
|
27
|
-
@status = status
|
|
28
|
-
@
|
|
29
|
-
@input
|
|
25
|
+
def initialize(repl)
|
|
26
|
+
@status = repl.status
|
|
27
|
+
@buffer = repl.buffer
|
|
28
|
+
@input = repl.input
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
##
|
|
@@ -48,7 +47,7 @@ class LLM::Repl
|
|
|
48
47
|
def redraw
|
|
49
48
|
draw_status(offset: input.height + 1)
|
|
50
49
|
draw_divider(offset: 5)
|
|
51
|
-
|
|
50
|
+
draw_buffer(offset: 0)
|
|
52
51
|
draw_input
|
|
53
52
|
Curses.refresh
|
|
54
53
|
end
|
|
@@ -86,23 +85,42 @@ class LLM::Repl
|
|
|
86
85
|
##
|
|
87
86
|
# @return [void]
|
|
88
87
|
def scroll_up
|
|
89
|
-
|
|
88
|
+
buffer.scroll_up(rows)
|
|
90
89
|
end
|
|
91
90
|
|
|
92
91
|
##
|
|
93
92
|
# @return [void]
|
|
94
93
|
def scroll_down
|
|
95
|
-
|
|
94
|
+
buffer.scroll_down
|
|
96
95
|
end
|
|
97
96
|
|
|
98
97
|
##
|
|
99
98
|
# @return [void]
|
|
100
99
|
def scroll_to_bottom
|
|
101
|
-
|
|
100
|
+
buffer.scroll_to_bottom
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
private
|
|
105
104
|
|
|
105
|
+
def draw_buffer(offset:)
|
|
106
|
+
rows = buffer.visible(self.rows)
|
|
107
|
+
rows.each.with_index(offset) do |row, index|
|
|
108
|
+
Curses.setpos(index, 0)
|
|
109
|
+
Curses.clrtoeol
|
|
110
|
+
row.each do |chunk|
|
|
111
|
+
text, attrs = chunk.text, chunk.attrs
|
|
112
|
+
Curses.attron(attrs) if attrs
|
|
113
|
+
Curses.addstr(text)
|
|
114
|
+
Curses.attroff(attrs) if attrs
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
last_drawn = offset + rows.size
|
|
118
|
+
(last_drawn...self.rows).each do |line|
|
|
119
|
+
Curses.setpos(line, 0)
|
|
120
|
+
Curses.clrtoeol
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
106
124
|
def draw_status(offset:)
|
|
107
125
|
Curses.setpos(Curses.lines - offset, 0)
|
|
108
126
|
Curses.clrtoeol
|
|
@@ -132,25 +150,6 @@ class LLM::Repl
|
|
|
132
150
|
Curses.setpos((Curses.lines - input.height) + line, col)
|
|
133
151
|
end
|
|
134
152
|
|
|
135
|
-
def draw_transcript(offset:)
|
|
136
|
-
rows = transcript.visible(self.rows)
|
|
137
|
-
rows.each.with_index(offset) do |row, index|
|
|
138
|
-
Curses.setpos(index, 0)
|
|
139
|
-
Curses.clrtoeol
|
|
140
|
-
row.each do |chunk|
|
|
141
|
-
text, attrs = chunk.values_at(:text, :attrs)
|
|
142
|
-
Curses.attron(attrs) if attrs
|
|
143
|
-
Curses.addstr(text)
|
|
144
|
-
Curses.attroff(attrs) if attrs
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
last_drawn = offset + rows.size
|
|
148
|
-
(last_drawn...self.rows).each do |line|
|
|
149
|
-
Curses.setpos(line, 0)
|
|
150
|
-
Curses.clrtoeol
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
153
|
##
|
|
155
154
|
# @return [Integer]
|
|
156
155
|
def columns
|
data/lib/llm/repl.rb
CHANGED
|
@@ -17,18 +17,19 @@ module LLM
|
|
|
17
17
|
class Repl
|
|
18
18
|
require_relative "repl/window"
|
|
19
19
|
require_relative "repl/status"
|
|
20
|
-
require_relative "repl/
|
|
20
|
+
require_relative "repl/buffer"
|
|
21
21
|
require_relative "repl/input"
|
|
22
22
|
require_relative "repl/bar"
|
|
23
23
|
require_relative "repl/stream"
|
|
24
|
+
require_relative "repl/node"
|
|
24
25
|
require_relative "repl/markdown"
|
|
25
26
|
require_relative "repl/command"
|
|
26
27
|
require_relative "repl/walker"
|
|
27
28
|
|
|
28
29
|
attr_reader :agent, :provider, :stream,
|
|
29
|
-
:status, :
|
|
30
|
+
:status, :buffer, :input,
|
|
30
31
|
:window, :tools, :thread,
|
|
31
|
-
:name, :path
|
|
32
|
+
:name, :path, :width
|
|
32
33
|
|
|
33
34
|
##
|
|
34
35
|
# @param [LLM::Agent] agent
|
|
@@ -42,14 +43,15 @@ module LLM
|
|
|
42
43
|
# Zero or more skills
|
|
43
44
|
# @return [LLM::Repl]
|
|
44
45
|
def initialize(agent:, name: nil, tools: [], skills: [], path: nil)
|
|
46
|
+
@width = 80
|
|
45
47
|
@path = path
|
|
46
48
|
@name = name || "agent"
|
|
47
49
|
@agent = configure(agent:, path:)
|
|
48
50
|
@provider = agent.llm.name
|
|
49
|
-
@status = Status.new(
|
|
50
|
-
@
|
|
51
|
+
@status = Status.new(self)
|
|
52
|
+
@buffer = Buffer.new(self)
|
|
51
53
|
@input = Input.new(self, height: 3)
|
|
52
|
-
@window = Window.new(
|
|
54
|
+
@window = Window.new(self)
|
|
53
55
|
@thread = nil
|
|
54
56
|
@queue = Queue.new
|
|
55
57
|
@stream = Stream.new(self, @queue)
|
|
@@ -70,6 +72,7 @@ module LLM
|
|
|
70
72
|
def start
|
|
71
73
|
window.open do
|
|
72
74
|
catch(:exit) do
|
|
75
|
+
write tree(agent.messages)
|
|
73
76
|
loop do
|
|
74
77
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
78
|
case input.on_char(window, input.paste? ? window.read_paste : window.getch, now)
|
|
@@ -86,22 +89,32 @@ module LLM
|
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
##
|
|
89
|
-
# @param [String] chars
|
|
92
|
+
# @param [String, Array] chars
|
|
90
93
|
# @param [Object] attrs
|
|
94
|
+
# @param [Symbol] method (:append, :replace)
|
|
91
95
|
# @return [void]
|
|
92
|
-
def write(chars, attrs = nil)
|
|
93
|
-
|
|
96
|
+
def write(chars, attrs = nil, method: :append)
|
|
97
|
+
buffer.write(chars, attrs, method:)
|
|
94
98
|
window.redraw
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
##
|
|
98
|
-
# @param [String]
|
|
102
|
+
# @param [String] user
|
|
103
|
+
# @param [String] content
|
|
99
104
|
# @return [void]
|
|
100
|
-
def
|
|
101
|
-
|
|
105
|
+
def write_message(user, content, method: :append)
|
|
106
|
+
buffer.write_message(user, content, method:)
|
|
102
107
|
window.redraw
|
|
103
108
|
end
|
|
104
109
|
|
|
110
|
+
##
|
|
111
|
+
# Returns an AST
|
|
112
|
+
# @param [String] chars
|
|
113
|
+
# @return [Array<Node>]
|
|
114
|
+
def markdown(chars)
|
|
115
|
+
LLM::Repl::Markdown.new(chars, width).ast
|
|
116
|
+
end
|
|
117
|
+
|
|
105
118
|
##
|
|
106
119
|
# @param [String] value
|
|
107
120
|
# @return [void]
|
|
@@ -118,6 +131,22 @@ module LLM
|
|
|
118
131
|
|
|
119
132
|
private
|
|
120
133
|
|
|
134
|
+
##
|
|
135
|
+
# @param [LLM::Buffer] messages
|
|
136
|
+
# A message buffer
|
|
137
|
+
# @return [Array<Node>]
|
|
138
|
+
def tree(messages)
|
|
139
|
+
messages.flat_map do |message|
|
|
140
|
+
next if message.tool_call? || message.tool_return?
|
|
141
|
+
user = message.assistant? ? name : "user"
|
|
142
|
+
[
|
|
143
|
+
Node.new("#{user}: ", Curses::A_BOLD),
|
|
144
|
+
Node.new(message.content),
|
|
145
|
+
Node.new(user == name ? "\n\n" : "\n")
|
|
146
|
+
]
|
|
147
|
+
end.compact
|
|
148
|
+
end
|
|
149
|
+
|
|
121
150
|
##
|
|
122
151
|
# @param [LLM::Agent] agent
|
|
123
152
|
# An agent
|
|
@@ -151,15 +180,14 @@ module LLM
|
|
|
151
180
|
case on_text(text)
|
|
152
181
|
in [:command, Command => command, Hash => parameters]
|
|
153
182
|
command.call(**parameters)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
183
|
+
write("\n")
|
|
184
|
+
in [:error, String => user, String => text]
|
|
185
|
+
write_message(user, text)
|
|
186
|
+
write("\n")
|
|
157
187
|
in [:input, String => text]
|
|
158
188
|
window.scroll_to_bottom
|
|
159
189
|
status.text = thinking_text
|
|
160
|
-
|
|
161
|
-
markdown(text)
|
|
162
|
-
write("\n#{name}: ", Curses::A_BOLD)
|
|
190
|
+
write_message("user", markdown(text))
|
|
163
191
|
@thread = Thread.new do
|
|
164
192
|
@queue << [:start]
|
|
165
193
|
agent.talk(text, tools:, stream:)
|
|
@@ -214,29 +242,27 @@ module LLM
|
|
|
214
242
|
type, value = @queue.pop(true)
|
|
215
243
|
case type
|
|
216
244
|
when :start
|
|
217
|
-
|
|
218
|
-
stream.
|
|
245
|
+
buffer.open
|
|
246
|
+
stream.clear
|
|
219
247
|
when :stream
|
|
220
|
-
|
|
248
|
+
write_message name, markdown(value), method: :replace
|
|
221
249
|
when :status
|
|
222
250
|
self.status = value
|
|
223
251
|
when :done
|
|
224
252
|
status.text = "idle"
|
|
225
|
-
|
|
226
|
-
transcript.finish
|
|
253
|
+
buffer.close
|
|
227
254
|
@thread = nil
|
|
255
|
+
write("\n")
|
|
228
256
|
when :cancel
|
|
257
|
+
buffer.close
|
|
229
258
|
status.text = "Idle"
|
|
230
|
-
|
|
231
|
-
write("request cancelled!")
|
|
232
|
-
write("\n\n")
|
|
233
|
-
transcript.finish
|
|
259
|
+
write_message(name, "Request cancelled")
|
|
234
260
|
@thread = nil
|
|
261
|
+
write("\n")
|
|
235
262
|
when :error
|
|
236
|
-
# Do this better
|
|
237
263
|
status.text = "error"
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
write_message(name, "(#{value.class}): #{value.message}")
|
|
265
|
+
buffer.close
|
|
240
266
|
@thread = nil
|
|
241
267
|
end
|
|
242
268
|
end
|
data/lib/llm/skill.rb
CHANGED
|
@@ -7,6 +7,11 @@ module LLM
|
|
|
7
7
|
# Skills can expose themselves as normal {LLM::Tool LLM::Tool} classes through
|
|
8
8
|
# {#to_tool}. This keeps skills on the same execution path as local tools.
|
|
9
9
|
class Skill
|
|
10
|
+
##
|
|
11
|
+
# @api private
|
|
12
|
+
File = ::File
|
|
13
|
+
private_constant :File
|
|
14
|
+
|
|
10
15
|
##
|
|
11
16
|
# Load a skill from a directory.
|
|
12
17
|
# @param [String, Pathname] path
|
|
@@ -63,8 +68,14 @@ module LLM
|
|
|
63
68
|
# Load and parse the skill.
|
|
64
69
|
# @return [LLM::Skill]
|
|
65
70
|
def load!
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if File.file?(@path)
|
|
72
|
+
parse(File.read(@path))
|
|
73
|
+
elsif File.directory?(@path)
|
|
74
|
+
path = File.join(@path, "SKILL.md")
|
|
75
|
+
parse(File.read(path))
|
|
76
|
+
else
|
|
77
|
+
raise LLM::Error, "'#{@path}' is neither a file or directory"
|
|
78
|
+
end
|
|
68
79
|
self
|
|
69
80
|
end
|
|
70
81
|
|
|
@@ -103,7 +114,8 @@ module LLM
|
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
##
|
|
106
|
-
# Returns true when a skill should inherit tools
|
|
117
|
+
# Returns true when a skill should inherit tools
|
|
118
|
+
# from its parent
|
|
107
119
|
# @return [Boolean]
|
|
108
120
|
def inherit_tools?
|
|
109
121
|
@inherit_tools
|
|
@@ -138,7 +150,11 @@ module LLM
|
|
|
138
150
|
def parse_tools(tools)
|
|
139
151
|
case tools
|
|
140
152
|
when String
|
|
141
|
-
|
|
153
|
+
case tools
|
|
154
|
+
when "inherit" then [true, []]
|
|
155
|
+
when "all", "*" then [false, LLM::Tool.registry]
|
|
156
|
+
else raise_invalid_error!(tools)
|
|
157
|
+
end
|
|
142
158
|
when Array
|
|
143
159
|
[false, [*@frontmatter.tools].map { LLM::Tool.find_by_name!(_1) }]
|
|
144
160
|
when NilClass
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::Tool::
|
|
5
|
+
# The {LLM::Tool::EditFile LLM::Tool::EditFile} class
|
|
6
6
|
# implements a tool that can substitute one piece of
|
|
7
7
|
# text for another piece of text in a given file.
|
|
8
|
-
class
|
|
9
|
-
name "
|
|
8
|
+
class EditFile < self
|
|
9
|
+
name "edit-file"
|
|
10
10
|
description "Replace an exact snippet in a file"
|
|
11
11
|
parameter :path, String, "Path to file"
|
|
12
12
|
parameter :before, String, "Exact text to replace"
|
data/lib/llm/tools/git.rb
CHANGED
data/lib/llm/tools/mkdir.rb
CHANGED
data/lib/llm/tools/rg.rb
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::Ruby LLM::Tool::Ruby} class implements
|
|
6
|
+
# a tool that can execute an arbitrary string of Ruby code
|
|
7
|
+
# and return the result. It uses `test-cmd.rb` under the
|
|
8
|
+
# hood for process management.
|
|
9
|
+
class Ruby < self
|
|
10
|
+
require_relative "utils"
|
|
11
|
+
include Utils
|
|
12
|
+
|
|
13
|
+
name "ruby"
|
|
14
|
+
description "runs a string of ruby code"
|
|
15
|
+
parameter :code, String, "a string of ruby code"
|
|
16
|
+
parameter :timeout, Integer, "maximum runtime before timeout"
|
|
17
|
+
required %i[code]
|
|
18
|
+
defaults timeout: 15
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# @param [String] code
|
|
22
|
+
# Ruby code
|
|
23
|
+
# @param [Integer] timeout
|
|
24
|
+
# Runtime timeout
|
|
25
|
+
def call(code:, timeout: 15)
|
|
26
|
+
command = spawn(code:)
|
|
27
|
+
wait(command:, timeout:)
|
|
28
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
29
|
+
rescue LLM::Interrupt
|
|
30
|
+
command.kill! if command&.running?
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def spawn(code:)
|
|
37
|
+
Command
|
|
38
|
+
.new(RbConfig.ruby)
|
|
39
|
+
.argv("-e", code)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
require "rbconfig"
|
|
43
|
+
LLM.require "test-cmd.rb", "~> 1.1"
|
|
44
|
+
Command = Test::Cmd
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/llm/tools/shell.rb
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tracer::PrettyLogger LLM::Tracer::PrettyLogger} class
|
|
6
|
+
# writes human-readable request and tool call logs to a console
|
|
7
|
+
# or file. Each event is a single line with the relevant context
|
|
8
|
+
# inline, no structured JSON.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# llm = LLM.openai(key: ENV["KEY"])
|
|
12
|
+
# llm.tracer = LLM::Tracer::PrettyLogger.new(llm)
|
|
13
|
+
#
|
|
14
|
+
# @example Writing to a file
|
|
15
|
+
# llm.tracer = LLM::Tracer::PrettyLogger.new(llm, io: File.open("log.txt", "a"))
|
|
16
|
+
class Tracer::PrettyLogger < Tracer
|
|
17
|
+
##
|
|
18
|
+
# @param (see LLM::Tracer#initialize)
|
|
19
|
+
def initialize(provider, options = {})
|
|
20
|
+
super
|
|
21
|
+
setup!(**options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @param (see LLM::Tracer#on_request_start)
|
|
26
|
+
# @return [void]
|
|
27
|
+
def on_request_start(operation:, model: nil, **)
|
|
28
|
+
@start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
29
|
+
name = operation == "chat" ? "chat" : operation
|
|
30
|
+
@io.puts "#{timestamp} #{provider_name} #{name} (#{model || "default"})"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @param (see LLM::Tracer#on_request_finish)
|
|
35
|
+
# @return [void]
|
|
36
|
+
def on_request_finish(operation:, res:, model: nil, **)
|
|
37
|
+
elapsed = @start ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start).round(2) : nil
|
|
38
|
+
tokens = format_tokens(res)
|
|
39
|
+
name = operation == "chat" ? "chat" : operation
|
|
40
|
+
parts = ["#{timestamp} #{provider_name} #{name} done"]
|
|
41
|
+
parts << tokens if tokens
|
|
42
|
+
parts << "#{elapsed}s" if elapsed
|
|
43
|
+
@io.puts parts.join(", ")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# @param (see LLM::Tracer#on_request_error)
|
|
48
|
+
# @return [void]
|
|
49
|
+
def on_request_error(ex:, **)
|
|
50
|
+
@io.puts "#{timestamp} #{provider_name} error #{ex.class}: #{ex.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @param (see LLM::Tracer#on_tool_start)
|
|
55
|
+
# @return [void]
|
|
56
|
+
def on_tool_start(id:, name:, arguments:, **)
|
|
57
|
+
@io.puts "#{timestamp} #{name}(#{format_arguments(arguments)})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @param (see LLM::Tracer#on_tool_finish)
|
|
62
|
+
# @return [void]
|
|
63
|
+
def on_tool_finish(result:, **)
|
|
64
|
+
@io.puts "#{timestamp} #{result.name} -> #{format_value(result.value)}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# @param (see LLM::Tracer#on_tool_error)
|
|
69
|
+
# @return [void]
|
|
70
|
+
def on_tool_error(ex:, **)
|
|
71
|
+
@io.puts "#{timestamp} tool error #{ex.class}: #{ex.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def setup!(io: $stderr)
|
|
77
|
+
@io = io
|
|
78
|
+
@start = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def timestamp
|
|
82
|
+
Time.now.strftime("%H:%M:%S")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_tokens(res)
|
|
86
|
+
usage = res.usage
|
|
87
|
+
if usage.input_tokens and usage.output_tokens
|
|
88
|
+
"in=#{usage.input_tokens} out=#{usage.output_tokens}"
|
|
89
|
+
elsif usage.input_tokens
|
|
90
|
+
"in=#{usage.input_tokens}"
|
|
91
|
+
elsif usage.output_tokens
|
|
92
|
+
"out=#{usage.output_tokens}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_arguments(args, max: 50)
|
|
97
|
+
return "" unless args
|
|
98
|
+
case args
|
|
99
|
+
when Hash, LLM::Object
|
|
100
|
+
result = args.map { |k, v| "#{k}: #{format_value(v)}" }.join(", ")
|
|
101
|
+
when Array
|
|
102
|
+
result = args.map { |v| format_value(v) }.join(", ")
|
|
103
|
+
else
|
|
104
|
+
result = args.inspect
|
|
105
|
+
end
|
|
106
|
+
result.size > max ? "#{result[0...max - 1]}..." : result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def format_value(value, max: 18)
|
|
110
|
+
case value
|
|
111
|
+
when String
|
|
112
|
+
value.size > max ? "#{value[0...max]}...".inspect : value.inspect
|
|
113
|
+
when Array
|
|
114
|
+
items = value.take(2).map { format_value(_1, max: 10) }
|
|
115
|
+
items << "..." if value.size > 2
|
|
116
|
+
"[#{items.join(", ")}]"
|
|
117
|
+
when Hash
|
|
118
|
+
"{...}"
|
|
119
|
+
when nil
|
|
120
|
+
"nil"
|
|
121
|
+
else
|
|
122
|
+
str = value.inspect
|
|
123
|
+
str.size > max ? "#{str[0...max]}..." : str
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/llm/tracer.rb
CHANGED
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = <<~DESCRIPTION
|
|
13
13
|
llm.rb is an advanced runtime for building capable AI applications
|
|
14
14
|
on CRuby. By default it has zero runtime dependencies although certain
|
|
15
|
-
functionality
|
|
15
|
+
functionality (such as ActiveRecord support) require
|
|
16
16
|
optional dependencies that are opt-in.
|
|
17
17
|
DESCRIPTION
|
|
18
18
|
|
|
@@ -30,9 +30,15 @@ DESCRIPTION
|
|
|
30
30
|
"lib/*.rb", "lib/**/*.rb",
|
|
31
31
|
"data/*.json", "CHANGELOG.md",
|
|
32
32
|
"resources/deepdive.md",
|
|
33
|
-
"llm.gemspec"
|
|
33
|
+
"llm.gemspec", "bin/llm.rb"
|
|
34
34
|
]
|
|
35
|
+
spec.executables = ["llm.rb"]
|
|
35
36
|
spec.require_paths = ["lib"]
|
|
37
|
+
spec.post_install_message = "\n" \
|
|
38
|
+
"Learn more about llm.rb by reading the deepdive:" \
|
|
39
|
+
"\n" \
|
|
40
|
+
"https://r.uby.dev/llm/deepdive" \
|
|
41
|
+
"\n\n"
|
|
36
42
|
|
|
37
43
|
spec.add_development_dependency "webmock", "~> 3.24.0"
|
|
38
44
|
spec.add_development_dependency "yard", "~> 0.9.37"
|