llm.rb 12.6.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 +571 -13
- data/LICENSE +21 -93
- data/README.md +183 -167
- data/bin/llm.rb +124 -0
- data/data/deepinfra.json +3 -0
- data/data/xai.json +1 -1
- data/lib/llm/a2a.rb +1 -1
- data/lib/llm/active_record/acts_as_llm.rb +6 -6
- data/lib/llm/agent.rb +136 -27
- data/lib/llm/buffer.rb +85 -3
- data/lib/llm/compactor/null.rb +19 -0
- data/lib/llm/compactor/truncate.rb +80 -0
- data/lib/llm/compactor.rb +42 -124
- data/lib/llm/context.rb +31 -37
- data/lib/llm/contract.rb +4 -25
- data/lib/llm/function/array.rb +18 -17
- data/lib/llm/function/async/group.rb +54 -0
- data/lib/llm/function/async/reactor.rb +48 -0
- data/lib/llm/function/async/task.rb +83 -0
- data/lib/llm/function/fiber/group.rb +46 -0
- data/lib/llm/function/fiber/task.rb +62 -0
- data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
- data/lib/llm/function/fork/job.rb +2 -2
- data/lib/llm/function/fork/task.rb +19 -10
- data/lib/llm/function/group.rb +40 -0
- data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
- data/lib/llm/function/ractor/job.rb +9 -3
- data/lib/llm/function/ractor/mailbox.rb +2 -0
- data/lib/llm/function/ractor/task.rb +23 -15
- data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
- data/lib/llm/function/sequential/task.rb +49 -0
- data/lib/llm/function/task.rb +25 -48
- data/lib/llm/function/thread/group.rb +46 -0
- data/lib/llm/function/thread/task.rb +60 -0
- data/lib/llm/function.rb +54 -65
- data/lib/llm/loop_guard.rb +1 -2
- data/lib/llm/mcp.rb +22 -0
- data/lib/llm/object.rb +2 -1
- data/lib/llm/provider.rb +6 -3
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- 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 +47 -13
- data/lib/llm/repl/commands/compact.rb +33 -0
- data/lib/llm/repl/commands/help.rb +3 -5
- data/lib/llm/repl/input.rb +80 -15
- data/lib/llm/repl/markdown/table.rb +80 -0
- data/lib/llm/repl/markdown.rb +33 -3
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +4 -4
- data/lib/llm/repl/stream.rb +12 -5
- data/lib/llm/repl/walker.rb +46 -0
- data/lib/llm/repl/window.rb +31 -32
- data/lib/llm/repl.rb +70 -38
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +5 -0
- data/lib/llm/schema/object.rb +11 -5
- data/lib/llm/sequel/plugin.rb +6 -6
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +24 -17
- data/lib/llm/tool.rb +20 -4
- data/lib/llm/tools/chdir.rb +0 -2
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +11 -4
- data/lib/llm/tools/mkdir.rb +4 -1
- data/lib/llm/tools/pwd.rb +0 -2
- data/lib/llm/tools/read_file.rb +0 -2
- data/lib/llm/tools/rg.rb +11 -4
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +11 -4
- data/lib/llm/tools/utils.rb +31 -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/lib/llm.rb +25 -5
- data/llm.gemspec +11 -5
- data/resources/deepdive.md +45 -1198
- metadata +39 -17
- data/lib/llm/function/call_task.rb +0 -46
- data/lib/llm/function/fiber_group.rb +0 -105
- data/lib/llm/function/task_group.rb +0 -97
- data/lib/llm/function/thread_group.rb +0 -102
data/bin/llm.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "llm"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# utils
|
|
8
|
+
|
|
9
|
+
def keys
|
|
10
|
+
@keys ||= Dir[File.join(__dir__, "..", "lib", "llm", "providers", "*")]
|
|
11
|
+
.select { File.file?(_1) }
|
|
12
|
+
.map { File.basename(_1, ".rb") }
|
|
13
|
+
.sort_by { _1 == "deepseek" ? 0 : 1 }
|
|
14
|
+
.map { "#{_1.upcase}_API_KEY" } - ["BEDROCK_API_KEY"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def help
|
|
18
|
+
prog = File.basename($PROGRAM_NAME)
|
|
19
|
+
warn ""
|
|
20
|
+
warn "Usage: #{prog} [options]"
|
|
21
|
+
warn ""
|
|
22
|
+
warn "Options:"
|
|
23
|
+
warn " -p PROVIDER Choose a provider"
|
|
24
|
+
warn " -t Temporary session that doesn't persist to disk"
|
|
25
|
+
warn " -h Show this help"
|
|
26
|
+
warn ""
|
|
27
|
+
warn "Examples:"
|
|
28
|
+
warn " #{prog} # auto-detect provider from $PROVIDER_API_KEY"
|
|
29
|
+
warn " #{prog} -p openai # use OpenAI"
|
|
30
|
+
warn " #{prog} -h # this help"
|
|
31
|
+
warn ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def loaderror(ex)
|
|
35
|
+
gem, = ex.message.split(" is an optional runtime dependency")
|
|
36
|
+
warn ""
|
|
37
|
+
warn " ── llm.rb ──────────────────────────────────────────────"
|
|
38
|
+
warn ""
|
|
39
|
+
warn " ✖ Missing dependency: #{gem}"
|
|
40
|
+
warn ""
|
|
41
|
+
warn " The repl needs this gem, but it's not installed."
|
|
42
|
+
warn ""
|
|
43
|
+
warn " Fix: gem install #{gem}"
|
|
44
|
+
warn " Or: bundle add #{gem}"
|
|
45
|
+
warn ""
|
|
46
|
+
warn " Tip: If you don't need the repl, you can use the"
|
|
47
|
+
warn " library directly with: require \"llm\""
|
|
48
|
+
warn ""
|
|
49
|
+
warn " ───────────────────────────────────────────────────────"
|
|
50
|
+
warn ""
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# main
|
|
55
|
+
|
|
56
|
+
def main(argv)
|
|
57
|
+
##
|
|
58
|
+
# Make sure the dependencies are satisified first
|
|
59
|
+
begin
|
|
60
|
+
require "llm/tools"
|
|
61
|
+
require "llm/repl"
|
|
62
|
+
rescue LLM::LoadError => ex
|
|
63
|
+
loaderror(ex)
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# C-Style option parser
|
|
69
|
+
# No external dep
|
|
70
|
+
while option = argv.shift
|
|
71
|
+
case option
|
|
72
|
+
when '-h'
|
|
73
|
+
help
|
|
74
|
+
exit 0
|
|
75
|
+
when '-t'
|
|
76
|
+
temp = true
|
|
77
|
+
when '-p'
|
|
78
|
+
provider = argv.shift
|
|
79
|
+
else
|
|
80
|
+
warn "llm.rb: unknown option #{option}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# Setup the home directory
|
|
86
|
+
# But only if the `-t` switch has not been provided
|
|
87
|
+
if temp.nil?
|
|
88
|
+
home = File.join(Dir.home, ".llm.rb")
|
|
89
|
+
FileUtils.mkdir_p(File.join(home, Dir.getwd))
|
|
90
|
+
session = File.join(home, Dir.getwd, "session.json")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# No provider has been given.
|
|
95
|
+
# Try to infer one.
|
|
96
|
+
if provider.nil?
|
|
97
|
+
key = keys.find { ENV[_1] }
|
|
98
|
+
if key.nil?
|
|
99
|
+
warn "llm.rb: provide a provider with the -p switch"
|
|
100
|
+
exit 1
|
|
101
|
+
else
|
|
102
|
+
provider, = key.split("_")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# We're ready to start the REPL
|
|
108
|
+
# This should always succeed unless -p gave garbage
|
|
109
|
+
provider = provider.downcase
|
|
110
|
+
if LLM.respond_to?(provider)
|
|
111
|
+
key ||= "#{provider.upcase}_API_KEY"
|
|
112
|
+
if ENV[key].nil? || ENV[key].to_s.empty?
|
|
113
|
+
warn "llm.rb: set #{key} to use #{provider}"
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
llm = LLM.method(provider).call(key: ENV[key])
|
|
117
|
+
agent = LLM::Agent.new(llm, path: temp ? nil : session, tools: LLM::Tool.subclasses)
|
|
118
|
+
agent.repl
|
|
119
|
+
else
|
|
120
|
+
warn "llm.rb: #{provider} was not recognized"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
main(ARGV)
|
data/data/deepinfra.json
CHANGED
|
@@ -503,6 +503,7 @@
|
|
|
503
503
|
"context": 131072,
|
|
504
504
|
"output": 131072
|
|
505
505
|
},
|
|
506
|
+
"status": "deprecated",
|
|
506
507
|
"cost": {
|
|
507
508
|
"input": 0.4,
|
|
508
509
|
"output": 0.4
|
|
@@ -537,6 +538,7 @@
|
|
|
537
538
|
"context": 262144,
|
|
538
539
|
"output": 65536
|
|
539
540
|
},
|
|
541
|
+
"status": "deprecated",
|
|
540
542
|
"cost": {
|
|
541
543
|
"input": 0.2,
|
|
542
544
|
"output": 0.8
|
|
@@ -832,6 +834,7 @@
|
|
|
832
834
|
"context": 196608,
|
|
833
835
|
"output": 131072
|
|
834
836
|
},
|
|
837
|
+
"status": "deprecated",
|
|
835
838
|
"cost": {
|
|
836
839
|
"input": 0.15,
|
|
837
840
|
"output": 1.15,
|
data/data/xai.json
CHANGED
data/lib/llm/a2a.rb
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# a2a = LLM::A2A.rest(url: "https://agent.example.com")
|
|
28
28
|
# ctx = LLM::Context.new(llm, tools: a2a.skills)
|
|
29
29
|
# ctx.talk("Analyze this data using the remote agent.")
|
|
30
|
-
# ctx.talk(ctx.wait(:
|
|
30
|
+
# ctx.talk(ctx.wait(:sequential)) while ctx.pending_functions?
|
|
31
31
|
class LLM::A2A
|
|
32
32
|
require_relative "a2a/card"
|
|
33
33
|
require_relative "a2a/error"
|
|
@@ -104,17 +104,17 @@ module LLM::ActiveRecord
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
##
|
|
107
|
-
# @see LLM::Context#
|
|
107
|
+
# @see LLM::Context#pending_functions
|
|
108
108
|
# @return [Array<LLM::Function>]
|
|
109
|
-
def
|
|
110
|
-
ctx.
|
|
109
|
+
def pending_functions
|
|
110
|
+
ctx.pending_functions
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
##
|
|
114
|
-
# @see LLM::Context#
|
|
114
|
+
# @see LLM::Context#pending_functions?
|
|
115
115
|
# @return [Boolean]
|
|
116
|
-
def
|
|
117
|
-
ctx.
|
|
116
|
+
def pending_functions?
|
|
117
|
+
ctx.pending_functions?
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
##
|
data/lib/llm/agent.rb
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
|
-
# {LLM::Agent LLM::Agent}
|
|
6
|
-
#
|
|
7
|
-
# tools, schema,
|
|
5
|
+
# {LLM::Agent LLM::Agent} is the recommended entry point for most
|
|
6
|
+
# use-cases. It provides a class-level DSL for defining reusable,
|
|
7
|
+
# preconfigured assistants with defaults for model, tools, schema,
|
|
8
|
+
# and instructions.
|
|
8
9
|
#
|
|
9
10
|
# It wraps the same stateful runtime surface as
|
|
10
11
|
# {LLM::Context LLM::Context}: message history, usage, persistence,
|
|
11
12
|
# streaming parameters, and provider-backed requests still flow through
|
|
12
13
|
# an underlying context. The defining behavior of an agent is that it
|
|
13
|
-
# automatically resolves pending tool calls for you during `talk
|
|
14
|
-
#
|
|
14
|
+
# automatically resolves pending tool calls for you during `talk`,
|
|
15
|
+
# instead of leaving tool loops to the caller.
|
|
15
16
|
#
|
|
16
17
|
# **Notes:**
|
|
17
18
|
# * Instructions are injected once unless a system message is already present.
|
|
@@ -22,10 +23,10 @@ module LLM
|
|
|
22
23
|
# * The default tool attempt budget is `25`. After that, the agent sends
|
|
23
24
|
# advisory tool errors back through the model and keeps the loop in-band.
|
|
24
25
|
# Set `tool_attempts: nil` to disable that advisory behavior.
|
|
25
|
-
# * Tool loop execution can be configured with `concurrency :
|
|
26
|
-
# `:thread`, `:
|
|
26
|
+
# * Tool loop execution can be configured with `concurrency :sequential`,
|
|
27
|
+
# `:thread`, `:async`, `:fiber`, `:fork`, or `:ractor`.
|
|
27
28
|
#
|
|
28
|
-
# @example
|
|
29
|
+
# @example Subclass with defaults
|
|
29
30
|
# class SystemAdmin < LLM::Agent
|
|
30
31
|
# set model: "gpt-4.1-nano",
|
|
31
32
|
# instructions: "You are a Linux system admin",
|
|
@@ -36,7 +37,31 @@ module LLM
|
|
|
36
37
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
37
38
|
# agent = SystemAdmin.new(llm)
|
|
38
39
|
# agent.talk("Run 'date'")
|
|
40
|
+
#
|
|
41
|
+
# @example Direct instance
|
|
42
|
+
# llm = LLM.deepseek(key: ENV["KEY"])
|
|
43
|
+
# agent = LLM::Agent.new(llm, stream: $stdout)
|
|
44
|
+
# agent.talk "Hello world"
|
|
45
|
+
#
|
|
46
|
+
# @see LLM::Context The low-level runtime that Agent wraps
|
|
47
|
+
# @see LLM::Tool Tools that Agent can call on your behalf
|
|
48
|
+
# @see LLM::Stream Stream callbacks for model output
|
|
39
49
|
class Agent
|
|
50
|
+
##
|
|
51
|
+
# @api private
|
|
52
|
+
UNDEFINED = Object.new
|
|
53
|
+
private_constant :UNDEFINED
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @api private
|
|
57
|
+
CASE_PATTERN = /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/
|
|
58
|
+
private_constant :CASE_PATTERN
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# @api private
|
|
62
|
+
File = ::File
|
|
63
|
+
private_constant :File
|
|
64
|
+
|
|
40
65
|
##
|
|
41
66
|
# Returns a provider
|
|
42
67
|
# @return [LLM::Provider]
|
|
@@ -51,7 +76,8 @@ module LLM
|
|
|
51
76
|
#
|
|
52
77
|
# @example
|
|
53
78
|
# class AdminAgent < LLM::Agent
|
|
54
|
-
# set
|
|
79
|
+
# set name: "admin",
|
|
80
|
+
# instructions: "You are a system administrator",
|
|
55
81
|
# model: "gpt-4.1-nano",
|
|
56
82
|
# tools: [Shell, ReadFile]
|
|
57
83
|
# end
|
|
@@ -78,6 +104,46 @@ module LLM
|
|
|
78
104
|
end
|
|
79
105
|
end
|
|
80
106
|
|
|
107
|
+
##
|
|
108
|
+
# Set or get an agent's name
|
|
109
|
+
# @note
|
|
110
|
+
# This method serves as a self-documenting string
|
|
111
|
+
# and it is used by {LLM::Repl LLM::Repl}. It is
|
|
112
|
+
# optional but recommended.
|
|
113
|
+
# @param [String] name
|
|
114
|
+
# The agent name
|
|
115
|
+
# @return [String]
|
|
116
|
+
# Return's the agents name
|
|
117
|
+
def self.name(name = UNDEFINED, &block)
|
|
118
|
+
if name.equal?(UNDEFINED)
|
|
119
|
+
if @name.nil?
|
|
120
|
+
name = to_s.split("::").last
|
|
121
|
+
@name = name.gsub(CASE_PATTERN, "-").downcase
|
|
122
|
+
else
|
|
123
|
+
@name
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
@name = block || name
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
##
|
|
131
|
+
# Set or get an agent's description
|
|
132
|
+
# @note
|
|
133
|
+
# This method serves as a self-documenting string.
|
|
134
|
+
# It is optional but recommended.
|
|
135
|
+
# @param [String] desc
|
|
136
|
+
# The agent's description
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
# Returns the agent's description
|
|
139
|
+
def self.description(desc = UNDEFINED, &block)
|
|
140
|
+
if desc.equal?(UNDEFINED)
|
|
141
|
+
@desc
|
|
142
|
+
else
|
|
143
|
+
@desc = block || desc
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
81
147
|
##
|
|
82
148
|
# Set or get the default model
|
|
83
149
|
# @param [String, nil] model
|
|
@@ -146,9 +212,9 @@ module LLM
|
|
|
146
212
|
#
|
|
147
213
|
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
148
214
|
# Controls how pending tool loops are executed:
|
|
149
|
-
# - `:
|
|
215
|
+
# - `:sequential`: sequential calls
|
|
150
216
|
# - `:thread`: concurrent threads
|
|
151
|
-
# - `:
|
|
217
|
+
# - `:async`: concurrent async tasks
|
|
152
218
|
# - `:fiber`: concurrent scheduler-backed fibers
|
|
153
219
|
# - `:fork`: forked child processes
|
|
154
220
|
# - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
|
|
@@ -231,6 +297,20 @@ module LLM
|
|
|
231
297
|
end
|
|
232
298
|
end
|
|
233
299
|
|
|
300
|
+
##
|
|
301
|
+
# Set the file path where an agent's memory
|
|
302
|
+
# can be restored from, and written to.
|
|
303
|
+
# @param [String] path
|
|
304
|
+
# The path to a file
|
|
305
|
+
# @return [String, nil]
|
|
306
|
+
def self.path(path = UNDEFINED, &block)
|
|
307
|
+
if path.equal?(UNDEFINED)
|
|
308
|
+
@path
|
|
309
|
+
else
|
|
310
|
+
@path = path || block
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
234
314
|
##
|
|
235
315
|
# @param [LLM::Provider] llm
|
|
236
316
|
# A provider
|
|
@@ -247,8 +327,8 @@ module LLM
|
|
|
247
327
|
# @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
|
|
248
328
|
def initialize(llm, params = {})
|
|
249
329
|
@llm = llm
|
|
250
|
-
fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
|
|
251
|
-
fields_ivar = %i[tracer concurrency instructions confirm]
|
|
330
|
+
fields = %i[name description path model skills schema tracer stream tools concurrency instructions confirm]
|
|
331
|
+
fields_ivar = %i[name description path tracer concurrency instructions confirm]
|
|
252
332
|
fields.each do |field|
|
|
253
333
|
resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
|
|
254
334
|
resolve_symbol = !%i[concurrency].include?(field)
|
|
@@ -263,6 +343,29 @@ module LLM
|
|
|
263
343
|
end
|
|
264
344
|
end
|
|
265
345
|
@ctx = LLM::Context.new(llm, {guard: true}.merge(params))
|
|
346
|
+
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
##
|
|
350
|
+
# Returns the agent's name
|
|
351
|
+
# @return [String]
|
|
352
|
+
def name
|
|
353
|
+
@name
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
##
|
|
357
|
+
# Returns a file path where an agent's memory is
|
|
358
|
+
# restored from, and written to after each turn.
|
|
359
|
+
# @return [String, nil]
|
|
360
|
+
def path
|
|
361
|
+
@path
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
##
|
|
365
|
+
# Returns the agent's description
|
|
366
|
+
# @return [String, nil]
|
|
367
|
+
def description
|
|
368
|
+
@description
|
|
266
369
|
end
|
|
267
370
|
|
|
268
371
|
##
|
|
@@ -282,13 +385,17 @@ module LLM
|
|
|
282
385
|
# response = agent.talk("Hello, what is your name?")
|
|
283
386
|
# puts response.choices[0].content
|
|
284
387
|
def talk(prompt, params = {})
|
|
285
|
-
run_loop(prompt, params, :talk)
|
|
388
|
+
res = run_loop(prompt, params, :talk)
|
|
389
|
+
path ? @ctx.save(path:) : nil
|
|
390
|
+
res
|
|
286
391
|
end
|
|
287
392
|
|
|
288
393
|
##
|
|
289
394
|
# @see LLM::Context#ask
|
|
290
395
|
def ask(prompt, params = {})
|
|
291
|
-
run_loop(prompt, params, :ask)
|
|
396
|
+
res = run_loop(prompt, params, :ask)
|
|
397
|
+
path ? @ctx.save(path:) : nil
|
|
398
|
+
res
|
|
292
399
|
end
|
|
293
400
|
|
|
294
401
|
##
|
|
@@ -299,10 +406,9 @@ module LLM
|
|
|
299
406
|
|
|
300
407
|
##
|
|
301
408
|
# @return [Array<LLM::Function>]
|
|
302
|
-
def
|
|
303
|
-
@tracer ? @llm.with_tracer(@tracer) { @ctx.
|
|
409
|
+
def pending_functions
|
|
410
|
+
@tracer ? @llm.with_tracer(@tracer) { @ctx.pending_functions } : @ctx.pending_functions
|
|
304
411
|
end
|
|
305
|
-
alias_method :pending_functions, :functions
|
|
306
412
|
|
|
307
413
|
##
|
|
308
414
|
# @see LLM::Context#returns
|
|
@@ -435,6 +541,9 @@ module LLM
|
|
|
435
541
|
# By default this method disables the tracer for
|
|
436
542
|
# the duration of the repl session, and restores
|
|
437
543
|
# it afterwards.
|
|
544
|
+
# @param [String] name
|
|
545
|
+
# The agent's name.
|
|
546
|
+
# Defaults to {LLM::Agent#name}.
|
|
438
547
|
# @param [String] path
|
|
439
548
|
# The path to a file where runtime state is read
|
|
440
549
|
# from, and written to
|
|
@@ -446,7 +555,7 @@ module LLM
|
|
|
446
555
|
# When true, the tracer is kept alive during the
|
|
447
556
|
# repl session. Default is false.
|
|
448
557
|
# @return [void]
|
|
449
|
-
def repl(path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
558
|
+
def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
|
|
450
559
|
if trace != nil
|
|
451
560
|
warn "llm.rb: trace option is deprecated, use tracer instead"
|
|
452
561
|
tracer = trace
|
|
@@ -456,7 +565,7 @@ module LLM
|
|
|
456
565
|
self.tracer = nil
|
|
457
566
|
end
|
|
458
567
|
require_relative "repl" unless defined?(::LLM::Repl)
|
|
459
|
-
LLM::Repl.new(agent: self, path:, tools:, skills:).start
|
|
568
|
+
LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
|
|
460
569
|
ensure
|
|
461
570
|
if !tracer
|
|
462
571
|
self.tracer = previous
|
|
@@ -516,7 +625,7 @@ module LLM
|
|
|
516
625
|
# @param [Symbol, Array<Symbol>] strategy
|
|
517
626
|
# The execution strategy that would be used for the tool call.
|
|
518
627
|
# @return [LLM::Function::Return]
|
|
519
|
-
# Return either `fn.
|
|
628
|
+
# Return either `fn.task(strategy).wait` to approve execution or
|
|
520
629
|
# `fn.cancel(...)` to cancel the call.
|
|
521
630
|
def on_tool_confirmation(fn, strategy)
|
|
522
631
|
fn.cancel
|
|
@@ -554,12 +663,12 @@ module LLM
|
|
|
554
663
|
##
|
|
555
664
|
# @return [Array<LLM::Function::Return>]
|
|
556
665
|
def call_functions
|
|
557
|
-
strategy = concurrency || :
|
|
666
|
+
strategy = concurrency || :sequential
|
|
558
667
|
return wait(strategy) unless @confirm&.any?
|
|
559
|
-
confirmables = @ctx.
|
|
668
|
+
confirmables = @ctx.pending_functions.select { @confirm.include?(_1.name.to_s) }
|
|
560
669
|
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
561
670
|
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
562
|
-
if (@ctx.
|
|
671
|
+
if (@ctx.pending_functions - confirmables).any?
|
|
563
672
|
[*results, *wait(strategy, except: confirmables)]
|
|
564
673
|
else
|
|
565
674
|
results
|
|
@@ -577,13 +686,13 @@ module LLM
|
|
|
577
686
|
stream = params[:stream] || @ctx.params[:stream]
|
|
578
687
|
params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
|
|
579
688
|
res = talk.call(apply_instructions(prompt), params)
|
|
580
|
-
while @ctx.
|
|
689
|
+
while @ctx.pending_functions?
|
|
581
690
|
if max
|
|
582
691
|
max.times do
|
|
583
|
-
break unless @ctx.
|
|
692
|
+
break unless @ctx.pending_functions?
|
|
584
693
|
res = talk.call(call_functions, params)
|
|
585
694
|
end
|
|
586
|
-
res = talk.call(@ctx.
|
|
695
|
+
res = talk.call(@ctx.pending_functions.map(&:rate_limit), params) if @ctx.pending_functions?
|
|
587
696
|
else
|
|
588
697
|
res = talk.call(call_functions, params)
|
|
589
698
|
end
|
data/lib/llm/buffer.rb
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
5
|
# {LLM::Buffer LLM::Buffer} provides an Enumerable object that
|
|
6
|
-
# tracks messages in a conversation thread.
|
|
6
|
+
# tracks messages in a conversation thread. Access it through
|
|
7
|
+
# {LLM::Context#messages}.
|
|
8
|
+
#
|
|
9
|
+
# @example Working with message history
|
|
10
|
+
# ctx.messages.last # => most recent message
|
|
11
|
+
# ctx.messages.first # => oldest message
|
|
12
|
+
# ctx.messages.select! { |m| m.assistant? }
|
|
13
|
+
# ctx.messages.reverse # => reversed copy
|
|
14
|
+
# ctx.messages.reject! { |m| m.compaction? }
|
|
15
|
+
#
|
|
16
|
+
# @see LLM::Message Individual messages in the buffer
|
|
17
|
+
# @see LLM::Context Where the buffer lives (ctx.messages)
|
|
7
18
|
class Buffer
|
|
8
19
|
include Enumerable
|
|
9
20
|
|
|
21
|
+
UNDEFINED = Object.new
|
|
22
|
+
private_constant :UNDEFINED
|
|
23
|
+
|
|
10
24
|
##
|
|
11
25
|
# @param [LLM::Provider] provider
|
|
12
26
|
# @return [LLM::Buffer]
|
|
@@ -65,8 +79,69 @@ module LLM
|
|
|
65
79
|
# @param [Integer, nil] n
|
|
66
80
|
# The number of messages to return
|
|
67
81
|
# @return [LLM::Message, Array<LLM::Message>, nil]
|
|
68
|
-
def last(n =
|
|
69
|
-
n.
|
|
82
|
+
def last(n = UNDEFINED)
|
|
83
|
+
n.equal?(UNDEFINED) ? @messages.last : @messages.last(n)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Returns the first message(s) in the buffer
|
|
88
|
+
# @param [Integer, nil] n
|
|
89
|
+
# The number of messages to return
|
|
90
|
+
# @return [LLM::Message, Array<LLM::Message>, nil]
|
|
91
|
+
def first(n = UNDEFINED)
|
|
92
|
+
n.equal?(UNDEFINED) ? @messages.first : @messages.first(n)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# Removes messages matching the block in-place.
|
|
97
|
+
# @yield [LLM::Message]
|
|
98
|
+
# @return [LLM::Buffer]
|
|
99
|
+
def reject!(&)
|
|
100
|
+
@messages.reject!(&)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
alias_method :delete_if, :reject!
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Keeps messages matching the block in-place.
|
|
107
|
+
# @yield [LLM::Message]
|
|
108
|
+
# @return [LLM::Buffer]
|
|
109
|
+
def select!(&)
|
|
110
|
+
@messages.select!(&)
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Removes and returns the first message.
|
|
116
|
+
# @return [LLM::Message, nil]
|
|
117
|
+
def shift
|
|
118
|
+
@messages.shift
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Removes all messages.
|
|
123
|
+
# @return [LLM::Buffer]
|
|
124
|
+
def clear
|
|
125
|
+
@messages.clear
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Returns all elements after the first n.
|
|
131
|
+
# @param [Integer] n
|
|
132
|
+
# The number of messages to skip
|
|
133
|
+
# @return [Array<LLM::Message>]
|
|
134
|
+
def drop(n)
|
|
135
|
+
@messages.drop(n)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
##
|
|
139
|
+
# Returns the first n elements without removing them.
|
|
140
|
+
# @param [Integer] n
|
|
141
|
+
# The number of messages to return
|
|
142
|
+
# @return [Array<LLM::Message>]
|
|
143
|
+
def take(n)
|
|
144
|
+
@messages.take(n)
|
|
70
145
|
end
|
|
71
146
|
|
|
72
147
|
##
|
|
@@ -103,6 +178,13 @@ module LLM
|
|
|
103
178
|
@messages[index]
|
|
104
179
|
end
|
|
105
180
|
|
|
181
|
+
##
|
|
182
|
+
# Returns a reversed copy of the internal array.
|
|
183
|
+
# @return [Array]
|
|
184
|
+
def reverse
|
|
185
|
+
@messages.reverse
|
|
186
|
+
end
|
|
187
|
+
|
|
106
188
|
##
|
|
107
189
|
# @return [String]
|
|
108
190
|
def to_json(...)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Compactor
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Compactor::Null LLM::Compactor::Null} is a compactor that
|
|
6
|
+
# does nothing. It is used as the default when no compactor strategy
|
|
7
|
+
# is configured.
|
|
8
|
+
#
|
|
9
|
+
# All methods return nil and produce no side effects.
|
|
10
|
+
class Null < self
|
|
11
|
+
##
|
|
12
|
+
# @param [Hash] opts
|
|
13
|
+
# Ignored
|
|
14
|
+
# @return [nil]
|
|
15
|
+
def call(**opts)
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Compactor
|
|
4
|
+
##
|
|
5
|
+
# An {LLM::Compactor::Truncate LLM::Compactor::Truncate}
|
|
6
|
+
# drops the oldest messages when the conversation grows
|
|
7
|
+
# beyond a configured size, keeping only the N most recent
|
|
8
|
+
# messages.
|
|
9
|
+
#
|
|
10
|
+
# No LLM call is made but this strategy is purely lossy. It
|
|
11
|
+
# also fast - no network required and operates purely on
|
|
12
|
+
# memory.
|
|
13
|
+
class Truncate < self
|
|
14
|
+
##
|
|
15
|
+
# @param [String, Integer] keep
|
|
16
|
+
# The last (approx) n number of messages to keep.
|
|
17
|
+
# This parameter can also be a percentage: eg "80%"
|
|
18
|
+
# to keep 80% of the most recent messages.
|
|
19
|
+
# @return [Array<LLM::Message>, nil]
|
|
20
|
+
def call(keep: 64)
|
|
21
|
+
keep = parse(keep)
|
|
22
|
+
if keep <= 0 || keep > messages.reject(&:system?).size
|
|
23
|
+
nil
|
|
24
|
+
else
|
|
25
|
+
stream.on_compaction(self)
|
|
26
|
+
kept = take(messages, keep)
|
|
27
|
+
messages.replace([messages.select(&:system?).first, *kept].compact)
|
|
28
|
+
ctx.compacted = true
|
|
29
|
+
stream.on_compaction_finish(self)
|
|
30
|
+
kept
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @param [String, Integer] input
|
|
38
|
+
# The given input
|
|
39
|
+
# @return [Integer]
|
|
40
|
+
# Returns the number of messages to keep
|
|
41
|
+
def parse(input)
|
|
42
|
+
if String === input
|
|
43
|
+
if input.end_with?("%")
|
|
44
|
+
count = ctx.messages.reject(&:system?).size
|
|
45
|
+
(count * (Float(input[0..-2]) / 100)).round
|
|
46
|
+
else
|
|
47
|
+
Integer(input)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
Integer(input)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def take(messages, limit)
|
|
55
|
+
subset, in_tool_call = [], false
|
|
56
|
+
messages.reverse_each.with_index(1) do |m, index|
|
|
57
|
+
# We travel backwards - so we see a
|
|
58
|
+
# tool return before we see a tool
|
|
59
|
+
# call.
|
|
60
|
+
#
|
|
61
|
+
# When we see a tool return, our next
|
|
62
|
+
# task is to find where it was called
|
|
63
|
+
# from, and we will even override the
|
|
64
|
+
# limit to do this.
|
|
65
|
+
#
|
|
66
|
+
# Otherwise, the conversation will become
|
|
67
|
+
# corrupted and any attempt to use it will
|
|
68
|
+
# be an API-level error.
|
|
69
|
+
in_tool_call = m.tool_return?
|
|
70
|
+
if index >= limit
|
|
71
|
+
subset.unshift(m)
|
|
72
|
+
in_tool_call ? next : break
|
|
73
|
+
else
|
|
74
|
+
subset.unshift(m)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
subset
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|