langchainrb 0.13.4 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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!
|