n2b 0.2.4 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf304be008b27bf4336085ec39f75fc8ac5207c6e17d442d5129a366795629eb
4
- data.tar.gz: f436a7dc4a59f2840b023d7e406aa5fa2aa02dee7ebb10f48c982efaaf489546
3
+ metadata.gz: 698bca4814bd42e8587efb9d278ce1cb090b3ea23afd645c9c082b1c277ea22d
4
+ data.tar.gz: c7ec9715a8b5807b1ea5eb90e448fd70767fdc3e6ace987e1b123e29ee37dd33
5
5
  SHA512:
6
- metadata.gz: 770f49a5df340f3f9eae3f9072eafcbd9e8de6e2a5a227da3a96c67ebb22ca998411d1098921a375a9203fe446593ac8b15cfc3b6c6e6d13503a48957fed29c9
7
- data.tar.gz: 88720e0bae3adf499830681214c142ea96787f3fbae01fcfe276593ce3860f65127783293cf2c1c993725747ad54165e260473f84d172fb290490165cd587708
6
+ metadata.gz: 7d00b68b8e2925b63d4bf6b2e4842a18034e2dc318686d55261480c2612f47f8574379df85ae166670a721aa6bd41406d08323cddb0ebd831e3777dc1773ca1a
7
+ data.tar.gz: e7b3621fcf9051d433ca0fba961b12d348b03f9d73981d641b252b4a4223b8edfec0f8ee438f6c603382ca62c1cf4679e9960f6c40f16b84c644d6939290dbe5
data/README.md CHANGED
@@ -56,29 +56,16 @@ n2rscrum "Create a user authentication system"
56
56
 
57
57
  ## Configuration
58
58
 
59
- Create a config file at `~/.n2b/config.yml` with your API keys. You can also use a custom config file by setting the `N2B_CONFIG_FILE` environment variable:
59
+ Create a config file at `~/.n2b/config.yml` with your API keys:
60
60
 
61
- ```bash
62
- export N2B_CONFIG_FILE=/path/to/your/config.yml
63
- ```
64
-
65
- Example config file:
66
61
  ```yaml
67
- llm: claude # or openai, gemini
62
+ llm: claude # or openai
68
63
  claude:
69
64
  key: your-anthropic-api-key
70
65
  model: claude-3-opus-20240229 # or opus, haiku, sonnet
71
66
  openai:
72
67
  key: your-openai-api-key
73
68
  model: gpt-4 # or gpt-3.5-turbo
74
- gemini:
75
- key: your-google-api-key
76
- model: gemini-flash # uses gemini-2.0-flash model
77
- ```
78
-
79
- You can also set the history file location using the `N2B_HISTORY_FILE` environment variable:
80
- ```bash
81
- export N2B_HISTORY_FILE=/path/to/your/history
82
69
  ```
83
70
 
84
71
  ## Quick Example N2B
data/lib/n2b/cli.rb CHANGED
@@ -11,12 +11,69 @@ module N2B
11
11
 
12
12
  def execute
13
13
  config = get_config(reconfigure: @options[:config])
14
- input_text = @args.join(' ')
15
- if input_text.empty?
14
+ user_input = @args.join(' ') # All remaining args form user input/prompt addition
15
+
16
+ if @options[:diff]
17
+ handle_diff_analysis(config)
18
+ elsif user_input.empty? # No input text after options
16
19
  puts "Enter your natural language command:"
17
20
  input_text = $stdin.gets.chomp
21
+ process_natural_language_command(input_text, config)
22
+ else # Natural language command
23
+ process_natural_language_command(user_input, config)
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ def handle_diff_analysis(config)
30
+ vcs_type = get_vcs_type
31
+ if vcs_type == :none
32
+ puts "Error: Not a git or hg repository."
33
+ exit 1
34
+ end
35
+
36
+ # Get requirements file from parsed options
37
+ requirements_filepath = @options[:requirements]
38
+ user_prompt_addition = @args.join(' ') # All remaining args are user prompt addition
39
+
40
+ requirements_content = nil
41
+ if requirements_filepath
42
+ unless File.exist?(requirements_filepath)
43
+ puts "Error: Requirements file not found: #{requirements_filepath}"
44
+ exit 1
45
+ end
46
+ requirements_content = File.read(requirements_filepath)
47
+ end
48
+
49
+ diff_output = execute_vcs_diff(vcs_type)
50
+ analyze_diff(diff_output, config, user_prompt_addition, requirements_content)
51
+ end
52
+
53
+ def get_vcs_type
54
+ if Dir.exist?(File.join(Dir.pwd, '.git'))
55
+ :git
56
+ elsif Dir.exist?(File.join(Dir.pwd, '.hg'))
57
+ :hg
58
+ else
59
+ :none
18
60
  end
