llm.rb 0.3.2 → 0.4.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/README.md +81 -8
- data/lib/json/schema/array.rb +22 -0
- data/lib/json/schema/boolean.rb +9 -0
- data/lib/json/schema/integer.rb +21 -0
- data/lib/json/schema/leaf.rb +40 -0
- data/lib/json/schema/null.rb +9 -0
- data/lib/json/schema/number.rb +21 -0
- data/lib/json/schema/object.rb +26 -0
- data/lib/json/schema/string.rb +9 -0
- data/lib/json/schema.rb +73 -0
- data/lib/llm/chat.rb +7 -3
- data/lib/llm/core_ext/ostruct.rb +1 -1
- data/lib/llm/file.rb +8 -1
- data/lib/llm/message.rb +7 -0
- data/lib/llm/model.rb +27 -2
- data/lib/llm/provider.rb +36 -28
- data/lib/llm/providers/anthropic/format.rb +19 -6
- data/lib/llm/providers/anthropic/models.rb +62 -0
- data/lib/llm/providers/anthropic.rb +22 -8
- data/lib/llm/providers/gemini/format.rb +6 -1
- data/lib/llm/providers/gemini/images.rb +3 -3
- data/lib/llm/providers/gemini/models.rb +69 -0
- data/lib/llm/providers/gemini/response_parser.rb +1 -5
- data/lib/llm/providers/gemini.rb +30 -5
- data/lib/llm/providers/ollama/format.rb +11 -3
- data/lib/llm/providers/ollama/models.rb +66 -0
- data/lib/llm/providers/ollama.rb +30 -8
- data/lib/llm/providers/openai/audio.rb +0 -2
- data/lib/llm/providers/openai/format.rb +6 -1
- data/lib/llm/providers/openai/images.rb +1 -1
- data/lib/llm/providers/openai/models.rb +62 -0
- data/lib/llm/providers/openai/response_parser.rb +1 -5
- data/lib/llm/providers/openai/responses.rb +12 -6
- data/lib/llm/providers/openai.rb +37 -7
- data/lib/llm/response/modellist.rb +18 -0
- data/lib/llm/response.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +2 -1
- data/spec/anthropic/completion_spec.rb +36 -0
- data/spec/anthropic/models_spec.rb +21 -0
- data/spec/gemini/images_spec.rb +4 -12
- data/spec/gemini/models_spec.rb +21 -0
- data/spec/llm/conversation_spec.rb +71 -3
- data/spec/ollama/models_spec.rb +20 -0
- data/spec/openai/completion_spec.rb +19 -0
- data/spec/openai/images_spec.rb +2 -6
- data/spec/openai/models_spec.rb +21 -0
- metadata +20 -6
- data/share/llm/models/anthropic.yml +0 -35
- data/share/llm/models/gemini.yml +0 -35
- data/share/llm/models/ollama.yml +0 -155
- data/share/llm/models/openai.yml +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d8311b461c49095ba393be6d3456bf7c3032a07e4480f4fd3e4cd9133f2b6e7
|
4
|
+
data.tar.gz: 5f9ce812a4a27e0982ea5072bca3f0494317ee0274987e57df792cc0e0d2fcfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d860cabe75a0718e0c06d686127d6d24c65b4dc8967aaf490b892bfad10bf88268791e9138b26e2d1c646a22e2eda7e74ad48e1fa31e9cb68c42de7ec53ac12
|
7
|
+
data.tar.gz: 8c2e14a316e87560e5d5dc3cdb416d151172ae12e2244bb813ff1bba61c582474607bd6920f82fa5f080e628b2dac319dd20d4f74eff55940f9c88ab7544b327
|
data/README.md
CHANGED
@@ -38,7 +38,9 @@ The following example enables lazy mode for a
|
|
38
38
|
object by entering into a "lazy" conversation where messages are buffered and
|
39
39
|
sent to the provider only when necessary. Both lazy and non-lazy conversations
|
40
40
|
maintain a message thread that can be reused as context throughout a conversation.
|
41
|
-
The example
|
41
|
+
The example captures the spirit of llm.rb by demonstrating how objects cooperate
|
42
|
+
together through composition, and it uses the stateless chat completions API that
|
43
|
+
all LLM providers support:
|
42
44
|
|
43
45
|
```ruby
|
44
46
|
#!/usr/bin/env ruby
|
@@ -108,6 +110,46 @@ bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
|
|
108
110
|
# The answer to ((5 + 15) * 2) / 10 is 4.
|
109
111
|
```
|
110
112
|
|
113
|
+
### Schema
|
114
|
+
|
115
|
+
#### Structured
|
116
|
+
|
117
|
+
All LLM providers except Anthropic allow a client to describe the structure
|
118
|
+
of a response that a LLM emits according to a schema that is described by JSON.
|
119
|
+
The schema lets a client describe what JSON object (or value) an LLM should emit,
|
120
|
+
and the LLM will abide by the schema. See also: [JSON Schema website](https://json-schema.org/overview/what-is-jsonschema).
|
121
|
+
|
122
|
+
True to the llm.rb spirit of doing one thing well, and solving problems through the
|
123
|
+
composition of objects, the generation of a schema is delegated to another object
|
124
|
+
who is responsible for and an expert in the generation of JSON schemas. We will use
|
125
|
+
the
|
126
|
+
[llmrb/json-schema](https://github.com/llmrb/json-schema)
|
127
|
+
library for the sake of the examples - it is an optional dependency that is loaded
|
128
|
+
on-demand. At least for the time being it is not necessary to install it separately.
|
129
|
+
The interface is designed so you could drop in any other library in its place:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
#!/usr/bin/env ruby
|
133
|
+
require "llm"
|
134
|
+
|
135
|
+
llm = LLM.openai(ENV["KEY"])
|
136
|
+
schema = llm.schema.object({os: llm.schema.string.enum("OpenBSD", "FreeBSD", "NetBSD").required})
|
137
|
+
bot = LLM::Chat.new(llm, schema:)
|
138
|
+
bot.chat "You secretly love NetBSD", :system
|
139
|
+
bot.chat "What operating system is the best?", :user
|
140
|
+
bot.messages.find(&:assistant?).content! # => {os: "NetBSD"}
|
141
|
+
|
142
|
+
schema = llm.schema.object({answer: llm.schema.integer.required})
|
143
|
+
bot = LLM::Chat.new(llm, schema:)
|
144
|
+
bot.chat "Tell me the answer to ((5 + 5) / 2)", :user
|
145
|
+
bot.messages.find(&:assistant?).content! # => {answer: 5}
|
146
|
+
|
147
|
+
schema = llm.schema.object({probability: llm.schema.number.required})
|
148
|
+
bot = LLM::Chat.new(llm, schema:)
|
149
|
+
bot.chat "Does the earth orbit the sun?", :user
|
150
|
+
bot.messages.find(&:assistant?).content! # => {probability: 1}
|
151
|
+
```
|
152
|
+
|
111
153
|
### Audio
|
112
154
|
|
113
155
|
#### Speech
|
@@ -126,8 +168,7 @@ require "llm"
|
|
126
168
|
|
127
169
|
llm = LLM.openai(ENV["KEY"])
|
128
170
|
res = llm.audio.create_speech(input: "Hello world")
|
129
|
-
|
130
|
-
res.audio.string
|
171
|
+
IO.copy_stream res.audio, File.join(Dir.home, "hello.mp3")
|
131
172
|
```
|
132
173
|
|
133
174
|
#### Transcribe
|
@@ -325,7 +366,7 @@ also understand URLs, and various file types (eg images, audio, video,
|
|
325
366
|
etc). The llm.rb approach to multimodal prompts is to let you pass `URI`
|
326
367
|
objects to describe links, `LLM::File` / `LLM::Response::File` objects
|
327
368
|
to describe files, `String` objects to describe text blobs, or an array
|
328
|
-
of the
|
369
|
+
of the aforementioned objects to describe multiple objects in a single
|
329
370
|
prompt. Each object is a first class citizen that can be passed directly
|
330
371
|
to a prompt.
|
331
372
|
|
@@ -388,6 +429,38 @@ print res.embeddings[0].size, "\n"
|
|
388
429
|
# 1536
|
389
430
|
```
|
390
431
|
|
432
|
+
### Models
|
433
|
+
|
434
|
+
#### List
|
435
|
+
|
436
|
+
Almost all LLM providers provide a models endpoint that allows a client to
|
437
|
+
query the list of models that are available to use. The list is dynamic,
|
438
|
+
maintained by LLM providers, and it is independent of a specific llm.rb release.
|
439
|
+
True to the llm.rb spirit of small, composable objects that cooperate with
|
440
|
+
each other, a
|
441
|
+
[LLM::Model](https://0x1eef.github.io/x/llm.rb/LLM/Model.html)
|
442
|
+
object can be used instead of a string that describes a model name (although
|
443
|
+
either works). Let's take a look at an example:
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
#!/usr/bin/env ruby
|
447
|
+
require "llm"
|
448
|
+
|
449
|
+
##
|
450
|
+
# List all models
|
451
|
+
llm = LLM.openai(ENV["KEY"])
|
452
|
+
llm.models.all.each do |model|
|
453
|
+
print "model: ", model.id, "\n"
|
454
|
+
end
|
455
|
+
|
456
|
+
##
|
457
|
+
# Select a model
|
458
|
+
model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
|
459
|
+
bot = LLM::Chat.new(llm, model:)
|
460
|
+
bot.chat "Hello #{model.id} :)"
|
461
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
462
|
+
```
|
463
|
+
|
391
464
|
### Memory
|
392
465
|
|
393
466
|
#### Child process
|
@@ -410,7 +483,7 @@ llm = LLM.gemini(ENV["KEY"])
|
|
410
483
|
fork do
|
411
484
|
%w[dog cat sheep goat capybara].each do |animal|
|
412
485
|
res = llm.images.create(prompt: "a #{animal} on a rocket to the moon")
|
413
|
-
|
486
|
+
IO.copy_stream res.images[0], "#{animal}.png"
|
414
487
|
end
|
415
488
|
end
|
416
489
|
Process.wait
|
@@ -440,9 +513,9 @@ magic or complex metaprogramming.
|
|
440
513
|
|
441
514
|
Every part of llm.rb is designed to be explicit, composable, memory-safe,
|
442
515
|
and production-ready without compromise. No unnecessary abstractions,
|
443
|
-
no global configuration, and no dependencies that aren't
|
444
|
-
Ruby. It has been inspired in part by other languages such
|
445
|
-
it is not a port of any other library.
|
516
|
+
no global configuration, no global state, and no dependencies that aren't
|
517
|
+
part of standard Ruby. It has been inspired in part by other languages such
|
518
|
+
as Python, but it is not a port of any other library.
|
446
519
|
|
447
520
|
## License
|
448
521
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JSON::Schema
|
4
|
+
class Array < Leaf
|
5
|
+
def initialize(items, **rest)
|
6
|
+
@items = items
|
7
|
+
super(**rest)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
super.merge!({type: "array", items:})
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_json(options = {})
|
15
|
+
to_h.to_json(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :items
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JSON::Schema
|
4
|
+
class Integer < Leaf
|
5
|
+
def min(i)
|
6
|
+
tap { @minimum = i }
|
7
|
+
end
|
8
|
+
|
9
|
+
def max(i)
|
10
|
+
tap { @maximum = i }
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
super.merge!({
|
15
|
+
type: "integer",
|
16
|
+
minimum: @minimum,
|
17
|
+
maximum: @maximum
|
18
|
+
}).compact
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JSON::Schema
|
4
|
+
class Leaf
|
5
|
+
def initialize
|
6
|
+
@description = nil
|
7
|
+
@default = nil
|
8
|
+
@enum = nil
|
9
|
+
@required = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def description(str)
|
13
|
+
tap { @description = str }
|
14
|
+
end
|
15
|
+
|
16
|
+
def default(value)
|
17
|
+
tap { @default = value }
|
18
|
+
end
|
19
|
+
|
20
|
+
def enum(*values)
|
21
|
+
tap { @enum = values }
|
22
|
+
end
|
23
|
+
|
24
|
+
def required
|
25
|
+
tap { @required = true }
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
{description: @description, default: @default, enum: @enum}.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_json(options = {})
|
33
|
+
to_h.to_json(options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def required?
|
37
|
+
@required
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JSON::Schema
|
4
|
+
class Number < Leaf
|
5
|
+
def min(i)
|
6
|
+
tap { @minimum = i }
|
7
|
+
end
|
8
|
+
|
9
|
+
def max(i)
|
10
|
+
tap { @maximum = i }
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
super.merge!({
|
15
|
+
type: "number",
|
16
|
+
minimum: @minimum,
|
17
|
+
maximum: @maximum
|
18
|
+
}).compact
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JSON::Schema
|
4
|
+
class Object < Leaf
|
5
|
+
def initialize(properties, **rest)
|
6
|
+
@properties = properties
|
7
|
+
super(**rest)
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
super.merge!({type: "object", properties:, required:})
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_json(options = {})
|
15
|
+
to_h.to_json(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :properties
|
21
|
+
|
22
|
+
def required
|
23
|
+
@properties.filter_map { _2.required? ? _1 : nil }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/json/schema.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
end unless defined?(JSON)
|
5
|
+
|
6
|
+
class JSON::Schema
|
7
|
+
require_relative "schema/leaf"
|
8
|
+
require_relative "schema/object"
|
9
|
+
require_relative "schema/array"
|
10
|
+
require_relative "schema/string"
|
11
|
+
require_relative "schema/number"
|
12
|
+
require_relative "schema/integer"
|
13
|
+
require_relative "schema/boolean"
|
14
|
+
require_relative "schema/null"
|
15
|
+
|
16
|
+
##
|
17
|
+
# Returns an object
|
18
|
+
# @param properties [Hash] A hash of properties
|
19
|
+
# @param rest [Hash] Any other options
|
20
|
+
# @return [JSON::Schema::Object]
|
21
|
+
def object(properties, **rest)
|
22
|
+
Object.new(properties, **rest)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Returns an array
|
27
|
+
# @param items [Array] An array of items
|
28
|
+
# @param rest [Hash] Any other options
|
29
|
+
# @return [JSON::Schema::Array]
|
30
|
+
def array(items, **rest)
|
31
|
+
Array.new(items, **rest)
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Returns a string
|
36
|
+
# @param rest [Hash] Any other options
|
37
|
+
# @return [JSON::Schema::String]
|
38
|
+
def string(...)
|
39
|
+
String.new(...)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Returns a number
|
44
|
+
# @param rest [Hash] Any other options
|
45
|
+
# @return [JSON::Schema::Number] a number
|
46
|
+
def number(...)
|
47
|
+
Number.new(...)
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Returns an integer
|
52
|
+
# @param rest [Hash] Any other options
|
53
|
+
# @return [JSON::Schema::Integer]
|
54
|
+
def integer(...)
|
55
|
+
Integer.new(...)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Returns a boolean
|
60
|
+
# @param rest [Hash] Any other options
|
61
|
+
# @return [JSON::Schema::Boolean]
|
62
|
+
def boolean(...)
|
63
|
+
Boolean.new(...)
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# Returns null
|
68
|
+
# @param rest [Hash] Any other options
|
69
|
+
# @return [JSON::Schema::Null]
|
70
|
+
def null(...)
|
71
|
+
Null.new(...)
|
72
|
+
end
|
73
|
+
end
|
data/lib/llm/chat.rb
CHANGED
@@ -27,11 +27,15 @@ module LLM
|
|
27
27
|
##
|
28
28
|
# @param [LLM::Provider] provider
|
29
29
|
# A provider
|
30
|
+
# @param [to_json] schema
|
31
|
+
# The JSON schema to maintain throughout the conversation
|
32
|
+
# @param [String] model
|
33
|
+
# The model to maintain throughout the conversation
|
30
34
|
# @param [Hash] params
|
31
|
-
#
|
32
|
-
def initialize(provider,
|
35
|
+
# Other parameters to maintain throughout the conversation
|
36
|
+
def initialize(provider, model: provider.default_model, schema: nil, **params)
|
33
37
|
@provider = provider
|
34
|
-
@params = params
|
38
|
+
@params = params.merge!(model:, schema:)
|
35
39
|
@lazy = false
|
36
40
|
@messages = []
|
37
41
|
end
|
data/lib/llm/core_ext/ostruct.rb
CHANGED
data/lib/llm/file.rb
CHANGED
@@ -7,13 +7,20 @@
|
|
7
7
|
class LLM::File
|
8
8
|
##
|
9
9
|
# @return [String]
|
10
|
-
# Returns the path to
|
10
|
+
# Returns the path to the file
|
11
11
|
attr_reader :path
|
12
12
|
|
13
13
|
def initialize(path)
|
14
14
|
@path = path
|
15
15
|
end
|
16
16
|
|
17
|
+
##
|
18
|
+
# @return [String]
|
19
|
+
# Returns basename of the file
|
20
|
+
def basename
|
21
|
+
File.basename(path)
|
22
|
+
end
|
23
|
+
|
17
24
|
##
|
18
25
|
# @return [String]
|
19
26
|
# Returns the MIME type of the file
|
data/lib/llm/message.rb
CHANGED
data/lib/llm/model.rb
CHANGED
@@ -1,7 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##
|
4
|
+
# The {LLM::Model LLM::Model} class represents an LLM model that
|
5
|
+
# is available to use. Its properties are delegated to the underlying
|
6
|
+
# response body, and vary by provider.
|
7
|
+
class LLM::Model < OpenStruct
|
8
|
+
##
|
9
|
+
# Returns a subclass of {LLM::Provider LLM::Provider}
|
10
|
+
# @return [LLM::Provider]
|
11
|
+
attr_accessor :provider
|
12
|
+
|
13
|
+
##
|
14
|
+
# Returns the model ID
|
15
|
+
# @return [String]
|
16
|
+
def id
|
17
|
+
case @provider.class.to_s
|
18
|
+
when "LLM::Ollama"
|
19
|
+
self["name"]
|
20
|
+
when "LLM::Gemini"
|
21
|
+
self["name"].sub(%r|\Amodels/|, "")
|
22
|
+
else
|
23
|
+
self["id"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# @return [String]
|
4
29
|
def to_json(*)
|
5
|
-
|
30
|
+
id.to_json(*)
|
6
31
|
end
|
7
32
|
end
|
data/lib/llm/provider.rb
CHANGED
@@ -44,7 +44,7 @@ class LLM::Provider
|
|
44
44
|
# @raise [NotImplementedError]
|
45
45
|
# When the method is not implemented by a subclass
|
46
46
|
# @return [LLM::Response::Embedding]
|
47
|
-
def embed(input, model
|
47
|
+
def embed(input, model: nil, **params)
|
48
48
|
raise NotImplementedError
|
49
49
|
end
|
50
50
|
|
@@ -64,12 +64,14 @@ class LLM::Provider
|
|
64
64
|
# The role of the prompt (e.g. :user, :system)
|
65
65
|
# @param [String] model
|
66
66
|
# The model to use for the completion
|
67
|
+
# @param [#to_json, nil] schema
|
68
|
+
# The schema that describes the expected response format
|
67
69
|
# @param [Hash] params
|
68
70
|
# Other completion parameters
|
69
71
|
# @raise [NotImplementedError]
|
70
72
|
# When the method is not implemented by a subclass
|
71
73
|
# @return [LLM::Response::Completion]
|
72
|
-
def complete(prompt, role = :user, model: nil, **params)
|
74
|
+
def complete(prompt, role = :user, model: default_model, schema: nil, **params)
|
73
75
|
raise NotImplementedError
|
74
76
|
end
|
75
77
|
|
@@ -81,12 +83,13 @@ class LLM::Provider
|
|
81
83
|
# @param prompt (see LLM::Provider#complete)
|
82
84
|
# @param role (see LLM::Provider#complete)
|
83
85
|
# @param model (see LLM::Provider#complete)
|
86
|
+
# @param schema (see LLM::Provider#complete)
|
84
87
|
# @param [Hash] params
|
85
88
|
# Other completion parameters to maintain throughout a chat
|
86
89
|
# @raise (see LLM::Provider#complete)
|
87
90
|
# @return [LLM::Chat]
|
88
|
-
def chat(prompt, role = :user, model: nil, **params)
|
89
|
-
LLM::Chat.new(self, params).lazy.chat(prompt, role)
|
91
|
+
def chat(prompt, role = :user, model: default_model, schema: nil, **params)
|
92
|
+
LLM::Chat.new(self, **params.merge(model:, schema:)).lazy.chat(prompt, role)
|
90
93
|
end
|
91
94
|
|
92
95
|
##
|
@@ -97,12 +100,13 @@ class LLM::Provider
|
|
97
100
|
# @param prompt (see LLM::Provider#complete)
|
98
101
|
# @param role (see LLM::Provider#complete)
|
99
102
|
# @param model (see LLM::Provider#complete)
|
103
|
+
# @param schema (see LLM::Provider#complete)
|
100
104
|
# @param [Hash] params
|
101
105
|
# Other completion parameters to maintain throughout a chat
|
102
106
|
# @raise (see LLM::Provider#complete)
|
103
107
|
# @return [LLM::Chat]
|
104
|
-
def chat!(prompt, role = :user, model: nil, **params)
|
105
|
-
LLM::Chat.new(self, params).chat(prompt, role)
|
108
|
+
def chat!(prompt, role = :user, model: default_model, schema: nil, **params)
|
109
|
+
LLM::Chat.new(self, **params.merge(model:, schema:)).chat(prompt, role)
|
106
110
|
end
|
107
111
|
|
108
112
|
##
|
@@ -113,12 +117,13 @@ class LLM::Provider
|
|
113
117
|
# @param prompt (see LLM::Provider#complete)
|
114
118
|
# @param role (see LLM::Provider#complete)
|
115
119
|
# @param model (see LLM::Provider#complete)
|
120
|
+
# @param schema (see LLM::Provider#complete)
|
116
121
|
# @param [Hash] params
|
117
122
|
# Other completion parameters to maintain throughout a chat
|
118
123
|
# @raise (see LLM::Provider#complete)
|
119
124
|
# @return [LLM::Chat]
|
120
|
-
def respond(prompt, role = :user, model: nil, **params)
|
121
|
-
LLM::Chat.new(self, params).lazy.respond(prompt, role)
|
125
|
+
def respond(prompt, role = :user, model: default_model, schema: nil, **params)
|
126
|
+
LLM::Chat.new(self, **params.merge(model:, schema:)).lazy.respond(prompt, role)
|
122
127
|
end
|
123
128
|
|
124
129
|
##
|
@@ -129,12 +134,13 @@ class LLM::Provider
|
|
129
134
|
# @param prompt (see LLM::Provider#complete)
|
130
135
|
# @param role (see LLM::Provider#complete)
|
131
136
|
# @param model (see LLM::Provider#complete)
|
137
|
+
# @param schema (see LLM::Provider#complete)
|
132
138
|
# @param [Hash] params
|
133
139
|
# Other completion parameters to maintain throughout a chat
|
134
140
|
# @raise (see LLM::Provider#complete)
|
135
141
|
# @return [LLM::Chat]
|
136
|
-
def respond!(prompt, role = :user, model: nil, **params)
|
137
|
-
LLM::Chat.new(self, params).respond(prompt, role)
|
142
|
+
def respond!(prompt, role = :user, model: default_model, schema: nil, **params)
|
143
|
+
LLM::Chat.new(self, **params.merge(model:, schema:)).respond(prompt, role)
|
138
144
|
end
|
139
145
|
|
140
146
|
##
|
@@ -169,6 +175,13 @@ class LLM::Provider
|
|
169
175
|
raise NotImplementedError
|
170
176
|
end
|
171
177
|
|
178
|
+
##
|
179
|
+
# @return [LLM::OpenAI::Models]
|
180
|
+
# Returns an interface to the models API
|
181
|
+
def models
|
182
|
+
raise NotImplementedError
|
183
|
+
end
|
184
|
+
|
172
185
|
##
|
173
186
|
# @return [String]
|
174
187
|
# Returns the role of the assistant in the conversation.
|
@@ -178,12 +191,22 @@ class LLM::Provider
|
|
178
191
|
end
|
179
192
|
|
180
193
|
##
|
181
|
-
# @return [
|
182
|
-
# Returns
|
183
|
-
def
|
194
|
+
# @return [String]
|
195
|
+
# Returns the default model for chat completions
|
196
|
+
def default_model
|
184
197
|
raise NotImplementedError
|
185
198
|
end
|
186
199
|
|
200
|
+
##
|
201
|
+
# Returns an object that can generate a JSON schema
|
202
|
+
# @return [JSON::Schema]
|
203
|
+
def schema
|
204
|
+
@schema ||= begin
|
205
|
+
require_relative "../json/schema"
|
206
|
+
JSON::Schema.new
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
187
210
|
private
|
188
211
|
|
189
212
|
##
|
@@ -228,8 +251,6 @@ class LLM::Provider
|
|
228
251
|
# When the rate limit is exceeded
|
229
252
|
# @raise [LLM::Error::ResponseError]
|
230
253
|
# When any other unsuccessful status code is returned
|
231
|
-
# @raise [LLM::Error::PromptError]
|
232
|
-
# When given an object a provider does not understand
|
233
254
|
# @raise [SystemCallError]
|
234
255
|
# When there is a network error at the operating system level
|
235
256
|
def request(http, req, &b)
|
@@ -250,17 +271,4 @@ class LLM::Provider
|
|
250
271
|
req.body_stream = io
|
251
272
|
req["transfer-encoding"] = "chunked" unless req["content-length"]
|
252
273
|
end
|
253
|
-
|
254
|
-
##
|
255
|
-
# @param [String] provider
|
256
|
-
# The name of the provider
|
257
|
-
# @return [Hash<String, Hash>]
|
258
|
-
def load_models!(provider)
|
259
|
-
require "yaml" unless defined?(YAML)
|
260
|
-
rootdir = File.realpath File.join(__dir__, "..", "..")
|
261
|
-
sharedir = File.join(rootdir, "share", "llm")
|
262
|
-
provider = provider.gsub(/[^a-z0-9]/i, "")
|
263
|
-
yaml = File.join(sharedir, "models", "#{provider}.yml")
|
264
|
-
YAML.safe_load_file(yaml).transform_values { LLM::Model.new(_1) }
|
265
|
-
end
|
266
274
|
end
|
@@ -26,13 +26,26 @@ class LLM::Anthropic
|
|
26
26
|
# @return [String, Hash]
|
27
27
|
# The formatted content
|
28
28
|
def format_content(content)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
}]
|
29
|
+
case content
|
30
|
+
when Array
|
31
|
+
content.flat_map { format_content(_1) }
|
32
|
+
when URI
|
33
|
+
[{type: :image, source: {type: "url", url: content.to_s}}]
|
34
|
+
when LLM::File
|
35
|
+
if content.image?
|
36
|
+
[{type: :image, source: {type: "base64", media_type: content.mime_type, data: content.to_b64}}]
|
37
|
+
else
|
38
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
39
|
+
"is not an image, and therefore not supported by the " \
|
40
|
+
"Anthropic API"
|
41
|
+
end
|
42
|
+
when String
|
43
|
+
[{type: :text, text: content}]
|
44
|
+
when LLM::Message
|
45
|
+
format_content(content.content)
|
34
46
|
else
|
35
|
-
content
|
47
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
48
|
+
"is not supported by the Anthropic API"
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|