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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/app/channels/llama_bot_rails/chat_channel.rb +75 -73
  4. data/app/controllers/llama_bot_rails/agent_controller.rb +36 -32
  5. data/app/views/llama_bot_rails/agent/chat.html.erb +2 -1
  6. data/app/views/llama_bot_rails/agent/chat_ws.html.erb +11 -2
  7. data/bin/bundle +29 -0
  8. data/bin/bundle.lock +0 -0
  9. data/bin/bundler +29 -0
  10. data/bin/bundler.lock +0 -0
  11. data/bin/byebug +29 -0
  12. data/bin/coderay +29 -0
  13. data/bin/erb +29 -0
  14. data/bin/faker +29 -0
  15. data/bin/htmldiff +29 -0
  16. data/bin/irb +29 -0
  17. data/bin/ldiff +29 -0
  18. data/bin/nokogiri +29 -0
  19. data/bin/pry +29 -0
  20. data/bin/puma +29 -0
  21. data/bin/pumactl +29 -0
  22. data/bin/racc +29 -0
  23. data/bin/rackup +29 -0
  24. data/bin/rake +29 -0
  25. data/bin/rdoc +29 -0
  26. data/bin/ri +29 -0
  27. data/bin/rspec +29 -0
  28. data/bin/ruby-parse +29 -0
  29. data/bin/ruby-rewrite +29 -0
  30. data/bin/sprockets +29 -0
  31. data/bin/thor +29 -0
  32. data/lib/generators/llama_bot_rails/install/install_generator.rb +68 -0
  33. data/lib/generators/llama_bot_rails/install/templates/agent_state_builder.rb.erb +3 -3
  34. data/lib/llama_bot_rails/agent_auth.rb +158 -0
  35. data/lib/llama_bot_rails/agent_auth_2.rb +149 -0
  36. data/lib/llama_bot_rails/agent_state_builder.rb +6 -6
  37. data/lib/llama_bot_rails/controller_extensions.rb +40 -0
  38. data/lib/llama_bot_rails/engine.rb +8 -2
  39. data/lib/llama_bot_rails/llama_bot.rb +3 -0
  40. data/lib/llama_bot_rails/railtie.rb +6 -0
  41. data/lib/llama_bot_rails/route_helper.rb +117 -0
  42. data/lib/llama_bot_rails/version.rb +1 -1
  43. data/lib/llama_bot_rails.rb +64 -21
  44. metadata +31 -4
  45. data/bin/rails +0 -26
  46. 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[:message], # Rails param from JS/chat UI. This is the user's message to the agent.
16
- thread_id: @params[:thread_id], # This is the thread id for the agent. It is used to track the conversation history.
17
- 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.)
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
- config.llama_bot_rails.websocket_url = ENV['LLAMABOT_WEBSOCKET_URL'] || 'ws://llamabot-backend:8000/ws' # <-- default for Docker Compose
15
- config.llama_bot_rails.llamabot_api_url = ENV['LLAMABOT_API_URL'] || "http://llamabot-backend:8000" # <-- default for Docker Compose
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
@@ -30,6 +30,9 @@ module LlamaBotRails
30
30
  http = Net::HTTP.new(uri.host, uri.port)
31
31
 
32
32
  request = Net::HTTP::Post.new(uri)
33
+
34
+ http.use_ssl = (uri.scheme == "https")
35
+
33
36
  request['Content-Type'] = 'application/json'
34
37
  request.body = agent_params.to_json
35
38
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module LlamaBotRails
2
- VERSION = "0.1.11"
2
+ VERSION = "0.1.14"
3
3
  end
@@ -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
- def config
11
- Rails.application.config.llama_bot_rails
12
- end
20
+ attr_accessor :user_resolver
21
+ attr_accessor :current_user_resolver
13
22
 
14
- def agent_prompt_path
15
- Rails.root.join("app", "llama_bot", "prompts", "agent_prompt.txt")
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
- def agent_prompt_text
19
- if File.exist?(agent_prompt_path)
20
- File.read(agent_prompt_path)
21
- else
22
- "You are LlamaBot, a helpful assistant." #Fallback default.
23
- end
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
- def add_instruction_to_agent_prompt!(new_instruction)
27
- FileUtils.mkdir_p(agent_prompt_path.dirname)
28
- File.write(agent_prompt_path, "\n#{new_instruction}", mode: 'a')
29
- end
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
- def send_agent_message(agent_params)
32
- LlamaBot.send_agent_message(agent_params)
33
- end
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
- end
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