61
+ end
19
62
 
63
+ def execute_vcs_diff(vcs_type)
64
+ case vcs_type
65
+ when :git
66
+ `git diff HEAD`
67
+ when :hg
68
+ `hg diff`
69
+ else
70
+ "" # Should not happen if get_vcs_type logic is correct and checked before calling
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def process_natural_language_command(input_text, config)
20
77
  bash_commands = call_llm(input_text, config)
21
78
 
22
79
  puts "\nTranslated #{get_user_shell} Commands:"
@@ -24,10 +81,10 @@ module N2B
24
81
  puts bash_commands['commands']
25
82
  puts "------------------------"
26
83
  if bash_commands['explanation']
27
- puts "Explanation:"
84
+ puts "Explanation:"
28
85
  puts bash_commands['explanation']
29
86
  puts "------------------------"
30
- end
87
+ end
31
88
 
32
89
  if @options[:execute]
33
90
  puts "Press Enter to execute these commands, or Ctrl+C to cancel."
@@ -38,9 +95,258 @@ module N2B
38
95
  end
39
96
  end
40
97
 
41
- private
98
+ def build_diff_analysis_prompt(diff_output, user_prompt_addition = "", requirements_content = nil)
99
+ default_system_prompt = <<-SYSTEM_PROMPT.strip
100
+ You are a senior software developer reviewing a code diff.
101
+ Your task is to provide a constructive and detailed analysis of the changes.
102
+ Focus on identifying potential bugs, suggesting improvements in code quality, style, performance, and security.
103
+ Also, provide a concise summary of the changes.
42
104
 
