langchainrb 0.13.4 → 0.14.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 +11 -0
- data/README.md +3 -18
- data/lib/langchain/assistants/assistant.rb +204 -79
- data/lib/langchain/assistants/messages/base.rb +35 -1
- data/lib/langchain/assistants/messages/ollama_message.rb +86 -0
- data/lib/langchain/assistants/thread.rb +8 -1
- data/lib/langchain/llm/ai21.rb +0 -4
- data/lib/langchain/llm/anthropic.rb +15 -6
- data/lib/langchain/llm/azure.rb +3 -3
- data/lib/langchain/llm/base.rb +1 -0
- data/lib/langchain/llm/cohere.rb +0 -2
- data/lib/langchain/llm/google_gemini.rb +17 -3
- data/lib/langchain/llm/google_palm.rb +1 -4
- data/lib/langchain/llm/ollama.rb +1 -1
- data/lib/langchain/llm/replicate.rb +1 -1
- data/lib/langchain/llm/response/google_gemini_response.rb +1 -1
- data/lib/langchain/llm/response/ollama_response.rb +19 -1
- data/lib/langchain/loader.rb +3 -1
- data/lib/langchain/utils/hash_transformer.rb +25 -0
- data/lib/langchain/vectorsearch/chroma.rb +3 -1
- data/lib/langchain/vectorsearch/milvus.rb +18 -3
- data/lib/langchain/version.rb +1 -1
- metadata +9 -27
- data/lib/langchain/utils/token_length/ai21_validator.rb +0 -41
- data/lib/langchain/utils/token_length/base_validator.rb +0 -42
- data/lib/langchain/utils/token_length/cohere_validator.rb +0 -49
- data/lib/langchain/utils/token_length/google_palm_validator.rb +0 -57
- data/lib/langchain/utils/token_length/openai_validator.rb +0 -138
- data/lib/langchain/utils/token_length/token_limit_exceeded.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68900cd116cf0fb1b77376a4906e5551f0d578ee2bb47c7ec86d32bf44f84e33
|
4
|
+
data.tar.gz: f68782c3cdc856799778618d78b6411a85b0c69adf6a4d33489b8025fdca3dce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 158410fd769caaf9074eddc1143ddee9256ac5a466a510c32b74d337eba62fab80b676661cbf1673604d236014a5cb4defdd4743e71abb713a659ddea0fe5e8c
|
7
|
+
data.tar.gz: 2e956356a443ff37ad711f6c42f8c4940925bcee4be075b403c78c3f702b487c12790dca9ba7d68a01acaf1c245b2910650b3f938e80cedd1fc2d5af14f7ffa8
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.14.0] - 2024-07-12
|
4
|
+
- Removed TokenLength validators
|
5
|
+
- Assistant works with a Mistral LLM now
|
6
|
+
- Assistant keeps track of tokens used
|
7
|
+
- Misc fixes and improvements
|
8
|
+
|
9
|
+
## [0.13.5] - 2024-07-01
|
10
|
+
- Add Milvus#remove_texts() method
|
11
|
+
- Langchain::Assistant has a `state` now
|
12
|
+
- Misc fixes and improvements
|
13
|
+
|
3
14
|
## [0.13.4] - 2024-06-16
|
4
15
|
- Fix Chroma#remove_texts() method
|
5
16
|
- Fix NewsRetriever Tool returning non UTF-8 characters
|
data/README.md
CHANGED
@@ -343,7 +343,7 @@ You can instantiate any other supported vector search database:
|
|
343
343
|
client = Langchain::Vectorsearch::Chroma.new(...) # `gem "chroma-db", "~> 0.6.0"`
|
344
344
|
client = Langchain::Vectorsearch::Epsilla.new(...) # `gem "epsilla-ruby", "~> 0.0.3"`
|
345
345
|
client = Langchain::Vectorsearch::Hnswlib.new(...) # `gem "hnswlib", "~> 0.8.1"`
|
346
|
-
client = Langchain::Vectorsearch::Milvus.new(...) # `gem "milvus", "~> 0.9.
|
346
|
+
client = Langchain::Vectorsearch::Milvus.new(...) # `gem "milvus", "~> 0.9.3"`
|
347
347
|
client = Langchain::Vectorsearch::Pinecone.new(...) # `gem "pinecone", "~> 0.1.6"`
|
348
348
|
client = Langchain::Vectorsearch::Pgvector.new(...) # `gem "pgvector", "~> 0.2"`
|
349
349
|
client = Langchain::Vectorsearch::Qdrant.new(...) # `gem "qdrant-ruby", "~> 0.9.3"`
|
@@ -428,25 +428,10 @@ Assistants are Agent-like objects that leverage helpful instructions, LLMs, tool
|
|
428
428
|
```ruby
|
429
429
|
llm = Langchain::LLM::OpenAI.new(api_key: ENV["OPENAI_API_KEY"])
|
430
430
|
```
|
431
|
-
2. Instantiate
|
432
|
-
```ruby
|
433
|
-
thread = Langchain::Thread.new
|
434
|
-
```
|
435
|
-
You can pass old message from previously using the Assistant:
|
436
|
-
```ruby
|
437
|
-
thread.messages = messages
|
438
|
-
```
|
439
|
-
Messages contain the conversation history and the whole message history is sent to the LLM every time. A Message belongs to 1 of the 4 roles:
|
440
|
-
* `Message(role: "system")` message usually contains the instructions.
|
441
|
-
* `Message(role: "user")` messages come from the user.
|
442
|
-
* `Message(role: "assistant")` messages are produced by the LLM.
|
443
|
-
* `Message(role: "tool")` messages are sent in response to tool calls with tool outputs.
|
444
|
-
|
445
|
-
3. Instantiate an Assistant
|
431
|
+
2. Instantiate an Assistant
|
446
432
|
```ruby
|
447
433
|
assistant = Langchain::Assistant.new(
|
448
434
|
llm: llm,
|
449
|
-
thread: thread,
|
450
435
|
instructions: "You are a Meteorologist Assistant that is able to pull the weather for any location",
|
451
436
|
tools: [
|
452
437
|
Langchain::Tool::Weather.new(api_key: ENV["OPEN_WEATHER_API_KEY"])
|
@@ -482,7 +467,7 @@ assistant.add_message_and_run content: "What about Sacramento, CA?", auto_tool_e
|
|
482
467
|
### Accessing Thread messages
|
483
468
|
You can access the messages in a Thread by calling `assistant.thread.messages`.
|
484
469
|
```ruby
|
485
|
-
assistant.
|
470
|
+
assistant.messages
|
486
471
|
```
|
487
472
|
|
488
473
|
The Assistant checks the context window limits before every request to the LLM and remove oldest thread messages one by one if the context window is exceeded.
|
@@ -15,14 +15,16 @@ module Langchain
|
|
15
15
|
extend Forwardable
|
16
16
|
def_delegators :thread, :messages, :messages=
|
17
17
|
|
18
|
-
attr_reader :llm, :thread, :instructions
|
18
|
+
attr_reader :llm, :thread, :instructions, :state
|
19
|
+
attr_reader :total_prompt_tokens, :total_completion_tokens, :total_tokens
|
19
20
|
attr_accessor :tools
|
20
21
|
|
21
22
|
SUPPORTED_LLMS = [
|
22
23
|
Langchain::LLM::Anthropic,
|
23
|
-
Langchain::LLM::OpenAI,
|
24
24
|
Langchain::LLM::GoogleGemini,
|
25
|
-
Langchain::LLM::GoogleVertexAI
|
25
|
+
Langchain::LLM::GoogleVertexAI,
|
26
|
+
Langchain::LLM::Ollama,
|
27
|
+
Langchain::LLM::OpenAI
|
26
28
|
]
|
27
29
|
|
28
30
|
# Create a new assistant
|
@@ -40,20 +42,26 @@ module Langchain
|
|
40
42
|
unless SUPPORTED_LLMS.include?(llm.class)
|
41
43
|
raise ArgumentError, "Invalid LLM; currently only #{SUPPORTED_LLMS.join(", ")} are supported"
|
42
44
|
end
|
45
|
+
if llm.is_a?(Langchain::LLM::Ollama)
|
46
|
+
raise ArgumentError, "Currently only `mistral:7b-instruct-v0.3-fp16` model is supported for Ollama LLM" unless llm.defaults[:completion_model_name] == "mistral:7b-instruct-v0.3-fp16"
|
47
|
+
end
|
43
48
|
raise ArgumentError, "Tools must be an array of Langchain::Tool::Base instance(s)" unless tools.is_a?(Array) && tools.all? { |tool| tool.is_a?(Langchain::Tool::Base) }
|
44
49
|
|
45
50
|
@llm = llm
|
46
51
|
@thread = thread || Langchain::Thread.new
|
47
52
|
@tools = tools
|
48
53
|
@instructions = instructions
|
54
|
+
@state = :ready
|
55
|
+
|
56
|
+
@total_prompt_tokens = 0
|
57
|
+
@total_completion_tokens = 0
|
58
|
+
@total_tokens = 0
|
49
59
|
|
50
60
|
raise ArgumentError, "Thread must be an instance of Langchain::Thread" unless @thread.is_a?(Langchain::Thread)
|
51
61
|
|
52
62
|
# The first message in the thread should be the system instructions
|
53
63
|
# TODO: What if the user added old messages and the system instructions are already in there? Should this overwrite the existing instructions?
|
54
|
-
|
55
|
-
add_message(role: "system", content: instructions) if instructions
|
56
|
-
end
|
64
|
+
initialize_instructions
|
57
65
|
# For Google Gemini, and Anthropic system instructions are added to the `system:` param in the `chat` method
|
58
66
|
end
|
59
67
|
|
@@ -66,7 +74,10 @@ module Langchain
|
|
66
74
|
# @return [Array<Langchain::Message>] The messages in the thread
|
67
75
|
def add_message(content: nil, role: "user", tool_calls: [], tool_call_id: nil)
|
68
76
|
message = build_message(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
69
|
-
thread.add_message(message)
|
77
|
+
messages = thread.add_message(message)
|
78
|
+
@state = :ready
|
79
|
+
|
80
|
+
messages
|
70
81
|
end
|
71
82
|
|
72
83
|
# Run the assistant
|
@@ -76,56 +87,12 @@ module Langchain
|
|
76
87
|
def run(auto_tool_execution: false)
|
77
88
|
if thread.messages.empty?
|
78
89
|
Langchain.logger.warn("No messages in the thread")
|
90
|
+
@state = :completed
|
79
91
|
return
|
80
92
|
end
|
81
93
|
|
82
|
-
|
83
|
-
|
84
|
-
while running
|
85
|
-
# TODO: I think we need to look at all messages and not just the last one.
|
86
|
-
last_message = thread.messages.last
|
87
|
-
|
88
|
-
if last_message.system?
|
89
|
-
# Do nothing
|
90
|
-
running = false
|
91
|
-
elsif last_message.llm?
|
92
|
-
if last_message.tool_calls.any?
|
93
|
-
if auto_tool_execution
|
94
|
-
run_tools(last_message.tool_calls)
|
95
|
-
else
|
96
|
-
# Maybe log and tell the user that there's outstanding tool calls?
|
97
|
-
running = false
|
98
|
-
end
|
99
|
-
else
|
100
|
-
# Last message was from the assistant without any tools calls.
|
101
|
-
# Do nothing
|
102
|
-
running = false
|
103
|
-
end
|
104
|
-
elsif last_message.user?
|
105
|
-
# Run it!
|
106
|
-
response = chat_with_llm
|
107
|
-
|
108
|
-
if response.tool_calls.any?
|
109
|
-
# Re-run the while(running) loop to process the tool calls
|
110
|
-
running = true
|
111
|
-
add_message(role: response.role, tool_calls: response.tool_calls)
|
112
|
-
elsif response.chat_completion
|
113
|
-
# Stop the while(running) loop and add the assistant's response to the thread
|
114
|
-
running = false
|
115
|
-
add_message(role: response.role, content: response.chat_completion)
|
116
|
-
end
|
117
|
-
elsif last_message.tool?
|
118
|
-
# Run it!
|
119
|
-
response = chat_with_llm
|
120
|
-
running = true
|
121
|
-
|
122
|
-
if response.tool_calls.any?
|
123
|
-
add_message(role: response.role, tool_calls: response.tool_calls)
|
124
|
-
elsif response.chat_completion
|
125
|
-
add_message(role: response.role, content: response.chat_completion)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
94
|
+
@state = :in_progress
|
95
|
+
@state = handle_state until run_finished?(auto_tool_execution)
|
129
96
|
|
130
97
|
thread.messages
|
131
98
|
end
|
@@ -146,13 +113,7 @@ module Langchain
|
|
146
113
|
# @param output [String] The output of the tool
|
147
114
|
# @return [Array<Langchain::Message>] The messages in the thread
|
148
115
|
def submit_tool_output(tool_call_id:, output:)
|
149
|
-
tool_role =
|
150
|
-
Langchain::Messages::OpenAIMessage::TOOL_ROLE
|
151
|
-
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
152
|
-
Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
|
153
|
-
elsif llm.is_a?(Langchain::LLM::Anthropic)
|
154
|
-
Langchain::Messages::AnthropicMessage::TOOL_ROLE
|
155
|
-
end
|
116
|
+
tool_role = determine_tool_role
|
156
117
|
|
157
118
|
# TODO: Validate that `tool_call_id` is valid by scanning messages and checking if this tool call ID was invoked
|
158
119
|
add_message(role: tool_role, content: output, tool_call_id: tool_call_id)
|
@@ -183,31 +144,181 @@ module Langchain
|
|
183
144
|
|
184
145
|
private
|
185
146
|
|
147
|
+
# Check if the run is finished
|
148
|
+
#
|
149
|
+
# @param auto_tool_execution [Boolean] Whether or not to automatically run tools
|
150
|
+
# @return [Boolean] Whether the run is finished
|
151
|
+
def run_finished?(auto_tool_execution)
|
152
|
+
finished_states = [:completed, :failed]
|
153
|
+
|
154
|
+
requires_manual_action = (@state == :requires_action) && !auto_tool_execution
|
155
|
+
finished_states.include?(@state) || requires_manual_action
|
156
|
+
end
|
157
|
+
|
158
|
+
# Handle the current state and transition to the next state
|
159
|
+
#
|
160
|
+
# @return [Symbol] The next state
|
161
|
+
def handle_state
|
162
|
+
case @state
|
163
|
+
when :in_progress
|
164
|
+
process_latest_message
|
165
|
+
when :requires_action
|
166
|
+
execute_tools
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Process the latest message in the thread
|
171
|
+
#
|
172
|
+
# @return [Symbol] The next state
|
173
|
+
def process_latest_message
|
174
|
+
last_message = thread.messages.last
|
175
|
+
|
176
|
+
case last_message.standard_role
|
177
|
+
when :system
|
178
|
+
handle_system_message
|
179
|
+
when :llm
|
180
|
+
handle_llm_message
|
181
|
+
when :user, :tool
|
182
|
+
handle_user_or_tool_message
|
183
|
+
else
|
184
|
+
handle_unexpected_message
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Handle system message scenario
|
189
|
+
#
|
190
|
+
# @return [Symbol] The completed state
|
191
|
+
def handle_system_message
|
192
|
+
Langchain.logger.warn("At least one user message is required after a system message")
|
193
|
+
:completed
|
194
|
+
end
|
195
|
+
|
196
|
+
# Handle LLM message scenario
|
197
|
+
#
|
198
|
+
# @return [Symbol] The next state
|
199
|
+
def handle_llm_message
|
200
|
+
thread.messages.last.tool_calls.any? ? :requires_action : :completed
|
201
|
+
end
|
202
|
+
|
203
|
+
# Handle unexpected message scenario
|
204
|
+
#
|
205
|
+
# @return [Symbol] The failed state
|
206
|
+
def handle_unexpected_message
|
207
|
+
Langchain.logger.error("Unexpected message role encountered: #{thread.messages.last.standard_role}")
|
208
|
+
:failed
|
209
|
+
end
|
210
|
+
|
211
|
+
# Handle user or tool message scenario by processing the LLM response
|
212
|
+
#
|
213
|
+
# @return [Symbol] The next state
|
214
|
+
def handle_user_or_tool_message
|
215
|
+
response = chat_with_llm
|
216
|
+
|
217
|
+
# With Ollama, we're calling the `llm.complete()` method
|
218
|
+
content = if llm.is_a?(Langchain::LLM::Ollama)
|
219
|
+
response.completion
|
220
|
+
else
|
221
|
+
response.chat_completion
|
222
|
+
end
|
223
|
+
|
224
|
+
add_message(role: response.role, content: content, tool_calls: response.tool_calls)
|
225
|
+
record_used_tokens(response.prompt_tokens, response.completion_tokens, response.total_tokens)
|
226
|
+
|
227
|
+
set_state_for(response: response)
|
228
|
+
end
|
229
|
+
|
230
|
+
def set_state_for(response:)
|
231
|
+
if response.tool_calls.any?
|
232
|
+
:in_progress
|
233
|
+
elsif response.chat_completion
|
234
|
+
:completed
|
235
|
+
elsif response.completion # Currently only used by Ollama
|
236
|
+
:completed
|
237
|
+
else
|
238
|
+
Langchain.logger.error("LLM response does not contain tool calls, chat or completion response")
|
239
|
+
:failed
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Execute the tools based on the tool calls in the last message
|
244
|
+
#
|
245
|
+
# @return [Symbol] The next state
|
246
|
+
def execute_tools
|
247
|
+
run_tools(thread.messages.last.tool_calls)
|
248
|
+
:in_progress
|
249
|
+
rescue => e
|
250
|
+
Langchain.logger.error("Error running tools: #{e.message}")
|
251
|
+
:failed
|
252
|
+
end
|
253
|
+
|
254
|
+
# Determine the tool role based on the LLM type
|
255
|
+
#
|
256
|
+
# @return [String] The tool role
|
257
|
+
def determine_tool_role
|
258
|
+
case llm
|
259
|
+
when Langchain::LLM::Ollama
|
260
|
+
Langchain::Messages::OllamaMessage::TOOL_ROLE
|
261
|
+
when Langchain::LLM::OpenAI
|
262
|
+
Langchain::Messages::OpenAIMessage::TOOL_ROLE
|
263
|
+
when Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI
|
264
|
+
Langchain::Messages::GoogleGeminiMessage::TOOL_ROLE
|
265
|
+
when Langchain::LLM::Anthropic
|
266
|
+
Langchain::Messages::AnthropicMessage::TOOL_ROLE
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def initialize_instructions
|
271
|
+
if llm.is_a?(Langchain::LLM::Ollama)
|
272
|
+
content = String.new # rubocop: disable Performance/UnfreezeString
|
273
|
+
if tools.any?
|
274
|
+
content << %([AVAILABLE_TOOLS] #{tools.map(&:to_openai_tools).flatten}[/AVAILABLE_TOOLS])
|
275
|
+
end
|
276
|
+
if instructions
|
277
|
+
content << "[INST] #{instructions}[/INST]"
|
278
|
+
end
|
279
|
+
|
280
|
+
add_message(role: "system", content: content)
|
281
|
+
elsif llm.is_a?(Langchain::LLM::OpenAI)
|
282
|
+
add_message(role: "system", content: instructions) if instructions
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
186
286
|
# Call to the LLM#chat() method
|
187
287
|
#
|
188
288
|
# @return [Langchain::LLM::BaseResponse] The LLM response object
|
189
289
|
def chat_with_llm
|
190
290
|
Langchain.logger.info("Sending a call to #{llm.class}", for: self.class)
|
191
291
|
|
192
|
-
params = {
|
292
|
+
params = {}
|
193
293
|
|
194
|
-
if
|
195
|
-
if
|
294
|
+
if llm.is_a?(Langchain::LLM::OpenAI)
|
295
|
+
if tools.any?
|
196
296
|
params[:tools] = tools.map(&:to_openai_tools).flatten
|
197
297
|
params[:tool_choice] = "auto"
|
198
|
-
|
298
|
+
end
|
299
|
+
elsif llm.is_a?(Langchain::LLM::Anthropic)
|
300
|
+
if tools.any?
|
199
301
|
params[:tools] = tools.map(&:to_anthropic_tools).flatten
|
200
|
-
params[:system] = instructions if instructions
|
201
302
|
params[:tool_choice] = {type: "auto"}
|
202
|
-
|
303
|
+
end
|
304
|
+
params[:system] = instructions if instructions
|
305
|
+
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
306
|
+
if tools.any?
|
203
307
|
params[:tools] = tools.map(&:to_google_gemini_tools).flatten
|
204
308
|
params[:system] = instructions if instructions
|
205
309
|
params[:tool_choice] = "auto"
|
206
310
|
end
|
207
|
-
# TODO: Not sure that tool_choice should always be "auto"; Maybe we can let the user toggle it.
|
208
311
|
end
|
209
|
-
|
210
|
-
|
312
|
+
# TODO: Not sure that tool_choice should always be "auto"; Maybe we can let the user toggle it.
|
313
|
+
|
314
|
+
if llm.is_a?(Langchain::LLM::Ollama)
|
315
|
+
params[:raw] = true
|
316
|
+
params[:prompt] = thread.prompt_of_concatenated_messages
|
317
|
+
llm.complete(**params)
|
318
|
+
else
|
319
|
+
params[:messages] = thread.array_of_message_hashes
|
320
|
+
llm.chat(**params)
|
321
|
+
end
|
211
322
|
end
|
212
323
|
|
213
324
|
# Run the tools automatically
|
@@ -216,7 +327,9 @@ module Langchain
|
|
216
327
|
def run_tools(tool_calls)
|
217
328
|
# Iterate over each function invocation and submit tool output
|
218
329
|
tool_calls.each do |tool_call|
|
219
|
-
tool_call_id, tool_name, method_name, tool_arguments = if llm.is_a?(Langchain::LLM::
|
330
|
+
tool_call_id, tool_name, method_name, tool_arguments = if llm.is_a?(Langchain::LLM::Ollama)
|
331
|
+
extract_ollama_tool_call(tool_call: tool_call)
|
332
|
+
elsif llm.is_a?(Langchain::LLM::OpenAI)
|
220
333
|
extract_openai_tool_call(tool_call: tool_call)
|
221
334
|
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
222
335
|
extract_google_gemini_tool_call(tool_call: tool_call)
|
@@ -232,14 +345,12 @@ module Langchain
|
|
232
345
|
|
233
346
|
submit_tool_output(tool_call_id: tool_call_id, output: output)
|
234
347
|
end
|
348
|
+
end
|
235
349
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
elsif response.chat_completion
|
241
|
-
add_message(role: response.role, content: response.chat_completion)
|
242
|
-
end
|
350
|
+
def extract_ollama_tool_call(tool_call:)
|
351
|
+
tool_name, method_name = tool_call.dig("name").split("__")
|
352
|
+
tool_arguments = tool_call.dig("arguments").transform_keys(&:to_sym)
|
353
|
+
[nil, tool_name, method_name, tool_arguments]
|
243
354
|
end
|
244
355
|
|
245
356
|
# Extract the tool call information from the OpenAI tool call hash
|
@@ -292,7 +403,9 @@ module Langchain
|
|
292
403
|
# @param tool_call_id [String] The ID of the tool call to include in the message
|
293
404
|
# @return [Langchain::Message] The Message object
|
294
405
|
def build_message(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
295
|
-
if llm.is_a?(Langchain::LLM::
|
406
|
+
if llm.is_a?(Langchain::LLM::Ollama)
|
407
|
+
Langchain::Messages::OllamaMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
408
|
+
elsif llm.is_a?(Langchain::LLM::OpenAI)
|
296
409
|
Langchain::Messages::OpenAIMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
297
410
|
elsif [Langchain::LLM::GoogleGemini, Langchain::LLM::GoogleVertexAI].include?(llm.class)
|
298
411
|
Langchain::Messages::GoogleGeminiMessage.new(role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id)
|
@@ -301,6 +414,18 @@ module Langchain
|
|
301
414
|
end
|
302
415
|
end
|
303
416
|
|
417
|
+
# Increment the tokens count based on the last interaction with the LLM
|
418
|
+
#
|
419
|
+
# @param prompt_tokens [Integer] The number of used prmopt tokens
|
420
|
+
# @param completion_tokens [Integer] The number of used completion tokens
|
421
|
+
# @param total_tokens [Integer] The total number of used tokens
|
422
|
+
# @return [Integer] The current total tokens count
|
423
|
+
def record_used_tokens(prompt_tokens, completion_tokens, total_tokens_from_operation)
|
424
|
+
@total_prompt_tokens += prompt_tokens if prompt_tokens
|
425
|
+
@total_completion_tokens += completion_tokens if completion_tokens
|
426
|
+
@total_tokens += total_tokens_from_operation if total_tokens_from_operation
|
427
|
+
end
|
428
|
+
|
304
429
|
# TODO: Fix the message truncation when context window is exceeded
|
305
430
|
end
|
306
431
|
end
|
@@ -7,10 +7,44 @@ module Langchain
|
|
7
7
|
|
8
8
|
# Check if the message came from a user
|
9
9
|
#
|
10
|
-
# @
|
10
|
+
# @return [Boolean] true/false whether the message came from a user
|
11
11
|
def user?
|
12
12
|
role == "user"
|
13
13
|
end
|
14
|
+
|
15
|
+
# Check if the message came from an LLM
|
16
|
+
#
|
17
|
+
# @raise NotImplementedError if the subclass does not implement this method
|
18
|
+
def llm?
|
19
|
+
raise NotImplementedError, "Class #{self.class.name} must implement the method 'llm?'"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if the message is a tool call
|
23
|
+
#
|
24
|
+
# @raise NotImplementedError if the subclass does not implement this method
|
25
|
+
def tool?
|
26
|
+
raise NotImplementedError, "Class #{self.class.name} must implement the method 'tool?'"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if the message is a system prompt
|
30
|
+
#
|
31
|
+
# @raise NotImplementedError if the subclass does not implement this method
|
32
|
+
def system?
|
33
|
+
raise NotImplementedError, "Class #{self.class.name} must implement the method 'system?'"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the standardized role symbol based on the specific role methods
|
37
|
+
#
|
38
|
+
# @return [Symbol] the standardized role symbol (:system, :llm, :tool, :user, or :unknown)
|
39
|
+
def standard_role
|
40
|
+
return :user if user?
|
41
|
+
return :llm if llm?
|
42
|
+
return :tool if tool?
|
43
|
+
return :system if system?
|
44
|
+
|
45
|
+
# TODO: Should we return :unknown or raise an error?
|
46
|
+
:unknown
|
47
|
+
end
|
14
48
|
end
|
15
49
|
end
|
16
50
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Langchain
|
4
|
+
module Messages
|
5
|
+
class OllamaMessage < Base
|
6
|
+
# OpenAI uses the following roles:
|
7
|
+
ROLES = [
|
8
|
+
"system",
|
9
|
+
"assistant",
|
10
|
+
"user",
|
11
|
+
"tool"
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
TOOL_ROLE = "tool"
|
15
|
+
|
16
|
+
# Initialize a new OpenAI message
|
17
|
+
#
|
18
|
+
# @param [String] The role of the message
|
19
|
+
# @param [String] The content of the message
|
20
|
+
# @param [Array<Hash>] The tool calls made in the message
|
21
|
+
# @param [String] The ID of the tool call
|
22
|
+
def initialize(role:, content: nil, tool_calls: [], tool_call_id: nil)
|
23
|
+
raise ArgumentError, "Role must be one of #{ROLES.join(", ")}" unless ROLES.include?(role)
|
24
|
+
raise ArgumentError, "Tool calls must be an array of hashes" unless tool_calls.is_a?(Array) && tool_calls.all? { |tool_call| tool_call.is_a?(Hash) }
|
25
|
+
|
26
|
+
@role = role
|
27
|
+
# Some Tools return content as a JSON hence `.to_s`
|
28
|
+
@content = content.to_s
|
29
|
+
@tool_calls = tool_calls
|
30
|
+
@tool_call_id = tool_call_id
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
send(:"to_#{role}_message_string")
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_system_message_string
|
38
|
+
content
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_user_message_string
|
42
|
+
"[INST] #{content}[/INST]"
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_tool_message_string
|
46
|
+
"[TOOL_RESULTS] #{content}[/TOOL_RESULTS]"
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_assistant_message_string
|
50
|
+
if tool_calls.any?
|
51
|
+
%("[TOOL_CALLS] #{tool_calls}")
|
52
|
+
else
|
53
|
+
content
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if the message came from an LLM
|
58
|
+
#
|
59
|
+
# @return [Boolean] true/false whether this message was produced by an LLM
|
60
|
+
def llm?
|
61
|
+
assistant?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if the message came from an LLM
|
65
|
+
#
|
66
|
+
# @return [Boolean] true/false whether this message was produced by an LLM
|
67
|
+
def assistant?
|
68
|
+
role == "assistant"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if the message are system instructions
|
72
|
+
#
|
73
|
+
# @return [Boolean] true/false whether this message are system instructions
|
74
|
+
def system?
|
75
|
+
role == "system"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check if the message is a tool call
|
79
|
+
#
|
80
|
+
# @return [Boolean] true/false whether this message is a tool call
|
81
|
+
def tool?
|
82
|
+
role == "tool"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -17,7 +17,14 @@ module Langchain
|
|
17
17
|
#
|
18
18
|
# @return [Array<Hash>] The thread as an OpenAI API-compatible array of hashes
|
19
19
|
def array_of_message_hashes
|
20
|
-
messages
|
20
|
+
messages
|
21
|
+
.map(&:to_hash)
|
22
|
+
.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
# Only used by the Assistant when it calls the LLM#complete() method
|
26
|
+
def prompt_of_concatenated_messages
|
27
|
+
messages.map(&:to_s).join
|
21
28
|
end
|
22
29
|
|
23
30
|
# Add a message to the thread
|
data/lib/langchain/llm/ai21.rb
CHANGED
@@ -16,8 +16,6 @@ module Langchain::LLM
|
|
16
16
|
model: "j2-ultra"
|
17
17
|
}.freeze
|
18
18
|
|
19
|
-
LENGTH_VALIDATOR = Langchain::Utils::TokenLength::AI21Validator
|
20
|
-
|
21
19
|
def initialize(api_key:, default_options: {})
|
22
20
|
depends_on "ai21"
|
23
21
|
|
@@ -35,8 +33,6 @@ module Langchain::LLM
|
|
35
33
|
def complete(prompt:, **params)
|
36
34
|
parameters = complete_parameters params
|
37
35
|
|
38
|
-
parameters[:maxTokens] = LENGTH_VALIDATOR.validate_max_tokens!(prompt, parameters[:model], {llm: client})
|
39
|
-
|
40
36
|
response = client.complete(prompt, parameters)
|
41
37
|
Langchain::LLM::AI21Response.new response, model: parameters[:model]
|
42
38
|
end
|
@@ -5,10 +5,10 @@ module Langchain::LLM
|
|
5
5
|
# Wrapper around Anthropic APIs.
|
6
6
|
#
|
7
7
|
# Gem requirements:
|
8
|
-
# gem "anthropic", "~> 0.
|
8
|
+
# gem "anthropic", "~> 0.3.0"
|
9
9
|
#
|
10
10
|
# Usage:
|
11
|
-
#
|
11
|
+
# anthropic = Langchain::LLM::Anthropic.new(api_key: ENV["ANTHROPIC_API_KEY"])
|
12
12
|
#
|
13
13
|
class Anthropic < Base
|
14
14
|
DEFAULTS = {
|
@@ -18,9 +18,6 @@ module Langchain::LLM
|
|
18
18
|
max_tokens_to_sample: 256
|
19
19
|
}.freeze
|
20
20
|
|
21
|
-
# TODO: Implement token length validator for Anthropic
|
22
|
-
# LENGTH_VALIDATOR = Langchain::Utils::TokenLength::AnthropicValidator
|
23
|
-
|
24
21
|
# Initialize an Anthropic LLM instance
|
25
22
|
#
|
26
23
|
# @param api_key [String] The API key to use
|
@@ -81,7 +78,10 @@ module Langchain::LLM
|
|
81
78
|
parameters[:metadata] = metadata if metadata
|
82
79
|
parameters[:stream] = stream if stream
|
83
80
|
|
84
|
-
response =
|
81
|
+
response = with_api_error_handling do
|
82
|
+
client.complete(parameters: parameters)
|
83
|
+
end
|
84
|
+
|
85
85
|
Langchain::LLM::AnthropicResponse.new(response)
|
86
86
|
end
|
87
87
|
|
@@ -114,6 +114,15 @@ module Langchain::LLM
|
|
114
114
|
Langchain::LLM::AnthropicResponse.new(response)
|
115
115
|
end
|
116
116
|
|
117
|
+
def with_api_error_handling
|
118
|
+
response = yield
|
119
|
+
return if response.empty?
|
120
|
+
|
121
|
+
raise Langchain::LLM::ApiError.new "Anthropic API error: #{response.dig("error", "message")}" if response&.dig("error")
|
122
|
+
|
123
|
+
response
|
124
|
+
end
|
125
|
+
|
117
126
|
private
|
118
127
|
|
119
128
|
def set_extra_headers!
|