llm.rb 0.2.0 → 0.2.1
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 +2 -2
- data/lib/llm/conversation.rb +49 -9
- data/lib/llm/message_queue.rb +18 -11
- data/lib/llm/provider.rb +29 -2
- data/lib/llm/providers/anthropic/format.rb +7 -1
- data/lib/llm/providers/gemini/format.rb +7 -1
- data/lib/llm/providers/ollama/format.rb +7 -1
- data/lib/llm/providers/openai/format.rb +7 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +1 -1
- data/spec/gemini/completion_spec.rb +21 -0
- data/spec/llm/conversation_spec.rb +93 -1
- data/spec/openai/completion_spec.rb +21 -0
- metadata +2 -4
- data/lib/llm/lazy_conversation.rb +0 -51
- data/spec/llm/lazy_conversation_spec.rb +0 -92
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 732a483717a5ec8e443077fb71294b1e301c3a8867b225c1fc2a58bd02fe3130
|
4
|
+
data.tar.gz: a1c2591a07c413cebfdffa99d133855bb177cc4a6607860333dbc9991da8d33e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f5983f97b3c1e25f4147ec81f6d91df5073ea03dc4031690979f68b2053bf73bae07f7c57c3f7c9813dfd5b43eb1bd7364d5f5929234013d6b19bb49f9271ec
|
7
|
+
data.tar.gz: 286f560ce2d9e048e481796d27fd6c9a658b92cec0267f9527fad49bf5349ece0d05b943c086cce3c12bbd82f53dbf086ed91c516d39b815bd6106edca21914a
|
data/README.md
CHANGED
@@ -31,8 +31,8 @@ llm = LLM.ollama(nil)
|
|
31
31
|
|
32
32
|
The
|
33
33
|
[LLM::Provider#chat](https://0x1eef.github.io/x/llm/LLM/Provider.html#chat-instance_method)
|
34
|
-
method returns a
|
35
|
-
[LLM::
|
34
|
+
method returns a lazy-variant of a
|
35
|
+
[LLM::Conversation](https://0x1eef.github.io/x/llm/LLM/Conversation.html)
|
36
36
|
object, and it allows for a "lazy" conversation where messages are batched and
|
37
37
|
sent to the provider only when necessary. The non-lazy counterpart is available via the
|
38
38
|
[LLM::Provider#chat!](https://0x1eef.github.io/x/llm/LLM/Provider.html#chat!-instance_method)
|
data/lib/llm/conversation.rb
CHANGED
@@ -3,14 +3,14 @@
|
|
3
3
|
module LLM
|
4
4
|
##
|
5
5
|
# {LLM::Conversation LLM::Conversation} provides a conversation
|
6
|
-
# object that maintains a thread of messages that
|
7
|
-
#
|
8
|
-
#
|
6
|
+
# object that maintains a thread of messages that acts as context
|
7
|
+
# throughout the conversation.
|
9
8
|
# @example
|
10
|
-
# llm = LLM.openai(
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
9
|
+
# llm = LLM.openai(ENV["KEY"])
|
10
|
+
# convo = llm.chat("You are my climate expert", :system)
|
11
|
+
# convo.chat("What's the climate like in Rio de Janerio?", :user)
|
12
|
+
# convo.chat("What's the climate like in Algiers?", :user)
|
13
|
+
# convo.chat("What's the climate like in Tokyo?", :user)
|
14
14
|
# p bot.messages.map { [_1.role, _1.content] }
|
15
15
|
class Conversation
|
16
16
|
##
|
@@ -20,9 +20,12 @@ module LLM
|
|
20
20
|
##
|
21
21
|
# @param [LLM::Provider] provider
|
22
22
|
# A provider
|
23
|
+
# @param [Hash] params
|
24
|
+
# The parameters to maintain throughout the conversation
|
23
25
|
def initialize(provider, params = {})
|
24
26
|
@provider = provider
|
25
27
|
@params = params
|
28
|
+
@lazy = false
|
26
29
|
@messages = []
|
27
30
|
end
|
28
31
|
|
@@ -31,12 +34,20 @@ module LLM
|
|
31
34
|
# @return [LLM::Conversation]
|
32
35
|
def chat(prompt, role = :user, **params)
|
33
36
|
tap do
|
34
|
-
|
35
|
-
|
37
|
+
if lazy?
|
38
|
+
@messages << [LLM::Message.new(role, prompt), @params.merge(params)]
|
39
|
+
else
|
40
|
+
completion = complete(prompt, role, params)
|
41
|
+
@messages.concat [Message.new(role, prompt), completion.choices[0]]
|
42
|
+
end
|
36
43
|
end
|
37
44
|
end
|
38
45
|
|
39
46
|
##
|
47
|
+
# @note
|
48
|
+
# The `read_response` and `recent_message` methods are aliases of
|
49
|
+
# the `last_message` method, and you can choose the name that best
|
50
|
+
# fits your context or code style.
|
40
51
|
# @param [#to_s] role
|
41
52
|
# The role of the last message.
|
42
53
|
# Defaults to the LLM's assistant role (eg "assistant" or "model")
|
@@ -46,5 +57,34 @@ module LLM
|
|
46
57
|
messages.reverse_each.find { _1.role == role.to_s }
|
47
58
|
end
|
48
59
|
alias_method :recent_message, :last_message
|
60
|
+
alias_method :read_response, :last_message
|
61
|
+
|
62
|
+
##
|
63
|
+
# Enables lazy mode for the conversation.
|
64
|
+
# @return [LLM::Conversation]
|
65
|
+
def lazy
|
66
|
+
tap do
|
67
|
+
next if lazy?
|
68
|
+
@lazy = true
|
69
|
+
@messages = LLM::MessageQueue.new(@provider)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# @return [Boolean]
|
75
|
+
# Returns true if the conversation is lazy
|
76
|
+
def lazy?
|
77
|
+
@lazy
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def complete(prompt, role, params)
|
83
|
+
@provider.complete(
|
84
|
+
prompt,
|
85
|
+
role,
|
86
|
+
**@params.merge(params.merge(messages:))
|
87
|
+
)
|
88
|
+
end
|
49
89
|
end
|
50
90
|
end
|
data/lib/llm/message_queue.rb
CHANGED
@@ -13,7 +13,8 @@ module LLM
|
|
13
13
|
# @return [LLM::MessageQueue]
|
14
14
|
def initialize(provider)
|
15
15
|
@provider = provider
|
16
|
-
@
|
16
|
+
@pending = []
|
17
|
+
@completed = []
|
17
18
|
end
|
18
19
|
|
19
20
|
##
|
@@ -22,26 +23,32 @@ module LLM
|
|
22
23
|
# @raise (see LLM::Provider#complete)
|
23
24
|
# @return [void]
|
24
25
|
def each
|
25
|
-
|
26
|
-
@
|
26
|
+
complete! unless @pending.empty?
|
27
|
+
@completed.each { yield(_1) }
|
27
28
|
end
|
28
29
|
|
29
30
|
##
|
30
|
-
# @param
|
31
|
-
# A message
|
31
|
+
# @param [[LLM::Message, Hash]] item
|
32
|
+
# A message and its parameters
|
32
33
|
# @return [void]
|
33
|
-
def <<(
|
34
|
-
@
|
34
|
+
def <<(item)
|
35
|
+
@pending << item
|
36
|
+
self
|
35
37
|
end
|
36
38
|
alias_method :push, :<<
|
37
39
|
|
38
40
|
private
|
39
41
|
|
40
42
|
def complete!
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
message, params = @pending[-1]
|
44
|
+
messages = @pending[0..-2].map { _1[0] }
|
45
|
+
completion = @provider.complete(
|
46
|
+
message.content,
|
47
|
+
message.role,
|
48
|
+
**params.merge(messages:)
|
49
|
+
)
|
50
|
+
@completed.concat([*messages, message, completion.choices[0]])
|
51
|
+
@pending.clear
|
45
52
|
end
|
46
53
|
end
|
47
54
|
end
|
data/lib/llm/provider.rb
CHANGED
@@ -2,7 +2,18 @@
|
|
2
2
|
|
3
3
|
##
|
4
4
|
# The Provider class represents an abstract class for
|
5
|
-
# LLM (Language Model) providers
|
5
|
+
# LLM (Language Model) providers.
|
6
|
+
#
|
7
|
+
# @note
|
8
|
+
# This class is not meant to be instantiated directly.
|
9
|
+
# Instead, use one of the subclasses that implement
|
10
|
+
# the methods defined here.
|
11
|
+
#
|
12
|
+
# @abstract
|
13
|
+
# @see LLM::Provider::OpenAI
|
14
|
+
# @see LLM::Provider::Anthropic
|
15
|
+
# @see LLM::Provider::Gemini
|
16
|
+
# @see LLM::Provider::Ollama
|
6
17
|
class LLM::Provider
|
7
18
|
require_relative "http_client"
|
8
19
|
include LLM::HTTPClient
|
@@ -44,10 +55,20 @@ class LLM::Provider
|
|
44
55
|
|
45
56
|
##
|
46
57
|
# Completes a given prompt using the LLM
|
58
|
+
# @example
|
59
|
+
# llm = LLM.openai(ENV["KEY"])
|
60
|
+
# context = [
|
61
|
+
# {role: "system", content: "Answer all of my questions"},
|
62
|
+
# {role: "system", content: "Your name is Pablo, you are 25 years old and you are my amigo"},
|
63
|
+
# ]
|
64
|
+
# res = llm.complete "What is your name and what age are you?", :user, messages: context
|
65
|
+
# print "[#{res.choices[0].role}]", res.choices[0].content, "\n"
|
47
66
|
# @param [String] prompt
|
48
67
|
# The input prompt to be completed
|
49
68
|
# @param [Symbol] role
|
50
69
|
# The role of the prompt (e.g. :user, :system)
|
70
|
+
# @param [Array<Hash, LLM::Message>] messages
|
71
|
+
# The messages to include in the completion
|
51
72
|
# @raise [NotImplementedError]
|
52
73
|
# When the method is not implemented by a subclass
|
53
74
|
# @return [LLM::Response::Completion]
|
@@ -57,16 +78,22 @@ class LLM::Provider
|
|
57
78
|
|
58
79
|
##
|
59
80
|
# Starts a new lazy conversation
|
81
|
+
# @note
|
82
|
+
# This method creates a lazy variant of a
|
83
|
+
# {LLM::Conversation LLM::Conversation} object.
|
60
84
|
# @param prompt (see LLM::Provider#complete)
|
61
85
|
# @param role (see LLM::Provider#complete)
|
62
86
|
# @raise (see LLM::Provider#complete)
|
63
87
|
# @return [LLM::LazyConversation]
|
64
88
|
def chat(prompt, role = :user, **params)
|
65
|
-
LLM::
|
89
|
+
LLM::Conversation.new(self, params).lazy.chat(prompt, role)
|
66
90
|
end
|
67
91
|
|
68
92
|
##
|
69
93
|
# Starts a new conversation
|
94
|
+
# @note
|
95
|
+
# This method creates a non-lazy variant of a
|
96
|
+
# {LLM::Conversation LLM::Conversation} object.
|
70
97
|
# @param prompt (see LLM::Provider#complete)
|
71
98
|
# @param role (see LLM::Provider#complete)
|
72
99
|
# @raise (see LLM::Provider#complete)
|
@@ -7,7 +7,13 @@ class LLM::Anthropic
|
|
7
7
|
# The messages to format
|
8
8
|
# @return [Array<Hash>]
|
9
9
|
def format(messages)
|
10
|
-
messages.map
|
10
|
+
messages.map do
|
11
|
+
if Hash === _1
|
12
|
+
{role: _1[:role], content: format_content(_1[:content])}
|
13
|
+
else
|
14
|
+
{role: _1.role, content: format_content(_1.content)}
|
15
|
+
end
|
16
|
+
end
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
@@ -7,7 +7,13 @@ class LLM::Gemini
|
|
7
7
|
# The messages to format
|
8
8
|
# @return [Array<Hash>]
|
9
9
|
def format(messages)
|
10
|
-
messages.map
|
10
|
+
messages.map do
|
11
|
+
if Hash === _1
|
12
|
+
{role: _1[:role], parts: [format_content(_1[:content])]}
|
13
|
+
else
|
14
|
+
{role: _1.role, parts: [format_content(_1.content)]}
|
15
|
+
end
|
16
|
+
end
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
@@ -7,7 +7,13 @@ class LLM::Ollama
|
|
7
7
|
# The messages to format
|
8
8
|
# @return [Array<Hash>]
|
9
9
|
def format(messages)
|
10
|
-
messages.map
|
10
|
+
messages.map do
|
11
|
+
if Hash === _1
|
12
|
+
{role: _1[:role], content: format_content(_1[:content])}
|
13
|
+
else
|
14
|
+
{role: _1.role, content: format_content(_1.content)}
|
15
|
+
end
|
16
|
+
end
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
@@ -7,7 +7,13 @@ class LLM::OpenAI
|
|
7
7
|
# The messages to format
|
8
8
|
# @return [Array<Hash>]
|
9
9
|
def format(messages)
|
10
|
-
messages.map
|
10
|
+
messages.map do
|
11
|
+
if Hash === _1
|
12
|
+
{role: _1[:role], content: format_content(_1[:content])}
|
13
|
+
else
|
14
|
+
{role: _1.role, content: format_content(_1.content)}
|
15
|
+
end
|
16
|
+
end
|
11
17
|
end
|
12
18
|
|
13
19
|
private
|
data/lib/llm/version.rb
CHANGED
data/lib/llm.rb
CHANGED
@@ -9,7 +9,7 @@ module LLM
|
|
9
9
|
require_relative "llm/model"
|
10
10
|
require_relative "llm/provider"
|
11
11
|
require_relative "llm/conversation"
|
12
|
-
require_relative "llm/
|
12
|
+
require_relative "llm/message_queue"
|
13
13
|
require_relative "llm/core_ext/ostruct"
|
14
14
|
|
15
15
|
module_function
|
@@ -46,6 +46,27 @@ RSpec.describe "LLM::Gemini: completions" do
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
+
context "when given a thread of messages",
|
50
|
+
vcr: {cassette_name: "gemini/completions/successful_response_thread"} do
|
51
|
+
subject(:response) do
|
52
|
+
gemini.complete "What is your name? What age are you?", :user, messages: [
|
53
|
+
{role: "user", content: "Answer all of my questions"},
|
54
|
+
{role: "user", content: "Your name is Pablo, you are 25 years old and you are my amigo"}
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
it "has choices" do
|
59
|
+
expect(response).to have_attributes(
|
60
|
+
choices: [
|
61
|
+
have_attributes(
|
62
|
+
role: "model",
|
63
|
+
content: "My name is Pablo, and I am 25 years old. ¡Amigo!\n"
|
64
|
+
)
|
65
|
+
]
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
49
70
|
context "when given an unauthorized response",
|
50
71
|
vcr: {cassette_name: "gemini/completions/unauthorized_response"} do
|
51
72
|
subject(:response) { gemini.complete("Hello!", :user) }
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "setup"
|
4
|
+
|
5
|
+
RSpec.describe "LLM::Conversation: non-lazy" do
|
4
6
|
shared_examples "a multi-turn conversation" do
|
5
7
|
context "when given a thread of messages" do
|
6
8
|
let(:inputs) do
|
@@ -54,3 +56,93 @@ RSpec.describe LLM::Conversation do
|
|
54
56
|
include_examples "a multi-turn conversation"
|
55
57
|
end
|
56
58
|
end
|
59
|
+
|
60
|
+
RSpec.describe "LLM::Conversation: lazy" do
|
61
|
+
let(:described_class) { LLM::Conversation }
|
62
|
+
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
63
|
+
let(:prompt) { "Keep your answers short and concise, and provide three answers to the three questions" }
|
64
|
+
|
65
|
+
context "with gemini",
|
66
|
+
vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
|
67
|
+
let(:provider) { LLM.gemini(token) }
|
68
|
+
let(:conversation) { described_class.new(provider).lazy }
|
69
|
+
|
70
|
+
context "when given a thread of messages" do
|
71
|
+
subject(:message) { conversation.messages.to_a[-1] }
|
72
|
+
|
73
|
+
before do
|
74
|
+
conversation.chat prompt
|
75
|
+
conversation.chat "What is 3+2 ?"
|
76
|
+
conversation.chat "What is 5+5 ?"
|
77
|
+
conversation.chat "What is 5+7 ?"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "maintains a conversation" do
|
81
|
+
is_expected.to have_attributes(
|
82
|
+
role: "model",
|
83
|
+
content: "5\n10\n12\n"
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "with openai" do
|
90
|
+
let(:provider) { LLM.openai(token) }
|
91
|
+
let(:conversation) { described_class.new(provider).lazy }
|
92
|
+
|
93
|
+
context "when given a thread of messages",
|
94
|
+
vcr: {cassette_name: "openai/lazy_conversation/successful_response"} do
|
95
|
+
subject(:message) { conversation.recent_message }
|
96
|
+
|
97
|
+
before do
|
98
|
+
conversation.chat prompt, :system
|
99
|
+
conversation.chat "What is 3+2 ?"
|
100
|
+
conversation.chat "What is 5+5 ?"
|
101
|
+
conversation.chat "What is 5+7 ?"
|
102
|
+
end
|
103
|
+
|
104
|
+
it "maintains a conversation" do
|
105
|
+
is_expected.to have_attributes(
|
106
|
+
role: "assistant",
|
107
|
+
content: "1. 5 \n2. 10 \n3. 12 "
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when given a specific model",
|
113
|
+
vcr: {cassette_name: "openai/lazy_conversation/successful_response_o3_mini"} do
|
114
|
+
let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
|
115
|
+
|
116
|
+
it "maintains the model throughout a conversation" do
|
117
|
+
conversation.chat(prompt, :system)
|
118
|
+
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
119
|
+
conversation.chat("What is 5+5?")
|
120
|
+
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "with ollama",
|
126
|
+
vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
|
127
|
+
let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
|
128
|
+
let(:conversation) { described_class.new(provider).lazy }
|
129
|
+
|
130
|
+
context "when given a thread of messages" do
|
131
|
+
subject(:message) { conversation.recent_message }
|
132
|
+
|
133
|
+
before do
|
134
|
+
conversation.chat prompt, :system
|
135
|
+
conversation.chat "What is 3+2 ?"
|
136
|
+
conversation.chat "What is 5+5 ?"
|
137
|
+
conversation.chat "What is 5+7 ?"
|
138
|
+
end
|
139
|
+
|
140
|
+
it "maintains a conversation" do
|
141
|
+
is_expected.to have_attributes(
|
142
|
+
role: "assistant",
|
143
|
+
content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -42,6 +42,27 @@ RSpec.describe "LLM::OpenAI: completions" do
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
+
context "when given a thread of messages",
|
46
|
+
vcr: {cassette_name: "openai/completions/successful_response_thread"} do
|
47
|
+
subject(:response) do
|
48
|
+
openai.complete "What is your name? What age are you?", :user, messages: [
|
49
|
+
{role: "system", content: "Answer all of my questions"},
|
50
|
+
{role: "system", content: "Your name is Pablo, you are 25 years old and you are my amigo"}
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
it "has choices" do
|
55
|
+
expect(response).to have_attributes(
|
56
|
+
choices: [
|
57
|
+
have_attributes(
|
58
|
+
role: "assistant",
|
59
|
+
content: "My name is Pablo, and I'm 25 years old! How can I help you today, amigo?"
|
60
|
+
)
|
61
|
+
]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
45
66
|
context "when given a 'bad request' response",
|
46
67
|
vcr: {cassette_name: "openai/completions/bad_request"} do
|
47
68
|
subject(:response) { openai.complete(URI("/foobar.exe"), :user) }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llm.rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Antar Azri
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2025-04-
|
12
|
+
date: 2025-04-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: net-http
|
@@ -197,7 +197,6 @@ files:
|
|
197
197
|
- lib/llm/error.rb
|
198
198
|
- lib/llm/file.rb
|
199
199
|
- lib/llm/http_client.rb
|
200
|
-
- lib/llm/lazy_conversation.rb
|
201
200
|
- lib/llm/message.rb
|
202
201
|
- lib/llm/message_queue.rb
|
203
202
|
- lib/llm/model.rb
|
@@ -235,7 +234,6 @@ files:
|
|
235
234
|
- spec/gemini/completion_spec.rb
|
236
235
|
- spec/gemini/embedding_spec.rb
|
237
236
|
- spec/llm/conversation_spec.rb
|
238
|
-
- spec/llm/lazy_conversation_spec.rb
|
239
237
|
- spec/ollama/completion_spec.rb
|
240
238
|
- spec/ollama/embedding_spec.rb
|
241
239
|
- spec/openai/completion_spec.rb
|
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LLM
|
4
|
-
require_relative "message_queue"
|
5
|
-
|
6
|
-
##
|
7
|
-
# {LLM::LazyConversation LLM::LazyConversation} provides a
|
8
|
-
# conversation object that allows input prompts to be queued
|
9
|
-
# and only sent to the LLM when a response is needed.
|
10
|
-
#
|
11
|
-
# @example
|
12
|
-
# llm = LLM.openai(key)
|
13
|
-
# bot = llm.chat("Be a helpful weather assistant", :system)
|
14
|
-
# bot.chat("What's the weather like in Rio?")
|
15
|
-
# bot.chat("What's the weather like in Algiers?")
|
16
|
-
# bot.messages.each do |message|
|
17
|
-
# # A single request is made at this point
|
18
|
-
# end
|
19
|
-
class LazyConversation
|
20
|
-
##
|
21
|
-
# @return [LLM::MessageQueue]
|
22
|
-
attr_reader :messages
|
23
|
-
|
24
|
-
##
|
25
|
-
# @param [LLM::Provider] provider
|
26
|
-
# A provider
|
27
|
-
def initialize(provider, params = {})
|
28
|
-
@provider = provider
|
29
|
-
@params = params
|
30
|
-
@messages = LLM::MessageQueue.new(provider)
|
31
|
-
end
|
32
|
-
|
33
|
-
##
|
34
|
-
# @param prompt (see LLM::Provider#prompt)
|
35
|
-
# @return [LLM::Conversation]
|
36
|
-
def chat(prompt, role = :user, **params)
|
37
|
-
tap { @messages << [prompt, role, @params.merge(params)] }
|
38
|
-
end
|
39
|
-
|
40
|
-
##
|
41
|
-
# @param [#to_s] role
|
42
|
-
# The role of the last message.
|
43
|
-
# Defaults to the LLM's assistant role (eg "assistant" or "model")
|
44
|
-
# @return [LLM::Message]
|
45
|
-
# The last message for the given role
|
46
|
-
def last_message(role: @provider.assistant_role)
|
47
|
-
messages.reverse_each.find { _1.role == role.to_s }
|
48
|
-
end
|
49
|
-
alias_method :recent_message, :last_message
|
50
|
-
end
|
51
|
-
end
|
@@ -1,92 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "setup"
|
4
|
-
|
5
|
-
RSpec.describe LLM::LazyConversation do
|
6
|
-
let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
|
7
|
-
let(:prompt) { "Keep your answers short and concise, and provide three answers to the three questions" }
|
8
|
-
|
9
|
-
context "with gemini",
|
10
|
-
vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
|
11
|
-
let(:provider) { LLM.gemini(token) }
|
12
|
-
let(:conversation) { described_class.new(provider) }
|
13
|
-
|
14
|
-
context "when given a thread of messages" do
|
15
|
-
subject(:message) { conversation.messages.to_a[-1] }
|
16
|
-
|
17
|
-
before do
|
18
|
-
conversation.chat prompt
|
19
|
-
conversation.chat "What is 3+2 ?"
|
20
|
-
conversation.chat "What is 5+5 ?"
|
21
|
-
conversation.chat "What is 5+7 ?"
|
22
|
-
end
|
23
|
-
|
24
|
-
it "maintains a conversation" do
|
25
|
-
is_expected.to have_attributes(
|
26
|
-
role: "model",
|
27
|
-
content: "5\n10\n12\n"
|
28
|
-
)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
context "with openai" do
|
34
|
-
let(:provider) { LLM.openai(token) }
|
35
|
-
let(:conversation) { described_class.new(provider) }
|
36
|
-
|
37
|
-
context "when given a thread of messages",
|
38
|
-
vcr: {cassette_name: "openai/lazy_conversation/successful_response"} do
|
39
|
-
subject(:message) { conversation.recent_message }
|
40
|
-
|
41
|
-
before do
|
42
|
-
conversation.chat prompt, :system
|
43
|
-
conversation.chat "What is 3+2 ?"
|
44
|
-
conversation.chat "What is 5+5 ?"
|
45
|
-
conversation.chat "What is 5+7 ?"
|
46
|
-
end
|
47
|
-
|
48
|
-
it "maintains a conversation" do
|
49
|
-
is_expected.to have_attributes(
|
50
|
-
role: "assistant",
|
51
|
-
content: "1. 5 \n2. 10 \n3. 12 "
|
52
|
-
)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
context "when given a specific model",
|
57
|
-
vcr: {cassette_name: "openai/lazy_conversation/successful_response_o3_mini"} do
|
58
|
-
let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]) }
|
59
|
-
|
60
|
-
it "maintains the model throughout a conversation" do
|
61
|
-
conversation.chat(prompt, :system)
|
62
|
-
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
63
|
-
conversation.chat("What is 5+5?")
|
64
|
-
expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
context "with ollama",
|
70
|
-
vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
|
71
|
-
let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
|
72
|
-
let(:conversation) { described_class.new(provider) }
|
73
|
-
|
74
|
-
context "when given a thread of messages" do
|
75
|
-
subject(:message) { conversation.recent_message }
|
76
|
-
|
77
|
-
before do
|
78
|
-
conversation.chat prompt, :system
|
79
|
-
conversation.chat "What is 3+2 ?"
|
80
|
-
conversation.chat "What is 5+5 ?"
|
81
|
-
conversation.chat "What is 5+7 ?"
|
82
|
-
end
|
83
|
-
|
84
|
-
it "maintains a conversation" do
|
85
|
-
is_expected.to have_attributes(
|
86
|
-
role: "assistant",
|
87
|
-
content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
|
88
|
-
)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|