43
-
105
+ IMPORTANT: When referring to specific issues or improvements, always include:
106
+ - The exact file path (e.g., "lib/n2b/cli.rb")
107
+ - The specific line numbers or line ranges (e.g., "line 42" or "lines 15-20")
108
+ - The exact code snippet you're referring to when possible
109
+
110
+ This helps users quickly locate and understand the issues you identify.
111
+
112
+ SPECIAL FOCUS ON TEST COVERAGE:
113
+ Pay special attention to whether the developer has provided adequate test coverage for the changes:
114
+ - Look for new test files or modifications to existing test files
115
+ - Check if new functionality has corresponding tests
116
+ - Evaluate if edge cases and error conditions are tested
117
+ - Assess if the tests are meaningful and comprehensive
118
+ - Note any missing test coverage that should be added
119
+
120
+ NOTE: In addition to the diff, you will also receive the current code context around the changed areas.
121
+ This provides better understanding of the surrounding code and helps with more accurate analysis.
122
+ The user may provide additional instructions or specific requirements below.
123
+ SYSTEM_PROMPT
124
+
125
+ user_instructions_section = ""
126
+ unless user_prompt_addition.to_s.strip.empty?
127
+ user_instructions_section = "User Instructions:\n#{user_prompt_addition.strip}\n\n"
128
+ end
129
+
130
+ requirements_section = ""
131
+ if requirements_content && !requirements_content.to_s.strip.empty?
132
+ requirements_section = <<-REQUIREMENTS_BLOCK
133
+ CRITICAL REQUIREMENTS EVALUATION:
134
+ You must carefully evaluate whether the code changes meet the following requirements from the ticket/task.
135
+ For each requirement, explicitly state whether it is:
136
+ - ✅ IMPLEMENTED: The requirement is fully satisfied by the changes
137
+ - ⚠️ PARTIALLY IMPLEMENTED: The requirement is partially addressed but needs more work
138
+ - ❌ NOT IMPLEMENTED: The requirement is not addressed by these changes
139
+ - 🔍 UNCLEAR: Cannot determine from the diff whether the requirement is met
140
+
141
+ --- BEGIN REQUIREMENTS ---
142
+ #{requirements_content.strip}
143
+ --- END REQUIREMENTS ---
144
+
145
+ REQUIREMENTS_BLOCK
146
+ end
147
+
148
+ analysis_intro = "Analyze the following diff based on the general instructions above and these specific requirements (if any):"
149
+
150
+ # Extract context around changed lines
151
+ context_sections = extract_code_context_from_diff(diff_output)
152
+ context_info = ""
153
+ unless context_sections.empty?
154
+ context_info = "\n\nCurrent Code Context (for better analysis):\n"
155
+ context_sections.each do |file_path, sections|
156
+ context_info += "\n--- #{file_path} ---\n"
157
+ sections.each do |section|
158
+ context_info += "Lines #{section[:start_line]}-#{section[:end_line]}:\n"
159
+ context_info += "```\n#{section[:content]}\n```\n\n"
160
+ end
161
+ end
162
+ end
163
+
164
+ json_instruction = <<-JSON_INSTRUCTION.strip
165
+ CRITICAL: Return ONLY a valid JSON object with the keys "summary", "errors" (as a list of strings), "improvements" (as a list of strings), "test_coverage" (as a string), and "requirements_evaluation" (as a string, only if requirements were provided).
166
+ Do not include any explanatory text before or after the JSON.
167
+ Each error and improvement should include specific file paths and line numbers.
168
+
169
+ Example format:
170
+ {
171
+ "summary": "Brief description of the changes",
172
+ "errors": [
173
+ "lib/example.rb line 42: Potential null pointer exception when accessing user.name without checking if user is nil",
174
+ "src/main.js lines 15-20: Missing error handling for async operation"
175
+ ],
176
+ "improvements": [
177
+ "lib/example.rb line 30: Consider using a constant for the magic number 42",
178
+ "src/utils.py lines 5-10: This method could be simplified using list comprehension"
179
+ ],
180
+ "test_coverage": "Good: New functionality in lib/example.rb has corresponding tests in test/example_test.rb. Missing: No tests for error handling edge cases in the new validation method.",
181
+ "requirements_evaluation": "✅ IMPLEMENTED: User authentication feature is fully implemented in auth.rb. ⚠️ PARTIALLY IMPLEMENTED: Error handling is present but lacks specific error codes. ❌ NOT IMPLEMENTED: Email notifications are not addressed in this diff."
182
+ }
183
+ JSON_INSTRUCTION
184
+
185
+ full_prompt = [
186
+ default_system_prompt,
187
+ user_instructions_section,
188
+ requirements_section,
189
+ analysis_intro,
190
+ "Diff:\n```\n#{diff_output}\n```",
191
+ context_info,
192
+ json_instruction
193
+ ].select { |s| s && !s.empty? }.join("\n\n") # Join non-empty sections with double newlines
194
+
195
+ full_prompt
196
+ end
197
+
198
+ def analyze_diff(diff_output, config, user_prompt_addition = "", requirements_content = nil)
199
+ prompt = build_diff_analysis_prompt(diff_output, user_prompt_addition, requirements_content)
200
+ analysis_json_str = call_llm_for_diff_analysis(prompt, config)
201
+
202
+ begin
203
+ # Try to extract JSON from response that might have text before it
204
+ json_content = extract_json_from_response(analysis_json_str)
205
+ analysis_result = JSON.parse(json_content)
206
+
207
+ puts "\nCode Diff Analysis:"
208
+ puts "-------------------"
209
+ puts "Summary:"
210
+ puts analysis_result['summary'] || "No summary provided."
211
+ puts "\nPotential Errors:"
212
+ errors_list = analysis_result['errors']
213
+ errors_list = [errors_list] if errors_list.is_a?(String) && !errors_list.empty?
214
+ errors_list = [] if errors_list.nil? || (errors_list.is_a?(String) && errors_list.empty?)
215
+ puts errors_list.any? ? errors_list.map{|err| "- #{err}"}.join("\n") : "No errors identified."
216
+
217
+ puts "\nSuggested Improvements:"
218
+ improvements_list = analysis_result['improvements']
219
+ improvements_list = [improvements_list] if improvements_list.is_a?(String) && !improvements_list.empty?
220
+ improvements_list = [] if improvements_list.nil? || (improvements_list.is_a?(String) && improvements_list.empty?)
221
+ puts improvements_list.any? ? improvements_list.map{|imp| "- #{imp}"}.join("\n") : "No improvements suggested."
222
+
223
+ puts "\nTest Coverage Assessment:"
224
+ test_coverage = analysis_result['test_coverage']
225
+ puts test_coverage && !test_coverage.to_s.strip.empty? ? test_coverage : "No test coverage assessment provided."
226
+
227
+ # Show requirements evaluation if requirements were provided
228
+ requirements_eval = analysis_result['requirements_evaluation']
229
+ if requirements_eval && !requirements_eval.to_s.strip.empty?
230
+ puts "\nRequirements Evaluation:"
231
+ puts requirements_eval
232
+ end
233
+ puts "-------------------"
234
+ rescue JSON::ParserError => e # Handles cases where the JSON string (even fallback) is malformed
235
+ puts "Critical Error: Failed to parse JSON response for diff analysis: #{e.message}"
236
+ puts "Raw response was: #{analysis_json_str}"
237
+ end
238
+ end
239
+
240
+ def extract_json_from_response(response)
241
+ # First try to parse the response as-is
242
+ begin
243
+ JSON.parse(response)
244
+ return response
245
+ rescue JSON::ParserError
246
+ # If that fails, try to find JSON within the response
247
+ end
248
+
249
+ # Look for JSON object starting with { and ending with }
250
+ json_start = response.index('{')
251
+ return response unless json_start
252
+
253
+ # Find the matching closing brace
254
+ brace_count = 0
255
+ json_end = nil
256
+ (json_start...response.length).each do |i|
257
+ case response[i]
258
+ when '{'
259
+ brace_count += 1
260
+ when '}'
261
+ brace_count -= 1
262
+ if brace_count == 0
263
+ json_end = i
264
+ break
265
+ end
266
+ end
267
+ end
268
+
269
+ return response unless json_end
270
+
271
+ response[json_start..json_end]
272
+ end
273
+
274
+ def extract_code_context_from_diff(diff_output)
275
+ context_sections = {}
276
+ current_file = nil
277
+
278
+ diff_output.each_line do |line|
279
+ line = line.chomp
280
+
281
+ # Parse file headers (e.g., "diff --git a/lib/n2b/cli.rb b/lib/n2b/cli.rb")
282
+ if line.start_with?('diff --git')
283
+ # Extract file path from "diff --git a/path b/path"
284
+ match = line.match(/diff --git a\/(.+) b\/(.+)/)
285
+ current_file = match[2] if match # Use the "b/" path (new file)
286
+ elsif line.start_with?('+++')
287
+ # Alternative way to get file path from "+++ b/path"
288
+ match = line.match(/\+\+\+ b\/(.+)/)
289
+ current_file = match[1] if match
290
+ elsif line.start_with?('@@') && current_file
291
+ # Parse hunk header (e.g., "@@ -10,7 +10,8 @@")
292
+ match = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/)
293
+ if match
294
+ old_start = match[1].to_i
295
+ new_start = match[2].to_i
296
+
297
+ # Use the new file line numbers for context
298
+ context_start = [new_start - 5, 1].max # 5 lines before, but not less than 1
299
+ context_end = new_start + 10 # 10 lines after the start
300
+
301
+ # Read the actual file content
302
+ if File.exist?(current_file)
303
+ file_lines = File.readlines(current_file)
304
+ # Adjust end to not exceed file length
305
+ context_end = [context_end, file_lines.length].min
306
+
307
+ if context_start <= file_lines.length
308
+ context_content = file_lines[(context_start-1)...context_end].map.with_index do |content, idx|
309
+ line_num = context_start + idx
310
+ "#{line_num.to_s.rjust(4)}: #{content.rstrip}"
311
+ end.join("\n")
312
+
313
+ context_sections[current_file] ||= []
314
+ context_sections[current_file] << {
315
+ start_line: context_start,
316
+ end_line: context_end,
317
+ content: context_content
318
+ }
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ context_sections
326
+ end
327
+
328
+ def call_llm_for_diff_analysis(prompt, config)
329
+ begin
330
+ llm_service_name = config['llm']
331
+ llm = case llm_service_name
332
+ when 'openai'
333
+ N2M::Llm::OpenAi.new(config)
334
+ when 'claude'
335
+ N2M::Llm::Claude.new(config)
336
+ when 'gemini'
337
+ N2M::Llm::Gemini.new(config)
338
+ else
339
+ # Should not happen if config is validated, but as a safeguard:
340
+ raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
341
+ end
342
+
343
+ response_json_str = llm.analyze_code_diff(prompt) # Call the new dedicated method
344
+ response_json_str
345
+ rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
346
+ puts "Error communicating with the LLM: #{e.message}"
347
+ return '{"summary": "Error: Could not analyze diff due to LLM API error.", "errors": [], "improvements": []}'
348
+ end
349
+ end
44
350
 
