n2b 0.2.1 → 0.2.2

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -17
  3. data/lib/n2b/irb.rb +911 -9
  4. data/lib/n2b/version.rb +1 -1
  5. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d89058ab48c54dc1d1a2d55cc5bd504b734455111bf27d4513d2461cf5e1d29f
4
- data.tar.gz: 772c01402fc6620f237100c66771a0cab1d8cea99c4e6e4bbdea3e2b8933ddf3
3
+ metadata.gz: 3a24dc543659153dabaf892f6bed38934425ac43a1a0b2a263e82c3913e0b5f9
4
+ data.tar.gz: 01ccd36a76124d3599a7dd26a5cb1f443c4465e21c5cd12505512c00f771a164
5
5
  SHA512:
6
- metadata.gz: dac9d312eea30bbe3314d5ba0c20139f5ea4e14ce6b1aa55a98c588e7a9e53688b9b3f64fb625f5b36d6a030ed09a6ec433073ff8f3b400648dc8c2f154d0d18
7
- data.tar.gz: 21a17faed1c73826ef19c2c52f8fd8624945ca57c5de94f720ecdbf306501978943c2ed238f43b6e5116f407971738d2239cc70c7b66a67d0571e0fc740d94c7
6
+ metadata.gz: 737d3f29477d61da584640389552ebb01809fc762ec6348ee032ca0ab9fe6a3c2360df103fbe8daa3751a980b0b9c41b1998523c5f74f0b6ebb906e5a4a939dc
7
+ data.tar.gz: 5e975752513f183640c391afd46bd322d80da3a488d046d3e7e487f496b96a65c6bc60fae47de4187e2ca455c41d3aa1a6f23acdd2095f2cccaa36cbacbe8427
data/README.md CHANGED
@@ -1,25 +1,72 @@
1
- # N2B: Natural Language to Bash Commands Converter
1
+ # N2B - Natural Language to Bash & Ruby
2
2
 
