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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7f9330df025e16999629b094202d78b504de9bedeafc45ec8d75bf729024567
4
- data.tar.gz: e3a22daa5ac7add9b815346fe810ed368b9f3e3a3bb135cb4e5bbbb3875eacae
3
+ metadata.gz: 732a483717a5ec8e443077fb71294b1e301c3a8867b225c1fc2a58bd02fe3130
4
+ data.tar.gz: a1c2591a07c413cebfdffa99d133855bb177cc4a6607860333dbc9991da8d33e
5
5
  SHA512:
6
- metadata.gz: b985c6f0b967354f3d1e5941ebeb2e5d6623b9d0abc33fd238f3764e72a6a6e10c293e7e61221d1be8ad89e631ecc7236bca597e809f1598ad54f0b305bb1103
7
- data.tar.gz: 721f373b9eada6417b864e723504e5168df8f4362bef9edb4026aad2b39a3f99de5c72081f40ab80828d2f53d24c62c55e035ccd4964e3dadca2a27f3784e6e6
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::LazyConversation](https://0x1eef.github.io/x/llm/LLM/LazyConversation.html)
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)
@@ -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 act as the
7
- # context of the conversation.
8
- #
6
+ # object that maintains a thread of messages that acts as context
7
+ # throughout the conversation.
9
8
  # @example
10
- # llm = LLM.openai(key)
11
- # bot = llm.chat("What is the capital of France?")
12
- # bot.chat("What should we eat in Paris?")
13
- # bot.chat("What is the weather like in Paris?")
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
- completion = @provider.complete(prompt, role, **@params.merge(params.merge(messages:)))
35
- @messages.concat [Message.new(role, prompt), completion.choices[0]]
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
@@ -13,7 +13,8 @@ module LLM
13
13
  # @return [LLM::MessageQueue]
14
14
  def initialize(provider)
15
15
  @provider = provider
16
- @messages = []
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
- @messages = complete! unless @messages.grep(LLM::Message).size == @messages.size
26
- @messages.each { yield(_1) }
26
+ complete! unless @pending.empty?
27
+ @completed.each { yield(_1) }
27
28
  end
28
29
 
29
30
  ##
30
- # @param message [Object]
31
- # A message to add to the conversation thread
31
+ # @param [[LLM::Message, Hash]] item
32
+ # A message and its parameters
32
33
  # @return [void]
33
- def <<(message)
34
- @messages << message
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
- prompt, role, params = @messages[-1]
42
- rest = @messages[0..-2].map { (Array === _1) ? LLM::Message.new(_1[1], _1[0]) : _1 }
43
- comp = @provider.complete(prompt, role, **params.merge(messages: rest)).choices.last
44
- [*rest, LLM::Message.new(role, prompt), comp]
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::LazyConversation.new(self, params).chat(prompt, role)
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 { {role: _1.role, content: format_content(_1.content)} }
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 { {role: _1.role, parts: [format_content(_1.content)]} }
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 { {role: _1.role, content: format_content(_1.content)} }
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 { {role: _1.role, content: format_content(_1.content)} }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
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/lazy_conversation"
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
- RSpec.describe LLM::Conversation do
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.0
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-01 00:00:00.000000000 Z
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