45
351
  def append_to_llm_history_file(commands)
46
352
  File.open(HISTORY_FILE, 'a') do |file|
@@ -56,10 +362,11 @@ module N2B
56
362
  end
57
363
 
58
364
  def call_llm(prompt, config)
59
-
60
- llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
61
-
62
- content = <<-EOF
365
+ begin # Added begin for LlmApiError rescue
366
+ llm = config['llm'] == 'openai' ? N2M::Llm::OpenAi.new(config) : N2M::Llm::Claude.new(config)
367
+
368
+ # This content is specific to bash command generation
369
+ content = <<-EOF
63
370
  Translate the following natural language command to bash commands: #{prompt}\n\nProvide only the #{get_user_shell} commands for #{ get_user_os }. the commands should be separated by newlines.
64
371
  #{' the user is in directory'+Dir.pwd if config['privacy']['send_current_directory']}.
65
372
  #{' the user sent past requests to you and got these answers '+read_llm_history_file if config['privacy']['send_llm_history'] }
@@ -70,10 +377,28 @@ module N2B
70
377
  EOF
71
378
 
72
379
 
73
- answer = llm.make_request(content)
380
+ response_json_str = llm.make_request(content)
74
381
 
75
- append_to_llm_history_file("#{prompt}\n#{answer}")
76
- answer
382
+ append_to_llm_history_file("#{prompt}\n#{response_json_str}") # Storing the raw JSON string
383
+ # The original call_llm was expected to return a hash after JSON.parse,
384
+ # but it was actually returning the string. Let's assume it should return a parsed Hash.
385
+ # However, the calling method `process_natural_language_command` accesses it like `bash_commands['commands']`
386
+ # which implies it expects a Hash. Let's ensure call_llm returns a Hash.
387
+ # This internal JSON parsing is for the *content* of a successful LLM response.
388
+ # The LlmApiError for network/auth issues should be caught before this.
389
+ begin
390
+ parsed_response = JSON.parse(response_json_str)
391
+ parsed_response
392
+ rescue JSON::ParserError => e
393
+ puts "Error parsing LLM response JSON for command generation: #{e.message}"
394
+ # This is a fallback for when the LLM response *content* is not valid JSON.
395
+ { "commands" => ["echo 'Error: LLM returned invalid JSON content.'"], "explanation" => "The response from the language model was not valid JSON." }
396
+ end
397
+ rescue N2B::LlmApiError => e
398
+ puts "Error communicating with the LLM: #{e.message}"
399
+ # This is the fallback for LlmApiError (network, auth, etc.)
400
+ { "commands" => ["echo 'LLM API error occurred. Please check your configuration and network.'"], "explanation" => "Failed to connect to the LLM." }
401
+ end
77
402
  end
