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.
- checksums.yaml +4 -4
- data/README.md +90 -17
- data/lib/n2b/irb.rb +911 -9
- data/lib/n2b/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a24dc543659153dabaf892f6bed38934425ac43a1a0b2a263e82c3913e0b5f9
|
4
|
+
data.tar.gz: 01ccd36a76124d3599a7dd26a5cb1f443c4465e21c5cd12505512c00f771a164
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 737d3f29477d61da584640389552ebb01809fc762ec6348ee032ca0ab9fe6a3c2360df103fbe8daa3751a980b0b9c41b1998523c5f74f0b6ebb906e5a4a939dc
|
7
|
+
data.tar.gz: 5e975752513f183640c391afd46bd322d80da3a488d046d3e7e487f496b96a65c6bc60fae47de4187e2ca455c41d3aa1a6f23acdd2095f2cccaa36cbacbe8427
|
data/README.md
CHANGED
@@ -1,25 +1,72 @@
|
|
1
|
-
# N2B
|
1
|
+
# N2B - Natural Language to Bash & Ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
[](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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
-
|
14
|
-
-
|
15
|
-
|
16
|
-
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
23
|
+
when defined?(Rails) && Rails.respond_to?(:application)
|
24
|
+
"You are in a Rails console"
|
16
25
|
when defined?(IRB)
|
17
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
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.
|
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:
|
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.
|
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.
|