chaos_to_the_rescue 0.1.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.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module ChaosToTheRescue
6
+ # Thread-safe configuration for ChaosToTheRescue gem
7
+ class Configuration
8
+ include MonitorMixin
9
+
10
+ # @return [Boolean] Whether the gem is enabled globally (default: false)
11
+ attr_accessor :enabled
12
+
13
+ # @return [Boolean] Whether to automatically define generated methods (default: false)
14
+ attr_accessor :auto_define_methods
15
+
16
+ # @return [Array<Regexp>] Patterns for allowed method names
17
+ attr_accessor :allowed_method_name_patterns
18
+
19
+ # @return [Array<String, Symbol>] Explicit allowlist of method names
20
+ attr_accessor :allowlist
21
+
22
+ # @return [String] LLM model to use (default: "gpt-4o-mini")
23
+ attr_accessor :model
24
+
25
+ # @return [Integer] Maximum characters to send in prompts (default: 8000)
26
+ attr_accessor :max_prompt_chars
27
+
28
+ # @return [Symbol] Log level (:debug, :info, :warn, :error, :fatal, :unknown)
29
+ attr_accessor :log_level
30
+
31
+ # @return [Array<Regexp>] Patterns to redact from prompts
32
+ attr_accessor :redact_patterns
33
+
34
+ # @return [Boolean] Whether to log suggestions to file (default: true)
35
+ attr_accessor :log_suggestions
36
+
37
+ # @return [String] Path for suggestion log file
38
+ attr_accessor :suggestions_log_path
39
+
40
+ # @return [String] OpenAI API key (optional, can also use ENV['OPENAI_API_KEY'])
41
+ attr_accessor :openai_api_key
42
+
43
+ # @return [String] Anthropic API key (optional, can also use ENV['ANTHROPIC_API_KEY'])
44
+ attr_accessor :anthropic_api_key
45
+
46
+ # @return [Hash] Global method guidance for LLM-generated methods
47
+ attr_accessor :method_guidance
48
+
49
+ def initialize
50
+ super # Initialize MonitorMixin
51
+
52
+ # Safe defaults - disabled by default
53
+ @enabled = false
54
+ @auto_define_methods = false
55
+ @allowed_method_name_patterns = []
56
+ @allowlist = []
57
+ @model = "gpt-5-mini"
58
+ @max_prompt_chars = 8000
59
+ @log_level = :info
60
+ @log_suggestions = true
61
+ @suggestions_log_path = "tmp/chaos_to_the_rescue_suggestions.log"
62
+ @redact_patterns = default_redact_patterns
63
+ @openai_api_key = nil
64
+ @anthropic_api_key = nil
65
+ @method_guidance = {}
66
+ end
67
+
68
+ # Thread-safe configuration block
69
+ # @yield [Configuration] the configuration object
70
+ def configure
71
+ synchronize do
72
+ yield self
73
+ end
74
+ end
75
+
76
+ # Check if a method name is allowed based on patterns and allowlist
77
+ # @param method_name [String, Symbol] the method name to check
78
+ # @return [Boolean]
79
+ def method_allowed?(method_name)
80
+ method_str = method_name.to_s
81
+ method_sym = method_name.to_sym
82
+
83
+ # Check explicit allowlist
84
+ return true if allowlist.include?(method_str) || allowlist.include?(method_sym)
85
+
86
+ # Check patterns
87
+ allowed_method_name_patterns.any? { |pattern| pattern.match?(method_str) }
88
+ end
89
+
90
+ # Allow all method names by adding a catch-all pattern
91
+ # WARNING: Use with caution - this allows ChaosRescue to intercept any method call
92
+ # @return [void]
93
+ def allow_everything!
94
+ synchronize do
95
+ @allowed_method_name_patterns = [/.*/]
96
+ end
97
+ end
98
+
99
+ # Reset configuration to defaults
100
+ def reset!
101
+ synchronize do
102
+ @enabled = false
103
+ @auto_define_methods = false
104
+ @allowed_method_name_patterns = []
105
+ @allowlist = []
106
+ @model = "gpt-5-mini"
107
+ @max_prompt_chars = 8000
108
+ @log_level = :info
109
+ @log_suggestions = true
110
+ @suggestions_log_path = "tmp/chaos_to_the_rescue_suggestions.log"
111
+ @redact_patterns = default_redact_patterns
112
+ @openai_api_key = nil
113
+ @anthropic_api_key = nil
114
+ @method_guidance = {}
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def default_redact_patterns
121
+ [
122
+ # Environment variable references
123
+ /ENV\[['"]([^'"]+)['"]\]/i,
124
+ /ENV\.fetch\(['"]+([^'"]+)['"]+.*?\)/i,
125
+
126
+ # AWS credentials
127
+ /AWS_ACCESS_KEY_ID[=:]\s*['"]?([A-Z0-9]{20})['"]?/i,
128
+ /AWS_SECRET_ACCESS_KEY[=:]\s*['"]?([A-Za-z0-9\/+=]{40})['"]?/i,
129
+ /AKIA[0-9A-Z]{16}/,
130
+
131
+ # OpenAI tokens
132
+ /sk-[a-zA-Z0-9]{20,}/,
133
+ /OPENAI_API_KEY[=:]\s*['"]?([^'"\s]+)['"]?/i,
134
+
135
+ # GitHub tokens
136
+ /ghp_[a-zA-Z0-9]{20,}/,
137
+ /gho_[a-zA-Z0-9]{20,}/,
138
+ /ghu_[a-zA-Z0-9]{20,}/,
139
+ /ghs_[a-zA-Z0-9]{20,}/,
140
+ /ghr_[a-zA-Z0-9]{20,}/,
141
+ /GITHUB_TOKEN[=:]\s*['"]?([^'"\s]+)['"]?/i,
142
+
143
+ # Rails credentials
144
+ /Rails\.application\.credentials\.[a-z_]+/i,
145
+
146
+ # Generic secrets
147
+ /API_KEY[=:]\s*['"]?([^'"\s]+)['"]?/i,
148
+ /SECRET[=:]\s*['"]?([^'"\s]+)['"]?/i,
149
+ /TOKEN[=:]\s*['"]?([^'"\s]+)['"]?/i,
150
+ /PASSWORD[=:]\s*['"]?([^'"\s]+)['"]?/i,
151
+
152
+ # Private keys
153
+ /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----/i
154
+ ]
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChaosToTheRescue
4
+ # Abstract interface for LLM interactions
5
+ class LLMClient
6
+ class LLMNotAvailableError < StandardError; end
7
+ class LLMRequestError < StandardError; end
8
+
9
+ def initialize
10
+ @redactor = Redactor.new
11
+ check_llm_availability!
12
+ configure_ruby_llm!
13
+ end
14
+
15
+ # Generate Ruby method code based on context
16
+ # @param context [Hash] context including :method_name, :class_name, :args, etc.
17
+ # @return [Hash] with keys: :method_name, :ruby_code, :explanation
18
+ def generate_method_code(context)
19
+ prompt = build_method_generation_prompt(context)
20
+ redacted_prompt = truncate_prompt(@redactor.redact(prompt))
21
+
22
+ ChaosToTheRescue.logger.debug("Generating method code for #{context[:method_name]}")
23
+
24
+ response = call_llm(redacted_prompt)
25
+ parse_method_generation_response(response, context[:method_name])
26
+ rescue => e
27
+ ChaosToTheRescue.logger.error("Failed to generate method code: #{e.message}")
28
+ raise LLMRequestError, "Failed to generate method code: #{e.message}"
29
+ end
30
+
31
+ # Suggest a fix for an exception
32
+ # @param context [Hash] context including :exception, :backtrace, :guidance, etc.
33
+ # @return [Hash] with keys: :title, :diagnosis, :proposed_changes, :files_to_edit
34
+ def suggest_fix(context)
35
+ prompt = build_fix_suggestion_prompt(context)
36
+ redacted_prompt = truncate_prompt(@redactor.redact(prompt))
37
+
38
+ ChaosToTheRescue.logger.debug("Generating fix suggestion for #{context[:exception_class]}")
39
+
40
+ response = call_llm(redacted_prompt)
41
+ parse_fix_suggestion_response(response)
42
+ rescue => e
43
+ ChaosToTheRescue.logger.error("Failed to generate fix suggestion: #{e.message}")
44
+ raise LLMRequestError, "Failed to generate fix suggestion: #{e.message}"
45
+ end
46
+
47
+ private
48
+
49
+ def check_llm_availability!
50
+ require "ruby_llm"
51
+ rescue LoadError
52
+ raise LLMNotAvailableError, <<~MSG
53
+ RubyLLM is not available. To use ChaosToTheRescue's LLM features, add this to your Gemfile:
54
+
55
+ gem "ruby_llm", "~> 0.1"
56
+
57
+ Then run: bundle install
58
+ MSG
59
+ end
60
+
61
+ def configure_ruby_llm!
62
+ # Configure ruby_llm with API keys from our configuration or ENV
63
+ RubyLLM.configure do |config|
64
+ if ChaosToTheRescue.configuration.openai_api_key
65
+ config.openai_api_key = ChaosToTheRescue.configuration.openai_api_key
66
+ end
67
+
68
+ if ChaosToTheRescue.configuration.anthropic_api_key
69
+ config.anthropic_api_key = ChaosToTheRescue.configuration.anthropic_api_key
70
+ end
71
+ end
72
+ rescue => e
73
+ ChaosToTheRescue.logger.debug("Could not configure ruby_llm: #{e.message}")
74
+ # Continue anyway - ruby_llm will try to use ENV vars
75
+ end
76
+
77
+ def call_llm(prompt)
78
+ # Use RubyLLM to interact with the configured model
79
+ model = ChaosToTheRescue.configuration.model
80
+
81
+ chat = RubyLLM.chat(model: model)
82
+ response = chat.ask(prompt)
83
+
84
+ # Return the response content as a string
85
+ response.content
86
+ rescue => e
87
+ raise LLMRequestError, "LLM call failed: #{e.message}"
88
+ end
89
+
90
+ def build_method_generation_prompt(context)
91
+ <<~PROMPT
92
+ You are a Ruby code generator. Generate a Ruby method implementation based on the following context:
93
+
94
+ Class: #{context[:class_name]}
95
+ Method name: #{context[:method_name]}
96
+ Arguments: #{context[:args].inspect}
97
+ Context: #{context[:additional_context] || "No additional context provided"}
98
+
99
+ #{context[:guidance] ? "IMPORTANT GUIDANCE:\n#{context[:guidance]}\n" : ""}
100
+
101
+ Requirements:
102
+ 1. Generate only valid Ruby code
103
+ 2. The method should be reasonable and safe
104
+ 3. Do not include any dangerous operations (file system access, network calls, etc.)
105
+ 4. Return a simple, working implementation
106
+ #{context[:guidance] ? "5. Follow the guidance provided above carefully" : ""}
107
+
108
+ Please respond in the following format:
109
+ METHOD_NAME: <method_name>
110
+ CODE:
111
+ <ruby code here>
112
+ EXPLANATION: <brief explanation of what the method does>
113
+ PROMPT
114
+ end
115
+
116
+ def build_fix_suggestion_prompt(context)
117
+ <<~PROMPT
118
+ You are a Ruby debugging assistant. Analyze the following exception and suggest a fix:
119
+
120
+ Exception class: #{context[:exception_class]}
121
+ Exception message: #{context[:exception_message]}
122
+ Backtrace:
123
+ #{context[:backtrace]&.first(10)&.join("\n") || "No backtrace available"}
124
+
125
+ #{"Additional guidance: #{context[:guidance]}" if context[:guidance]}
126
+
127
+ Controller: #{context[:controller_name] || "N/A"}
128
+ Action: #{context[:action_name] || "N/A"}
129
+
130
+ Please respond in the following format:
131
+ TITLE: <brief title for this fix>
132
+ DIAGNOSIS: <explanation of what went wrong>
133
+ PROPOSED_CHANGES:
134
+ <description of changes to make>
135
+ FILES_TO_EDIT:
136
+ <list of files that should be modified>
137
+ PROMPT
138
+ end
139
+
140
+ def parse_method_generation_response(response, method_name)
141
+ # Parse the LLM response into structured data
142
+ lines = response.split("\n")
143
+
144
+ code_start = lines.index { |l| l.strip == "CODE:" }
145
+ explanation_start = lines.index { |l| l.start_with?("EXPLANATION:") }
146
+
147
+ {
148
+ method_name: method_name,
149
+ ruby_code: code_start ? lines[(code_start + 1)...explanation_start].join("\n").strip : "",
150
+ explanation: explanation_start ? lines[explanation_start].sub("EXPLANATION:", "").strip : ""
151
+ }
152
+ end
153
+
154
+ def parse_fix_suggestion_response(response)
155
+ # Parse the LLM response into structured data
156
+ lines = response.split("\n")
157
+
158
+ title = lines.find { |l| l.start_with?("TITLE:") }&.sub("TITLE:", "")&.strip || "Suggested Fix"
159
+ diagnosis_idx = lines.index { |l| l.start_with?("DIAGNOSIS:") }
160
+ changes_idx = lines.index { |l| l.start_with?("PROPOSED_CHANGES:") }
161
+ files_idx = lines.index { |l| l.start_with?("FILES_TO_EDIT:") }
162
+
163
+ {
164
+ title: title,
165
+ diagnosis: diagnosis_idx ? lines[diagnosis_idx].sub("DIAGNOSIS:", "").strip : "",
166
+ proposed_changes: (changes_idx && files_idx) ? lines[(changes_idx + 1)...files_idx].join("\n").strip : "",
167
+ files_to_edit: files_idx ? lines[(files_idx + 1)..].map(&:strip).reject(&:empty?) : []
168
+ }
169
+ end
170
+
171
+ def truncate_prompt(prompt)
172
+ max_chars = ChaosToTheRescue.configuration.max_prompt_chars
173
+ return prompt if prompt.length <= max_chars
174
+
175
+ truncation_suffix = "\n[TRUNCATED]"
176
+ truncate_at = max_chars - truncation_suffix.length
177
+
178
+ ChaosToTheRescue.logger.warn("Prompt truncated from #{prompt.length} to #{max_chars} characters")
179
+ prompt[0...truncate_at] + truncation_suffix
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module ChaosToTheRescue
6
+ # Logger wrapper that respects configuration settings
7
+ class Logger
8
+ def initialize
9
+ @logger = ::Logger.new($stdout)
10
+ @logger.formatter = proc do |severity, datetime, progname, msg|
11
+ "[ChaosToTheRescue] #{severity} #{datetime.strftime("%Y-%m-%d %H:%M:%S")}: #{msg}\n"
12
+ end
13
+ end
14
+
15
+ # Log a debug message
16
+ # @param message [String] the message to log
17
+ def debug(message)
18
+ return unless enabled?
19
+ ensure_level
20
+ @logger.debug(message)
21
+ end
22
+
23
+ # Log an info message
24
+ # @param message [String] the message to log
25
+ def info(message)
26
+ return unless enabled?
27
+ ensure_level
28
+ @logger.info(message)
29
+ end
30
+
31
+ # Log a warning message
32
+ # @param message [String] the message to log
33
+ def warn(message)
34
+ return unless enabled?
35
+ ensure_level
36
+ @logger.warn(message)
37
+ end
38
+
39
+ # Log an error message
40
+ # @param message [String] the message to log
41
+ def error(message)
42
+ return unless enabled?
43
+ ensure_level
44
+ @logger.error(message)
45
+ end
46
+
47
+ # Log a fatal message
48
+ # @param message [String] the message to log
49
+ def fatal(message)
50
+ return unless enabled?
51
+ ensure_level
52
+ @logger.fatal(message)
53
+ end
54
+
55
+ private
56
+
57
+ def enabled?
58
+ ChaosToTheRescue.configuration.enabled
59
+ end
60
+
61
+ def ensure_level
62
+ @logger.level = log_level_constant
63
+ end
64
+
65
+ def log_level_constant
66
+ level = ChaosToTheRescue.configuration.log_level
67
+ case level
68
+ when :debug then ::Logger::DEBUG
69
+ when :info then ::Logger::INFO
70
+ when :warn then ::Logger::WARN
71
+ when :error then ::Logger::ERROR
72
+ when :fatal then ::Logger::FATAL
73
+ else ::Logger::INFO
74
+ end
75
+ end
76
+ end
77
+
78
+ # Module-level logger instance
79
+ def self.logger
80
+ @logger ||= Logger.new
81
+ end
82
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChaosToTheRescue
4
+ # Rails integration via Railtie
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :chaos_to_the_rescue
7
+
8
+ # Make RescueFrom available to ActionController
9
+ initializer "chaos_to_the_rescue.action_controller" do
10
+ ActiveSupport.on_load(:action_controller) do
11
+ require "chaos_to_the_rescue/rescue_from"
12
+ end
13
+ end
14
+
15
+ # Log when ChaosToTheRescue is loaded in Rails
16
+ config.after_initialize do
17
+ if ChaosToTheRescue.configuration.enabled
18
+ Rails.logger.info("[ChaosToTheRescue] Enabled - method generation and rescue suggestions are active")
19
+ else
20
+ Rails.logger.info("[ChaosToTheRescue] Disabled - use ChaosToTheRescue.configure to enable")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChaosToTheRescue
4
+ # Redacts sensitive information from strings before sending to LLM
5
+ class Redactor
6
+ REDACTED_PLACEHOLDER = "[REDACTED]"
7
+
8
+ # @param patterns [Array<Regexp>] patterns to redact
9
+ def initialize(patterns = nil)
10
+ @patterns = patterns || ChaosToTheRescue.configuration.redact_patterns
11
+ end
12
+
13
+ # Redact sensitive information from text
14
+ # @param text [String] the text to redact
15
+ # @return [String] the redacted text
16
+ def redact(text)
17
+ return "" if text.nil? || text.empty?
18
+
19
+ redacted = text.dup
20
+
21
+ @patterns.each do |pattern|
22
+ redacted.gsub!(pattern, REDACTED_PLACEHOLDER)
23
+ end
24
+
25
+ redacted
26
+ end
27
+
28
+ # Redact sensitive information from a hash (recursive)
29
+ # @param hash [Hash] the hash to redact
30
+ # @return [Hash] a new hash with redacted values
31
+ def redact_hash(hash)
32
+ return {} unless hash.is_a?(Hash)
33
+
34
+ hash.transform_values do |value|
35
+ case value
36
+ when String
37
+ redact(value)
38
+ when Hash
39
+ redact_hash(value)
40
+ when Array
41
+ value.map { |item| item.is_a?(String) ? redact(item) : item }
42
+ else
43
+ value
44
+ end
45
+ end
46
+ end
47
+
48
+ # Redact sensitive information from an array
49
+ # @param array [Array] the array to redact
50
+ # @return [Array] a new array with redacted values
51
+ def redact_array(array)
52
+ return [] unless array.is_a?(Array)
53
+
54
+ array.map do |item|
55
+ case item
56
+ when String
57
+ redact(item)
58
+ when Hash
59
+ redact_hash(item)
60
+ when Array
61
+ redact_array(item)
62
+ else
63
+ item
64
+ end
65
+ end
66
+ end
67
+
68
+ # Redact sensitive information from any object by converting to string
69
+ # @param obj [Object] the object to redact
70
+ # @return [String] the redacted string representation
71
+ def redact_object(obj)
72
+ return "" if obj.nil?
73
+ redact(obj.to_s)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module ChaosToTheRescue
6
+ # Controller concern for Rails rescue_from integration
7
+ module RescueFrom
8
+ extend ActiveSupport::Concern
9
+
10
+ # Generate a fix suggestion for an exception
11
+ # @param exception [Exception] the exception to analyze
12
+ # @param guidance [String] optional guidance for the LLM
13
+ # @return [Hash] suggestion with :title, :diagnosis, :proposed_changes, :files_to_edit
14
+ def chaos_suggest_fix(exception, guidance: nil)
15
+ unless ChaosToTheRescue.configuration.enabled
16
+ ChaosToTheRescue.logger.warn("chaos_suggest_fix called but ChaosToTheRescue is disabled")
17
+ return {
18
+ title: "ChaosToTheRescue Disabled",
19
+ diagnosis: "ChaosToTheRescue is not enabled. Enable it in your configuration.",
20
+ proposed_changes: "",
21
+ files_to_edit: []
22
+ }
23
+ end
24
+
25
+ context = build_exception_context(exception, guidance)
26
+
27
+ begin
28
+ suggestion = llm_client.suggest_fix(context)
29
+
30
+ # Log the suggestion if configured
31
+ log_suggestion(exception, suggestion) if ChaosToTheRescue.configuration.log_suggestions
32
+
33
+ suggestion
34
+ rescue LLMClient::LLMNotAvailableError => e
35
+ ChaosToTheRescue.logger.error("LLM not available: #{e.message}")
36
+ {
37
+ title: "LLM Not Available",
38
+ diagnosis: e.message,
39
+ proposed_changes: "",
40
+ files_to_edit: []
41
+ }
42
+ rescue => e
43
+ ChaosToTheRescue.logger.error("Failed to generate fix suggestion: #{e.message}")
44
+ {
45
+ title: "Error Generating Suggestion",
46
+ diagnosis: "Failed to generate suggestion: #{e.message}",
47
+ proposed_changes: "",
48
+ files_to_edit: []
49
+ }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # Build context hash for exception analysis
56
+ # @param exception [Exception] the exception
57
+ # @param guidance [String] optional guidance
58
+ # @return [Hash] context for LLM
59
+ def build_exception_context(exception, guidance)
60
+ {
61
+ exception_class: exception.class.name,
62
+ exception_message: sanitize_exception_message(exception.message),
63
+ backtrace: sanitize_backtrace(exception.backtrace),
64
+ guidance: guidance,
65
+ controller_name: controller_name,
66
+ action_name: action_name,
67
+ request_method: request.request_method,
68
+ request_path: request.path,
69
+ params: sanitize_params(params.to_unsafe_h)
70
+ }
71
+ rescue => e
72
+ ChaosToTheRescue.logger.warn("Failed to build full exception context: #{e.message}")
73
+ {
74
+ exception_class: exception.class.name,
75
+ exception_message: exception.message.to_s,
76
+ backtrace: exception.backtrace,
77
+ guidance: guidance
78
+ }
79
+ end
80
+
81
+ # Sanitize exception message to remove sensitive data
82
+ # @param message [String] the exception message
83
+ # @return [String] sanitized message
84
+ def sanitize_exception_message(message)
85
+ redactor.redact(message.to_s)
86
+ end
87
+
88
+ # Sanitize backtrace to remove sensitive paths
89
+ # @param backtrace [Array<String>] the backtrace
90
+ # @return [Array<String>] sanitized backtrace
91
+ def sanitize_backtrace(backtrace)
92
+ return [] unless backtrace
93
+
94
+ backtrace.first(20).map do |line|
95
+ # Replace absolute paths with relative paths for privacy
96
+ line.gsub(Rails.root.to_s, "[RAILS_ROOT]")
97
+ end
98
+ end
99
+
100
+ # Sanitize params to remove sensitive data
101
+ # @param params [Hash] the params hash
102
+ # @return [Hash] sanitized params
103
+ def sanitize_params(params)
104
+ # Remove common sensitive keys
105
+ sensitive_keys = %w[password password_confirmation token secret api_key access_token]
106
+
107
+ params.except(*sensitive_keys).transform_values do |value|
108
+ if value.is_a?(String)
109
+ redactor.redact(value)
110
+ else
111
+ value
112
+ end
113
+ end
114
+ end
115
+
116
+ # Log the suggestion to file
117
+ # @param exception [Exception] the exception
118
+ # @param suggestion [Hash] the suggestion
119
+ def log_suggestion(exception, suggestion)
120
+ log_path = ChaosToTheRescue.configuration.suggestions_log_path
121
+
122
+ # Ensure directory exists
123
+ FileUtils.mkdir_p(File.dirname(log_path))
124
+
125
+ File.open(log_path, "a") do |f|
126
+ f.puts "=" * 80
127
+ f.puts "Timestamp: #{Time.now.iso8601}"
128
+ f.puts "Exception: #{exception.class.name}: #{exception.message}"
129
+ f.puts "Controller: #{controller_name}##{action_name}"
130
+ f.puts "\nSuggestion:"
131
+ f.puts "Title: #{suggestion[:title]}"
132
+ f.puts "Diagnosis: #{suggestion[:diagnosis]}"
133
+ f.puts "\nProposed Changes:"
134
+ f.puts suggestion[:proposed_changes]
135
+ f.puts "\nFiles to Edit:"
136
+ suggestion[:files_to_edit].each { |file| f.puts " - #{file}" }
137
+ f.puts "=" * 80
138
+ f.puts
139
+ end
140
+ rescue => e
141
+ ChaosToTheRescue.logger.error("Failed to log suggestion: #{e.message}")
142
+ end
143
+
144
+ # Get the LLM client instance
145
+ # @return [LLMClient]
146
+ def llm_client
147
+ @llm_client ||= LLMClient.new
148
+ end
149
+
150
+ # Get the redactor instance
151
+ # @return [Redactor]
152
+ def redactor
153
+ @redactor ||= Redactor.new
154
+ end
155
+ end
156
+ end