78
403
 
79
404
  def get_user_shell
@@ -165,15 +490,23 @@ module N2B
165
490
 
166
491
 
167
492
  def parse_options
168
- options = { execute: false, config: nil }
493
+ options = { execute: false, config: nil, diff: false, requirements: nil }
169
494
 
170
- OptionParser.new do |opts|
495
+ parser = OptionParser.new do |opts|
171
496
  opts.banner = "Usage: n2b [options] [natural language command]"
172
497
 
173
498
  opts.on('-x', '--execute', 'Execute the commands after confirmation') do
174
499
  options[:execute] = true
175
500
  end
176
501
 
502
+ opts.on('-d', '--diff', 'Analyze git/hg diff with AI') do
503
+ options[:diff] = true
504
+ end
505
+
506
+ opts.on('-r', '--requirements FILE', 'Requirements file for diff analysis') do |file|
507
+ options[:requirements] = file
508
+ end
509
+
177
510
  opts.on('-h', '--help', 'Print this help') do
178
511
  puts opts
179
512
  exit
@@ -182,7 +515,16 @@ module N2B
182
515
  opts.on('-c', '--config', 'Configure the API key and model') do
183
516
  options[:config] = true
184
517
  end
185
- end.parse!(@args)
518
+ end
519
+
520
+ begin
521
+ parser.parse!(@args)
522
+ rescue OptionParser::InvalidOption => e
523
+ puts "Error: #{e.message}"
524
+ puts ""
525
+ puts parser.help
526
+ exit 1
527
+ end
186
528
 