3
- N2B (Natural to Bash) is a Ruby gem that converts natural language instructions into executable shell commands using the Claude AI or OpenAI API. It's designed to help users quickly generate shell commands without needing to remember exact syntax.
4
- Also it has the n2r method which can help you with any Ruby or Rails related issues
3
+ [![Gem Version](https://badge.fury.io/rb/n2b.svg)](https://badge.fury.io/rb/n2b)
4
+
5
+ N2B (Natural Language to Bash & Ruby) is a Ruby gem that leverages AI to convert natural language instructions into bash commands and Ruby code.
5
6
 
6
7
  ## Features
7
8
 
8
- ### N2B
9
+ - Convert natural language to bash commands
10
+ - Generate Ruby code from natural language instructions
11
+ - Analyze Errbit errors and generate detailed reports
12
+ - Create formatted Scrum tickets from errors
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'n2b'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install n2b
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ Create a config file at `~/.n2b/config.yml` with your API keys:
9
37
 
10
- - Convert natural language to shell commands
11
- - Support for multiple Claude AI models (Haiku, Sonnet, Sonnet 3.5)
12
- - Support for OpenAI models
13
- - Option to execute generated commands directly
14
- - Configurable privacy settings
15
- - Shell history integration
16
- - Command history tracking for improved context
38
+ ```yaml
39
+ llm: claude # or openai
40
+ claude:
41
+ key: your-anthropic-api-key
42
+ model: claude-3-opus-20240229 # or opus, haiku, sonnet
43
+ openai:
44
+ key: your-openai-api-key
45
+ model: gpt-4 # or gpt-3.5-turbo
46
+ ```
17
47
 
18
- ### N2R
19
- - Convert natural language to ruby code or explain it
20
- - analyze an exception and find the cause
21
- - analyze existing ruby files
22
-
48
+ ## Usage
49
+
50
+ ### Convert Natural Language to Bash
51
+
52
+ ```ruby
53
+ require 'n2b'
54
+
55
+ # Convert a natural language instruction to a bash command
56
+ N2B::Base.new.n2b("list all jpg files in the current directory")
57
+ # => find . -name "*.jpg"
58
+ ```
59
+
60
+ ### Generate Ruby Code
61
+
62
+ ```ruby
63
+ require 'n2b'
64
+
65
+ # In an IRB console
66
+ n2r("create a function that calculates fibonacci numbers")
67
+ ```
68
+
69
+ This will output both the code and an explanation:
23
70
 
24
71
  ## Quick Example N2B
25
72
 
@@ -167,4 +214,30 @@ This project is licensed under the MIT License.
167
214
 
168
215
  ## Support
169
216
 
170
- If you encounter any issues or have questions, please file an issue on the GitHub repository.
217
+ If you encounter any issues or have questions, please file an issue on the GitHub repository.
218
+
219
+ ### Generate Scrum Tickets from Errors
220
+
221
+ Create well-formatted Scrum tickets from Errbit errors:
222
+
223
+ ```ruby
224
+ require 'n2b'
225
+
226
+ # Generate a Scrum ticket from an Errbit error
227
+ n2rscrum(
228
+ url: "https://your-errbit-instance/apps/12345/problems/67890",
229
+ cookie: "your_errbit_session_cookie",
230
+ source_dir: "/path/to/your/app" # Optional: source code directory
231
+ )
232
+ ```
233
+
234
+ The generated tickets include:
235
+ - Clear title and description
236
+ - Technical details with error context
237
+ - Request parameters analysis
238
+ - Root cause analysis
239
+ - Suggested fixes with code examples
240
+ - Acceptance criteria
241
+ - Story point estimate
242
+ - Priority level
243
+ - Reference to the original Errbit URL
data/lib/n2b/irb.rb CHANGED
@@ -1,20 +1,29 @@
1
1
  module N2B
2
2
  class IRB
3
3
  MAX_SOURCE_FILES = 4
4
+ DEFAULT_CONTEXT_LINES = 20
4
5
 
5
6
  def self.n2r(input_string='', files: [], exception: nil, log: false)
6
7
  new.n2r(input_string, files: files, exception: exception, log: log)
7
8
  end
8
9
 
10
+ def self.n2rrbit(url:, cookie:, source_dir: nil, context_lines: DEFAULT_CONTEXT_LINES, log: false)
11
+ new.n2rrbit(url: url, cookie: cookie, source_dir: source_dir, context_lines: context_lines, log: log)
12
+ end
13
+
14
+ def self.n2rscrum(input_string='', files: [], exception: nil, url: nil, cookie: nil, source_dir: nil, context_lines: DEFAULT_CONTEXT_LINES, log: false)
15
+ new.n2rscrum(input_string: input_string, files: files, exception: exception, url: url, cookie: cookie, source_dir: source_dir, context_lines: context_lines, log: log)
16
+ end
17
+
9
18
  def n2r(input_string='', files: [], exception: nil, log: false)
10
19
  config = N2B::Base.new.get_config
11
20
  llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
12
21
  # detect if inside rails console
13
22
  console = case
14
- when defined?(Rails)
15
- "You are in a Rails console"
23
+ when defined?(Rails) && Rails.respond_to?(:application)
24
+ "You are in a Rails console"
16
25
  when defined?(IRB)
17
- "You are in an IRB console"
26
+ "You are in an IRB console"
18
27
  else
19
28
  "You are in a standard Ruby console"
20
29
  end
@@ -82,12 +91,905 @@ module N2B
82
91
  end
83
92
  nil
84
93
  end
85
- end
86
- end
87
94
 
88
- # shortcut for IRB
89
- if defined?(IRB)
90
- def n2r(input_string = '', files: [], exception: nil)
91
- N2B::IRB.n2r(input_string, files: files, exception: exception)
95
+ def n2rrbit(url:, cookie:, source_dir: nil, context_lines: DEFAULT_CONTEXT_LINES, log: false)
96
+ require 'net/http'
97
+ require 'uri'
98
+ require 'nokogiri'
99
+
100
+ # Download the Errbit page
101
+ errbit_html = fetch_errbit(url, cookie)
102
+
103
+ if errbit_html.nil?
104
+ puts "Failed to download Errbit error from #{url}"
105
+ return nil
106
+ end
107
+
108
+ # Parse the error information
109
+ error_info = parse_errbit(errbit_html)
110
+
111
+ if error_info.nil?
112
+ puts "Failed to parse Errbit error information"
113
+ return nil
114
+ end
115
+
116
+ # Find related files in the current project using source_dir if provided
117
+ related_files = find_related_files(error_info[:backtrace], source_dir: source_dir, context_lines: context_lines)
118
+
119
+ # Analyze the error
120
+ analysis = analyze_error(error_info, related_files)
121
+
122
+ # Log if requested
123
+ if log
124
+ log_file_path = File.expand_path('~/.n2b/n2rrbit.log')
125
+ File.open(log_file_path, 'a') do |file|
126
+ file.puts("===== N2RRBIT REQUEST LOG =====")
127
+ file.puts("URL: #{url}")
128
+ file.puts("Error: #{error_info[:error_class]} - #{error_info[:error_message]}")
129
+ file.puts("Backtrace: #{error_info[:backtrace].join("\n")}")
130
+ file.puts("Source directory: #{source_dir || Dir.pwd}")
131
+ file.puts("\nFound Related Files:")
132
+
133
+ # Log detailed information about each file that was found
134
+ related_files.each do |file_path, content|
135
+ file.puts("\n--- File: #{file_path}")
136
+ if content.is_a?(Hash) && content[:full_path]
137
+ file.puts("Full path: #{content[:full_path]}")
138
+ file.puts("Error occurred at line: #{content[:line_number]}")
139
+ file.puts("Context lines: #{content[:start_line]}-#{content[:end_line]}")
140
+ file.puts("\nContext code:")
141
+ file.puts("```ruby")
142
+ file.puts(content[:context])
143
+ file.puts("```")
144
+ else
145
+ file.puts("Full content (no specific line context)")
146
+ file.puts("```ruby")
147
+ file.puts(content)
148
+ file.puts("```")
149
+ end
150
+ end
151
+
152
+ # Log the actual prompt sent to the LLM
153
+ file_content_section = related_files.map do |file_path, content|
154
+ if content.is_a?(Hash) && content[:context]
155
+ "#{file_path} (around line #{content[:line_number]}, showing lines #{content[:start_line]}-#{content[:end_line]}):\n```ruby\n#{content[:context]}\n```"
156
+ else
157
+ "#{file_path}:\n```ruby\n#{content.is_a?(Hash) ? content[:full_content] : content}\n```"
158
+ end
159
+ end.join("\n\n")
160
+
161
+ llm_prompt = <<~HEREDOC
162
+ You are an expert Ruby programmer analyzing application errors.
163
+
164
+ Error Type: #{error_info[:error_class]}
165
+ Error Message: #{error_info[:error_message]}
166
+ Application: #{error_info[:app_name]}
167
+ Environment: #{error_info[:environment]}
168
+
169
+ Backtrace:
170
+ #{error_info[:backtrace].join("\n")}
171
+
172
+ Related Files with Context:
173
+ #{file_content_section}
174
+
175
+ Please analyze this error and provide:
176
+ 1. A clear explanation of what caused the error
177
+ 2. Specific code that might be causing the issue
178
+ 3. Suggested fixes for the problem
179
+ 4. If the error seems related to specific parameters, explain which parameter values might be triggering it
180
+
181
+ Your analysis should be detailed but concise.
182
+ HEREDOC
183
+
184
+ file.puts("\n=== PROMPT SENT TO LLM ===")
185
+ file.puts(llm_prompt)
186
+ file.puts("\n=== LLM RESPONSE ===")
187
+ file.puts(analysis)
188
+ file.puts("\n===== END OF LOG =====\n\n")
189
+ end
190
+ end
191
+
192
+ # Display the error analysis
193
+ puts "Error Type: #{error_info[:error_class]}"
194
+ puts "Message: #{error_info[:error_message]}"
195
+
196
+ if error_info[:parameters] && !error_info[:parameters].empty?
197
+ puts "\nRequest Parameters:"
198
+ error_info[:parameters].each do |key, value|
199
+ # Truncate long values for display
200
+ display_value = value.to_s.length > 100 ? "#{value.to_s[0..100]}..." : value
201
+ puts " #{key} => #{display_value}"
202
+ end
203
+ end
204
+
205
+ if error_info[:session] && !error_info[:session].empty?
206
+ puts "\nSession Data:"
207
+ puts " (Available but not displayed - see log for details)"
208
+ end
209
+
210
+ puts "\nBacktrace Highlights:"
211
+ error_info[:backtrace].first(5).each do |line|
212
+ puts " #{line}"
213
+ end
214
+
215
+ puts "\nSource Directory: #{source_dir || Dir.pwd}"
216
+ puts "\nRelated Files:"
217
+ related_files.each do |file, content|
218
+ if content.is_a?(Hash) && content[:context]
219
+ puts " #{file} (with context around line #{content[:line_number]})"
220
+ else
221
+ puts " #{file}"
222
+ end
223
+ end
224
+
225
+ puts "\nAnalysis:"
226
+ puts analysis
227
+
228
+ nil
229
+ end
230
+
231
+ def n2rscrum(input_string: '', files: [], exception: nil, url: nil, cookie: nil, source_dir: nil, context_lines: DEFAULT_CONTEXT_LINES, log: false)
232
+ # Determine which mode we're running in
233
+ if url && cookie
234
+ # Errbit URL mode
235
+ require 'net/http'
236
+ require 'uri'
237
+ require 'nokogiri'
238
+
239
+ # Download the Errbit page
240
+ errbit_html = fetch_errbit(url, cookie)
241
+
242
+ if errbit_html.nil?
243
+ puts "Failed to download Errbit error from #{url}"
244
+ return nil
245
+ end
246
+
247
+ # Parse the error information
248
+ error_info = parse_errbit(errbit_html)
249
+
250
+ if error_info.nil?
251
+ puts "Failed to parse Errbit error information"
252
+ return nil
253
+ end
254
+
255
+ # Find related files with context for better analysis
256
+ related_files = find_related_files(error_info[:backtrace], source_dir: source_dir, context_lines: context_lines)
257
+
258
+ # Generate a Scrum ticket from error info, passing the URL
259
+ ticket = generate_error_ticket(error_info, related_files, url)
260
+ else
261
+ # If we have an exception, convert it to error_info format and use generate_error_ticket
262
+ if exception
263
+ files += exception.backtrace.map do |line|
264
+ line.split(':').first
265
+ end
266
+
267
+ # Create error_info hash from exception with better Rails detection
268
+ app_name = if defined?(Rails) && Rails.respond_to?(:application) &&
269
+ Rails.application.respond_to?(:class) &&
270
+ Rails.application.class.respond_to?(:module_parent_name)
271
+ Rails.application.class.module_parent_name
272
+ else
273
+ 'Ruby Application'
274
+ end
275
+
276
+ environment = if defined?(Rails) && Rails.respond_to?(:env)
277
+ Rails.env
278
+ else
279
+ 'development'
280
+ end
281
+
282
+ error_info = {
283
+ error_class: exception.class.name,
284
+ error_message: exception.message,
285
+ backtrace: exception.backtrace,
286
+ app_name: app_name,
287
+ environment: environment
288
+ }
289
+
290
+ # Find related files with context
291
+ related_files = find_related_files(error_info[:backtrace], source_dir: source_dir, context_lines: context_lines)
292
+
293
+ # Use the same ticket generator for consistency
294
+ ticket = generate_error_ticket(error_info, related_files)
295
+ else
296
+ # Standard mode (input string/files)
297
+ config = N2B::Base.new.get_config
298
+ llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
299
+
300
+ # detect if inside rails console
301
+ console = case
302
+ when defined?(Rails) && Rails.respond_to?(:application)
303
+ "You are in a Rails console"
304
+ when defined?(IRB)
305
+ "You are in an IRB console"
306
+ else
307
+ "You are in a standard Ruby console"
308
+ end
309
+
310
+ # Read file contents
311
+ file_content = files.inject({}) do |h, file|
312
+ h[file] = File.read(file) if File.exist?(file)
313
+ h
314
+ end
315
+
316
+ # Generate ticket content using LLM
317
+ content = <<~HEREDOC
318
+ you are a professional ruby programmer and scrum master
319
+ #{console}
320
+ your task is to create a scrum ticket for the following issue/question:
321
+ answer in a valid json object with the key 'code' with only the ruby code to be executed and a key 'explanation' with a markdown string containing a well-formatted scrum ticket with:
322
+
323
+ 1. A clear and concise title
324
+ 2. Description of the issue with technical details
325
+ 3. Acceptance criteria
326
+ 4. Estimate of complexity (story points)
327
+ 5. Priority level suggestion
328
+
329
+ #{input_string}
330
+ #{"the user provided the following files: #{file_content.collect{|k,v| "#{k}:#{v}" }.join("\n") }" if file_content.any?}
331
+ HEREDOC
332
+
333
+ response = safe_llm_request(llm, content)
334
+ ticket = response['explanation'] || response['code'] || "Failed to generate Scrum ticket."
335
+ end
336
+ end
337
+
338
+ # Log if requested
339
+ if log
340
+ log_file_path = File.expand_path('~/.n2b/n2rscrum.log')
341
+ File.open(log_file_path, 'a') do |file|
342
+ file.puts("===== N2RSCRUM REQUEST LOG =====")
343
+ file.puts("Timestamp: #{Time.now}")
344
+
345
+ if url
346
+ file.puts("Mode: Errbit URL")
347
+ file.puts("URL: #{url}")
348
+ elsif exception
349
+ file.puts("Mode: Exception")
350
+ file.puts("Exception: #{exception.class.name} - #{exception.message}")
351
+ else
352
+ file.puts("Mode: Input String")
353
+ file.puts("Input: #{input_string}")
354
+ end
355
+
356
+ file.puts("Files: #{files.join(', ')}") if files.any?
357
+ file.puts("Source directory: #{source_dir || Dir.pwd}")
358
+
359
+ # If we have related files (from Errbit or exception mode)
360
+ if defined?(related_files) && related_files.any?
361
+ file.puts("\nFound Related Files:")
362
+
363
+ related_files.each do |file_path, content|
364
+ file.puts("\n--- File: #{file_path}")
365
+ if content.is_a?(Hash) && content[:full_path]
366
+ file.puts("Full path: #{content[:full_path]}")
367
+ file.puts("Error occurred at line: #{content[:line_number]}")
368
+ file.puts("Context lines: #{content[:start_line]}-#{content[:end_line]}")
369
+ file.puts("\nContext code:")
370
+ file.puts("```ruby")
371
+ file.puts(content[:context])
372
+ file.puts("```")
373
+ else
374
+ file.puts("Full content (no specific line context)")
375
+ file.puts("```ruby")
376
+ file.puts(content)
377
+ file.puts("```")
378
+ end
379
+ end
380
+
381
+ # Log the actual prompt sent to the LLM
382
+ file_content_section = related_files.map do |file_path, content|
383
+ if content.is_a?(Hash) && content[:context]
384
+ "#{file_path} (around line #{content[:line_number]}, showing lines #{content[:start_line]}-#{content[:end_line]}):\n```ruby\n#{content[:context]}\n```"
385
+ else
386
+ "#{file_path}:\n```ruby\n#{content.is_a?(Hash) ? content[:full_content] : content}\n```"
387
+ end
388
+ end.join("\n\n")
389
+
390
+ if defined?(error_info) && error_info
391
+ llm_prompt = <<~HEREDOC
392
+ You are a software developer creating a Scrum task for a bug fix.
393
+
394
+ Error details:
395
+ Type: #{error_info[:error_class]}
396
+ Message: #{error_info[:error_message]}
397
+ Application: #{error_info[:app_name] || 'Local Application'}
398
+ Environment: #{error_info[:environment] || 'Development'}
399
+
400
+ Backtrace highlights:
401
+ #{error_info[:backtrace]&.first(5)&.join("\n") || 'No backtrace available'}
402
+
403
+ Related Files with Context:
404
+ #{file_content_section}
405
+
406
+ Please generate a well-formatted Scrum ticket that includes:
407
+ 1. A clear and concise title
408
+ 2. Description of the issue with technical details
409
+ 3. Details about the parameter values that were present when the error occurred
410
+ 4. Likely root causes and assumptions about what's causing the problem (be specific)
411
+ 5. Detailed suggested fixes with code examples where possible
412
+ 6. Acceptance criteria
413
+ 7. Estimate of complexity (story points)
414
+ 8. Priority level suggestion
415
+
416
+ IMPORTANT: Your response must be a valid JSON object with ONLY two keys:
417
+ - 'explanation': containing the formatted Scrum ticket as a markdown string
418
+ - 'code': set to null or omitted
419
+
420
+ For example: {"explanation": "# Ticket Title\\n## Description\\n...", "code": null}
421
+
422
+ Ensure all code examples are properly formatted with markdown code blocks using triple backticks.
423
+ HEREDOC
424
+
425
+ file.puts("\n=== PROMPT SENT TO LLM ===")
426
+ file.puts(llm_prompt)
427
+ elsif !input_string.empty?
428
+ # Log the prompt for input string mode
429
+ file.puts("\n=== PROMPT SENT TO LLM ===")
430
+ file.puts("Input string mode prompt with input: #{input_string}")
431
+ if file_content.any?
432
+ file.puts("With files content included")
433
+ end
434
+ end
435
+ end
436
+
437
+ file.puts("\n=== LLM RESPONSE (RAW) ===")
438
+ file.puts(ticket.inspect)
439
+ file.puts("\n=== FORMATTED TICKET ===")
440
+ file.puts(ticket)
441
+ file.puts("\n===== END OF LOG =====\n\n")
442
+ end
443
+ end
444
+
445
+ # Safely handle the ticket
446
+ begin
447
+ # Display the ticket
448
+ puts "Generated Scrum Ticket:"
449
+ puts ticket
450
+
451
+ # Add reference section if URL is provided
452
+ if url
453
+ puts "\n## Reference"
454
+ puts "Errbit URL: #{url}"
455
+ end
456
+ rescue => e
457
+ puts "Error displaying ticket: #{e.message}"
458
+ puts "Raw ticket data: #{ticket.inspect}"
459
+ end
460
+
461
+ nil
462
+ end
463
+
464
+ private
465
+
466
+ def fetch_errbit(url, cookie)
467
+ uri = URI.parse(url)
468
+ http = Net::HTTP.new(uri.host, uri.port)
469
+ http.use_ssl = (uri.scheme == 'https')
470
+
471
+ request = Net::HTTP::Get.new(uri.request_uri)
472
+ request['Cookie'] = cookie
473
+
474
+ response = http.request(request)
475
+
476
+ if response.code == '200'
477
+ response.body
478
+ else
479
+ puts "HTTP Error: #{response.code} - #{response.message}"
480
+ nil
481
+ end
482
+ end
483
+
484
+ def parse_errbit(html)
485
+ doc = Nokogiri::HTML(html)
486
+
487
+ error_class = doc.css('h1').text.strip
488
+ error_message = doc.css('h4').text.strip
489
+
490
+ backtrace = []
491
+ doc.css('#backtrace .line.in-app').each do |line|
492
+ file_path = line.css('.path').text.strip + line.css('.file').text.strip
493
+ line_number = line.css('.number').text.strip.tr(':', '')
494
+ method_name = line.css('.method').text.strip
495
+ backtrace << "#{file_path}:#{line_number} in `#{method_name}`"
496
+ end
497
+
498
+ app_name = doc.css('#content-title .meta a').text.strip
499
+ environment = doc.css('#content-title .meta strong:contains("Environment:")').first&.next&.text&.strip
500
+
501
+ # Extract parameters
502
+ parameters = {}
503
+
504
+ # Find the parameters section
505
+ params_div = doc.css('#params')
506
+ if params_div && !params_div.empty?
507
+ # Try to find a hash structure in the params section
508
+ hash_content = params_div.css('.raw_data pre.hash').text
509
+
510
+ if hash_content && !hash_content.empty?
511
+ # Simple parsing of the hash structure
512
+ # This is a basic approach - you might need to enhance this for complex structures
513
+ parameters = parse_hash_content(hash_content)
514
+ end
515
+ end
516
+
517
+ # Extract session data too, if available
518
+ session_data = {}
519
+ session_div = doc.css('#session')
520
+ if session_div && !session_div.empty?
521
+ hash_content = session_div.css('.raw_data pre.hash').text
522
+
523
+ if hash_content && !hash_content.empty?
524
+ session_data = parse_hash_content(hash_content)
525
+ end
526
+ end
527
+
528
+ {
529
+ error_class: error_class,
530
+ error_message: error_message,
531
+ backtrace: backtrace,
532
+ app_name: app_name,
533
+ environment: environment,
534
+ parameters: parameters,
535
+ session: session_data
536
+ }
537
+ end
538
+
539
+ def parse_hash_content(content)
540
+ # Very basic parsing - this could be improved for more complex structures
541
+ result = {}
542
+
543
+ # Remove the outer braces
544
+ content = content.strip.sub(/^\{/, '').sub(/\}$/, '')
545
+
546
+ # Split by top-level keys
547
+ current_key = nil
548
+ current_value = ""
549
+ level = 0
550
+
551
+ content.each_line do |line|
552
+ line = line.strip
553
+
554
+ # Skip empty lines
555
+ next if line.empty?
556
+
557
+ # Check for key-value pattern at top level
558
+ if level == 0 && line =~ /^"([^"]+)"\s*=>\s*(.+)$/
559
+ # Save previous key-value if any
560
+ if current_key
561
+ result[current_key] = current_value.strip
562
+ end
563
+
564
+ # Start new key-value
565
+ current_key = $1
566
+ current_value = $2
567
+
568
+ # Adjust brace/bracket level
569
+ level += line.count('{') + line.count('[') - line.count('}') - line.count(']')
570
+ else
571
+ # Continue previous value
572
+ current_value += "\n" + line
573
+
574
+ # Adjust brace/bracket level
575
+ level += line.count('{') + line.count('[') - line.count('}') - line.count(']')
576
+ end
577
+ end
578
+
579
+ # Save the last key-value
580
+ if current_key
581
+ result[current_key] = current_value.strip
582
+ end
583
+
584
+ result
585
+ end
586
+
587
+ def analyze_error(error_info, related_files)
588
+ config = N2B::Base.new.get_config
589
+ llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
590
+
591
+ # Build file content section, showing context if available
592
+ file_content_section = related_files.map do |file_path, content|
593
+ if content.is_a?(Hash) && content[:context]
594
+ # Show context around the error line
595
+ "#{file_path} (around line #{content[:line_number]}, showing lines #{content[:start_line]}-#{content[:end_line]}):\n```ruby\n#{content[:context]}\n```"
596
+ else
597
+ # Show the whole file content
598
+ "#{file_path}:\n```ruby\n#{content.is_a?(Hash) ? content[:full_content] : content}\n```"
599
+ end
600
+ end.join("\n\n")
601
+
602
+ # Format parameters for better readability
603
+ params_section = ""
604
+ if error_info[:parameters] && !error_info[:parameters].empty?
605
+ params_section = "Request Parameters:\n```\n"
606
+ error_info[:parameters].each do |key, value|
607
+ params_section += "#{key} => #{value}\n"
608
+ end
609
+ params_section += "```\n\n"
610
+ end
611
+
612
+ # Format session data
613
+ session_section = ""
614
+ if error_info[:session] && !error_info[:session].empty?
615
+ session_section = "Session Data:\n```\n"
616
+ error_info[:session].each do |key, value|
617
+ session_section += "#{key} => #{value}\n"
618
+ end
619
+ session_section += "```\n\n"
620
+ end
621
+
622
+ content = <<~HEREDOC
623
+ You are an expert Ruby programmer analyzing application errors.
624
+
625
+ Error Type: #{error_info[:error_class]}
626
+ Error Message: #{error_info[:error_message]}
627
+ Application: #{error_info[:app_name]}
628
+ Environment: #{error_info[:environment]}
629
+
630
+ Backtrace:
631
+ #{error_info[:backtrace].join("\n")}
632
+
633
+ #{params_section}
634
+ #{session_section}
635
+
636
+ Related Files with Context:
637
+ #{file_content_section}
638
+
639
+ Please analyze this error and provide:
640
+ 1. A clear explanation of what caused the error
641
+ 2. Specific code that might be causing the issue
642
+ 3. Suggested fixes for the problem
643
+ 4. If the error seems related to specific parameters, explain which parameter values might be triggering it
644
+
645
+ Your analysis should be detailed but concise.
646
+ HEREDOC
647
+
648
+ response = llm.make_request(content)
649
+ response['explanation'] || response['code'] || "Failed to analyze the error."
650
+ end
651
+
652
+ def generate_error_ticket(error_info, related_files = {}, url = nil)
653
+ config = N2B::Base.new.get_config
654
+ llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
655
+
656
+ # Build file content section, showing context if available
657
+ file_content_section = related_files.map do |file_path, content|
658
+ if content.is_a?(Hash) && content[:context]
659
+ # Show context around the error line
660
+ "#{file_path} (around line #{content[:line_number]}, showing lines #{content[:start_line]}-#{content[:end_line]}):\n```ruby\n#{content[:context]}\n```"
661
+ else
662
+ # Show the whole file content
663
+ "#{file_path}:\n```ruby\n#{content.is_a?(Hash) ? content[:full_content] : content}\n```"
664
+ end
665
+ end.join("\n\n")
666
+
667
+ # Format parameters for better readability
668
+ params_section = ""
669
+ if error_info[:parameters] && !error_info[:parameters].empty?
670
+ params_section = "Request Parameters:\n```\n"
671
+ error_info[:parameters].each do |key, value|
672
+ params_section += "#{key} => #{value}\n"
673
+ end
674
+ params_section += "```\n\n"
675
+ end
676
+
677
+ # Format session data
678
+ session_section = ""
679
+ if error_info[:session] && !error_info[:session].empty?
680
+ session_section = "Session Data:\n```\n"
681
+ error_info[:session].each do |key, value|
682
+ session_section += "#{key} => #{value}\n"
683
+ end
684
+ session_section += "```\n\n"
685
+ end
686
+
687
+ content = <<~HEREDOC
688
+ You are a software developer creating a Scrum task for a bug fix.
689
+
690
+ Error details:
691
+ Type: #{error_info[:error_class]}
692
+ Message: #{error_info[:error_message]}
693
+ Application: #{error_info[:app_name] || 'Local Application'}
694
+ Environment: #{error_info[:environment] || 'Development'}
695
+
696
+ Backtrace highlights:
697
+ #{error_info[:backtrace]&.first(5)&.join("\n") || 'No backtrace available'}
698
+
699
+ #{params_section}
700
+ #{session_section}
701
+
702
+ Related Files with Context:
703
+ #{file_content_section}
704
+
705
+ Please generate a well-formatted Scrum ticket that includes:
706
+ 1. A clear and concise title
707
+ 2. Description of the issue with technical details
708
+ 3. Details about the parameter values that were present when the error occurred
709
+ 4. Likely root causes and assumptions about what's causing the problem (be specific)
710
+ 5. Detailed suggested fixes with code examples where possible
711
+ 6. Acceptance criteria
712
+ 7. Estimate of complexity (story points)
713
+ 8. Priority level suggestion
714
+
715
+ IMPORTANT: Your response must be a valid JSON object with ONLY two keys:
716
+ - 'explanation': containing the formatted Scrum ticket as a markdown string
717
+ - 'code': set to null or omitted
718
+
719
+ For example: {"explanation": "# Ticket Title\\n## Description\\n...", "code": null}
720
+
721
+ Ensure all code examples are properly formatted with markdown code blocks using triple backticks.
722
+ HEREDOC
723
+
724
+ # Safe response handling
725
+ begin
726
+ response = llm.make_request(content)
727
+
728
+ # Check if response is a Hash with the expected keys
729
+ ticket = nil
730
+ if response.is_a?(Hash) && (response['explanation'] || response['code'])
731
+ ticket = response['explanation'] || response['code']
732
+ elsif response.is_a?(Hash)
733
+ # Try to convert the response to the expected format
734
+ ticket = fix_malformed_response(llm, response)
735
+ else
736
+ # If response is not a hash, convert to string safely
737
+ ticket = "Failed to generate properly formatted ticket. Raw response: #{response.inspect}"
738
+ end
739
+
740
+ # Append the Errbit URL if it's provided and not already included in the ticket
741
+ if url && !ticket.include?(url)
742
+ ticket += "\n\n## Reference\nErrbit URL: #{url}"
743
+ end
744
+
745
+ return ticket
746
+ rescue => e
747
+ # Handle any errors during LLM request or parsing
748
+ result = "Error generating ticket: #{e.message}\n\nPlease try again or check your source directory path."
749
+
750
+ # Still append the URL even to error messages
751
+ if url
752
+ result += "\n\nErrbit URL: #{url}"
753
+ end
754
+
755
+ return result
756
+ end
757
+ end
758
+
759
+ def find_related_files(backtrace, source_dir: nil, context_lines: DEFAULT_CONTEXT_LINES)
760
+ related_files = {}
761
+ source_root = source_dir || Dir.pwd
762
+
763
+ backtrace.each do |trace_line|
764
+ # Extract file path and line number from backtrace line
765
+ parts = trace_line.split(':')
766
+ file_path = parts[0]
767
+ line_number = parts[1].to_i if parts.size > 1
768
+
769
+ # Skip gem files, only look for app files
770
+ next if file_path.include?('/gems/')
771
+
772
+ # Try multiple search paths
773
+ search_paths = []
774
+
775
+ # If file starts with app/, try direct match from source_root
776
+ if file_path.start_with?('app/')
777
+ search_paths << File.join(source_root, file_path)
778
+ end
779
+
780
+ # Also try just the basename as it might be in a different structure
781
+ search_paths << File.join(source_root, file_path)
782
+
783
+ # If none of the above match, try a find operation for the file name only
784
+ basename = File.basename(file_path)
785
+
786
+ # Try to find the file in any of the search paths
787
+ full_path = nil
788
+ search_paths.each do |path|
789
+ if File.exist?(path)
790
+ full_path = path
791
+ break
792
+ end
793
+ end
794
+
795
+ # If not found via direct paths, try to search for it
796
+ if full_path.nil? && source_dir
797
+ # Find command to locate the file in the source directory
798
+ # We're limiting depth to avoid searching too deep
799
+ begin
800
+ find_result = `find #{source_root} -name #{basename} -type f -not -path "*/\\.*" -not -path "*/vendor/*" -not -path "*/node_modules/*" 2>/dev/null | head -1`.strip
801
+ full_path = find_result unless find_result.empty?
802
+ rescue => e
803
+ # If find command fails for any reason, just log and continue
804
+ puts "Warning: find command failed: #{e.message}"
805
+ end
806
+ end
807
+
808
+ # Skip if we couldn't find the file
809
+ next if full_path.nil? || !File.exist?(full_path)
810
+
811
+ # Skip if we already have this file
812
+ next if related_files.key?(file_path)
813
+
814
+ begin
815
+ if line_number && context_lines > 0
816
+ # Read file with context
817
+ file_content = File.read(full_path)
818
+ file_lines = file_content.lines
819
+
820
+ # Calculate start and end line for context
821
+ start_line = [line_number - context_lines, 1].max
822
+ end_line = [line_number + context_lines, file_lines.length].min
823
+
824
+ # Extract context
825
+ context_lines_array = file_lines[(start_line-1)..(end_line-1)]
826
+
827
+ # Guard against nil context
828
+ if context_lines_array.nil?
829
+ context = "# Failed to extract context lines"
830
+ else
831
+ context = context_lines_array.join
832
+ end
833
+
834
+ # Store file content with context information
835
+ related_files[file_path] = {
836
+ full_path: full_path,
837
+ line_number: line_number,
838
+ context: context,
839
+ start_line: start_line,
840
+ end_line: end_line,
841
+ full_content: file_content
842
+ }
843
+ else
844
+ # Just store the whole file content
845
+ related_files[file_path] = File.read(full_path)
846
+ end
847
+ rescue => e
848
+ # If reading the file fails, add a placeholder
849
+ related_files[file_path] = "# Error reading file: #{e.message}"
850
+ end
851
+ end
852
+
853
+ related_files
854
+ end
855
+
856
+ def safe_llm_request(llm, content)
857
+ begin
858
+ response = llm.make_request(content)
859
+
860
+ # Check if response is a Hash with the expected keys
861
+ if response.is_a?(Hash) && (response.key?('explanation') || response.key?('code'))
862
+ return response
863
+ else
864
+ # Try to convert to a valid format if possible
865
+ return {
866
+ 'explanation' => response.is_a?(String) ? response : response.inspect,
867
+ 'code' => nil
868
+ }
869
+ end
870
+ rescue => e
871
+ # Return a valid response format even if there's an error
872
+ return {
873
+ 'explanation' => "Error in LLM request: #{e.message}",
874
+ 'code' => nil
875
+ }
876
+ end
877
+ end
878
+
879
+ def fix_malformed_response(llm, original_response)
880
+ # Prepare a prompt asking the LLM to reformat the response
881
+ fix_prompt = <<~HEREDOC
882
+ I received a response from you that's not in the expected format. Please reformat this response
883
+ into a valid JSON object with ONLY the keys 'explanation' (containing all the content as a markdown string)
884
+ and 'code' (which should be null).
885
+
886
+ Original response:
887
+ #{original_response.inspect}
888
+
889
+ Please provide ONLY a valid JSON object like:
890
+ {"explanation": "# Your title here\\n\\nAll the content here as markdown...", "code": null}
891
+
892
+ Do not include any other keys or explanatory text outside the JSON.
893
+ HEREDOC
894
+
895
+ begin
896
+ # Get a fixed response
897
+ fixed_response = llm.make_request(fix_prompt)
898
+
899
+ # If the fixed response is in the right format, use it
900
+ if fixed_response.is_a?(Hash) && fixed_response['explanation']
901
+ return fixed_response['explanation']
902
+ end
903
+
904
+ # If still not in right format, try to auto-fix it
905
+ if original_response.is_a?(Hash)
906
+ # Try to convert the original response to a markdown string
907
+ markdown = []
908
+
909
+ # Try to extract title
910
+ markdown << "# #{original_response['title']}" if original_response['title']
911
+
912
+ # Try to extract description
913
+ if original_response['description']
914
+ markdown << "\n## Description"
915
+ markdown << original_response['description']
916
+ end
917
+
918
+ # Try to extract technical details
919
+ if original_response['technicalDetails']
920
+ markdown << "\n## Technical Details"
921
+ markdown << "```"
922
+ markdown << original_response['technicalDetails']
923
+ markdown << "```"
924
+ end
925
+
926
+ # Try to extract root causes
927
+ if original_response['rootCauses'] && original_response['rootCauses'].is_a?(Array)
928
+ markdown << "\n## Root Causes & Assumptions"
929
+ original_response['rootCauses'].each_with_index do |cause, i|
930
+ markdown << "#{i+1}. #{cause}"
931
+ end
932
+ end
933
+
934
+ # Try to extract suggested fixes
935
+ if original_response['suggestedFixes'] && original_response['suggestedFixes'].is_a?(Array)
936
+ markdown << "\n## Suggested Fixes"
937
+ original_response['suggestedFixes'].each do |fix|
938
+ markdown << fix
939
+ end
940
+ end
941
+
942
+ # Try to extract acceptance criteria
943
+ if original_response['acceptanceCriteria'] && original_response['acceptanceCriteria'].is_a?(Array)
944
+ markdown << "\n## Acceptance Criteria"
945
+ original_response['acceptanceCriteria'].each do |criteria|
946
+ markdown << "- #{criteria}"
947
+ end
948
+ end
949
+
950
+ # Try to extract story points
951
+ if original_response['storyPoints']
952
+ markdown << "\n## Story Points"
953
+ markdown << original_response['storyPoints'].to_s
954
+ end
955
+
956
+ # Try to extract priority
957
+ if original_response['priority']
958
+ markdown << "\n## Priority"
959
+ markdown << original_response['priority']
960
+ end
961
+
962
+ # Try to extract additional notes
963
+ if original_response['additionalNotes']
964
+ markdown << "\n## Additional Notes"
965
+ markdown << original_response['additionalNotes']
966
+ end
967
+
968
+ return markdown.join("\n")
969
+ end
970
+
971
+ # If we still can't fix it, return a formatted version of the original
972
+ "# Auto-formatted Ticket\n\n```\n#{original_response.inspect}\n```"
973
+ rescue => e
974
+ # If anything goes wrong in the fixing process, return a readable version of the original
975
+ if original_response.is_a?(Hash)
976
+ # Try to create a readable markdown from the hash
977
+ sections = []
978
+ original_response.each do |key, value|
979
+ sections << "## #{key.to_s.gsub(/([A-Z])/, ' \\1').capitalize}"
980
+ if value.is_a?(Array)
981
+ value.each_with_index do |item, i|
982
+ sections << "#{i+1}. #{item}"
983
+ end
984
+ else
985
+ sections << value.to_s
986
+ end
987
+ end
988
+ return "# Auto-formatted Ticket\n\n#{sections.join("\n\n")}"
989
+ else
990
+ return "Failed to format response: #{original_response.inspect}"
991
+ end
992
+ end
993
+ end
92
994
  end
93
995
  end
data/lib/n2b/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # lib/n2b/version.rb
2
2
  module N2B
3
- VERSION = "0.2.1"
3
+ VERSION = "0.2.2"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n2b
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Nothegger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-24 00:00:00.000000000 Z
11
+ date: 2025-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  requirements: []
96
- rubygems_version: 3.4.2
96
+ rubygems_version: 3.5.3
97
97
  signing_key:
98
98
  specification_version: 4
99
99
  summary: Convert natural language to bash commands or ruby code and help with debugging.