llm.rb 15.0.3 → 15.2.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 +433 -3
- data/README.md +188 -71
- data/bin/llm.rb +50 -7
- data/data/alibaba.json +912 -823
- data/data/anthropic.json +234 -187
- data/data/bedrock.json +3702 -2058
- data/data/deepinfra.json +1288 -951
- data/data/deepseek.json +87 -53
- data/data/google.json +670 -670
- data/data/mistral.json +501 -460
- data/data/moonshot.json +43 -248
- data/data/openai.json +1008 -914
- data/data/openrouter.json +14417 -0
- data/data/xai.json +213 -201
- data/data/zai.json +242 -149
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +8 -6
- data/docs/deepdive/advanced/guard.md +2 -2
- data/docs/deepdive/features/builtin_tools.md +93 -22
- data/docs/deepdive/features/{repl.md → console.md} +28 -28
- data/docs/deepdive/features/database.md +3 -3
- data/docs/deepdive/fundamentals/agents.md +13 -12
- data/docs/deepdive/fundamentals/providers.md +91 -6
- data/docs/deepdive/fundamentals/skills.md +14 -6
- data/docs/deepdive/fundamentals/stream.md +4 -4
- data/docs/deepdive/fundamentals/tools.md +63 -31
- data/docs/deepdive/reference/cost.md +2 -2
- data/docs/deepdive/reference/model_registry.md +2 -2
- data/docs/deepdive/reference/tracer.md +15 -13
- data/docs/deepdive.md +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +9 -5
- data/lib/llm/agent.rb +40 -15
- data/lib/llm/{repl → console}/bar.rb +3 -3
- data/lib/llm/{repl → console}/buffer.rb +24 -9
- data/lib/llm/{repl → console}/color.rb +2 -2
- data/lib/llm/{repl → console}/command.rb +12 -12
- data/lib/llm/{repl → console}/commands/exit.rb +4 -4
- data/lib/llm/{repl → console}/commands/help.rb +1 -1
- data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
- data/lib/llm/{repl → console}/commands/model.rb +2 -2
- data/lib/llm/{repl → console}/input/cache.rb +2 -2
- data/lib/llm/{repl → console}/input/char.rb +2 -2
- data/lib/llm/{repl → console}/input/row.rb +1 -1
- data/lib/llm/{repl → console}/input.rb +18 -10
- data/lib/llm/console/markdown/parser.rb +78 -0
- data/lib/llm/{repl → console}/markdown/table.rb +8 -5
- data/lib/llm/{repl → console}/markdown.rb +13 -30
- data/lib/llm/console/node.rb +69 -0
- data/lib/llm/{repl → console}/status.rb +11 -11
- data/lib/llm/{repl → console}/stream.rb +36 -9
- data/lib/llm/{repl → console}/walker.rb +1 -1
- data/lib/llm/{repl → console}/window.rb +17 -17
- data/lib/llm/{repl.rb → console.rb} +39 -19
- data/lib/llm/context/deserializer.rb +2 -1
- data/lib/llm/context.rb +29 -13
- data/lib/llm/cost.rb +13 -0
- data/lib/llm/function/async/reactor.rb +20 -1
- data/lib/llm/function/fork/task.rb +14 -10
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +40 -28
- data/lib/llm/message.rb +7 -0
- data/lib/llm/provider.rb +31 -10
- data/lib/llm/providers/alibaba.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +2 -2
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/deepseek.rb +1 -1
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +2 -1
- data/lib/llm/providers/openai.rb +2 -1
- data/lib/llm/providers/openrouter.rb +87 -0
- data/lib/llm/schema/leaf.rb +34 -2
- data/lib/llm/schema.rb +4 -2
- data/lib/llm/sequel/agent.rb +9 -5
- data/lib/llm/skill.rb +7 -1
- data/lib/llm/stream.rb +8 -3
- data/lib/llm/tool/param.rb +5 -1
- data/lib/llm/tool.rb +5 -0
- data/lib/llm/tools/bundle.rb +53 -0
- data/lib/llm/tools/edit-file.rb +7 -2
- data/lib/llm/tools/exec.rb +78 -0
- data/lib/llm/tools/git.rb +27 -26
- data/lib/llm/tools/mkdir.rb +12 -19
- data/lib/llm/tools/read_file.rb +69 -9
- data/lib/llm/tools/rg.rb +20 -24
- data/lib/llm/tools/ruby.rb +17 -25
- data/lib/llm/tools/utils.rb +75 -2
- data/lib/llm/tools/write_file.rb +4 -1
- data/lib/llm/tracer/logger.rb +2 -2
- data/lib/llm/tracer/pretty_logger.rb +4 -4
- data/lib/llm/tracer/telemetry.rb +2 -2
- data/lib/llm/tracer.rb +33 -0
- data/lib/llm/transport/curb.rb +5 -3
- data/lib/llm/transport/http.rb +5 -2
- data/lib/llm/transport/persistent_http.rb +6 -4
- data/lib/llm/transport/utils.rb +8 -6
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +18 -12
- data/llm.gemspec +8 -8
- metadata +80 -37
- data/lib/llm/repl/node.rb +0 -44
- data/lib/llm/tools/shell.rb +0 -55
|
@@ -210,7 +210,7 @@ module LLM
|
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
def normalize_complete_params(params)
|
|
213
|
-
params = {role: :user, model: default_model, max_tokens: 2048}.merge!(params)
|
|
213
|
+
params = {role: :user, model: params.delete(:model) || default_model, max_tokens: 2048}.merge!(params)
|
|
214
214
|
tools = resolve_tools(params.delete(:tools))
|
|
215
215
|
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
216
216
|
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -202,7 +202,7 @@ module LLM
|
|
|
202
202
|
|
|
203
203
|
def normalize_complete_params(params)
|
|
204
204
|
except = %i[role model messages stream]
|
|
205
|
-
params = {role: :user, model: default_model}.merge!(params)
|
|
205
|
+
params = {role: :user, model: params.delete(:model) || default_model}.merge!(params)
|
|
206
206
|
tools = resolve_tools(params.delete(:tools))
|
|
207
207
|
config = adapt_generation_config(params.except(*except))
|
|
208
208
|
params = [params.except(:schema), config, adapt_tools(tools)].inject({}, &:merge!).compact
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -129,7 +129,7 @@ module LLM
|
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def normalize_complete_params(params)
|
|
132
|
-
params = {role: :user, model: default_model, stream: true}.merge!(params)
|
|
132
|
+
params = {role: :user, model: params.delete(:model) || default_model, stream: true}.merge!(params)
|
|
133
133
|
tools = resolve_tools(params.delete(:tools))
|
|
134
134
|
params = [params, {format: params[:schema]}, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
135
135
|
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
@@ -35,7 +35,8 @@ class LLM::OpenAI
|
|
|
35
35
|
# When given an object a provider does not understand
|
|
36
36
|
# @return [LLM::Response]
|
|
37
37
|
def create(prompt, params = {})
|
|
38
|
-
params = {
|
|
38
|
+
params = {}.merge!(params)
|
|
39
|
+
params = {role: :user, model: params.delete(:model) || @provider.default_model}.merge!(params)
|
|
39
40
|
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
40
41
|
tools = resolve_tools(params.delete(:tools))
|
|
41
42
|
params = [
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -219,7 +219,7 @@ module LLM
|
|
|
219
219
|
end
|
|
220
220
|
|
|
221
221
|
def normalize_complete_params(params)
|
|
222
|
-
params = {role: :user, model: default_model}.merge!(params)
|
|
222
|
+
params = {role: :user, model: params.delete(:model) || default_model}.merge!(params)
|
|
223
223
|
tools = resolve_tools(params.delete(:tools))
|
|
224
224
|
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
225
225
|
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
@@ -231,6 +231,7 @@ module LLM
|
|
|
231
231
|
|
|
232
232
|
def build_complete_request(prompt, params, role)
|
|
233
233
|
messages = build_messages(prompt, params, role)
|
|
234
|
+
messages = [*params.delete(:messages), *messages] if params[:messages]
|
|
234
235
|
body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
|
|
235
236
|
req = LLM::Transport::Request.post(completions_path, headers)
|
|
236
237
|
transport.set_body_stream(req, StringIO.new(body))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai" unless defined?(LLM::OpenAI)
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
##
|
|
7
|
+
# The OpenRouter class implements a provider for
|
|
8
|
+
# [OpenRouter](https://openrouter.ai) through its OpenAI-compatible API.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# #!/usr/bin/env ruby
|
|
12
|
+
# require "llm"
|
|
13
|
+
#
|
|
14
|
+
# llm = LLM.openrouter(key: ENV["KEY"])
|
|
15
|
+
# ctx = LLM::Context.new(llm)
|
|
16
|
+
# ctx.talk "Hello"
|
|
17
|
+
class OpenRouter < OpenAI
|
|
18
|
+
HOST = "openrouter.ai"
|
|
19
|
+
BASE_PATH = "/api/v1"
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @param key (see LLM::Provider#initialize)
|
|
23
|
+
# @param host (see LLM::Provider#initialize)
|
|
24
|
+
# @param base_path (see LLM::Provider#initialize)
|
|
25
|
+
# @return [LLM::OpenRouter]
|
|
26
|
+
def initialize(host: HOST, base_path: BASE_PATH, **)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @return [Symbol]
|
|
32
|
+
# Returns the provider's name
|
|
33
|
+
def name
|
|
34
|
+
:openrouter
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Provides an embedding.
|
|
39
|
+
# @see https://openrouter.ai/docs/api/api-reference/embeddings/create-embeddings OpenRouter docs
|
|
40
|
+
# @param input (see LLM::Provider#embed)
|
|
41
|
+
# @param model (see LLM::Provider#embed)
|
|
42
|
+
# @param params (see LLM::Provider#embed)
|
|
43
|
+
# @raise (see LLM::Provider#request)
|
|
44
|
+
# @return (see LLM::Provider#embed)
|
|
45
|
+
def embed(input, model: "openai/text-embedding-3-small", **params)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# @raise [NotImplementedError]
|
|
51
|
+
def files
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @raise [NotImplementedError]
|
|
57
|
+
def images
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# @raise [NotImplementedError]
|
|
63
|
+
def audio
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# @raise [NotImplementedError]
|
|
69
|
+
def moderations
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# @raise [NotImplementedError]
|
|
75
|
+
def vector_stores
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Returns the default model for chat completions
|
|
81
|
+
# @see https://openrouter.ai/docs/guides/routing/routers/auto-router OpenRouter Auto Router
|
|
82
|
+
# @return [String]
|
|
83
|
+
def default_model
|
|
84
|
+
"openrouter/auto"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -19,6 +19,26 @@ class LLM::Schema
|
|
|
19
19
|
@required = nil
|
|
20
20
|
@const = nil
|
|
21
21
|
@index = nil
|
|
22
|
+
@owner = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @param [Object] owner
|
|
27
|
+
# The owner of a schema, typically an LLM::Tool or LLM::Schema class
|
|
28
|
+
# @return [void]
|
|
29
|
+
def owner=(owner)
|
|
30
|
+
@owner = owner
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Resolves a configured value against the owner,
|
|
35
|
+
# following {LLM::Utils#resolve_option}. A Proc runs
|
|
36
|
+
# against the owner, a Symbol is sent to it, and
|
|
37
|
+
# everything else passes through unchanged.
|
|
38
|
+
# @param [Object] value
|
|
39
|
+
# @return [Object]
|
|
40
|
+
def resolve(value)
|
|
41
|
+
LLM::Utils.resolve_option(@owner, value, resolve_symbol: true)
|
|
22
42
|
end
|
|
23
43
|
|
|
24
44
|
##
|
|
@@ -39,7 +59,7 @@ class LLM::Schema
|
|
|
39
59
|
# @return [LLM::Schema::Leaf]
|
|
40
60
|
def default(value = nil)
|
|
41
61
|
if value.nil?
|
|
42
|
-
@default
|
|
62
|
+
resolve(@default)
|
|
43
63
|
else
|
|
44
64
|
tap { @default = value }
|
|
45
65
|
end
|
|
@@ -100,7 +120,7 @@ class LLM::Schema
|
|
|
100
120
|
##
|
|
101
121
|
# @return [Hash]
|
|
102
122
|
def to_h
|
|
103
|
-
{description: @description, default
|
|
123
|
+
{description: @description, default:, enum: @enum, const: @const}.compact
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
##
|
|
@@ -124,5 +144,17 @@ class LLM::Schema
|
|
|
124
144
|
to_h == other.to_h
|
|
125
145
|
end
|
|
126
146
|
alias_method :eql?, :==
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# @return [Integer]
|
|
150
|
+
def hash
|
|
151
|
+
to_h.hash
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def owner
|
|
157
|
+
@owner
|
|
158
|
+
end
|
|
127
159
|
end
|
|
128
160
|
end
|
data/lib/llm/schema.rb
CHANGED
|
@@ -128,8 +128,10 @@ class LLM::Schema
|
|
|
128
128
|
def self.defaults(defaults)
|
|
129
129
|
lock do
|
|
130
130
|
object.tap do |schema|
|
|
131
|
-
defaults.each do |name,
|
|
132
|
-
Utils.fetch(schema.properties, name)
|
|
131
|
+
defaults.each do |name, value|
|
|
132
|
+
leaf = Utils.fetch(schema.properties, name)
|
|
133
|
+
leaf.owner = self
|
|
134
|
+
leaf.default(value)
|
|
133
135
|
end
|
|
134
136
|
end
|
|
135
137
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -35,7 +35,10 @@ module LLM::Sequel
|
|
|
35
35
|
##
|
|
36
36
|
# @return [Class<LLM::Agent>]
|
|
37
37
|
def agent
|
|
38
|
-
@agent
|
|
38
|
+
return @agent if defined?(@agent)
|
|
39
|
+
@agent = Class.new(LLM::Agent)
|
|
40
|
+
@agent.name(self)
|
|
41
|
+
@agent
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
##
|
|
@@ -75,11 +78,12 @@ module LLM::Sequel
|
|
|
75
78
|
# This method does not persist to the database,
|
|
76
79
|
# but it can inspect and alter runtime state in
|
|
77
80
|
# a way that is temporary.
|
|
78
|
-
# @param (see LLM::Agent#
|
|
79
|
-
# @return (see LLM::Agent#
|
|
80
|
-
def
|
|
81
|
-
ctx.
|
|
81
|
+
# @param (see LLM::Agent#console)
|
|
82
|
+
# @return (see LLM::Agent#console)
|
|
83
|
+
def console(**params)
|
|
84
|
+
ctx.console(**params)
|
|
82
85
|
end
|
|
86
|
+
alias_method :repl, :console
|
|
83
87
|
|
|
84
88
|
private
|
|
85
89
|
|
data/lib/llm/skill.rb
CHANGED
|
@@ -35,6 +35,11 @@ module LLM
|
|
|
35
35
|
# @return [String]
|
|
36
36
|
attr_reader :description
|
|
37
37
|
|
|
38
|
+
##
|
|
39
|
+
# Returns the model the skill will use.
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
attr_reader :model
|
|
42
|
+
|
|
38
43
|
##
|
|
39
44
|
# Returns the skill instructions.
|
|
40
45
|
# @return [String]
|
|
@@ -90,7 +95,7 @@ module LLM
|
|
|
90
95
|
stream = ctx.stream
|
|
91
96
|
stream.on_skill_call(self)
|
|
92
97
|
agent = self.agent(ctx)
|
|
93
|
-
res = agent.talk("Solve the user's query.")
|
|
98
|
+
res = agent.talk("Solve the user's query.", model: model || ctx.model)
|
|
94
99
|
stream.on_skill_return(agent, self, res)
|
|
95
100
|
{content: res.content}
|
|
96
101
|
end
|
|
@@ -156,6 +161,7 @@ module LLM
|
|
|
156
161
|
@frontmatter = LLM::Object.from(YAML.safe_load(match[1]) || {})
|
|
157
162
|
@name = @frontmatter.name || @name
|
|
158
163
|
@description = @frontmatter.description || @description
|
|
164
|
+
@model = @frontmatter.model || nil
|
|
159
165
|
@instructions = match[2]
|
|
160
166
|
@inherit_tools, @tools = parse_tools(@frontmatter.tools)
|
|
161
167
|
end
|
data/lib/llm/stream.rb
CHANGED
|
@@ -185,12 +185,17 @@ module LLM
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
##
|
|
188
|
-
# Called when a request is
|
|
189
|
-
#
|
|
188
|
+
# Called when a request is retried because it was rate limited or
|
|
189
|
+
# timed out.
|
|
190
|
+
# @param [Exception] error
|
|
191
|
+
# The error that caused the retry
|
|
192
|
+
# @param [Integer] attempt
|
|
193
|
+
# The one-based retry attempt number
|
|
190
194
|
# @return [nil]
|
|
191
|
-
def
|
|
195
|
+
def on_retry(error, attempt)
|
|
192
196
|
nil
|
|
193
197
|
end
|
|
198
|
+
alias_method :on_rate_limit, :on_retry
|
|
194
199
|
|
|
195
200
|
##
|
|
196
201
|
# Called before a skill's subagent runs.
|
data/lib/llm/tool/param.rb
CHANGED
|
@@ -63,7 +63,11 @@ class LLM::Tool
|
|
|
63
63
|
def defaults(defaults)
|
|
64
64
|
lock do
|
|
65
65
|
function.params.tap do |schema|
|
|
66
|
-
defaults.each
|
|
66
|
+
defaults.each do |name, value|
|
|
67
|
+
leaf = Utils.fetch(schema.properties, name)
|
|
68
|
+
leaf.owner = self
|
|
69
|
+
leaf.default(value)
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
end
|
|
69
73
|
end
|
data/lib/llm/tool.rb
CHANGED
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
# @see LLM::Agent Tools are attached to agents
|
|
35
35
|
# @see LLM::Function The function object that Tool wraps
|
|
36
36
|
class LLM::Tool
|
|
37
|
+
##
|
|
38
|
+
# @api private
|
|
39
|
+
UNDEFINED = Object.new
|
|
40
|
+
private_constant :UNDEFINED
|
|
41
|
+
|
|
37
42
|
require_relative "tool/param"
|
|
38
43
|
extend LLM::Tool::Param
|
|
39
44
|
extend LLM::Function::Registry
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::Bundle} class implements
|
|
6
|
+
# a tool that runs a command through `bundle`.
|
|
7
|
+
# It inherits the `BUNDLE_GEMFILE` environment
|
|
8
|
+
# variable when set, or defaults to a `Gemfile`
|
|
9
|
+
# in the current working directory.
|
|
10
|
+
class Bundle < self
|
|
11
|
+
require_relative "exec"
|
|
12
|
+
|
|
13
|
+
name "bundle"
|
|
14
|
+
description "Run the 'bundle' command\n" \
|
|
15
|
+
"This command (bundle) is spawned without a shell"
|
|
16
|
+
parameter :arguments, Array[String], "one or more command arguments"
|
|
17
|
+
parameter :timeout, Integer, "the maximum allowed time for the command to run (in seconds)"
|
|
18
|
+
parameter :max_bytes, Integer, "max number of bytes to emit"
|
|
19
|
+
defaults arguments: [], timeout: 60, max_bytes: -> { Exec.max_bytes }
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @return [LLM::Tool::BundleExec]
|
|
23
|
+
def initialize
|
|
24
|
+
@env = {"BUNDLE_GEMFILE" => ENV["BUNDLE_GEMFILE"] || File.join(Dir.getwd, "Gemfile")}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# @param [Array<String>] arguments
|
|
29
|
+
# One or more command-line arguments
|
|
30
|
+
# @param [Integer] timeout
|
|
31
|
+
# The maximum allowed time for the command to run (in seconds)
|
|
32
|
+
# @param [Integer] max_bytes
|
|
33
|
+
# Max number of bytes to emit
|
|
34
|
+
# @return [Hash]
|
|
35
|
+
def call(arguments: [], timeout: 60, max_bytes: Exec.max_bytes)
|
|
36
|
+
Exec.new(env:).call(
|
|
37
|
+
arguments: ["bundle", *arguments],
|
|
38
|
+
timeout:,
|
|
39
|
+
max_bytes:
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Returns the `Gemfile` used by this tool:
|
|
47
|
+
# the `BUNDLE_GEMFILE` environment variable
|
|
48
|
+
# when set, otherwise a `Gemfile` in the
|
|
49
|
+
# current working directory.
|
|
50
|
+
# @return [Hash{String => String}]
|
|
51
|
+
attr_reader :env
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/llm/tools/edit-file.rb
CHANGED
|
@@ -22,9 +22,14 @@ class LLM::Tool
|
|
|
22
22
|
# @return [Hash]
|
|
23
23
|
def call(path:, before:, after:, expected_count: 1)
|
|
24
24
|
content = File.read(path)
|
|
25
|
-
|
|
25
|
+
re = Regexp.new(Regexp.escape(before))
|
|
26
|
+
count = content.scan(re).length
|
|
26
27
|
raise "expected #{expected_count} match(es), found #{count}" unless count == expected_count.to_i
|
|
27
|
-
|
|
28
|
+
##
|
|
29
|
+
# Block form keeps the replacement literal:
|
|
30
|
+
# `sub(re, str)` would expand backreferences
|
|
31
|
+
# like \1 and \& in the replacement text.
|
|
32
|
+
File.write(path, content.sub(re) { after })
|
|
28
33
|
{ok: true, replaced: count}
|
|
29
34
|
end
|
|
30
35
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::Exec} class implements a tool that can
|
|
6
|
+
# spawn a command. That can be dangerous given a low-quality
|
|
7
|
+
# model, or a high-quality model that simply makes a bad
|
|
8
|
+
# decision. The risk can be reduced through a confirmation
|
|
9
|
+
# step such as {LLM::Agent.confirm LLM::Agent.confirm}, or
|
|
10
|
+
# by managing the tool loop manually through
|
|
11
|
+
# {LLM::Context LLM::Context}.
|
|
12
|
+
class Exec < self
|
|
13
|
+
require_relative "utils"
|
|
14
|
+
include Utils
|
|
15
|
+
|
|
16
|
+
name "exec"
|
|
17
|
+
description "Run a command without a shell"
|
|
18
|
+
parameter :arguments, Array[String], "a command and its argument(s)"
|
|
19
|
+
parameter :timeout, Integer, "the maximum allowed time for the command to run (in seconds)"
|
|
20
|
+
parameter :max_bytes, Integer, "max number of bytes to emit"
|
|
21
|
+
required %i[arguments]
|
|
22
|
+
defaults timeout: 60, max_bytes: :max_bytes
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Returns (or sets) the advisory maximum number of bytes
|
|
26
|
+
# this tool returns to the model.
|
|
27
|
+
# @param [Integer, nil] bytes
|
|
28
|
+
# When given, sets the maximum
|
|
29
|
+
# @return [Integer]
|
|
30
|
+
def self.max_bytes(bytes = UNDEFINED)
|
|
31
|
+
if bytes.equal?(UNDEFINED)
|
|
32
|
+
@max_bytes || 75_000
|
|
33
|
+
else
|
|
34
|
+
@max_bytes = bytes
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @param [Hash] env
|
|
40
|
+
# Extra environment variables to set for the command.
|
|
41
|
+
# This is configuration for the tool instance, not a
|
|
42
|
+
# model-provided parameter.
|
|
43
|
+
# @return [LLM::Tool::Exec]
|
|
44
|
+
def initialize(env: {})
|
|
45
|
+
@env = env
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @param [String] name
|
|
50
|
+
# The name of a command
|
|
51
|
+
# @param [Array<String>] arguments
|
|
52
|
+
# One or more command-line arguments
|
|
53
|
+
# @param [Integer] timeout
|
|
54
|
+
# The maximum allowed time for the command to run (in seconds)
|
|
55
|
+
# @param [Integer] max_bytes
|
|
56
|
+
# the max number of bytes to emit
|
|
57
|
+
# @return [Hash]
|
|
58
|
+
def call(arguments: [], timeout: 60, max_bytes: self.class.max_bytes)
|
|
59
|
+
name = arguments[0]
|
|
60
|
+
command = spawn(name:, arguments: arguments[1..], env:, max_bytes:)
|
|
61
|
+
wait(command:, timeout:)
|
|
62
|
+
if command.not_found?
|
|
63
|
+
{ok: false, error: "command '#{name}' was not found on this system"}
|
|
64
|
+
else
|
|
65
|
+
{ok: command.success?,
|
|
66
|
+
stdout: truncate(command.stdout, max_bytes:),
|
|
67
|
+
stderr: truncate(command.stderr, max_bytes:)}
|
|
68
|
+
end
|
|
69
|
+
rescue LLM::Interrupt
|
|
70
|
+
command.kill! if command&.running?
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
attr_reader :env
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/llm/tools/git.rb
CHANGED
|
@@ -3,45 +3,46 @@
|
|
|
3
3
|
class LLM::Tool
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::Tool::Git LLM::Tool::Git} class implements
|
|
6
|
-
# a tool that can perform a select number of git
|
|
7
|
-
# The actions it can perform are read-only - at least for
|
|
8
|
-
# the time being.
|
|
6
|
+
# a tool that can perform a select number of git subcommands.
|
|
9
7
|
class Git < self
|
|
10
|
-
require_relative "
|
|
11
|
-
include Utils
|
|
8
|
+
require_relative "exec"
|
|
12
9
|
|
|
13
10
|
name "git"
|
|
14
|
-
description "
|
|
15
|
-
|
|
16
|
-
parameter :arguments, Array[String], "one or more arguments
|
|
17
|
-
parameter :timeout, Integer, "the maximum time to allow the command to run"
|
|
18
|
-
required %i[
|
|
11
|
+
description "Perform a git subcommand\n" \
|
|
12
|
+
"This command (git) is spawned without a shell"
|
|
13
|
+
parameter :arguments, Array[String], "one or more arguments forwarded to git"
|
|
14
|
+
parameter :timeout, Integer, "the maximum time to allow the command to run (in seconds)"
|
|
15
|
+
required %i[arguments]
|
|
19
16
|
defaults arguments: [], timeout: 5
|
|
20
17
|
|
|
21
18
|
##
|
|
22
|
-
# @param [String]
|
|
19
|
+
# @param [String] subcommand
|
|
23
20
|
# @param [Array<String>, nil] arguments
|
|
24
21
|
# @return [Hash]
|
|
25
|
-
def call(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
def call(arguments: [], timeout: 5)
|
|
23
|
+
subcommand = arguments[0]
|
|
24
|
+
validate!(subcommand:, arguments:)
|
|
25
|
+
Exec.new.call(
|
|
26
|
+
arguments: ["git", subcommand, *arguments[1..]],
|
|
27
|
+
timeout:
|
|
28
|
+
)
|
|
32
29
|
end
|
|
33
30
|
|
|
34
31
|
private
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
|
|
33
|
+
##
|
|
34
|
+
# @return [void]
|
|
35
|
+
def validate!(subcommand:, arguments:)
|
|
36
|
+
unless subcommands.include?(subcommand.to_s)
|
|
37
|
+
raise RuntimeError, "git subcommand must be one of: #{subcommands.join(",")}"
|
|
38
|
+
end
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
##
|
|
42
|
+
# @return [Array<String>]
|
|
43
|
+
# The git subcommands that can be performed by this tool.
|
|
44
|
+
def subcommands
|
|
45
|
+
["log", "diff", "commit", "checkout", "branch", "show"]
|
|
46
|
+
end
|
|
46
47
|
end
|
|
47
48
|
end
|
data/lib/llm/tools/mkdir.rb
CHANGED
|
@@ -5,31 +5,24 @@ class LLM::Tool
|
|
|
5
5
|
# The {LLM::Tool::Mkdir LLM::Tool::Mkdir} class implements
|
|
6
6
|
# a tool that can create a tree of new directories.
|
|
7
7
|
class Mkdir < self
|
|
8
|
+
require_relative "exec"
|
|
9
|
+
|
|
8
10
|
name "mkdir"
|
|
9
|
-
description "
|
|
11
|
+
description "Create a new directory\n" \
|
|
12
|
+
"This command (mkdir) is spawned without a shell"
|
|
10
13
|
parameter :path, String, "the path to the directory"
|
|
14
|
+
parameter :max_bytes, Integer, "max number of bytes to emit"
|
|
15
|
+
required %i[path]
|
|
16
|
+
defaults max_bytes: -> { Exec.max_bytes }
|
|
11
17
|
|
|
12
18
|
##
|
|
13
19
|
# @param [String] path
|
|
14
20
|
# @return [Hash]
|
|
15
|
-
def call(path:)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
raise
|
|
21
|
+
def call(path:, max_bytes: Exec.max_bytes)
|
|
22
|
+
Exec.new.call(
|
|
23
|
+
arguments: ["mkdir", "-p", path],
|
|
24
|
+
max_bytes:
|
|
25
|
+
)
|
|
21
26
|
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def spawn(path:)
|
|
26
|
-
Command
|
|
27
|
-
.new("mkdir")
|
|
28
|
-
.argv("-p", path)
|
|
29
|
-
.spawn
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
LLM.require "test-cmd.rb", "~> 1.1"
|
|
33
|
-
Command = Test::Cmd
|
|
34
27
|
end
|
|
35
28
|
end
|