187
529
  options
188
530
  end
data/lib/n2b/errors.rb ADDED
@@ -0,0 +1,7 @@
1
+ module N2B
2
+ class Error < StandardError
3
+ end
4
+
5
+ class LlmApiError < Error
6
+ end
7
+ end
@@ -31,12 +31,12 @@ module N2M
31
31
  end
32
32
  # check for errors
33
33
  if response.code != '200'
34
- puts "Error: #{response.code} #{response.message}"
35
- puts response.body
36
- exit 1
34
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
37
35
  end
38
36
  answer = JSON.parse(response.body)['content'].first['text']
39
37
  begin
38
+ # The llm_response.json file is likely for debugging and can be kept or removed.
39
+ # For this refactoring, I'll keep it as it doesn't affect the error handling logic.
40
40
  File.open('llm_response.json', 'w') do |f|
41
41
  f.write(answer)
42
42
  end
@@ -46,16 +46,54 @@ module N2M
46
46
  # gsub all \n with \\n that are inside "
47
47
  #
48
48
  answer.gsub!(/"([^"]*)"/) { |match| match.gsub(/\n/, "\\n") }
49
+ # The llm_response.json file is likely for debugging and can be kept or removed.
49
50
  File.open('llm_response.json', 'w') do |f|
50
51
  f.write(answer)
51
52
  end
52
53
  answer = JSON.parse(answer)
53
54
  rescue JSON::ParserError
54
- puts "Error parsing JSON: #{answer}"
55
- answer = { 'explanation' => answer}
55
+ # This specific JSON parsing error is about the LLM's *response content*, not an API error.
56
+ # It should probably be handled differently, but the subtask is about LlmApiError.
57
+ # For now, keeping existing behavior for this part.
58
+ puts "Error parsing JSON from LLM response: #{answer}" # Clarified error message
59
+ answer = { 'explanation' => answer} # Default fallback
56
60
  end
57
61
  answer
58
62
  end
63
+
64
+ def analyze_code_diff(prompt_content)
65
+ # This method assumes prompt_content is the full, ready-to-send prompt
66
+ # including all instructions for the LLM (system message, diff, user additions, JSON format).
67
+ uri = URI.parse('https://api.anthropic.com/v1/messages')
68
+ request = Net::HTTP::Post.new(uri)
69
+ request.content_type = 'application/json'
70
+ request['X-API-Key'] = @config['access_key']
71
+ request['anthropic-version'] = '2023-06-01'
72
+
73
+ request.body = JSON.dump({
74
+ "model" => MODELS[@config['model']],
75
+ "max_tokens" => @config['max_tokens'] || 1024, # Allow overriding max_tokens from config
76
+ "messages" => [
77
+ {
78
+ "role" => "user", # The entire prompt is passed as a single user message
79
+ "content" => prompt_content
80
+ }
81
+ ]
82
+ })
83
+
84
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
85
+ http.request(request)
86
+ end
87
+
88
+ if response.code != '200'
89
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
90
+ end
91
+
92
+ # Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
93
+ # The Claude API for messages returns the analysis in response.body['content'].first['text']
94
+ # which should itself be a JSON string as per our prompt's instructions.
95
+ JSON.parse(response.body)['content'].first['text']
96
+ end
59
97
  end
60
98
  end
61
99
  end
@@ -35,10 +35,7 @@ module N2M
35
35
 
36
36
  # check for errors
37
37
  if response.code != '200'
38
- puts "Error: #{response.code} #{response.message}"
39
- puts response.body
40
- puts "Config: #{@config.inspect}"
41
- exit 1
38
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
42
39
  end
43
40
 
44
41
  parsed_response = JSON.parse(response.body)
@@ -64,6 +61,43 @@ module N2M
64
61
  end
65
62
  answer
66
63
  end
