ruby_llm 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -2
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +41 -7
- data/lib/ruby_llm/active_record/chat_methods.rb +41 -7
- data/lib/ruby_llm/agent.rb +323 -0
- data/lib/ruby_llm/aliases.json +50 -32
- data/lib/ruby_llm/chat.rb +27 -3
- data/lib/ruby_llm/configuration.rb +4 -0
- data/lib/ruby_llm/models.json +19806 -5991
- data/lib/ruby_llm/models.rb +35 -6
- data/lib/ruby_llm/provider.rb +13 -1
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/azure/chat.rb +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +297 -56
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +88 -65
- data/lib/ruby_llm/providers/bedrock/streaming.rb +305 -8
- data/lib/ruby_llm/providers/bedrock.rb +61 -52
- data/lib/ruby_llm/providers/openai/media.rb +1 -1
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +28 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +10 -4
- data/lib/tasks/vcr.rake +32 -0
- metadata +16 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -128
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -85
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8ff2f4da0c39e4909925217affa2b76908207e2c936a6dc05b56cffb2781863
|
|
4
|
+
data.tar.gz: 0eebb76434b049d4332a247e7581bb721d4dd86b0496be8655d15a4adcb42f65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95a3eb5a1c6a50c69dd8166044d99d77848fc8dcdd5d31b748e8303a3e8c5756cc86a8ce0220dd3c643bc0b32080f6c2125c79f5e437bb654725a119b4b4b601
|
|
7
|
+
data.tar.gz: 5efbd317193ca7f5e28df0a2d9bcc7ffd8a1740f40d29e7b872bed8a2ee910db376dc2dc890723b0c253718404993e55deab87356d414afdfd741d9392bd5e0f
|
data/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
Build chatbots, AI agents, RAG applications. Works with OpenAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
|
|
25
|
+
Build chatbots, AI agents, RAG applications. Works with OpenAI, xAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
|
|
26
26
|
|
|
27
27
|
## Why RubyLLM?
|
|
28
28
|
|
|
@@ -95,6 +95,17 @@ end
|
|
|
95
95
|
chat.with_tool(Weather).ask "What's the weather in Berlin?"
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
```ruby
|
|
99
|
+
# Define an agent with instructions + tools
|
|
100
|
+
class WeatherAssistant < RubyLLM::Agent
|
|
101
|
+
model "gpt-4.1-nano"
|
|
102
|
+
instructions "Be concise and always use tools for weather."
|
|
103
|
+
tools Weather
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
WeatherAssistant.new.ask "What's the weather in Berlin?"
|
|
107
|
+
```
|
|
108
|
+
|
|
98
109
|
```ruby
|
|
99
110
|
# Get structured output
|
|
100
111
|
class ProductSchema < RubyLLM::Schema
|
|
@@ -118,13 +129,14 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
|
|
|
118
129
|
* **Embeddings:** Generate embeddings with `RubyLLM.embed`
|
|
119
130
|
* **Moderation:** Content safety with `RubyLLM.moderate`
|
|
120
131
|
* **Tools:** Let AI call your Ruby methods
|
|
132
|
+
* **Agents:** Reusable assistants with `RubyLLM::Agent`
|
|
121
133
|
* **Structured output:** JSON schemas that just work
|
|
122
134
|
* **Streaming:** Real-time responses with blocks
|
|
123
135
|
* **Rails:** ActiveRecord integration with `acts_as_chat`
|
|
124
136
|
* **Async:** Fiber-based concurrency
|
|
125
137
|
* **Model registry:** 800+ models with capability detection and pricing
|
|
126
138
|
* **Extended thinking:** Control, view, and persist model deliberation
|
|
127
|
-
* **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
|
|
139
|
+
* **Providers:** OpenAI, xAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
|
|
128
140
|
|
|
129
141
|
## Installation
|
|
130
142
|
|
|
@@ -95,19 +95,19 @@ module RubyLLM
|
|
|
95
95
|
end
|
|
96
96
|
@chat.reset_messages!
|
|
97
97
|
|
|
98
|
-
messages.
|
|
98
|
+
ordered_messages = order_messages_for_llm(messages.to_a)
|
|
99
|
+
ordered_messages.each do |msg|
|
|
99
100
|
@chat.add_message(msg.to_llm)
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
setup_persistence_callbacks
|
|
103
104
|
end
|
|
104
105
|
|
|
105
|
-
def with_instructions(instructions, replace:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
to_llm.with_instructions(instructions)
|
|
106
|
+
def with_instructions(instructions, append: false, replace: nil)
|
|
107
|
+
append = append_instructions?(append:, replace:)
|
|
108
|
+
persist_system_instruction(instructions, append:)
|
|
109
|
+
|
|
110
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
111
111
|
self
|
|
112
112
|
end
|
|
113
113
|
|
|
@@ -233,6 +233,40 @@ module RubyLLM
|
|
|
233
233
|
end
|
|
234
234
|
end
|
|
235
235
|
|
|
236
|
+
def replace_persisted_system_instructions(instructions)
|
|
237
|
+
system_messages = messages.where(role: :system).order(:id).to_a
|
|
238
|
+
|
|
239
|
+
if system_messages.empty?
|
|
240
|
+
messages.create!(role: :system, content: instructions)
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
primary_message = system_messages.shift
|
|
245
|
+
primary_message.update!(content: instructions) if primary_message.content != instructions
|
|
246
|
+
system_messages.each(&:destroy!)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def append_instructions?(append:, replace:)
|
|
250
|
+
return append if replace.nil?
|
|
251
|
+
|
|
252
|
+
append || (replace == false)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def persist_system_instruction(instructions, append:)
|
|
256
|
+
transaction do
|
|
257
|
+
if append
|
|
258
|
+
messages.create!(role: :system, content: instructions)
|
|
259
|
+
else
|
|
260
|
+
replace_persisted_system_instructions(instructions)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def order_messages_for_llm(messages)
|
|
266
|
+
system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
|
|
267
|
+
system_messages + non_system_messages
|
|
268
|
+
end
|
|
269
|
+
|
|
236
270
|
def setup_persistence_callbacks
|
|
237
271
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
238
272
|
|
|
@@ -83,19 +83,19 @@ module RubyLLM
|
|
|
83
83
|
)
|
|
84
84
|
@chat.reset_messages!
|
|
85
85
|
|
|
86
|
-
messages_association.
|
|
86
|
+
ordered_messages = order_messages_for_llm(messages_association.to_a)
|
|
87
|
+
ordered_messages.each do |msg|
|
|
87
88
|
@chat.add_message(msg.to_llm)
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
setup_persistence_callbacks
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def with_instructions(instructions, replace:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
to_llm.with_instructions(instructions)
|
|
94
|
+
def with_instructions(instructions, append: false, replace: nil)
|
|
95
|
+
append = append_instructions?(append:, replace:)
|
|
96
|
+
persist_system_instruction(instructions, append:)
|
|
97
|
+
|
|
98
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
99
99
|
self
|
|
100
100
|
end
|
|
101
101
|
|
|
@@ -244,6 +244,40 @@ module RubyLLM
|
|
|
244
244
|
@chat
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
+
def replace_persisted_system_instructions(instructions)
|
|
248
|
+
system_messages = messages_association.where(role: :system).order(:id).to_a
|
|
249
|
+
|
|
250
|
+
if system_messages.empty?
|
|
251
|
+
messages_association.create!(role: :system, content: instructions)
|
|
252
|
+
return
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
primary_message = system_messages.shift
|
|
256
|
+
primary_message.update!(content: instructions) if primary_message.content != instructions
|
|
257
|
+
system_messages.each(&:destroy!)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def append_instructions?(append:, replace:)
|
|
261
|
+
return append if replace.nil?
|
|
262
|
+
|
|
263
|
+
append || (replace == false)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def persist_system_instruction(instructions, append:)
|
|
267
|
+
transaction do
|
|
268
|
+
if append
|
|
269
|
+
messages_association.create!(role: :system, content: instructions)
|
|
270
|
+
else
|
|
271
|
+
replace_persisted_system_instructions(instructions)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def order_messages_for_llm(messages)
|
|
277
|
+
system_messages, non_system_messages = messages.partition { |msg| msg.role.to_s == 'system' }
|
|
278
|
+
system_messages + non_system_messages
|
|
279
|
+
end
|
|
280
|
+
|
|
247
281
|
def persist_new_message
|
|
248
282
|
@message = messages_association.create!(role: :assistant, content: '')
|
|
249
283
|
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
# Base class for simple, class-configured agents.
|
|
8
|
+
class Agent
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def inherited(subclass)
|
|
13
|
+
super
|
|
14
|
+
subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
|
|
15
|
+
subclass.instance_variable_set(:@tools, (@tools || []).dup)
|
|
16
|
+
subclass.instance_variable_set(:@instructions, @instructions)
|
|
17
|
+
subclass.instance_variable_set(:@temperature, @temperature)
|
|
18
|
+
subclass.instance_variable_set(:@thinking, @thinking)
|
|
19
|
+
subclass.instance_variable_set(:@params, (@params || {}).dup)
|
|
20
|
+
subclass.instance_variable_set(:@headers, (@headers || {}).dup)
|
|
21
|
+
subclass.instance_variable_set(:@schema, @schema)
|
|
22
|
+
subclass.instance_variable_set(:@context, @context)
|
|
23
|
+
subclass.instance_variable_set(:@chat_model, @chat_model)
|
|
24
|
+
subclass.instance_variable_set(:@input_names, (@input_names || []).dup)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def model(model_id = nil, **options)
|
|
28
|
+
options[:model] = model_id unless model_id.nil?
|
|
29
|
+
@chat_kwargs = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tools(*tools, &block)
|
|
33
|
+
return @tools || [] if tools.empty? && !block_given?
|
|
34
|
+
|
|
35
|
+
@tools = block_given? ? block : tools.flatten
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def instructions(text = nil, **prompt_locals, &block)
|
|
39
|
+
if text.nil? && prompt_locals.empty? && !block_given?
|
|
40
|
+
@instructions ||= { prompt: 'instructions', locals: {} }
|
|
41
|
+
return @instructions
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@instructions = block || text || { prompt: 'instructions', locals: prompt_locals }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def temperature(value = nil)
|
|
48
|
+
return @temperature if value.nil?
|
|
49
|
+
|
|
50
|
+
@temperature = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def thinking(effort: nil, budget: nil)
|
|
54
|
+
return @thinking if effort.nil? && budget.nil?
|
|
55
|
+
|
|
56
|
+
@thinking = { effort: effort, budget: budget }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def params(**params, &block)
|
|
60
|
+
return @params || {} if params.empty? && !block_given?
|
|
61
|
+
|
|
62
|
+
@params = block_given? ? block : params
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def headers(**headers, &block)
|
|
66
|
+
return @headers || {} if headers.empty? && !block_given?
|
|
67
|
+
|
|
68
|
+
@headers = block_given? ? block : headers
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def schema(value = nil, &block)
|
|
72
|
+
return @schema if value.nil? && !block_given?
|
|
73
|
+
|
|
74
|
+
@schema = block_given? ? block : value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def context(value = nil)
|
|
78
|
+
return @context if value.nil?
|
|
79
|
+
|
|
80
|
+
@context = value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chat_model(value = nil)
|
|
84
|
+
return @chat_model if value.nil?
|
|
85
|
+
|
|
86
|
+
@chat_model = value
|
|
87
|
+
remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def inputs(*names)
|
|
91
|
+
return @input_names || [] if names.empty?
|
|
92
|
+
|
|
93
|
+
@input_names = names.flatten.map(&:to_sym)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def chat_kwargs
|
|
97
|
+
@chat_kwargs || {}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def chat(**kwargs)
|
|
101
|
+
input_values, chat_options = partition_inputs(kwargs)
|
|
102
|
+
chat = RubyLLM.chat(**chat_kwargs, **chat_options)
|
|
103
|
+
apply_configuration(chat, input_values:, persist_instructions: true)
|
|
104
|
+
chat
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def create(**kwargs)
|
|
108
|
+
with_rails_chat_record(:create, **kwargs)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def create!(**kwargs)
|
|
112
|
+
with_rails_chat_record(:create!, **kwargs)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def find(id, **kwargs)
|
|
116
|
+
raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model
|
|
117
|
+
|
|
118
|
+
input_values, = partition_inputs(kwargs)
|
|
119
|
+
record = resolved_chat_model.find(id)
|
|
120
|
+
apply_configuration(record, input_values:, persist_instructions: false)
|
|
121
|
+
record
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sync_instructions!(chat_or_id, **kwargs)
|
|
125
|
+
raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
|
|
126
|
+
|
|
127
|
+
input_values, = partition_inputs(kwargs)
|
|
128
|
+
record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
|
|
129
|
+
runtime = runtime_context(chat: record, inputs: input_values)
|
|
130
|
+
instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
|
|
131
|
+
return record if instructions_value.nil?
|
|
132
|
+
|
|
133
|
+
record.with_instructions(instructions_value)
|
|
134
|
+
record
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def render_prompt(name, chat:, inputs:, locals:)
|
|
138
|
+
path = prompt_path_for(name)
|
|
139
|
+
return nil unless File.exist?(path)
|
|
140
|
+
|
|
141
|
+
resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
|
|
142
|
+
ERB.new(File.read(path)).result_with_hash(resolved_locals)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def with_rails_chat_record(method_name, **kwargs)
|
|
148
|
+
raise ArgumentError, 'chat_model must be configured to use create/create!' unless resolved_chat_model
|
|
149
|
+
|
|
150
|
+
input_values, chat_options = partition_inputs(kwargs)
|
|
151
|
+
record = resolved_chat_model.public_send(method_name, **chat_kwargs, **chat_options)
|
|
152
|
+
apply_configuration(record, input_values:, persist_instructions: true) if record
|
|
153
|
+
record
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def apply_configuration(chat_object, input_values:, persist_instructions:)
|
|
157
|
+
runtime = runtime_context(chat: chat_object, inputs: input_values)
|
|
158
|
+
llm_chat = llm_chat_for(chat_object)
|
|
159
|
+
|
|
160
|
+
apply_context(llm_chat)
|
|
161
|
+
apply_instructions(chat_object, runtime, inputs: input_values, persist: persist_instructions)
|
|
162
|
+
apply_tools(llm_chat, runtime)
|
|
163
|
+
apply_temperature(llm_chat)
|
|
164
|
+
apply_thinking(llm_chat)
|
|
165
|
+
apply_params(llm_chat, runtime)
|
|
166
|
+
apply_headers(llm_chat, runtime)
|
|
167
|
+
apply_schema(llm_chat, runtime)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def apply_context(llm_chat)
|
|
171
|
+
llm_chat.with_context(context) if context
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def apply_instructions(chat_object, runtime, inputs:, persist:)
|
|
175
|
+
value = resolved_instructions_value(chat_object, runtime, inputs:)
|
|
176
|
+
return if value.nil?
|
|
177
|
+
|
|
178
|
+
instruction_target(chat_object, persist:).with_instructions(value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def apply_tools(llm_chat, runtime)
|
|
182
|
+
tools_to_apply = Array(evaluate(tools, runtime))
|
|
183
|
+
llm_chat.with_tools(*tools_to_apply) unless tools_to_apply.empty?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def apply_temperature(llm_chat)
|
|
187
|
+
llm_chat.with_temperature(temperature) unless temperature.nil?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def apply_thinking(llm_chat)
|
|
191
|
+
llm_chat.with_thinking(**thinking) if thinking
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def apply_params(llm_chat, runtime)
|
|
195
|
+
value = evaluate(params, runtime)
|
|
196
|
+
llm_chat.with_params(**value) if value && !value.empty?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def apply_headers(llm_chat, runtime)
|
|
200
|
+
value = evaluate(headers, runtime)
|
|
201
|
+
llm_chat.with_headers(**value) if value && !value.empty?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def apply_schema(llm_chat, runtime)
|
|
205
|
+
value = evaluate(schema, runtime)
|
|
206
|
+
llm_chat.with_schema(value) if value
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def llm_chat_for(chat_object)
|
|
210
|
+
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def evaluate(value, runtime)
|
|
214
|
+
value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def resolved_instructions_value(chat_object, runtime, inputs:)
|
|
218
|
+
value = evaluate(instructions, runtime)
|
|
219
|
+
return value unless prompt_instruction?(value)
|
|
220
|
+
|
|
221
|
+
runtime.prompt(
|
|
222
|
+
value[:prompt],
|
|
223
|
+
**resolve_prompt_locals(value[:locals] || {}, runtime:, chat: chat_object, inputs:)
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def prompt_instruction?(value)
|
|
228
|
+
value.is_a?(Hash) && value[:prompt]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def instruction_target(chat_object, persist:)
|
|
232
|
+
if persist || !chat_object.respond_to?(:to_llm)
|
|
233
|
+
chat_object
|
|
234
|
+
else
|
|
235
|
+
chat_object.to_llm
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
|
|
240
|
+
base = { chat: chat }.merge(inputs)
|
|
241
|
+
evaluated = locals.each_with_object({}) do |(key, value), acc|
|
|
242
|
+
acc[key.to_sym] = value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
243
|
+
end
|
|
244
|
+
base.merge(evaluated)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def partition_inputs(kwargs)
|
|
248
|
+
input_values = {}
|
|
249
|
+
chat_options = {}
|
|
250
|
+
|
|
251
|
+
kwargs.each do |key, value|
|
|
252
|
+
symbolized_key = key.to_sym
|
|
253
|
+
if inputs.include?(symbolized_key)
|
|
254
|
+
input_values[symbolized_key] = value
|
|
255
|
+
else
|
|
256
|
+
chat_options[symbolized_key] = value
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
[input_values, chat_options]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def runtime_context(chat:, inputs:)
|
|
264
|
+
agent_class = self
|
|
265
|
+
Object.new.tap do |runtime|
|
|
266
|
+
runtime.define_singleton_method(:chat) { chat }
|
|
267
|
+
runtime.define_singleton_method(:prompt) do |name, **locals|
|
|
268
|
+
agent_class.render_prompt(name, chat:, inputs:, locals:)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
inputs.each do |name, value|
|
|
272
|
+
runtime.define_singleton_method(name) { value }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def prompt_path_for(name)
|
|
278
|
+
filename = name.to_s
|
|
279
|
+
filename += '.txt.erb' unless filename.end_with?('.txt.erb')
|
|
280
|
+
prompt_root.join(prompt_agent_path, filename)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def prompt_agent_path
|
|
284
|
+
class_name = name || 'agent'
|
|
285
|
+
class_name.gsub('::', '/')
|
|
286
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
287
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
288
|
+
.tr('-', '_')
|
|
289
|
+
.downcase
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def prompt_root
|
|
293
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
294
|
+
Rails.root.join('app/prompts')
|
|
295
|
+
else
|
|
296
|
+
Pathname.new(Dir.pwd).join('app/prompts')
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def resolved_chat_model
|
|
301
|
+
return @resolved_chat_model if defined?(@resolved_chat_model)
|
|
302
|
+
|
|
303
|
+
@resolved_chat_model = case @chat_model
|
|
304
|
+
when String then Object.const_get(@chat_model)
|
|
305
|
+
else @chat_model
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
|
|
311
|
+
input_values, chat_options = self.class.send(:partition_inputs, kwargs)
|
|
312
|
+
@chat = chat || RubyLLM.chat(**self.class.chat_kwargs, **chat_options)
|
|
313
|
+
self.class.send(:apply_configuration, @chat, input_values: input_values.merge(inputs || {}),
|
|
314
|
+
persist_instructions:)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
attr_reader :chat
|
|
318
|
+
|
|
319
|
+
delegate :ask, :say, :complete, :add_message, :messages,
|
|
320
|
+
:on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each,
|
|
321
|
+
to: :chat
|
|
322
|
+
end
|
|
323
|
+
end
|