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
|
@@ -4,30 +4,21 @@ class LLM::XAI
|
|
|
4
4
|
##
|
|
5
5
|
# The {LLM::XAI::Images LLM::XAI::Images} class provides an interface
|
|
6
6
|
# for [xAI's images API](https://docs.x.ai/docs/guides/image-generations).
|
|
7
|
-
# xAI
|
|
8
|
-
# encoded in base64. The default is to return base64-encoded image data.
|
|
7
|
+
# xAI returns base64-encoded image data.
|
|
9
8
|
#
|
|
10
|
-
# @example
|
|
9
|
+
# @example
|
|
11
10
|
# #!/usr/bin/env ruby
|
|
12
11
|
# require "llm"
|
|
13
|
-
# require "open-uri"
|
|
14
|
-
# require "fileutils"
|
|
15
12
|
#
|
|
16
13
|
# llm = LLM.xai(key: ENV["KEY"])
|
|
17
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
18
|
-
# response_format: "url"
|
|
19
|
-
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
|
20
|
-
# "rocket.png"
|
|
21
|
-
#
|
|
22
|
-
# @example Binary strings
|
|
23
|
-
# #!/usr/bin/env ruby
|
|
24
|
-
# require "llm"
|
|
25
|
-
#
|
|
26
|
-
# llm = LLM.xai(key: ENV["KEY"])
|
|
27
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
|
28
|
-
# response_format: "b64_json"
|
|
14
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
29
15
|
# IO.copy_stream res.images[0], "rocket.png"
|
|
30
16
|
class Images < LLM::OpenAI::Images
|
|
17
|
+
##
|
|
18
|
+
# @api private
|
|
19
|
+
PATTERN = %r{\A(?:https?://|data:)}
|
|
20
|
+
private_constant :PATTERN
|
|
21
|
+
|
|
31
22
|
##
|
|
32
23
|
# Create an image
|
|
33
24
|
# @example
|
|
@@ -40,20 +31,52 @@ class LLM::XAI
|
|
|
40
31
|
# @param [Hash] params Other parameters (see xAI docs)
|
|
41
32
|
# @raise (see LLM::Provider#request)
|
|
42
33
|
# @return [LLM::Response]
|
|
43
|
-
def create(prompt:, model: "grok-imagine-image", **params)
|
|
44
|
-
|
|
34
|
+
def create(prompt:, model: "grok-imagine-image-quality", **params)
|
|
35
|
+
req = LLM::Transport::Request.post(path("/images/generations"), headers)
|
|
36
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:, response_format: "b64_json"}.merge!(params))
|
|
37
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
38
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
39
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
40
|
+
res
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
##
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
# Edit an image
|
|
45
|
+
# @example
|
|
46
|
+
# llm = LLM.xai(key: ENV["KEY"])
|
|
47
|
+
# res = llm.images.edit(image: "/images/book.png", prompt: "The book is floating in the clouds")
|
|
48
|
+
# IO.copy_stream res.images[0], "floating-book.png"
|
|
49
|
+
# @see https://docs.x.ai/docs/guides/image-generations xAI docs
|
|
50
|
+
# @param [String, LLM::File, File] image The image to edit
|
|
51
|
+
# @param [String] prompt The prompt
|
|
52
|
+
# @param [String] model The model to use
|
|
53
|
+
# @param [Hash] params Other parameters (see xAI docs)
|
|
54
|
+
# @raise (see LLM::Provider#request)
|
|
55
|
+
# @return [LLM::Response]
|
|
56
|
+
def edit(image:, prompt:, model: "grok-imagine-image-quality", **params)
|
|
57
|
+
req = LLM::Transport::Request.post(path("/images/edits"), headers)
|
|
58
|
+
req.body = LLM.json.dump({
|
|
59
|
+
prompt:,
|
|
60
|
+
model:,
|
|
61
|
+
image: image_url(image),
|
|
62
|
+
response_format: "b64_json"
|
|
63
|
+
}.merge!(params))
|
|
64
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
65
|
+
res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
|
|
66
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
67
|
+
res
|
|
51
68
|
end
|
|
52
69
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def
|
|
56
|
-
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def image_url(image)
|
|
73
|
+
case image
|
|
74
|
+
when String
|
|
75
|
+
url = image.match?(PATTERN) ? image : LLM.File(image).to_data_uri
|
|
76
|
+
else
|
|
77
|
+
url = LLM.File(image).to_data_uri
|
|
78
|
+
end
|
|
79
|
+
{url:, type: "image_url"}
|
|
57
80
|
end
|
|
58
81
|
end
|
|
59
82
|
end
|
data/lib/llm/providers/xai.rb
CHANGED
|
@@ -70,10 +70,10 @@ module LLM
|
|
|
70
70
|
|
|
71
71
|
##
|
|
72
72
|
# Returns the default model for chat completions
|
|
73
|
-
# #see https://docs.x.ai/docs/models grok-4
|
|
73
|
+
# #see https://docs.x.ai/docs/models grok-4.3
|
|
74
74
|
# @return [String]
|
|
75
75
|
def default_model
|
|
76
|
-
"grok-4
|
|
76
|
+
"grok-4.3"
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Input LLM::Repl::Input} class manages
|
|
6
|
+
# the editable input line shown at the bottom of the REPL.
|
|
7
|
+
# @api private
|
|
8
|
+
class Input
|
|
9
|
+
UP = Curses::Key::UP
|
|
10
|
+
DOWN = Curses::Key::DOWN
|
|
11
|
+
ENTER = [Curses::Key::ENTER, 10, 13]
|
|
12
|
+
BACKSPACE = [Curses::Key::BACKSPACE, 127]
|
|
13
|
+
EOF = [nil, 4]
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @param [String, Symbol] provider
|
|
17
|
+
# @return [LLM::Repl::Input]
|
|
18
|
+
def initialize(provider)
|
|
19
|
+
@provider = provider
|
|
20
|
+
@buffer = +""
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [LLM::Repl::Window] window
|
|
25
|
+
# @return [String, nil]
|
|
26
|
+
def readline(window)
|
|
27
|
+
catch(:done) do
|
|
28
|
+
@buffer.clear
|
|
29
|
+
loop do
|
|
30
|
+
on_char(window, window.getch)
|
|
31
|
+
window.redraw
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @return [String]
|
|
38
|
+
def to_s
|
|
39
|
+
"> #{@buffer}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def on_char(window, char)
|
|
45
|
+
if EOF.include?(char)
|
|
46
|
+
throw(:done, nil)
|
|
47
|
+
elsif BACKSPACE.include?(char)
|
|
48
|
+
@buffer.chop!
|
|
49
|
+
elsif ENTER.include?(char)
|
|
50
|
+
buf = @buffer.dup
|
|
51
|
+
@buffer.clear
|
|
52
|
+
throw(:done, buf)
|
|
53
|
+
elsif char == UP
|
|
54
|
+
window.scroll_up
|
|
55
|
+
elsif char == DOWN
|
|
56
|
+
window.scroll_down
|
|
57
|
+
elsif String === char
|
|
58
|
+
@buffer << char
|
|
59
|
+
else
|
|
60
|
+
# ???
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Status LLM::Repl::Status} class stores
|
|
6
|
+
# the small status line shown at the top of the REPL.
|
|
7
|
+
# @api private
|
|
8
|
+
class Status
|
|
9
|
+
##
|
|
10
|
+
# @param [String, Symbol] provider
|
|
11
|
+
# @return [LLM::Repl::Status]
|
|
12
|
+
def initialize(provider)
|
|
13
|
+
@provider = provider
|
|
14
|
+
@text = "idle"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @return [String]
|
|
19
|
+
attr_reader :provider
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @param [String] value
|
|
23
|
+
# @return [void]
|
|
24
|
+
attr_accessor :text
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# @return [String]
|
|
28
|
+
alias_method :to_s, :text
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Stream LLM::Repl::Stream} class manages
|
|
6
|
+
# the stream for the {LLM::Repl LLM::Repl} class. This class
|
|
7
|
+
# has defined hooks that receive text, tool calls, and
|
|
8
|
+
# tool returns.
|
|
9
|
+
# @api private
|
|
10
|
+
class Stream < LLM::Stream
|
|
11
|
+
##
|
|
12
|
+
# @param [LLM::Repl] repl
|
|
13
|
+
# @return [LLM::Repl::Stream]
|
|
14
|
+
def initialize(repl)
|
|
15
|
+
@repl = repl
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @param [String] chars
|
|
20
|
+
# One or more chars
|
|
21
|
+
# @return [void]
|
|
22
|
+
def on_content(chars)
|
|
23
|
+
@repl.write(chars)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# @param [LLM::Function] tool
|
|
28
|
+
# @param [LLM::Function::Return, nil] error
|
|
29
|
+
# @return [void]
|
|
30
|
+
def on_tool_call(tool, error)
|
|
31
|
+
if error
|
|
32
|
+
@repl.status = "tool error: #{tool.name}"
|
|
33
|
+
else
|
|
34
|
+
@repl.status = "tool: #{tool.name}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @param [LLM::Function] _tool
|
|
40
|
+
# @param [LLM::Function::Return] result
|
|
41
|
+
# @return [void]
|
|
42
|
+
def on_tool_return(_tool, result)
|
|
43
|
+
@repl.status = "tool done: #{result.name}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Transcript LLM::Repl::Transcript} class
|
|
6
|
+
# stores streamed output for the REPL.
|
|
7
|
+
# @api private
|
|
8
|
+
class Transcript
|
|
9
|
+
WIDTH = 80
|
|
10
|
+
|
|
11
|
+
##
|
|
12
|
+
# @return [LLM::Repl::Transcript]
|
|
13
|
+
def initialize
|
|
14
|
+
@lines = [+""]
|
|
15
|
+
@offset = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# @param [String] chars
|
|
20
|
+
# @return [void]
|
|
21
|
+
def write(chars)
|
|
22
|
+
chars.each_char { write_char(_1) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [void]
|
|
27
|
+
def scroll_up(height)
|
|
28
|
+
max = [@lines.size - height, 0].max
|
|
29
|
+
@offset = [@offset + 1, max].min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @return [void]
|
|
34
|
+
def scroll_down
|
|
35
|
+
@offset = [@offset - 1, 0].max
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @param [Integer] height
|
|
40
|
+
# @return [Array<String>]
|
|
41
|
+
def visible(height)
|
|
42
|
+
last = @lines.size - 1 - @offset
|
|
43
|
+
first = [last - height + 1, 0].max
|
|
44
|
+
@lines[first..last] || []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def write_char(char)
|
|
50
|
+
if char == "\n"
|
|
51
|
+
@offset += 1 if @offset > 0
|
|
52
|
+
@lines << +""
|
|
53
|
+
elsif char == " " and @lines.last.length >= WIDTH
|
|
54
|
+
@offset += 1 if @offset > 0
|
|
55
|
+
@lines << +""
|
|
56
|
+
else
|
|
57
|
+
@lines.last << char
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Repl
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Repl::Window LLM::Repl::Window} class draws the
|
|
6
|
+
# curses screen for the REPL.
|
|
7
|
+
# @api private
|
|
8
|
+
class Window
|
|
9
|
+
##
|
|
10
|
+
# @return [LLM::Repl::Status]
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
##
|
|
14
|
+
# @return [LLM::Repl::Transcript]
|
|
15
|
+
attr_reader :transcript
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# @return [LLM::Repl::Input]
|
|
19
|
+
attr_reader :input
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @param [LLM::Repl::Status] status
|
|
23
|
+
# @param [LLM::Repl::Transcript] transcript
|
|
24
|
+
# @param [LLM::Repl::Input] input
|
|
25
|
+
# @return [LLM::Repl::Window]
|
|
26
|
+
def initialize(status, transcript, input)
|
|
27
|
+
@status = status
|
|
28
|
+
@transcript = transcript
|
|
29
|
+
@input = input
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @yield
|
|
34
|
+
# @return [void]
|
|
35
|
+
def open
|
|
36
|
+
Curses.init_screen
|
|
37
|
+
Curses.cbreak
|
|
38
|
+
Curses.noecho
|
|
39
|
+
Curses.stdscr.keypad(true)
|
|
40
|
+
yield
|
|
41
|
+
ensure
|
|
42
|
+
Curses.close_screen
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# @return [void]
|
|
47
|
+
def redraw
|
|
48
|
+
Curses.clear
|
|
49
|
+
draw_status
|
|
50
|
+
draw_transcript
|
|
51
|
+
draw_input
|
|
52
|
+
Curses.refresh
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
def rows
|
|
58
|
+
[Curses.lines - 3, 1].max
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# @return [Object]
|
|
63
|
+
def getch
|
|
64
|
+
Curses.getch
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# @return [void]
|
|
69
|
+
def scroll_up
|
|
70
|
+
transcript.scroll_up(rows)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# @return [void]
|
|
75
|
+
def scroll_down
|
|
76
|
+
transcript.scroll_down
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def draw_status
|
|
82
|
+
Curses.setpos(0, 0)
|
|
83
|
+
Curses.addstr(status.to_s)
|
|
84
|
+
provider = status.provider.to_s
|
|
85
|
+
Curses.setpos(0, [columns - provider.length, 0].max)
|
|
86
|
+
Curses.addstr(provider)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def draw_transcript
|
|
90
|
+
visible = transcript.visible(rows)
|
|
91
|
+
visible.each_with_index do |row, index|
|
|
92
|
+
Curses.setpos(index + 2, 0)
|
|
93
|
+
Curses.addstr(row)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def draw_input
|
|
98
|
+
Curses.setpos(Curses.lines - 1, 0)
|
|
99
|
+
Curses.clrtoeol
|
|
100
|
+
Curses.addstr(input.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def columns
|
|
104
|
+
Curses.cols
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/llm/repl.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
LLM.require "curses"
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
##
|
|
7
|
+
# The {LLM::Repl LLM::Repl} class provides a small
|
|
8
|
+
# read-eval-print loop around an instance of
|
|
9
|
+
# {LLM::Agent LLM::Agent}.
|
|
10
|
+
#
|
|
11
|
+
# It can be used to keep talking to an agent after it
|
|
12
|
+
# has been set up or has performed a task. This can be
|
|
13
|
+
# useful when you want to confirm the agent handled the
|
|
14
|
+
# task correctly, or for it to correct course after a
|
|
15
|
+
# mistake was made.
|
|
16
|
+
class Repl
|
|
17
|
+
require_relative "repl/window"
|
|
18
|
+
require_relative "repl/status"
|
|
19
|
+
require_relative "repl/transcript"
|
|
20
|
+
require_relative "repl/input"
|
|
21
|
+
require_relative "repl/stream"
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [LLM::Agent] agent
|
|
25
|
+
# @return [LLM::Repl]
|
|
26
|
+
def initialize(agent)
|
|
27
|
+
@agent = agent
|
|
28
|
+
@provider = agent.llm.name
|
|
29
|
+
@status = Status.new(@provider)
|
|
30
|
+
@transcript = Transcript.new
|
|
31
|
+
@input = Input.new(@provider)
|
|
32
|
+
@window = Window.new(@status, @transcript, @input)
|
|
33
|
+
@stream = Stream.new(self)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# @return [void]
|
|
38
|
+
def start
|
|
39
|
+
window.open do
|
|
40
|
+
loop do
|
|
41
|
+
window.redraw
|
|
42
|
+
text = input.readline(window)
|
|
43
|
+
break if text.nil?
|
|
44
|
+
next if text.empty?
|
|
45
|
+
status.text = "thinking"
|
|
46
|
+
write("user: #{text}\n")
|
|
47
|
+
window.redraw
|
|
48
|
+
write("agent: ")
|
|
49
|
+
agent.talk(text, stream:)
|
|
50
|
+
status.text = "idle"
|
|
51
|
+
write("\n\n")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @param [String] chars
|
|
58
|
+
# @return [void]
|
|
59
|
+
def write(chars)
|
|
60
|
+
transcript.write(chars)
|
|
61
|
+
window.redraw
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# @param [String] value
|
|
66
|
+
# @return [void]
|
|
67
|
+
def status=(value)
|
|
68
|
+
status.text = value
|
|
69
|
+
window.redraw
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :agent, :provider, :stream,
|
|
75
|
+
:status, :transcript, :input,
|
|
76
|
+
:window
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/llm/response.rb
CHANGED
|
@@ -56,6 +56,16 @@ module LLM
|
|
|
56
56
|
@res.success?
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
##
|
|
60
|
+
# Returns the provider response id when present.
|
|
61
|
+
# @return [String, nil]
|
|
62
|
+
def id
|
|
63
|
+
return nil unless LLM::Object === body
|
|
64
|
+
body.id ||
|
|
65
|
+
body.responseId || body.response_id ||
|
|
66
|
+
body.requestId || body.request_id
|
|
67
|
+
end
|
|
68
|
+
|
|
59
69
|
##
|
|
60
70
|
# Returns true if the response is from the Files API
|
|
61
71
|
# @return [Boolean]
|
data/lib/llm/schema/leaf.rb
CHANGED
|
@@ -95,7 +95,7 @@ class LLM::Schema
|
|
|
95
95
|
##
|
|
96
96
|
# @return [Hash]
|
|
97
97
|
def to_h
|
|
98
|
-
{description: @description, default: @default, enum: @enum}.compact
|
|
98
|
+
{description: @description, default: @default, enum: @enum, const: @const}.compact
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
##
|
|
@@ -104,6 +104,12 @@ class LLM::Schema
|
|
|
104
104
|
to_h.to_json(options)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
##
|
|
108
|
+
# @return [String]
|
|
109
|
+
def to_s
|
|
110
|
+
LLM::Schema::Renderer.render(self)
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
##
|
|
108
114
|
# @param [LLM::Schema::Leaf] other
|
|
109
115
|
# An object to compare
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Schema
|
|
4
|
+
##
|
|
5
|
+
# Internal renderer for prompt-friendly schema output.
|
|
6
|
+
# @api private
|
|
7
|
+
module Renderer
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# Render a schema node as a human-readable string.
|
|
12
|
+
# @param [LLM::Schema::Leaf] node
|
|
13
|
+
# The schema node to render
|
|
14
|
+
# @param [Integer] indent
|
|
15
|
+
# The indentation level
|
|
16
|
+
# @param [String, Symbol, nil] name
|
|
17
|
+
# The property name for nested nodes
|
|
18
|
+
# @param [Boolean] root
|
|
19
|
+
# Whether the node is the root schema object
|
|
20
|
+
# @return [String]
|
|
21
|
+
def render(node, indent: 0, name: nil, root: false)
|
|
22
|
+
line = (" " * indent).to_s
|
|
23
|
+
if name
|
|
24
|
+
line << name.to_s
|
|
25
|
+
line << "?" unless node.required?
|
|
26
|
+
line << ": "
|
|
27
|
+
end
|
|
28
|
+
line << type_name(node)
|
|
29
|
+
metadata = metadata_for(node, include_required: !root)
|
|
30
|
+
line << " (#{metadata.join(", ")})" unless metadata.empty?
|
|
31
|
+
line << " - #{node.description}" if node.respond_to?(:description) && node.description
|
|
32
|
+
nested = nested_lines(node, indent: indent + 2)
|
|
33
|
+
([line] + nested).join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Render nested lines for compound schema nodes.
|
|
40
|
+
# @param [LLM::Schema::Leaf] node
|
|
41
|
+
# The schema node
|
|
42
|
+
# @param [Integer] indent
|
|
43
|
+
# The indentation level
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
def nested_lines(node, indent:)
|
|
46
|
+
case node
|
|
47
|
+
when LLM::Schema::Object
|
|
48
|
+
node.properties.map { |key, val| render(val, indent:, name: key) }
|
|
49
|
+
when LLM::Schema::Array
|
|
50
|
+
items = node.to_h[:items]
|
|
51
|
+
items.is_a?(LLM::Schema::Object) ? [render(items, indent:, name: "items")] : []
|
|
52
|
+
else
|
|
53
|
+
[]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Return the printable type name for a schema node.
|
|
59
|
+
# @param [LLM::Schema::Leaf] node
|
|
60
|
+
# The schema node
|
|
61
|
+
# @return [String]
|
|
62
|
+
def type_name(node)
|
|
63
|
+
h = node.to_h
|
|
64
|
+
return "array<#{inline_type(h[:items])}>" if node.is_a?(LLM::Schema::Array)
|
|
65
|
+
return "anyOf<#{inline_types(h[:anyOf])}>" if node.is_a?(LLM::Schema::AnyOf)
|
|
66
|
+
return "oneOf<#{inline_types(h[:oneOf])}>" if node.is_a?(LLM::Schema::OneOf)
|
|
67
|
+
return "allOf<#{inline_types(h[:allOf])}>" if node.is_a?(LLM::Schema::AllOf)
|
|
68
|
+
h[:type] || "unknown"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Return the inline type description for a nested node.
|
|
73
|
+
# @param [LLM::Schema::Leaf, Object] node
|
|
74
|
+
# The nested schema node
|
|
75
|
+
# @return [String]
|
|
76
|
+
def inline_type(node)
|
|
77
|
+
return type_name(node) if node.is_a?(LLM::Schema::Leaf)
|
|
78
|
+
node.inspect
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Return the inline type description for a list of nodes.
|
|
83
|
+
# @param [Array<LLM::Schema::Leaf>] values
|
|
84
|
+
# The union members
|
|
85
|
+
# @return [String]
|
|
86
|
+
def inline_types(values)
|
|
87
|
+
values.map { inline_type(_1) }.join(", ")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Extract printable metadata for a schema node.
|
|
92
|
+
# @param [LLM::Schema::Leaf] node
|
|
93
|
+
# The schema node
|
|
94
|
+
# @param [Boolean] include_required
|
|
95
|
+
# Whether to include the required marker
|
|
96
|
+
# @return [Array<String>]
|
|
97
|
+
def metadata_for(node, include_required:)
|
|
98
|
+
h = node.to_h.dup
|
|
99
|
+
details = []
|
|
100
|
+
details << "required" if include_required && node.required?
|
|
101
|
+
details << "default: #{value(node.default)}" if node.default
|
|
102
|
+
details << "enum: #{node.enum.map { value(_1) }.join(" | ")}" if node.enum
|
|
103
|
+
details << "const: #{value(node.const)}" if node.const
|
|
104
|
+
h.except(:type, :description, :default, :enum, :const, :required, :properties, :items, :anyOf, :oneOf, :allOf)
|
|
105
|
+
.each { |key, val| details << "#{key}: #{value(val)}" }
|
|
106
|
+
details
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Convert a scalar value into its printable representation.
|
|
111
|
+
# @param [Object] val
|
|
112
|
+
# The value to render
|
|
113
|
+
# @return [String]
|
|
114
|
+
def value(val)
|
|
115
|
+
case val
|
|
116
|
+
when ::String then val.inspect
|
|
117
|
+
else val.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|