64
+
65
+ def analyze_code_diff(prompt_content)
66
+ # This method assumes prompt_content is the full, ready-to-send prompt
67
+ # including all instructions for the LLM (system message, diff, user additions, JSON format).
68
+ model = MODELS[@config['model']] || 'gemini-flash' # Or a specific model for analysis if different
69
+ uri = URI.parse("#{API_URI}/#{model}:generateContent?key=#{@config['access_key']}")
70
+
71
+ request = Net::HTTP::Post.new(uri)
72
+ request.content_type = 'application/json'
73
+
74
+ request.body = JSON.dump({
75
+ "contents" => [{
76
+ "parts" => [{
77
+ "text" => prompt_content # The entire prompt is passed as text
78
+ }]
79
+ }],
80
+ # Gemini specific: Ensure JSON output if possible via generationConfig
81
+ # However, the primary method is instructing it within the prompt itself.
82
+ # "generationConfig": {
83
+ # "responseMimeType": "application/json", # This might be too restrictive or not always work as expected
84
+ # }
85
+ })
86
+
87
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
88
+ http.request(request)
89
+ end
90
+
91
+ if response.code != '200'
92
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
93
+ end
94
+
95
+ parsed_response = JSON.parse(response.body)
96
+ # Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
97
+ # The Gemini API returns the analysis in parsed_response['candidates'].first['content']['parts'].first['text']
98
+ # which should itself be a JSON string as per our prompt's instructions.
99
+ parsed_response['candidates'].first['content']['parts'].first['text']
100
+ end
67
101
  end
68
102
  end
69
103
  end
@@ -33,9 +33,7 @@ module N2M
33
33
 
34
34
  # check for errors
35
35
  if response.code != '200'
36
- puts "Error: #{response.code} #{response.message}"
37
- puts response.body
38
- exit 1
36
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
39
37
  end
40
38
  answer = JSON.parse(response.body)['choices'].first['message']['content']
41
39
  begin
@@ -47,6 +45,38 @@ module N2M
47
45
  end
48
46
  answer
49
47
  end
48
+
49
+ def analyze_code_diff(prompt_content)
50
+ # This method assumes prompt_content is the full, ready-to-send prompt
51
+ # including all instructions for the LLM (system message, diff, user additions, JSON format).
52
+ request = Net::HTTP::Post.new(API_URI)
53
+ request.content_type = 'application/json'
54
+ request['Authorization'] = "Bearer #{@config['access_key']}"
55
+
56
+ request.body = JSON.dump({
57
+ "model" => MODELS[@config['model']],
58
+ "response_format" => { "type" => "json_object" }, # Crucial for OpenAI to return JSON
59
+ "messages" => [
60
+ {
61
+ "role" => "user", # The entire prompt is passed as a single user message
62
+ "content" => prompt_content
63
+ }
64
+ ],
65
+ "max_tokens" => @config['max_tokens'] || 1500 # Allow overriding, ensure it's enough for JSON
66
+ })
67
+
68
+ response = Net::HTTP.start(API_URI.hostname, API_URI.port, use_ssl: true) do |http|
69
+ http.request(request)
70
+ end
71
+
72
+ if response.code != '200'
73
+ raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
74
+ end
75
+
76
+ # Return the raw JSON string. CLI's call_llm_for_diff_analysis will handle parsing.
77
+ # OpenAI with json_object mode should return the JSON directly in 'choices'.first.message.content
78
+ JSON.parse(response.body)['choices'].first['message']['content']
79
+ end
50
80
  end
51
81
  end
52
82
  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.4"
3
+ VERSION = "0.3.0"
4
4
  end
data/lib/n2b.rb CHANGED
@@ -9,12 +9,12 @@ require 'n2b/version'
9
9
  require 'n2b/llm/claude'
10
10
  require 'n2b/llm/open_ai'
11
11
  require 'n2b/llm/gemini'
12
+ require 'n2b/errors' # Load custom errors
12
13
  require 'n2b/base'
13
14
  require 'n2b/cli'
14
15
 
15
16
  require 'n2b/irb'
16
17
 
17
18
  module N2B
18
- class Error < StandardError; end
19
-
19
+ # Error class is now defined in n2b/errors.rb
20
20
  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.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Nothegger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-21 00:00:00.000000000 Z
11
+ date: 2025-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -67,6 +67,7 @@ files:
67
67
  - lib/n2b.rb
68
68
  - lib/n2b/base.rb
69
69
  - lib/n2b/cli.rb
70
+ - lib/n2b/errors.rb
70
71
  - lib/n2b/irb.rb
71
72
  - lib/n2b/llm/claude.rb
72
73
  - lib/n2b/llm/gemini.rb
@@ -94,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubygems_version: 3.5.3
98
+ rubygems_version: 3.5.22
98
99
  signing_key:
99
100
  specification_version: 4
100
101
  summary: Convert natural language to bash commands or ruby code and help with debugging.