llama_bot_rails 0.1.11 → 0.1.14
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 +17 -0
- data/app/channels/llama_bot_rails/chat_channel.rb +75 -73
- data/app/controllers/llama_bot_rails/agent_controller.rb +36 -32
- data/app/views/llama_bot_rails/agent/chat.html.erb +2 -1
- data/app/views/llama_bot_rails/agent/chat_ws.html.erb +11 -2
- data/bin/bundle +29 -0
- data/bin/bundle.lock +0 -0
- data/bin/bundler +29 -0
- data/bin/bundler.lock +0 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/erb +29 -0
- data/bin/faker +29 -0
- data/bin/htmldiff +29 -0
- data/bin/irb +29 -0
- data/bin/ldiff +29 -0
- data/bin/nokogiri +29 -0
- data/bin/pry +29 -0
- data/bin/puma +29 -0
- data/bin/pumactl +29 -0
- data/bin/racc +29 -0
- data/bin/rackup +29 -0
- data/bin/rake +29 -0
- data/bin/rdoc +29 -0
- data/bin/ri +29 -0
- data/bin/rspec +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/sprockets +29 -0
- data/bin/thor +29 -0
- data/lib/generators/llama_bot_rails/install/install_generator.rb +68 -0
- data/lib/generators/llama_bot_rails/install/templates/agent_state_builder.rb.erb +3 -3
- data/lib/llama_bot_rails/agent_auth.rb +158 -0
- 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/controller_extensions.rb +40 -0
- data/lib/llama_bot_rails/engine.rb +8 -2
- data/lib/llama_bot_rails/llama_bot.rb +3 -0
- data/lib/llama_bot_rails/railtie.rb +6 -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 +64 -21
- metadata +31 -4
- data/bin/rails +0 -26
- data/bin/rubocop +0 -8
@@ -0,0 +1,158 @@
|
|
1
|
+
module LlamaBotRails
|
2
|
+
module AgentAuth
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
AUTH_SCHEME = "LlamaBot"
|
5
|
+
|
6
|
+
included do
|
7
|
+
# Add before_action filter to automatically check agent authentication for LlamaBot requests
|
8
|
+
|
9
|
+
if self < ActionController::Base
|
10
|
+
before_action :check_agent_authentication, if: :should_check_agent_auth?
|
11
|
+
end
|
12
|
+
|
13
|
+
# ------------------------------------------------------------------
|
14
|
+
# 1) For every Devise scope, alias authenticate_<scope>! so it now
|
15
|
+
# accepts *either* a logged-in browser session OR a valid agent
|
16
|
+
# token. Existing before/skip filters keep working.
|
17
|
+
# ------------------------------------------------------------------
|
18
|
+
if defined?(Devise)
|
19
|
+
Devise.mappings.keys.each do |scope|
|
20
|
+
scope_filter = :"authenticate_#{scope}!"
|
21
|
+
|
22
|
+
# Next line is a no-op if the method wasn’t already defined.
|
23
|
+
alias_method scope_filter, :authenticate_user_or_agent! \
|
24
|
+
if method_defined?(scope_filter)
|
25
|
+
|
26
|
+
# Emit a gentle nudge during development
|
27
|
+
define_method(scope_filter) do |*args|
|
28
|
+
Rails.logger.warn(
|
29
|
+
"#{scope_filter} is now handled by LlamaBotRails::AgentAuth "\
|
30
|
+
"and will be removed in a future version. "\
|
31
|
+
"Use authenticate_user_or_agent! instead."
|
32
|
+
)
|
33
|
+
authenticate_user_or_agent!(*args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# ------------------------------------------------------------------
|
39
|
+
# 2) If Devise isn’t loaded at all, fall back to one alias so apps
|
40
|
+
# that had authenticate_user! manually defined don’t break.
|
41
|
+
# ------------------------------------------------------------------
|
42
|
+
unless defined?(Devise)
|
43
|
+
# Store the original method if it exists
|
44
|
+
original_authenticate_user = if method_defined?(:authenticate_user!)
|
45
|
+
instance_method(:authenticate_user!)
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
# Define the new method that calls authenticate_user_or_agent!
|
51
|
+
define_method(:authenticate_user!) do |*args|
|
52
|
+
authenticate_user_or_agent!(*args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# --------------------------------------------------------------------
|
58
|
+
# Public helper: true if the request carries a *valid* agent token
|
59
|
+
# --------------------------------------------------------------------
|
60
|
+
def should_check_agent_auth?
|
61
|
+
# Skip agent authentication entirely if a Devise user is already signed in
|
62
|
+
return false if devise_user_signed_in?
|
63
|
+
|
64
|
+
# Only check for LlamaBot requests if no Devise user is signed in
|
65
|
+
llama_bot_request?
|
66
|
+
end
|
67
|
+
|
68
|
+
def llama_bot_request?
|
69
|
+
return false unless request&.headers
|
70
|
+
scheme, token = request.headers["Authorization"]&.split(" ", 2)
|
71
|
+
Rails.logger.debug("[LlamaBot] auth header = #{scheme.inspect} #{token&.slice(0,8)}…")
|
72
|
+
return false unless scheme == AUTH_SCHEME && token.present?
|
73
|
+
|
74
|
+
Rails.application.message_verifier(:llamabot_ws).verify(token)
|
75
|
+
true
|
76
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# --------------------------------------------------------------------
|
83
|
+
# Automatic check for LlamaBot requests - called by before_action filter
|
84
|
+
# --------------------------------------------------------------------
|
85
|
+
def check_agent_authentication
|
86
|
+
# Check if this controller has LlamaBot-aware actions
|
87
|
+
has_permitted_actions = self.class.respond_to?(:llama_bot_permitted_actions)
|
88
|
+
|
89
|
+
# Skip if controller doesn't use llama_bot_allow at all
|
90
|
+
return unless has_permitted_actions
|
91
|
+
|
92
|
+
is_llama_request = llama_bot_request?
|
93
|
+
action_is_whitelisted = self.class.llama_bot_permitted_actions.include?(action_name)
|
94
|
+
|
95
|
+
if is_llama_request
|
96
|
+
# If it's a LlamaBot request, only allow whitelisted actions
|
97
|
+
unless action_is_whitelisted
|
98
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' isn't white-listed for LlamaBot. To fix this, add `llama_bot_allow :#{action_name}` in your controller.")
|
99
|
+
render json: { error: "Action '#{action_name}' isn't white-listed for LlamaBot. To fix this, add `llama_bot_allow :#{action_name}` in your controller." }, status: :forbidden
|
100
|
+
return
|
101
|
+
end
|
102
|
+
Rails.logger.debug("[[LlamaBot Debug]] Valid LlamaBot request for action '#{action_name}'")
|
103
|
+
elsif action_is_whitelisted
|
104
|
+
# If action requires LlamaBot auth but request isn't a LlamaBot request, reject it
|
105
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' requires LlamaBot authentication, but request is not a valid LlamaBot request.")
|
106
|
+
render json: { error: "Action '#{action_name}' requires LlamaBot authentication" }, status: :forbidden
|
107
|
+
return
|
108
|
+
end
|
109
|
+
|
110
|
+
# All other cases: non-LlamaBot requests to non-whitelisted actions are allowed
|
111
|
+
end
|
112
|
+
|
113
|
+
# --------------------------------------------------------------------
|
114
|
+
# Unified guard — browser OR agent
|
115
|
+
# --------------------------------------------------------------------
|
116
|
+
def devise_user_signed_in?
|
117
|
+
return false unless defined?(Devise)
|
118
|
+
return false unless request&.env
|
119
|
+
request.env["warden"]&.authenticated?
|
120
|
+
end
|
121
|
+
|
122
|
+
def authenticate_user_or_agent!(*)
|
123
|
+
return if devise_user_signed_in? # any logged-in Devise scope
|
124
|
+
|
125
|
+
# 2) LlamaBot token present AND action allowed?
|
126
|
+
if llama_bot_request?
|
127
|
+
scheme, token = request.headers["Authorization"]&.split(" ", 2)
|
128
|
+
data = Rails.application.message_verifier(:llamabot_ws).verify(token)
|
129
|
+
|
130
|
+
allowed = self.class.respond_to?(:llama_bot_permitted_actions) &&
|
131
|
+
self.class.llama_bot_permitted_actions.include?(action_name)
|
132
|
+
|
133
|
+
if allowed
|
134
|
+
|
135
|
+
user_object = LlamaBotRails.user_resolver.call(data[:user_id])
|
136
|
+
unless LlamaBotRails.sign_in_method.call(request.env, user_object)
|
137
|
+
head :unauthorized
|
138
|
+
end
|
139
|
+
|
140
|
+
return # ✅ token + allow-listed action + user found and set properly for rack environment
|
141
|
+
else
|
142
|
+
# ❌ auth token is valid, but the attempted controller action is not added to the whitelist.
|
143
|
+
Rails.logger.warn("[LlamaBot] Action '#{action_name}' isn't white-listed for LlamaBot. To fix this, include LlamaBotRails::ControllerExtensions and add `llama_bot_allow :method` in your controller.")
|
144
|
+
render json: { error: "Action '#{action_name}' isn't white-listed for LlamaBot. To fix this, include LlamaBotRails::ControllerExtensions and add `llama_bot_allow :method` in your controller." }, status: :forbidden
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Neither path worked — fall back to Devise's normal behaviour and let Devise handle 401
|
150
|
+
if defined?(Devise) && request&.env
|
151
|
+
request.env["warden"].authenticate! # 401 or redirect
|
152
|
+
else
|
153
|
+
head :unauthorized
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
@@ -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,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module LlamaBotRails
|
3
|
+
module ControllerExtensions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
# NEW: per-controller class attribute that stores the allow-list
|
8
|
+
class_attribute :llama_bot_permitted_actions,
|
9
|
+
instance_writer: false,
|
10
|
+
default: []
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
# Usage: llama_bot_allow :update, :preview
|
15
|
+
def llama_bot_allow(*actions)
|
16
|
+
# normalise to strings so `include?(action_name)` works
|
17
|
+
acts = actions.map(&:to_s)
|
18
|
+
|
19
|
+
# Check if this specific class has had llama_bot_allow called directly on it
|
20
|
+
if instance_variable_defined?(:@_llama_bot_allow_called)
|
21
|
+
# This class has been configured before, accumulate with existing
|
22
|
+
current_actions = llama_bot_permitted_actions || []
|
23
|
+
else
|
24
|
+
# First time configuring this class, start fresh (ignore inherited values)
|
25
|
+
current_actions = []
|
26
|
+
@_llama_bot_allow_called = true
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a new array to ensure inheritance doesn't share state
|
30
|
+
self.llama_bot_permitted_actions = (current_actions + acts).uniq
|
31
|
+
|
32
|
+
# (optional) keep your global registry if you still need it
|
33
|
+
if defined?(LlamaBotRails.allowed_routes)
|
34
|
+
acts.each { |a| LlamaBotRails.allowed_routes << "#{controller_path}##{a}" }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -11,8 +11,9 @@ module LlamaBotRails
|
|
11
11
|
end
|
12
12
|
|
13
13
|
config.llama_bot_rails = ActiveSupport::OrderedOptions.new
|
14
|
-
|
15
|
-
config.llama_bot_rails.
|
14
|
+
|
15
|
+
config.llama_bot_rails.websocket_url = 'ws://llamabot-backend:8000/ws'
|
16
|
+
config.llama_bot_rails.llamabot_api_url ="http://llamabot-backend:8000"
|
16
17
|
config.llama_bot_rails.enable_console_tool = true
|
17
18
|
|
18
19
|
initializer "llama_bot_rails.assets.precompile" do |app|
|
@@ -22,5 +23,10 @@ module LlamaBotRails
|
|
22
23
|
initializer "llama_bot_rails.defaults" do |app|
|
23
24
|
app.config.llama_bot_rails.state_builder_class ||= "LlamaBotRails::AgentStateBuilder"
|
24
25
|
end
|
26
|
+
|
27
|
+
initializer "llama_bot_rails.message_verifier" do |app|
|
28
|
+
# Ensure the message verifier is available
|
29
|
+
Rails.application.message_verifier(:llamabot_ws)
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
@@ -1,5 +1,11 @@
|
|
1
1
|
module LlamaBotRails
|
2
2
|
class Railtie < ::Rails::Railtie
|
3
|
+
initializer "llama_bot.include_controller_extensions" do
|
4
|
+
ActiveSupport.on_load(:action_controller) do
|
5
|
+
include LlamaBotRails::ControllerExtensions
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
3
9
|
config.before_configuration do |app|
|
4
10
|
llama_bot_path = Rails.root.join("app", "llama_bot")
|
5
11
|
|
@@ -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
@@ -1,35 +1,78 @@
|
|
1
|
+
require "set" # ← you call Set.new
|
1
2
|
require "llama_bot_rails/version"
|
2
3
|
require "llama_bot_rails/engine"
|
3
|
-
|
4
|
-
# require "llama_bot_rails/railtie" # We don't need this, as we're loading the LlamaBot path directly in the engine.
|
5
4
|
require "llama_bot_rails/llama_bot"
|
6
5
|
require "llama_bot_rails/agent_state_builder"
|
6
|
+
require "llama_bot_rails/controller_extensions"
|
7
|
+
require "llama_bot_rails/agent_auth"
|
8
|
+
require "llama_bot_rails/route_helper"
|
7
9
|
|
8
10
|
module LlamaBotRails
|
11
|
+
# ------------------------------------------------------------------
|
12
|
+
# Public configuration
|
13
|
+
# ------------------------------------------------------------------
|
14
|
+
|
15
|
+
# Allow-list of routes the agent may hit
|
16
|
+
mattr_accessor :allowed_routes, default: Set.new
|
17
|
+
|
18
|
+
# Lambda that receives Rack env and returns a user-like object
|
9
19
|
class << self
|
10
|
-
|
11
|
-
|
12
|
-
end
|
20
|
+
attr_accessor :user_resolver
|
21
|
+
attr_accessor :current_user_resolver
|
13
22
|
|
14
|
-
|
15
|
-
|
23
|
+
attr_accessor :sign_in_method
|
24
|
+
end
|
25
|
+
|
26
|
+
# Default (Devise / Warden); returns nil if Devise absent
|
27
|
+
self.user_resolver = ->(user_id) do
|
28
|
+
# Try to find a User model, fallback to nil if not found
|
29
|
+
# byebug
|
30
|
+
if defined?(Devise)
|
31
|
+
default_scope = Devise.default_scope # e.g., :user
|
32
|
+
user_class = Devise.mappings[default_scope].to
|
33
|
+
user_class.find_by(id: user_id)
|
34
|
+
else
|
35
|
+
Rails.logger.warn("[[LlamaBot]] Implement a user_resolver! in your app to resolve the user from the user_id.")
|
36
|
+
nil
|
16
37
|
end
|
38
|
+
end
|
17
39
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
40
|
+
# Default (Devise / Warden); returns nil if Devise absent
|
41
|
+
self.current_user_resolver = ->(env) do
|
42
|
+
# Try to find a User model, fallback to nil if not found
|
43
|
+
if defined?(Devise)
|
44
|
+
env['warden']&.user
|
45
|
+
else
|
46
|
+
Rails.logger.warn("[[LlamaBot]] Implement a current_user_resolver! in your app to resolve the current user from the environment.")
|
47
|
+
nil
|
24
48
|
end
|
49
|
+
end
|
25
50
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
51
|
+
# Lambda that receives Rack env and user_id, and sets the user in the warden session
|
52
|
+
# Default sign-in method is configured for Devise with Warden.
|
53
|
+
self.sign_in_method = ->(env, user) do
|
54
|
+
env['warden']&.set_user(user)
|
55
|
+
end
|
30
56
|
|
31
|
-
|
32
|
-
|
33
|
-
|
57
|
+
# Convenience helper for host-app initializers
|
58
|
+
def self.config = Rails.application.config.llama_bot_rails
|
59
|
+
|
60
|
+
# ------------------------------------------------------------------
|
61
|
+
# Prompt helpers
|
62
|
+
# ------------------------------------------------------------------
|
63
|
+
def self.agent_prompt_path = Rails.root.join("app", "llama_bot", "prompts", "agent_prompt.txt")
|
64
|
+
|
65
|
+
def self.agent_prompt_text
|
66
|
+
File.exist?(agent_prompt_path) ? File.read(agent_prompt_path) : "You are LlamaBot, a helpful assistant."
|
34
67
|
end
|
35
|
-
|
68
|
+
|
69
|
+
def self.add_instruction_to_agent_prompt!(str)
|
70
|
+
FileUtils.mkdir_p(agent_prompt_path.dirname)
|
71
|
+
File.write(agent_prompt_path, "\n#{str}", mode: "a")
|
72
|
+
end
|
73
|
+
|
74
|
+
# ------------------------------------------------------------------
|
75
|
+
# Bridge to backend service
|
76
|
+
# ------------------------------------------------------------------
|
77
|
+
def self.send_agent_message(params) = LlamaBot.send_agent_message(params)
|
78
|
+
end
|