llama_bot_rails 0.1.13 → 0.1.15
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/app/channels/llama_bot_rails/chat_channel.rb +79 -70
- data/app/controllers/llama_bot_rails/agent_controller.rb +1 -1
- data/app/views/llama_bot_rails/agent/chat.html.erb +2 -1
- data/app/views/llama_bot_rails/agent/chat_ws.html.erb +10 -1
- data/lib/generators/llama_bot_rails/install/templates/agent_state_builder.rb.erb +3 -3
- data/lib/llama_bot_rails/agent_auth.rb +4 -1
- data/lib/llama_bot_rails/agent_auth_2.rb +149 -0
- data/lib/llama_bot_rails/agent_state_builder.rb +6 -6
- data/lib/llama_bot_rails/llama_bot.rb +3 -0
- data/lib/llama_bot_rails/route_helper.rb +117 -0
- data/lib/llama_bot_rails/version.rb +1 -1
- data/lib/llama_bot_rails.rb +2 -1
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2980680620ba107e40e8a37a0c885904d48ef1c1e48a2fe1ccfffc59257e72dc
|
4
|
+
data.tar.gz: fbab7ffd1aa4fd6c43568bda008094cb4c5341626284aea8b9c0c4fbf565fd82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f445dd978190f9e987979322e24d18e70a364deaf07e521a6bb1b6e26ecd6b9e82448a0a48a08c22e83942ac7f711c4719444a1fd8f20b6098e9990c552fa4e
|
7
|
+
data.tar.gz: 1453feba5f8148ce85afcfcf2401b5763e39d1893678b71bcdb693a13090638ce7ead883caa6bc18f83cc8ee792f3da9e0bd8d660cd6a31e51b7390a8eb78104
|
@@ -88,6 +88,16 @@ module LlamaBotRails
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
# Close the external WebSocket connection BEFORE stopping async tasks
|
92
|
+
if @external_ws_connection
|
93
|
+
begin
|
94
|
+
@external_ws_connection.close
|
95
|
+
Rails.logger.info "👋 [LlamaBot] Gracefully closed external WebSocket connection for: #{connection_id}"
|
96
|
+
rescue => e
|
97
|
+
Rails.logger.warn "❌ [LlamaBot] Could not close WebSocket connection: #{e.message}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
91
101
|
# Clean up async tasks with better error handling
|
92
102
|
begin
|
93
103
|
@listener_task&.stop rescue nil
|
@@ -97,16 +107,6 @@ module LlamaBotRails
|
|
97
107
|
Rails.logger.error "[LlamaBot] Error stopping async tasks: #{e.message}"
|
98
108
|
end
|
99
109
|
|
100
|
-
# Clean up the connection
|
101
|
-
if @external_ws_connection
|
102
|
-
begin
|
103
|
-
@external_ws_connection.close
|
104
|
-
Rails.logger.info "[LlamaBot] Closed external WebSocket connection for: #{connection_id}"
|
105
|
-
rescue => e
|
106
|
-
Rails.logger.warn "[LlamaBot] Could not close WebSocket connection: #{e.message}"
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
110
|
# Force garbage collection in development/test environments to help clean up
|
111
111
|
if !Rails.env.production?
|
112
112
|
GC.start
|
@@ -138,7 +138,7 @@ module LlamaBotRails
|
|
138
138
|
|
139
139
|
builder = state_builder_class.new(
|
140
140
|
params: data,
|
141
|
-
context: { api_token: @api_token }
|
141
|
+
context: { api_token: @api_token }.with_indifferent_access
|
142
142
|
)
|
143
143
|
|
144
144
|
# 2. Construct the LangGraph-ready state
|
@@ -209,9 +209,20 @@ module LlamaBotRails
|
|
209
209
|
end
|
210
210
|
|
211
211
|
uri = URI(websocket_url)
|
212
|
-
|
213
|
-
|
214
|
-
|
212
|
+
|
213
|
+
# Normalize the WebSocket URI scheme so it is always either ws:// or wss://.
|
214
|
+
# We want to gracefully handle users passing in http/https URLs or omitting a scheme entirely.
|
215
|
+
case uri.scheme&.downcase
|
216
|
+
when 'wss', 'ws'
|
217
|
+
# already valid, do nothing
|
218
|
+
when 'https'
|
219
|
+
uri.scheme = 'wss'
|
220
|
+
when 'http'
|
221
|
+
uri.scheme = 'ws'
|
222
|
+
else
|
223
|
+
# If a scheme is missing or unrecognized, fall back to sensible defaults
|
224
|
+
uri.scheme = Rails.env.development? ? 'ws' : 'wss'
|
225
|
+
end
|
215
226
|
|
216
227
|
endpoint = Async::HTTP::Endpoint.new(
|
217
228
|
uri,
|
@@ -253,84 +264,82 @@ module LlamaBotRails
|
|
253
264
|
# Wait for tasks to complete or connection to close
|
254
265
|
[@listener_task, @keepalive_task].each(&:wait)
|
255
266
|
rescue => e
|
256
|
-
Rails.logger.error "[LlamaBot] Failed to connect to external WebSocket for connection #{connection_id}: #{e.message}"
|
267
|
+
Rails.logger.error "❌ [LlamaBot] Failed to connect to external WebSocket for connection #{connection_id}: #{e.message}"
|
257
268
|
ensure
|
258
269
|
# Clean up tasks if they exist
|
259
270
|
@listener_task&.stop
|
260
271
|
@keepalive_task&.stop
|
261
|
-
@external_ws_connection
|
272
|
+
if @external_ws_connection
|
273
|
+
@external_ws_connection.close
|
274
|
+
Rails.logger.info "👋 [LlamaBot] Cleaned up external WebSocket connection in ensure block"
|
275
|
+
end
|
262
276
|
end
|
263
277
|
end
|
264
278
|
end
|
265
279
|
|
266
280
|
# Listen for messages from the LlamaBot Backend
|
267
281
|
def listen_to_external_websocket(connection)
|
268
|
-
|
282
|
+
begin
|
283
|
+
while message = connection.read
|
284
|
+
# Extract the actual message content
|
285
|
+
message_content = message.buffer if message.buffer
|
286
|
+
next unless message_content.present?
|
269
287
|
|
270
|
-
|
271
|
-
# if message.type == :ping
|
272
|
-
|
273
|
-
# # respond with :pong
|
274
|
-
# connection.write(Async::WebSocket::Messages::ControlFrame.new(:pong, frame.data))
|
275
|
-
# connection.flush
|
276
|
-
# next
|
277
|
-
# end
|
278
|
-
# Extract the actual message content
|
279
|
-
if message.buffer
|
280
|
-
message_content = message.buffer # Use .data to get the message content
|
281
|
-
else
|
282
|
-
message_content = message.content
|
283
|
-
end
|
288
|
+
Rails.logger.info "[LlamaBot] Received from external WebSocket: #{message_content}"
|
284
289
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
Rails.logger.error "[LlamaBot]
|
303
|
-
|
304
|
-
|
305
|
-
Rails.logger.error "[LlamaBot] ---------------------> Response: #{response}"
|
306
|
-
Rails.logger.error "[LlamaBot] ---------Completed error message!----------"
|
307
|
-
when "pong"
|
308
|
-
# Tell llamabot frontend that we've received a pong response, and we're still connected
|
309
|
-
formatted_message = { message: {type: "pong"} }.to_json
|
290
|
+
begin
|
291
|
+
parsed_message = JSON.parse(message_content)
|
292
|
+
|
293
|
+
formatted_message = { message: {type: parsed_message["type"], content: parsed_message['content'], base_message: parsed_message["base_message"]} }.to_json
|
294
|
+
case parsed_message["type"]
|
295
|
+
when "error"
|
296
|
+
Rails.logger.error "[LlamaBot] ---------Received error message!----------"
|
297
|
+
response = parsed_message['content']
|
298
|
+
formatted_message = { message: message_content }.to_json
|
299
|
+
Rails.logger.error "[LlamaBot] ---------------------> Response: #{response}"
|
300
|
+
Rails.logger.error "[LlamaBot] ---------Completed error message!----------"
|
301
|
+
when "pong"
|
302
|
+
# Tell llamabot frontend that we've received a pong response, and we're still connected
|
303
|
+
formatted_message = { message: {type: "pong"} }.to_json
|
304
|
+
end
|
305
|
+
ActionCable.server.broadcast "chat_channel_#{params[:session_id]}", formatted_message
|
306
|
+
rescue JSON::ParserError => e
|
307
|
+
Rails.logger.error "[LlamaBot] Failed to parse message as JSON: #{e.message}"
|
308
|
+
# Continue to the next message without crashing the listener.
|
309
|
+
next
|
310
310
|
end
|
311
|
-
rescue JSON::ParserError => e
|
312
|
-
Rails.logger.error "[LlamaBot] Failed to parse message as JSON: #{e.message}"
|
313
311
|
end
|
314
|
-
|
312
|
+
rescue IOError, Errno::ECONNRESET => e
|
313
|
+
# This is a recoverable error. Log it and allow the task to end gracefully.
|
314
|
+
# The `ensure` block in `setup_external_websocket` will handle the cleanup.
|
315
|
+
Rails.logger.warn "❌ [LlamaBot] Connection lost while listening: #{e.message}. The connection will be closed."
|
315
316
|
end
|
316
317
|
end
|
317
318
|
|
318
319
|
###
|
319
320
|
def send_keep_alive_pings(connection)
|
320
321
|
loop do
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
322
|
+
# Stop the loop gracefully if the connection has already been closed.
|
323
|
+
break if connection.closed?
|
324
|
+
|
325
|
+
begin
|
326
|
+
ping_message = {
|
327
|
+
type: 'ping',
|
328
|
+
connection_id: @connection_id,
|
329
|
+
connection_state: !connection.closed? ? 'connected' : 'disconnected',
|
330
|
+
connection_class: connection.class.name
|
331
|
+
}.to_json
|
332
|
+
connection.write(ping_message)
|
333
|
+
connection.flush
|
334
|
+
Rails.logger.debug "[LlamaBot] Sent keep-alive ping: #{ping_message}"
|
335
|
+
rescue IOError, Errno::ECONNRESET => e
|
336
|
+
Rails.logger.warn "❌ [LlamaBot] Could not send ping, connection likely closed: #{e.message}"
|
337
|
+
# Break the loop to allow the task to terminate gracefully.
|
338
|
+
break
|
339
|
+
end
|
340
|
+
|
330
341
|
Async::Task.current.sleep(30)
|
331
342
|
end
|
332
|
-
rescue => e
|
333
|
-
Rails.logger.error "[LlamaBot] Error in keep-alive ping: #{e.message} | Connection type: #{connection.class.name}"
|
334
343
|
end
|
335
344
|
|
336
345
|
# Send messages from the user to the LlamaBot Backend Socket
|
@@ -100,7 +100,7 @@ module LlamaBotRails
|
|
100
100
|
# 1. Instantiate the builder
|
101
101
|
builder = state_builder_class.new(
|
102
102
|
params: params,
|
103
|
-
context: { api_token: @api_token }
|
103
|
+
context: { api_token: @api_token }.with_indifferent_access
|
104
104
|
)
|
105
105
|
|
106
106
|
# 2. Construct the LangGraph-ready state
|
@@ -586,7 +586,7 @@
|
|
586
586
|
let currentThreadId = null;
|
587
587
|
let isSidebarCollapsed = false;
|
588
588
|
let streamingTimeout = null;
|
589
|
-
const STREAMING_TIMEOUT_MS =
|
589
|
+
const STREAMING_TIMEOUT_MS = 3000000; // 3000 seconds timeout
|
590
590
|
|
591
591
|
// Initialize the app
|
592
592
|
document.addEventListener('DOMContentLoaded', function() {
|
@@ -968,6 +968,7 @@
|
|
968
968
|
}
|
969
969
|
|
970
970
|
} catch (parseError) {
|
971
|
+
addMessage(`Error: ${parseError} - Data: ${jsonData}`, 'error');
|
971
972
|
console.error('Error parsing SSE data:', parseError, 'Data:', jsonData);
|
972
973
|
}
|
973
974
|
}
|
@@ -659,7 +659,10 @@ This deprecated and will be removed over time.
|
|
659
659
|
},
|
660
660
|
received(data) {
|
661
661
|
const parsedData = JSON.parse(data).message;
|
662
|
+
console.log("LLM Response:", parsedData);
|
662
663
|
switch (parsedData.type) {
|
664
|
+
case "AIMessageChunk":
|
665
|
+
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
663
666
|
case "ai":
|
664
667
|
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
665
668
|
break;
|
@@ -966,6 +969,11 @@ This deprecated and will be removed over time.
|
|
966
969
|
const messageDiv = document.createElement('div');
|
967
970
|
messageDiv.className = `message ${sender}-message`;
|
968
971
|
|
972
|
+
if (sender == "AIMessageChunk"){
|
973
|
+
console.log("AIMessageChunk" + base_message);
|
974
|
+
messageDiv.innerHTML += text;
|
975
|
+
}
|
976
|
+
|
969
977
|
// Parse markdown for bot messages using Snarkdown, keep plain text for user messages
|
970
978
|
if (sender === 'ai') { //Arghh. We're having issues with difference in formats between when we're streaming from updates mode, and when pulling state from checkpoint.
|
971
979
|
if (text == ''){ //this is most likely a tool call.
|
@@ -1151,7 +1159,8 @@ This deprecated and will be removed over time.
|
|
1151
1159
|
|
1152
1160
|
}
|
1153
1161
|
else {
|
1154
|
-
messageDiv.innerHTML =
|
1162
|
+
messageDiv.innerHTML = text;
|
1163
|
+
// messageDiv.innerHTML = snarkdown(text);
|
1155
1164
|
}
|
1156
1165
|
} else if (sender === 'tool') { //tool messages are not parsed as markdown
|
1157
1166
|
if (base_message.name == 'run_rails_console_command') {
|
@@ -11,9 +11,9 @@ module <%= app_name %>
|
|
11
11
|
|
12
12
|
def build
|
13
13
|
{
|
14
|
-
message: @params[
|
15
|
-
thread_id: @
|
16
|
-
api_token: @context[
|
14
|
+
message: @params["message"], # Rails param from JS/chat UI. This is the user's message to the agent.
|
15
|
+
thread_id: @params["thread_id"], # This is the thread id for the agent. It is used to track the conversation history.
|
16
|
+
api_token: @context["api_token"], # This is an authenticated API token for the agent, so that it can authenticate with us. (It may need access to resources on our Rails app, such as the Rails Console.)
|
17
17
|
agent_prompt: LlamaBotRails.agent_prompt_text, # System prompt instructions for the agent. Can be customized in app/llama_bot/prompts/agent_prompt.txt
|
18
18
|
agent_name: "llamabot" # This routes to the appropriate LangGraph agent as defined in LlamaBot/langgraph.json, and enables us to access different agents on our LlamaBot server.
|
19
19
|
}
|
@@ -5,7 +5,10 @@ module LlamaBotRails
|
|
5
5
|
|
6
6
|
included do
|
7
7
|
# Add before_action filter to automatically check agent authentication for LlamaBot requests
|
8
|
-
|
8
|
+
|
9
|
+
if self < ActionController::Base
|
10
|
+
before_action :check_agent_authentication, if: :should_check_agent_auth?
|
11
|
+
end
|
9
12
|
|
10
13
|
# ------------------------------------------------------------------
|
11
14
|
# 1) For every Devise scope, alias authenticate_<scope>! so it now
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# lib/llama_bot_rails/agent_auth.rb
|
2
|
+
module LlamaBotRails
|
3
|
+
module AgentAuth
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
AUTH_SCHEME = "LlamaBot"
|
6
|
+
|
7
|
+
included do
|
8
|
+
# ------------------------------------------------------------------
|
9
|
+
# Use the right callback macro for the including class:
|
10
|
+
# • Controllers → before_action (old behaviour)
|
11
|
+
# • ActiveJob → before_perform (uses same checker)
|
12
|
+
# • Anything
|
13
|
+
# else → do nothing
|
14
|
+
# ------------------------------------------------------------------
|
15
|
+
if respond_to?(:before_action)
|
16
|
+
before_action :check_agent_authentication, if: :should_check_agent_auth?
|
17
|
+
elsif respond_to?(:before_perform)
|
18
|
+
before_perform :check_agent_authentication
|
19
|
+
end
|
20
|
+
|
21
|
+
# ------------------------------------------------------------------
|
22
|
+
# 1) For every Devise scope, alias authenticate_<scope>! so it now
|
23
|
+
# accepts *either* a logged-in browser session OR a valid agent
|
24
|
+
# token. Existing before/skip filters keep working.
|
25
|
+
# ------------------------------------------------------------------
|
26
|
+
if defined?(Devise)
|
27
|
+
Devise.mappings.keys.each do |scope|
|
28
|
+
scope_filter = :"authenticate_#{scope}!"
|
29
|
+
|
30
|
+
alias_method scope_filter, :authenticate_user_or_agent! \
|
31
|
+
if method_defined?(scope_filter)
|
32
|
+
|
33
|
+
define_method(scope_filter) do |*args|
|
34
|
+
Rails.logger.warn(
|
35
|
+
"#{scope_filter} is now handled by LlamaBotRails::AgentAuth "\
|
36
|
+
"and will be removed in a future version. "\
|
37
|
+
"Use authenticate_user_or_agent! instead."
|
38
|
+
)
|
39
|
+
authenticate_user_or_agent!(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# ------------------------------------------------------------------
|
45
|
+
# 2) If Devise isn’t loaded at all, fall back to one alias so apps
|
46
|
+
# that had authenticate_user! manually defined don’t break.
|
47
|
+
# ------------------------------------------------------------------
|
48
|
+
unless defined?(Devise)
|
49
|
+
original_authenticate_user =
|
50
|
+
instance_method(:authenticate_user!) if method_defined?(:authenticate_user!)
|
51
|
+
|
52
|
+
define_method(:authenticate_user!) do |*args|
|
53
|
+
authenticate_user_or_agent!(*args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# --------------------------------------------------------------------
|
59
|
+
# Public helper: true if the request carries a *valid* agent token
|
60
|
+
# --------------------------------------------------------------------
|
61
|
+
def should_check_agent_auth?
|
62
|
+
# Skip if a Devise user is already signed in
|
63
|
+
return false if devise_user_signed_in?
|
64
|
+
llama_bot_request?
|
65
|
+
end
|
66
|
+
|
67
|
+
def llama_bot_request?
|
68
|
+
return false unless respond_to?(:request) && request&.headers
|
69
|
+
scheme, token = request.headers["Authorization"]&.split(" ", 2)
|
70
|
+
Rails.logger.debug("[LlamaBot] auth header = #{scheme.inspect} #{token&.slice(0,8)}…")
|
71
|
+
return false unless scheme == AUTH_SCHEME && token.present?
|
72
|
+
|
73
|
+
Rails.application.message_verifier(:llamabot_ws).verify(token)
|
74
|
+
true
|
75
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# --------------------------------------------------------------------
|
82
|
+
# Automatic check for LlamaBot requests
|
83
|
+
# --------------------------------------------------------------------
|
84
|
+
def check_agent_authentication
|
85
|
+
# Jobs don’t have a request object, so skip token logic there
|
86
|
+
return if is_a?(ActiveJob::Base)
|
87
|
+
|
88
|
+
has_permitted_actions = self.class.respond_to?(:llama_bot_permitted_actions)
|
89
|
+
return unless has_permitted_actions
|
90
|
+
|
91
|
+
is_llama_request = llama_bot_request?
|
92
|
+
action_is_whitelisted = self.class.llama_bot_permitted_actions.include?(action_name)
|
93
|
+
|
94
|
+
if is_llama_request
|
95
|
+
unless action_is_whitelisted
|
96
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' isn't white-listed for LlamaBot.")
|
97
|
+
render json: { error: "Action '#{action_name}' isn't white-listed for LlamaBot." },
|
98
|
+
status: :forbidden
|
99
|
+
end
|
100
|
+
elsif action_is_whitelisted
|
101
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' requires LlamaBot authentication.")
|
102
|
+
render json: { error: "Action '#{action_name}' requires LlamaBot authentication" },
|
103
|
+
status: :forbidden
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# --------------------------------------------------------------------
|
108
|
+
# Unified guard — browser OR agent
|
109
|
+
# --------------------------------------------------------------------
|
110
|
+
def devise_user_signed_in?
|
111
|
+
return false unless defined?(Devise)
|
112
|
+
return false unless respond_to?(:request) && request&.env
|
113
|
+
request.env["warden"]&.authenticated?
|
114
|
+
end
|
115
|
+
|
116
|
+
def authenticate_user_or_agent!(*)
|
117
|
+
return if devise_user_signed_in? # any logged-in Devise scope
|
118
|
+
|
119
|
+
if llama_bot_request?
|
120
|
+
scheme, token = request.headers["Authorization"]&.split(" ", 2)
|
121
|
+
data = Rails.application.message_verifier(:llamabot_ws).verify(token)
|
122
|
+
|
123
|
+
allowed = self.class.respond_to?(:llama_bot_permitted_actions) &&
|
124
|
+
self.class.llama_bot_permitted_actions.include?(action_name)
|
125
|
+
|
126
|
+
if allowed
|
127
|
+
user_object = LlamaBotRails.user_resolver.call(data[:user_id])
|
128
|
+
unless LlamaBotRails.sign_in_method.call(request.env, user_object)
|
129
|
+
head :unauthorized
|
130
|
+
end
|
131
|
+
return # ✅ token + allow-listed action
|
132
|
+
else
|
133
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' isn't white-listed for LlamaBot.")
|
134
|
+
render json: { error: "Action '#{action_name}' isn't white-listed for LlamaBot." },
|
135
|
+
status: :forbidden
|
136
|
+
return false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Fall back to Devise or plain 401
|
141
|
+
if defined?(Devise) && respond_to?(:request) && request&.env
|
142
|
+
request.env["warden"].authenticate!
|
143
|
+
else
|
144
|
+
head :unauthorized
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
@@ -6,17 +6,17 @@ module LlamaBotRails
|
|
6
6
|
@context = context
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
9
|
# Warning: Types must match exactly or you'll get Pydantic errors. It's brittle - If these don't match exactly what's in nodes.py LangGraph state pydantic types, (For example, having a null value/None type when it should be a string) it will the agent..
|
11
10
|
# So if it doesn't map state types properly from the frontend, it will break. (must be exactly what's defined here).
|
12
11
|
# There won't be an exception thrown -- instead, you'll get an pydantic error message showing up in the BaseMessage content field. (In my case, it was a broken ToolMessage, but serializes from the inherited BaseMessage)
|
13
|
-
def build
|
12
|
+
def build
|
14
13
|
{
|
15
|
-
message: @params[
|
16
|
-
thread_id: @params[
|
17
|
-
api_token: @context[
|
14
|
+
message: @params["message"], # Rails param from JS/chat UI. This is the user's message to the agent.
|
15
|
+
thread_id: @params["thread_id"], # This is the thread id for the agent. It is used to track the conversation history.
|
16
|
+
api_token: @context["api_token"], # This is an authenticated API token for the agent, so that it can authenticate with us. (It may need access to resources on our Rails app, such as the Rails Console.)
|
18
17
|
agent_prompt: LlamaBotRails.agent_prompt_text, # System prompt instructions for the agent. Can be customized in app/llama_bot/prompts/agent_prompt.txt
|
19
|
-
agent_name: "llamabot" #This routes to the appropriate LangGraph agent as defined in LlamaBot/langgraph.json, and enables us to access different agents on our LlamaBot server.
|
18
|
+
agent_name: "llamabot", #This routes to the appropriate LangGraph agent as defined in LlamaBot/langgraph.json, and enables us to access different agents on our LlamaBot server.
|
19
|
+
available_routes: @context[:available_routes] # This is an array of routes that the agent can access. It is used to track the conversation history.
|
20
20
|
}
|
21
21
|
end
|
22
22
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module LlamaBotRails
|
2
|
+
module RouteHelper
|
3
|
+
# Extracts the description from YARD comments
|
4
|
+
def self.extract_yard_description(comment_text)
|
5
|
+
comment_text.lines.map { |l| l.sub(/^# ?/, '') }
|
6
|
+
.take_while { |l| !l.strip.start_with?('@') }
|
7
|
+
.join(' ').strip
|
8
|
+
end
|
9
|
+
|
10
|
+
# Extracts a specific YARD tag from comments
|
11
|
+
def self.extract_yard_tag(comment_text, tag)
|
12
|
+
if match = comment_text.match(/@#{tag} (.+)/)
|
13
|
+
match[1].strip
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Main method: returns XML string of formatted routes for allowed_routes
|
18
|
+
def self.formatted_routes_xml(allowed_routes)
|
19
|
+
xml_routes = ""
|
20
|
+
allowed_routes.each do |route_str|
|
21
|
+
controller, action = route_str.split('#')
|
22
|
+
matching_routes = Rails.application.routes.routes.select do |r|
|
23
|
+
r.defaults[:controller] == controller && r.defaults[:action] == action
|
24
|
+
end
|
25
|
+
|
26
|
+
matching_routes.each do |r|
|
27
|
+
verb = r.verb.to_s.gsub(/[$^]/, '') # Handles both Regexp and String
|
28
|
+
path = r.path.spec.to_s
|
29
|
+
path_params = path.scan(/:\w+/).map { |p| p[1..-1] } # e.g. ["id"]
|
30
|
+
|
31
|
+
# Extract controller class and strong parameters
|
32
|
+
controller_class = "#{controller.camelize}Controller".safe_constantize
|
33
|
+
strong_params = []
|
34
|
+
yard_metadata = {}
|
35
|
+
|
36
|
+
if controller_class
|
37
|
+
# Extract YARD documentation for the action
|
38
|
+
begin
|
39
|
+
method_obj = controller_class.instance_method(action.to_sym)
|
40
|
+
source_location = method_obj.source_location
|
41
|
+
if source_location
|
42
|
+
file_path, line_number = source_location
|
43
|
+
file_lines = File.readlines(file_path)
|
44
|
+
# Look for YARD comments above the method
|
45
|
+
comment_lines = []
|
46
|
+
current_line = line_number - 2 # Start above the method definition
|
47
|
+
while current_line >= 0 && file_lines[current_line].strip.start_with?('#')
|
48
|
+
comment_lines.unshift(file_lines[current_line].strip)
|
49
|
+
current_line -= 1
|
50
|
+
end
|
51
|
+
# Parse YARD tags
|
52
|
+
comment_text = comment_lines.join("\n")
|
53
|
+
yard_metadata[:description] = extract_yard_description(comment_text)
|
54
|
+
yard_metadata[:tool_description] = extract_yard_tag(comment_text, 'tool_description')
|
55
|
+
yard_metadata[:example] = extract_yard_tag(comment_text, 'example')
|
56
|
+
yard_metadata[:params] = extract_yard_tag(comment_text, 'params')
|
57
|
+
end
|
58
|
+
rescue => e
|
59
|
+
# Silently continue if YARD parsing fails
|
60
|
+
end
|
61
|
+
# Look for the strong parameter method (e.g., page_params, user_params, etc.)
|
62
|
+
param_method = "#{controller.singularize}_params"
|
63
|
+
if controller_class.private_method_defined?(param_method.to_sym)
|
64
|
+
source_location = controller_class.instance_method(param_method.to_sym).source_location
|
65
|
+
if source_location
|
66
|
+
file_path, line_number = source_location
|
67
|
+
file_lines = File.readlines(file_path)
|
68
|
+
method_lines = []
|
69
|
+
current_line = line_number - 1
|
70
|
+
while current_line < file_lines.length
|
71
|
+
line = file_lines[current_line].strip
|
72
|
+
method_lines << line
|
73
|
+
break if line.include?('end') && !line.include?('permit')
|
74
|
+
current_line += 1
|
75
|
+
end
|
76
|
+
method_source = method_lines.join(' ')
|
77
|
+
if match = method_source.match(/\.permit\((.*?)\)/)
|
78
|
+
permit_content = match[1]
|
79
|
+
strong_params = permit_content.scan(/:(\w+)/).flatten
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
# Also check for any additional params the action might accept
|
84
|
+
additional_params = []
|
85
|
+
case action
|
86
|
+
when 'update', 'create'
|
87
|
+
if controller == 'pages' && action == 'update'
|
88
|
+
additional_params << 'message'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
all_params = (path_params + strong_params + additional_params).uniq
|
92
|
+
else
|
93
|
+
all_params = path_params
|
94
|
+
end
|
95
|
+
|
96
|
+
xml = <<~XML
|
97
|
+
<route>
|
98
|
+
<name>#{route_str}</name>
|
99
|
+
<verb>#{verb}</verb>
|
100
|
+
<path>#{path}</path>
|
101
|
+
<path_params>#{path_params.join(', ')}</path_params>
|
102
|
+
<accepted_params>#{all_params.join(', ')}</accepted_params>
|
103
|
+
<strong_params>#{strong_params.join(', ')}</strong_params>
|
104
|
+
<description>#{yard_metadata[:description]}</description>
|
105
|
+
<tool_description>#{yard_metadata[:tool_description]}</tool_description>
|
106
|
+
<example>#{yard_metadata[:example]}</example>
|
107
|
+
<params>#{yard_metadata[:params]}</params>
|
108
|
+
</route>
|
109
|
+
XML
|
110
|
+
|
111
|
+
xml_routes += xml
|
112
|
+
end
|
113
|
+
end
|
114
|
+
xml_routes
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/llama_bot_rails.rb
CHANGED
@@ -5,6 +5,7 @@ require "llama_bot_rails/llama_bot"
|
|
5
5
|
require "llama_bot_rails/agent_state_builder"
|
6
6
|
require "llama_bot_rails/controller_extensions"
|
7
7
|
require "llama_bot_rails/agent_auth"
|
8
|
+
require "llama_bot_rails/route_helper"
|
8
9
|
|
9
10
|
module LlamaBotRails
|
10
11
|
# ------------------------------------------------------------------
|
@@ -74,4 +75,4 @@ module LlamaBotRails
|
|
74
75
|
# Bridge to backend service
|
75
76
|
# ------------------------------------------------------------------
|
76
77
|
def self.send_agent_message(params) = LlamaBot.send_agent_message(params)
|
77
|
-
end
|
78
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: llama_bot_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kody Kendall
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -151,11 +150,13 @@ files:
|
|
151
150
|
- lib/generators/llama_bot_rails/install/templates/agent_state_builder.rb.erb
|
152
151
|
- lib/llama_bot_rails.rb
|
153
152
|
- lib/llama_bot_rails/agent_auth.rb
|
153
|
+
- lib/llama_bot_rails/agent_auth_2.rb
|
154
154
|
- lib/llama_bot_rails/agent_state_builder.rb
|
155
155
|
- lib/llama_bot_rails/controller_extensions.rb
|
156
156
|
- lib/llama_bot_rails/engine.rb
|
157
157
|
- lib/llama_bot_rails/llama_bot.rb
|
158
158
|
- lib/llama_bot_rails/railtie.rb
|
159
|
+
- lib/llama_bot_rails/route_helper.rb
|
159
160
|
- lib/llama_bot_rails/tools/rails_console_tool.rb
|
160
161
|
- lib/llama_bot_rails/version.rb
|
161
162
|
- lib/tasks/llama_bot_rails_tasks.rake
|
@@ -164,7 +165,6 @@ licenses:
|
|
164
165
|
- MIT
|
165
166
|
metadata:
|
166
167
|
homepage_uri: https://llamapress.ai
|
167
|
-
post_install_message:
|
168
168
|
rdoc_options: []
|
169
169
|
require_paths:
|
170
170
|
- lib
|
@@ -179,8 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0'
|
181
181
|
requirements: []
|
182
|
-
rubygems_version: 3.
|
183
|
-
signing_key:
|
182
|
+
rubygems_version: 3.7.1
|
184
183
|
specification_version: 4
|
185
184
|
summary: LlamaBotRails is a gem that turns your existing Rails App into an AI Agent
|
186
185
|
by connecting it to an open source LangGraph agent, LlamaBot.
|