llm.rb 11.3.1 → 12.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 +354 -2
- data/LICENSE +93 -17
- data/README.md +226 -616
- data/data/anthropic.json +322 -426
- data/data/bedrock.json +2634 -1144
- data/data/deepinfra.json +1513 -0
- data/data/deepseek.json +57 -28
- data/data/google.json +411 -771
- data/data/openai.json +1104 -771
- data/data/xai.json +141 -292
- data/data/zai.json +263 -141
- data/lib/llm/active_record/acts_as_agent.rb +3 -41
- data/lib/llm/active_record/acts_as_llm.rb +18 -0
- data/lib/llm/active_record.rb +3 -3
- data/lib/llm/agent.rb +25 -1
- data/lib/llm/context.rb +17 -5
- data/lib/llm/contract/completion.rb +2 -2
- data/lib/llm/json_adapter.rb +29 -3
- data/lib/llm/provider.rb +3 -3
- data/lib/llm/providers/deepinfra/audio.rb +66 -0
- data/lib/llm/providers/deepinfra/images.rb +90 -0
- data/lib/llm/providers/deepinfra/response_adapter.rb +36 -0
- data/lib/llm/providers/deepinfra.rb +100 -0
- data/lib/llm/providers/deepseek/images.rb +109 -0
- data/lib/llm/providers/deepseek/request_adapter.rb +32 -0
- data/lib/llm/providers/deepseek/response_adapter/image.rb +9 -0
- data/lib/llm/providers/deepseek/response_adapter.rb +29 -0
- data/lib/llm/providers/deepseek.rb +4 -2
- data/lib/llm/providers/google/request_adapter.rb +22 -5
- data/lib/llm/providers/google.rb +4 -4
- data/lib/llm/providers/llamacpp.rb +5 -5
- data/lib/llm/providers/openai/audio.rb +6 -2
- data/lib/llm/providers/openai/images.rb +9 -50
- data/lib/llm/providers/openai/request_adapter/respond.rb +38 -4
- data/lib/llm/providers/openai/response_adapter/audio.rb +5 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/image.rb +0 -4
- data/lib/llm/providers/openai/responses.rb +1 -0
- data/lib/llm/providers/openai/stream_parser.rb +5 -6
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/xai/images.rb +49 -26
- data/lib/llm/providers/xai.rb +2 -2
- data/lib/llm/repl/input.rb +64 -0
- data/lib/llm/repl/status.rb +30 -0
- data/lib/llm/repl/stream.rb +46 -0
- data/lib/llm/repl/transcript.rb +61 -0
- data/lib/llm/repl/window.rb +107 -0
- data/lib/llm/repl.rb +78 -0
- data/lib/llm/response.rb +10 -0
- data/lib/llm/schema/leaf.rb +7 -1
- data/lib/llm/schema/renderer.rb +121 -0
- data/lib/llm/schema.rb +30 -0
- data/lib/llm/sequel/agent.rb +2 -43
- data/lib/llm/sequel/plugin.rb +25 -7
- data/lib/llm/tools/chdir.rb +23 -0
- data/lib/llm/tools/git.rb +41 -0
- data/lib/llm/tools/mkdir.rb +32 -0
- data/lib/llm/tools/pwd.rb +20 -0
- data/lib/llm/tools/read_file.rb +40 -0
- data/lib/llm/tools/rg.rb +46 -0
- data/lib/llm/tools/shell.rb +48 -0
- data/lib/llm/tools/swap_text.rb +25 -0
- data/lib/llm/tools/write_file.rb +24 -0
- data/lib/llm/tools.rb +5 -0
- data/lib/llm/tracer/telemetry.rb +4 -6
- data/lib/llm/tracer.rb +9 -21
- data/lib/llm/transport/execution.rb +16 -1
- data/lib/llm/transport/net_http_adapter.rb +1 -1
- data/lib/llm/uridata.rb +16 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +9 -0
- data/llm.gemspec +5 -18
- data/resources/deepdive.md +829 -263
- metadata +31 -18
- data/lib/llm/tracer/langsmith.rb +0 -144
data/lib/llm/schema.rb
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
class LLM::Schema
|
|
35
35
|
require_relative "schema/version"
|
|
36
36
|
require_relative "schema/parser"
|
|
37
|
+
require_relative "schema/renderer"
|
|
37
38
|
require_relative "schema/leaf"
|
|
38
39
|
require_relative "schema/object"
|
|
39
40
|
require_relative "schema/array"
|
|
@@ -121,6 +122,19 @@ class LLM::Schema
|
|
|
121
122
|
end
|
|
122
123
|
end
|
|
123
124
|
|
|
125
|
+
##
|
|
126
|
+
# @param [Hash] defaults
|
|
127
|
+
# @return [LLM::Schema::Object]
|
|
128
|
+
def self.defaults(defaults)
|
|
129
|
+
lock do
|
|
130
|
+
object.tap do |schema|
|
|
131
|
+
defaults.each do |name, val|
|
|
132
|
+
Utils.fetch(schema.properties, name).default(val)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
124
138
|
##
|
|
125
139
|
# @api private
|
|
126
140
|
# @return [LLM::Schema]
|
|
@@ -139,6 +153,14 @@ class LLM::Schema
|
|
|
139
153
|
end
|
|
140
154
|
end
|
|
141
155
|
|
|
156
|
+
##
|
|
157
|
+
# Render the schema as a prompt-friendly string.
|
|
158
|
+
# @return [String]
|
|
159
|
+
def self.to_s
|
|
160
|
+
Renderer.render(object, root: true)
|
|
161
|
+
end
|
|
162
|
+
(class << self; self; end).alias_method(:inspect, :to_s)
|
|
163
|
+
|
|
142
164
|
##
|
|
143
165
|
# @api private
|
|
144
166
|
def self.lock(&)
|
|
@@ -220,4 +242,12 @@ class LLM::Schema
|
|
|
220
242
|
def null
|
|
221
243
|
Null.new
|
|
222
244
|
end
|
|
245
|
+
|
|
246
|
+
##
|
|
247
|
+
# Render a schema leaf as a prompt-friendly string.
|
|
248
|
+
# @return [String]
|
|
249
|
+
def to_s
|
|
250
|
+
self.class.to_s
|
|
251
|
+
end
|
|
252
|
+
alias_method :inspect, :to_s
|
|
223
253
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -7,8 +7,7 @@ module LLM::Sequel
|
|
|
7
7
|
# This wrapper reuses the same record-backed runtime surface as
|
|
8
8
|
# {LLM::Sequel::Plugin}, but builds an {LLM::Agent LLM::Agent} instead of an
|
|
9
9
|
# {LLM::Context LLM::Context}. Agent defaults such as model, tools, schema,
|
|
10
|
-
# instructions, and concurrency are configured on
|
|
11
|
-
# forwarded to an internal agent subclass.
|
|
10
|
+
# instructions, and concurrency are configured on an internal agent subclass.
|
|
12
11
|
module Agent
|
|
13
12
|
require_relative "plugin"
|
|
14
13
|
EMPTY_HASH = LLM::Sequel::Plugin::EMPTY_HASH
|
|
@@ -25,7 +24,7 @@ module LLM::Sequel
|
|
|
25
24
|
options = DEFAULTS.merge(options)
|
|
26
25
|
model.db.extension :pg_json if %i[json jsonb].include?(options[:format])
|
|
27
26
|
model.instance_variable_set(:@llm_agent_options, options.freeze)
|
|
28
|
-
model.instance_exec(&block)
|
|
27
|
+
block_given? ? model.instance_exec(model.agent, &block) : nil
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
module ClassMethods
|
|
@@ -33,46 +32,6 @@ module LLM::Sequel
|
|
|
33
32
|
@llm_agent_options || Agent::DEFAULTS
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
def model(model = nil, &block)
|
|
37
|
-
return agent.model if model.nil? && !block
|
|
38
|
-
agent.model(model, &block)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def tools(*tools, &block)
|
|
42
|
-
return agent.tools if tools.empty? && !block
|
|
43
|
-
agent.tools(*tools, &block)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def skills(*skills, &block)
|
|
47
|
-
return agent.skills if skills.empty? && !block
|
|
48
|
-
agent.skills(*skills, &block)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def schema(schema = nil, &block)
|
|
52
|
-
return agent.schema if schema.nil? && !block
|
|
53
|
-
agent.schema(schema, &block)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def instructions(instructions = nil)
|
|
57
|
-
return agent.instructions if instructions.nil?
|
|
58
|
-
agent.instructions(instructions)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def concurrency(concurrency = nil)
|
|
62
|
-
return agent.concurrency if concurrency.nil?
|
|
63
|
-
agent.concurrency(concurrency)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def confirm(*tool_names, &block)
|
|
67
|
-
return agent.confirm if tool_names.empty? && !block
|
|
68
|
-
agent.confirm(*tool_names, &block)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def tracer(tracer = nil, &block)
|
|
72
|
-
return agent.tracer if tracer.nil? && !block
|
|
73
|
-
agent.tracer(tracer, &block)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
35
|
def agent
|
|
77
36
|
@agent ||= Class.new(LLM::Agent)
|
|
78
37
|
end
|
data/lib/llm/sequel/plugin.rb
CHANGED
|
@@ -16,6 +16,13 @@ module LLM::Sequel
|
|
|
16
16
|
# JSON typecasting for the model. `provider:`, `context:`, and `tracer:`
|
|
17
17
|
# can also be configured as symbols that are called on the model.
|
|
18
18
|
module Plugin
|
|
19
|
+
DEFAULTS = {
|
|
20
|
+
data_column: :data,
|
|
21
|
+
format: :string,
|
|
22
|
+
provider: :set_provider,
|
|
23
|
+
context: :set_context,
|
|
24
|
+
tracer: :set_tracer
|
|
25
|
+
}.freeze
|
|
19
26
|
EMPTY_HASH = {}.freeze
|
|
20
27
|
|
|
21
28
|
##
|
|
@@ -94,13 +101,6 @@ module LLM::Sequel
|
|
|
94
101
|
end
|
|
95
102
|
end
|
|
96
103
|
end
|
|
97
|
-
DEFAULTS = {
|
|
98
|
-
data_column: :data,
|
|
99
|
-
format: :string,
|
|
100
|
-
tracer: nil,
|
|
101
|
-
provider: nil,
|
|
102
|
-
context: EMPTY_HASH
|
|
103
|
-
}.freeze
|
|
104
104
|
|
|
105
105
|
##
|
|
106
106
|
# Called by Sequel when the plugin is first applied to a model class.
|
|
@@ -304,6 +304,24 @@ module LLM::Sequel
|
|
|
304
304
|
|
|
305
305
|
private
|
|
306
306
|
|
|
307
|
+
##
|
|
308
|
+
# @return [LLM::Provider]
|
|
309
|
+
def set_provider
|
|
310
|
+
raise NotImplementedError, "implement the set_provider callback"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
# @return [Hash]
|
|
315
|
+
def set_context
|
|
316
|
+
Plugin::EMPTY_HASH.dup
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
##
|
|
320
|
+
# @return [LLM::Tracer]
|
|
321
|
+
def set_tracer
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
307
325
|
##
|
|
308
326
|
# @return [LLM::Context]
|
|
309
327
|
def ctx
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::Chdir LLM::Tool::Chdir} class implements
|
|
8
|
+
# a tool that can change the current working directory.
|
|
9
|
+
class Chdir < self
|
|
10
|
+
name "chdir"
|
|
11
|
+
description "change the current working directory"
|
|
12
|
+
parameter :path, String, "the new working directory"
|
|
13
|
+
required %i[path]
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @param [String] path
|
|
17
|
+
# @return [Hash]
|
|
18
|
+
def call(path:)
|
|
19
|
+
Dir.chdir(path)
|
|
20
|
+
{ok: true, cwd: path}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::Git LLM::Tool::Git} class implements
|
|
8
|
+
# a tool that can perform a select number of git actions.
|
|
9
|
+
# The actions it can perform are read-only - at least for
|
|
10
|
+
# the time being.
|
|
11
|
+
class Git < self
|
|
12
|
+
name "git"
|
|
13
|
+
description "perform an action with git"
|
|
14
|
+
parameter :action, Enum["log", "diff", "commit", "checkout", "branch", "show"], "the git operation to perform"
|
|
15
|
+
parameter :arguments, Array[String], "one or more arguments for the git action"
|
|
16
|
+
required %i[action]
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @param [String] path
|
|
20
|
+
# @param [Integer] start
|
|
21
|
+
# @param [Integer] stop
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
def call(action:, arguments: nil)
|
|
24
|
+
command = spawn(action:, arguments:)
|
|
25
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def spawn(action:, arguments:)
|
|
31
|
+
Command
|
|
32
|
+
.new("git")
|
|
33
|
+
.argv(action)
|
|
34
|
+
.argv(*[*arguments])
|
|
35
|
+
.spawn
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
LLM.require "test-cmd.rb"
|
|
39
|
+
Command = Test::Cmd
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::Mkdir LLM::Tool::Mkdir} class implements
|
|
6
|
+
# a tool that can create a tree of new directories.
|
|
7
|
+
class Mkdir < self
|
|
8
|
+
name "mkdir"
|
|
9
|
+
description "create a new directory"
|
|
10
|
+
parameter :path, String, "the path to the directory"
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param [String] path
|
|
14
|
+
# @return [Hash]
|
|
15
|
+
def call(path:)
|
|
16
|
+
command = spawn(path:)
|
|
17
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def spawn(path:)
|
|
23
|
+
Command
|
|
24
|
+
.new("mkdir")
|
|
25
|
+
.argv("-p", path)
|
|
26
|
+
.spawn
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
LLM.require "test-cmd.rb"
|
|
30
|
+
Command = Test::Cmd
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::Pwd LLM::Tool::Pwd} class implements
|
|
8
|
+
# a tool that can reveal the current working directory.
|
|
9
|
+
class Pwd < self
|
|
10
|
+
name "pwd"
|
|
11
|
+
description "returns the current working directory"
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @param [String] path
|
|
15
|
+
# @return [Hash]
|
|
16
|
+
def call
|
|
17
|
+
{ok: true, cwd: Dir.getwd}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::ReadFile LLM::Tool::ReadFile} class implements
|
|
8
|
+
# a tool that can read the contents of a file. The tool accepts
|
|
9
|
+
# two optional offsets: a start line, and a stop line. Without
|
|
10
|
+
# either the entire file contents are read into memory.
|
|
11
|
+
class ReadFile < self
|
|
12
|
+
name "read-file"
|
|
13
|
+
description "read the contents of a file"
|
|
14
|
+
parameter :path, String, "the path to the file"
|
|
15
|
+
parameter :start, Integer, "start line number"
|
|
16
|
+
parameter :stop, Integer, "stop line number"
|
|
17
|
+
required %i[path]
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @param [String] path
|
|
21
|
+
# @param [Integer] start
|
|
22
|
+
# @param [Integer] stop
|
|
23
|
+
# @return [Hash]
|
|
24
|
+
def call(path:, start: 1, stop: -1)
|
|
25
|
+
content, cursor = nil, 1
|
|
26
|
+
File.open(path, "r") do |f|
|
|
27
|
+
while cursor < start
|
|
28
|
+
f.gets
|
|
29
|
+
cursor += 1
|
|
30
|
+
end
|
|
31
|
+
if stop == -1
|
|
32
|
+
content = f.read
|
|
33
|
+
else
|
|
34
|
+
content = start.upto(stop).map { f.gets }.join
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
{ok: true, content:}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/llm/tools/rg.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::Rg LLM::Tool::Rg} class implements
|
|
8
|
+
# a frontend to the popular 'rg' tool. The tool can
|
|
9
|
+
# recursively search the current working directory
|
|
10
|
+
# for one or more patterns.
|
|
11
|
+
class Rg < self
|
|
12
|
+
name "rg"
|
|
13
|
+
description "recursively search the current directory for lines matching a pattern"
|
|
14
|
+
parameter :patterns, Array[String], "one or more search patterns"
|
|
15
|
+
parameter :path, String, "the path where the search is performed (default is cwd)"
|
|
16
|
+
required %i[patterns]
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @param [String] pattern
|
|
20
|
+
# @return [Hash]
|
|
21
|
+
def call(patterns:, path: Dir.getwd)
|
|
22
|
+
validate!(patterns:, path:)
|
|
23
|
+
command = spawn(patterns:, path:)
|
|
24
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate!(patterns:, path:)
|
|
30
|
+
if path == "/"
|
|
31
|
+
raise RuntimeError, "you can't search from the root of the filesystem"
|
|
32
|
+
elsif patterns == ["."]
|
|
33
|
+
raise RuntimeError, "narrow your search"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def spawn(patterns:, path:)
|
|
38
|
+
Command.new("rg")
|
|
39
|
+
.argv(*[*patterns].flat_map { ["-e", _1] }, path)
|
|
40
|
+
.spawn
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
LLM.require "test-cmd.rb"
|
|
44
|
+
Command = Test::Cmd
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# frozen_string_literal
|
|
4
|
+
|
|
5
|
+
class LLM::Tool
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Tool::Shell} class implements a tool that can
|
|
8
|
+
# spawn a command. That can be dangerous given a low-quality
|
|
9
|
+
# model, or a high-quality model that simply makes a bad
|
|
10
|
+
# decision. The risk can be reduced through a confirmation
|
|
11
|
+
# step such as {LLM::Agent.confirm LLM::Agent.confirm}, or
|
|
12
|
+
# by managing the tool loop manually through
|
|
13
|
+
# {LLM::Context LLM::Context}.
|
|
14
|
+
class Shell < self
|
|
15
|
+
name "shell"
|
|
16
|
+
description "run a shell command"
|
|
17
|
+
parameter :name, String, "the command name"
|
|
18
|
+
parameter :arguments, Array[String], "one or more command arguments"
|
|
19
|
+
required %i[name]
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @param [String] name
|
|
23
|
+
# The name of a command
|
|
24
|
+
# @param [Array<String>] arguments
|
|
25
|
+
# One or more command-line arguments
|
|
26
|
+
# @return [Hash]
|
|
27
|
+
def call(name:, arguments: nil)
|
|
28
|
+
command = spawn(name:, arguments:)
|
|
29
|
+
{ok: command.success?, stdout: command.stdout, stderr: command.stderr}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# @param [String] name
|
|
36
|
+
# @param [Array<String>] arguments
|
|
37
|
+
# @return [Command]
|
|
38
|
+
def spawn(name:, arguments:)
|
|
39
|
+
Command
|
|
40
|
+
.new(name)
|
|
41
|
+
.argv(*[*arguments])
|
|
42
|
+
.spawn
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
LLM.require "test-cmd.rb"
|
|
46
|
+
Command = Test::Cmd
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::SwapText LLM::Tool::SwapText} class
|
|
6
|
+
# implements a tool that can substitute one piece of
|
|
7
|
+
# text for another piece of text in a given file.
|
|
8
|
+
class SwapText < self
|
|
9
|
+
name "swap-text"
|
|
10
|
+
description "Replace an exact snippet in a file"
|
|
11
|
+
parameter :path, String, "Path to file"
|
|
12
|
+
parameter :before, String, "Exact text to replace"
|
|
13
|
+
parameter :after, String, "Replacement text"
|
|
14
|
+
parameter :expected_count, Integer, "How many matches should be replaced"
|
|
15
|
+
required %i[path before after]
|
|
16
|
+
|
|
17
|
+
def call(path:, before:, after:, expected_count: 1)
|
|
18
|
+
content = File.read(path)
|
|
19
|
+
count = content.scan(before).length
|
|
20
|
+
raise "expected #{expected_count} match(es), found #{count}" unless count == expected_count.to_i
|
|
21
|
+
File.write(path, content.sub(before, after))
|
|
22
|
+
{ok: true, replaced: count}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Tool
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Tool::WriteFile LLM::Tool::WriteFile} class
|
|
6
|
+
# implements a tool that can write a given string to a
|
|
7
|
+
# given file path.
|
|
8
|
+
class WriteFile < self
|
|
9
|
+
name "write-file"
|
|
10
|
+
description "write to a file"
|
|
11
|
+
parameter :path, String, "The file path"
|
|
12
|
+
parameter :content, String, "The file content"
|
|
13
|
+
required %i[path content]
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @param [String] path
|
|
17
|
+
# @param [String] content
|
|
18
|
+
# @return [Hash]
|
|
19
|
+
def call(path:, content:)
|
|
20
|
+
File.open(path, "w") { _1.write(content) }
|
|
21
|
+
{ok: true}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/llm/tools.rb
ADDED
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -30,8 +30,7 @@ module LLM
|
|
|
30
30
|
# require "llm"
|
|
31
31
|
# require "opentelemetry-exporter-otlp"
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
# exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint:)
|
|
33
|
+
# exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
|
|
35
34
|
#
|
|
36
35
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
37
36
|
# llm.tracer = LLM::Tracer::Telemetry.new(llm, exporter:)
|
|
@@ -59,7 +58,6 @@ module LLM
|
|
|
59
58
|
# @return [self]
|
|
60
59
|
def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
|
|
61
60
|
return self if trace_group_id.to_s.empty?
|
|
62
|
-
|
|
63
61
|
span_context = span_context_from_trace_group_id(trace_group_id.to_s)
|
|
64
62
|
parent_ctx = ::OpenTelemetry::Trace.context_with_span(
|
|
65
63
|
::OpenTelemetry::Trace.non_recording_span(span_context)
|
|
@@ -316,7 +314,7 @@ module LLM
|
|
|
316
314
|
set_span_attributes(span, consume_extra_outputs.merge(outputs || {}))
|
|
317
315
|
finish_metadata = consume_finish_metadata_proc(res)
|
|
318
316
|
metadata = (metadata || {}).merge(finish_metadata || {})
|
|
319
|
-
set_span_attributes(span, metadata.transform_keys { "
|
|
317
|
+
set_span_attributes(span, metadata.transform_keys { "llm.metadata.#{_1}" })
|
|
320
318
|
span.add_event("gen_ai.request.finish")
|
|
321
319
|
span.tap(&:finish)
|
|
322
320
|
end
|
|
@@ -326,7 +324,7 @@ module LLM
|
|
|
326
324
|
"gen_ai.operation.name" => operation
|
|
327
325
|
}.merge!(finish_attributes(operation, res)).compact
|
|
328
326
|
chunks_json = retrieval_chunks_json(res)
|
|
329
|
-
attributes["
|
|
327
|
+
attributes["llm.metadata.chunks"] = chunks_json if chunks_json
|
|
330
328
|
attributes.each { span.set_attribute(_1, _2) }
|
|
331
329
|
span.add_event("gen_ai.request.finish")
|
|
332
330
|
span.tap(&:finish)
|
|
@@ -334,7 +332,7 @@ module LLM
|
|
|
334
332
|
|
|
335
333
|
##
|
|
336
334
|
# @api private
|
|
337
|
-
# Serialize retrieval response chunks for span attributes
|
|
335
|
+
# Serialize retrieval response chunks for span attributes.
|
|
338
336
|
# Returns a JSON string or nil when res has no data.
|
|
339
337
|
def consume_finish_metadata_proc(res)
|
|
340
338
|
key = LLM::Tracer::FINISH_METADATA_PROC_KEY
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -11,7 +11,6 @@ module LLM
|
|
|
11
11
|
class Tracer
|
|
12
12
|
require_relative "tracer/logger"
|
|
13
13
|
require_relative "tracer/telemetry"
|
|
14
|
-
require_relative "tracer/langsmith"
|
|
15
14
|
require_relative "tracer/null"
|
|
16
15
|
|
|
17
16
|
##
|
|
@@ -45,7 +44,7 @@ module LLM
|
|
|
45
44
|
# @param [Object, nil] span
|
|
46
45
|
# @param [String] model
|
|
47
46
|
# @param [Hash, nil] outputs Optional span attributes (e.g. gen_ai.output.messages) from llm.rb or caller.
|
|
48
|
-
# @param [Hash, nil] metadata Optional metadata
|
|
47
|
+
# @param [Hash, nil] metadata Optional metadata from llm.rb or caller.
|
|
49
48
|
# @return [void]
|
|
50
49
|
def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
|
|
51
50
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
@@ -110,8 +109,7 @@ module LLM
|
|
|
110
109
|
# @param [Hash] attributes
|
|
111
110
|
# OpenTelemetry attributes to set on the root span.
|
|
112
111
|
# @param [Hash, nil] metadata
|
|
113
|
-
# Optional. Trace-level metadata merged into the trace
|
|
114
|
-
# Only used by tracers that support it (e.g. {LLM::Tracer::Langsmith}).
|
|
112
|
+
# Optional. Trace-level metadata merged into the trace by tracers that support it.
|
|
115
113
|
# @return [self]
|
|
116
114
|
def start_trace(trace_group_id: nil, name: "llm", attributes: {}, metadata: nil)
|
|
117
115
|
self
|
|
@@ -150,11 +148,10 @@ module LLM
|
|
|
150
148
|
##
|
|
151
149
|
# Merges extra attributes for the current trace/span. Used by applications
|
|
152
150
|
# (e.g. chatbot) to add metadata, span inputs, or span outputs to the next
|
|
153
|
-
# span or to the trace. No-op by default
|
|
154
|
-
# into fiber-local storage and emits them as langsmith/GenAI attributes.
|
|
151
|
+
# span or to the trace. No-op by default.
|
|
155
152
|
#
|
|
156
153
|
# @param [Hash, nil] metadata
|
|
157
|
-
# Key-value pairs merged into trace/span metadata
|
|
154
|
+
# Key-value pairs merged into trace/span metadata.
|
|
158
155
|
# @param [Hash, nil] inputs
|
|
159
156
|
# Key-value pairs set on the next span at start (e.g. gen_ai.input.messages).
|
|
160
157
|
# Consumed when the span is created.
|
|
@@ -169,9 +166,9 @@ module LLM
|
|
|
169
166
|
##
|
|
170
167
|
# Optional: set a proc to supply metadata when the next chat span finishes.
|
|
171
168
|
# The proc is called with the response (res) and should return a Hash of
|
|
172
|
-
# metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span
|
|
173
|
-
#
|
|
174
|
-
#
|
|
169
|
+
# metadata (e.g. { intent: "...", confidence: 1.0 }) to merge onto the span.
|
|
170
|
+
# Cleared after use. Used by apps to attach routing/intent that is only
|
|
171
|
+
# known after the response.
|
|
175
172
|
#
|
|
176
173
|
# @param [Proc, nil] proc (res) -> Hash or nil
|
|
177
174
|
# @return [self]
|
|
@@ -182,19 +179,10 @@ module LLM
|
|
|
182
179
|
|
|
183
180
|
FINISH_METADATA_PROC_KEY = :"llm.tracer.finish_metadata_proc"
|
|
184
181
|
|
|
185
|
-
##
|
|
186
|
-
# Returns the current extra bag (metadata, inputs, outputs) for the current
|
|
187
|
-
# thread/trace. Used by subclasses; default returns empty hashes.
|
|
188
|
-
#
|
|
189
|
-
# @return [Hash] { metadata: {}, inputs: {}, outputs: {} }
|
|
190
|
-
def current_extra
|
|
191
|
-
{}
|
|
192
|
-
end
|
|
193
|
-
|
|
194
182
|
##
|
|
195
183
|
# Returns and clears extra inputs for the next span. Called by the telemetry
|
|
196
|
-
# tracer when starting a span. Subclasses
|
|
197
|
-
#
|
|
184
|
+
# tracer when starting a span. Subclasses can override to return stored
|
|
185
|
+
# inputs; default returns {}.
|
|
198
186
|
#
|
|
199
187
|
# @return [Hash] Attribute key => value to set on the span at start
|
|
200
188
|
def consume_extra_inputs
|
|
@@ -59,9 +59,24 @@ class LLM::Transport
|
|
|
59
59
|
# @return [LLM::Object, String]
|
|
60
60
|
def parse_response(res)
|
|
61
61
|
case res["content-type"]
|
|
62
|
-
when %r{\Aapplication/json\s*}
|
|
62
|
+
when %r{\Aapplication/json\s*}
|
|
63
|
+
body = read_body(res.body)
|
|
64
|
+
LLM::Object.from(LLM.json.load(body))
|
|
63
65
|
else res.body
|
|
64
66
|
end
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @param [#class] body
|
|
71
|
+
# @return [String]
|
|
72
|
+
def read_body(body)
|
|
73
|
+
case body.class.to_s
|
|
74
|
+
when "Net::ReadAdapter"
|
|
75
|
+
str = +""
|
|
76
|
+
body.read_body { str << _1 }
|
|
77
|
+
str
|
|
78
|
+
else body
|
|
79
|
+
end
|
|
80
|
+
end
|
|
66
81
|
end
|
|
67
82
|
end
|
|
@@ -21,7 +21,7 @@ class LLM::Transport
|
|
|
21
21
|
when :put then ::Net::HTTP::Put.new(path, headers)
|
|
22
22
|
when :patch then ::Net::HTTP::Patch.new(path, headers)
|
|
23
23
|
when :delete then ::Net::HTTP::Delete.new(path, headers)
|
|
24
|
-
else ::Net::
|
|
24
|
+
else ::Net::HTTPGenericRequest.new(method, path, nil, headers)
|
|
25
25
|
end
|
|
26
26
|
if req.body
|
|
27
27
|
http_req.body = req.body
|
data/lib/llm/uridata.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
class URIData < Struct.new(:content_type, :encoding_type, :encoded, :decoded)
|
|
5
|
+
##
|
|
6
|
+
# @param [String] str
|
|
7
|
+
# A string
|
|
8
|
+
# @return [URIData]
|
|
9
|
+
def self.parse(str)
|
|
10
|
+
_, data = str.split(":")
|
|
11
|
+
content_type, data = data.split(";")
|
|
12
|
+
encoding_type, data = data.split(",")
|
|
13
|
+
URIData.new(content_type, encoding_type, data, StringIO.new(data.unpack1("m0")))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/llm/version.rb
CHANGED