n2b 0.5.0 → 0.7.1

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.
@@ -67,9 +67,12 @@ module N2B
67
67
 
68
68
  puts "Updating Jira ticket #{ticket_key} with analysis comment..."
69
69
 
70
+ # Generate comment using template system
71
+ template_comment = generate_templated_comment(comment)
72
+
70
73
  # Prepare the comment body in Jira's Atlassian Document Format (ADF)
71
74
  comment_body = {
72
- "body" => format_comment_as_adf(comment)
75
+ "body" => format_comment_as_adf(template_comment)
73
76
  }
74
77
 
75
78
  # Make the API call to add a comment
@@ -111,6 +114,75 @@ module N2B
111
114
  end
112
115
  end
113
116
 
117
+ def extract_requirements_from_description(description_string)
118
+ extracted_lines = []
119
+ in_requirements_section = false
120
+
121
+ # Headers that trigger requirement extraction. Case-insensitive.
122
+ # Jira often uses h1, h2, etc. for headers, or bold text.
123
+ # We'll look for lines that *start* with these, possibly after Jira's header markup like "hN. "
124
+ # Or common text like "Acceptance Criteria:", "Requirements:"
125
+ # Also include comment-specific implementation keywords
126
+ requirement_headers_regex = /^(h[1-6]\.\s*)?(Requirements|Acceptance Criteria|Tasks|Key Deliverables|Scope|User Stories|Implementation|Testing|Technical|Additional|Clarification|Comment \d+)/i
127
+
128
+ # Regex to identify common list item markers
129
+ _list_item_regex = /^\s*[\*\-\+]\s+/ # Unused but kept for potential future use
130
+ # Regex for lines that look like section headers (to stop capturing)
131
+ # This is a simple heuristic: a line with a few words, ending with a colon, or Jira hN. style
132
+ section_break_regex = /^(h[1-6]\.\s*)?\w+(\s+\w+){0,3}:?\s*$/i
133
+
134
+
135
+ description_string.to_s.each_line do |line| # Handle nil description_string
136
+ stripped_line = line.strip
137
+
138
+ if stripped_line.match?(requirement_headers_regex)
139
+ in_requirements_section = true
140
+ # Add the header itself to the extracted content if desired, or just use it as a trigger
141
+ # For now, let's add the line to give context.
142
+ extracted_lines << stripped_line
143
+ next # Move to the next line
144
+ end
145
+
146
+ if in_requirements_section
147
+ # If we encounter another significant header, stop capturing this section
148
+ # (unless it's another requirements header, which is fine)
149
+ if stripped_line.match?(section_break_regex) && !stripped_line.match?(requirement_headers_regex)
150
+ # Check if this new header is one of the requirement types. If so, continue.
151
+ # Otherwise, break. This logic is simplified: if it's any other header, stop.
152
+ is_another_req_header = false # Placeholder for more complex logic if needed
153
+ requirement_headers_regex.match(stripped_line) { is_another_req_header = true }
154
+
155
+ unless is_another_req_header
156
+ in_requirements_section = false # Stop capturing
157
+ # Potentially add a separator if concatenating multiple distinct sections later
158
+ # extracted_lines << "---"
159
+ next # Don't include this new non-req header in current section
160
+ else
161
+ # It's another requirement-related header, so add it and continue
162
+ extracted_lines << stripped_line
163
+ next
164
+ end
165
+ end
166
+
167
+ # Capture list items or general text within the section
168
+ # For now, we are quite inclusive of lines within a detected section.
169
+ # We could be more strict and only take list_item_regex lines,
170
+ # but often text paragraphs under a heading are relevant too.
171
+ extracted_lines << stripped_line unless stripped_line.empty?
172
+ end
173
+ end
174
+
175
+ if extracted_lines.empty?
176
+ # Fallback: return the entire description if no specific sections found
177
+ return description_string.to_s.strip # Handle nil and strip
178
+ else
179
+ # Join extracted lines and clean up excessive newlines
180
+ # Replace 3+ newlines with 2, and 2+ newlines with 2 (effectively max 2 newlines)
181
+ # Also, strip leading/trailing whitespace from the final result.
182
+ return extracted_lines.join("\n").gsub(/\n{3,}/, "\n\n").gsub(/\n{2,}/, "\n\n").strip
183
+ end
184
+ end
185
+
114
186
  private
115
187
 
116
188
  def process_ticket_data(ticket_data, comments_data)
@@ -327,95 +399,383 @@ module N2B
327
399
  end
328
400
  end
329
401
 
330
- def extract_requirements_from_description(description_string)
331
- extracted_lines = []
332
- in_requirements_section = false
402
+ def generate_templated_comment(comment_data)
403
+ # Prepare template data from the analysis results
404
+ template_data = prepare_template_data(comment_data)
333
405
 
334
- # Headers that trigger requirement extraction. Case-insensitive.
335
- # Jira often uses h1, h2, etc. for headers, or bold text.
336
- # We'll look for lines that *start* with these, possibly after Jira's header markup like "hN. "
337
- # Or common text like "Acceptance Criteria:", "Requirements:"
338
- # Also include comment-specific implementation keywords
339
- requirement_headers_regex = /^(h[1-6]\.\s*)?(Requirements|Acceptance Criteria|Tasks|Key Deliverables|Scope|User Stories|Implementation|Testing|Technical|Additional|Clarification|Comment \d+)/i
406
+ # Load and render template
407
+ config = get_config(reconfigure: false, advanced_flow: false)
408
+ template_path = resolve_template_path('jira_comment', config)
409
+ template_content = File.read(template_path)
340
410
 
341
- # Regex to identify common list item markers
342
- _list_item_regex = /^\s*[\*\-\+]\s+/ # Unused but kept for potential future use
343
- # Regex for lines that look like section headers (to stop capturing)
344
- # This is a simple heuristic: a line with a few words, ending with a colon, or Jira hN. style
345
- section_break_regex = /^(h[1-6]\.\s*)?\w+(\s+\w+){0,3}:?\s*$/i
411
+ engine = N2B::TemplateEngine.new(template_content, template_data)
412
+ engine.render
413
+ end
346
414
 
415
+ def prepare_template_data(comment_data)
416
+ # Handle both string and hash inputs
417
+ if comment_data.is_a?(String)
418
+ # For simple string comments, create a basic template data structure
419
+ git_info = extract_git_info
420
+ return {
421
+ 'implementation_summary' => comment_data,
422
+ 'critical_errors' => [],
423
+ 'important_errors' => [],
424
+ 'improvements' => [],
425
+ 'missing_tests' => [],
426
+ 'requirements' => [],
427
+ 'test_coverage_summary' => "No specific test coverage analysis available",
428
+ 'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
429
+ 'branch_name' => git_info[:branch],
430
+ 'files_changed' => git_info[:files_changed],
431
+ 'lines_added' => git_info[:lines_added],
432
+ 'lines_removed' => git_info[:lines_removed],
433
+ 'critical_errors_empty' => true,
434
+ 'important_errors_empty' => true,
435
+ 'improvements_empty' => true,
436
+ 'missing_tests_empty' => true
437
+ }
438
+ end
347
439
 
348
- description_string.to_s.each_line do |line| # Handle nil description_string
349
- stripped_line = line.strip
440
+ # Handle hash input (structured analysis data)
441
+ # Extract and classify errors by severity
442
+ errors = comment_data[:issues] || comment_data['issues'] || []
443
+ critical_errors = []
444
+ important_errors = []
445
+ low_errors = []
446
+
447
+ errors.each do |error|
448
+ severity = classify_error_severity(error)
449
+ file_ref = extract_file_reference(error)
450
+
451
+ error_item = {
452
+ 'file_reference' => file_ref,
453
+ 'description' => clean_error_description(error),
454
+ 'severity' => severity
455
+ }
350
456
 
351
- if stripped_line.match?(requirement_headers_regex)
352
- in_requirements_section = true
353
- # Add the header itself to the extracted content if desired, or just use it as a trigger
354
- # For now, let's add the line to give context.
355
- extracted_lines << stripped_line
356
- next # Move to the next line
457
+ case severity
458
+ when 'CRITICAL'
459
+ critical_errors << error_item
460
+ when 'IMPORTANT'
461
+ important_errors << error_item
462
+ else
463
+ low_errors << error_item
357
464
  end
465
+ end
358
466
 
359
- if in_requirements_section
360
- # If we encounter another significant header, stop capturing this section
361
- # (unless it's another requirements header, which is fine)
362
- if stripped_line.match?(section_break_regex) && !stripped_line.match?(requirement_headers_regex)
363
- # Check if this new header is one of the requirement types. If so, continue.
364
- # Otherwise, break. This logic is simplified: if it's any other header, stop.
365
- is_another_req_header = false # Placeholder for more complex logic if needed
366
- requirement_headers_regex.match(stripped_line) { is_another_req_header = true }
467
+ # Process improvements
468
+ improvements = (comment_data[:improvements] || comment_data['improvements'] || []).map do |improvement|
469
+ {
470
+ 'file_reference' => extract_file_reference(improvement),
471
+ 'description' => clean_error_description(improvement)
472
+ }
473
+ end
367
474
 
368
- unless is_another_req_header
369
- in_requirements_section = false # Stop capturing
370
- # Potentially add a separator if concatenating multiple distinct sections later
371
- # extracted_lines << "---"
372
- next # Don't include this new non-req header in current section
373
- else
374
- # It's another requirement-related header, so add it and continue
375
- extracted_lines << stripped_line
376
- next
377
- end
475
+ # Process missing tests
476
+ missing_tests = extract_missing_tests(comment_data[:test_coverage] || comment_data['test_coverage'] || "")
477
+
478
+ # Process requirements
479
+ requirements = extract_requirements_status(comment_data[:requirements_evaluation] || comment_data['requirements_evaluation'] || "")
480
+
481
+ # Get git/hg info
482
+ git_info = extract_git_info
483
+
484
+ {
485
+ 'implementation_summary' => comment_data[:implementation_summary] || comment_data['implementation_summary'] || "Code analysis completed",
486
+ 'critical_errors' => critical_errors,
487
+ 'important_errors' => important_errors,
488
+ 'improvements' => improvements,
489
+ 'missing_tests' => missing_tests,
490
+ 'requirements' => requirements,
491
+ 'test_coverage_summary' => comment_data[:test_coverage] || comment_data['test_coverage'] || "No specific test coverage analysis available",
492
+ 'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
493
+ 'branch_name' => git_info[:branch],
494
+ 'files_changed' => git_info[:files_changed],
495
+ 'lines_added' => git_info[:lines_added],
496
+ 'lines_removed' => git_info[:lines_removed],
497
+ 'critical_errors_empty' => critical_errors.empty?,
498
+ 'important_errors_empty' => important_errors.empty?,
499
+ 'improvements_empty' => improvements.empty?,
500
+ 'missing_tests_empty' => missing_tests.empty?
501
+ }
502
+ end
503
+
504
+ def classify_error_severity(error_text)
505
+ text = error_text.downcase
506
+ case text
507
+ when /security|sql injection|xss|csrf|vulnerability|exploit|attack/
508
+ 'CRITICAL'
509
+ when /performance|n\+1|timeout|memory leak|slow query|bottleneck/
510
+ 'IMPORTANT'
511
+ when /error|exception|bug|fail|crash|break/
512
+ 'IMPORTANT'
513
+ when /style|convention|naming|format|indent|space/
514
+ 'LOW'
515
+ else
516
+ 'IMPORTANT'
517
+ end
518
+ end
519
+
520
+ def extract_file_reference(text)
521
+ # Parse various file reference formats
522
+ if match = text.match(/(\S+\.(?:rb|js|py|java|cpp|c|h|ts|jsx|tsx|php|go|rs|swift|kt))(?:\s+(?:line|lines?)\s+(\d+(?:-\d+)?)|:(\d+(?:-\d+)?)|\s*\(line\s+(\d+)\))?/i)
523
+ file = match[1]
524
+ line = match[2] || match[3] || match[4]
525
+ line ? "*#{file}:#{line}*" : "*#{file}*"
526
+ else
527
+ "*General*"
528
+ end
529
+ end
530
+
531
+ def clean_error_description(text)
532
+ # Remove file references from description to avoid duplication
533
+ text.gsub(/\S+\.(?:rb|js|py|java|cpp|c|h|ts|jsx|tsx|php|go|rs|swift|kt)(?:\s+(?:line|lines?)\s+\d+(?:-\d+)?|:\d+(?:-\d+)?|\s*\(line\s+\d+\))?:?\s*/i, '').strip
534
+ end
535
+
536
+ def extract_missing_tests(test_coverage_text)
537
+ # Extract test-related items from coverage analysis
538
+ missing_tests = []
539
+
540
+ # Look for common patterns indicating missing tests
541
+ test_coverage_text.scan(/(?:missing|need|add|require).*?test.*?(?:\.|$)/i) do |match|
542
+ missing_tests << { 'description' => match.strip }
543
+ end
544
+
545
+ # If no specific missing tests found, create generic ones based on coverage
546
+ if missing_tests.empty? && test_coverage_text.include?('%')
547
+ if coverage_match = test_coverage_text.match(/(\d+)%/)
548
+ coverage = coverage_match[1].to_i
549
+ if coverage < 80
550
+ missing_tests << { 'description' => "Increase test coverage from #{coverage}% to target 80%+" }
378
551
  end
552
+ end
553
+ end
379
554
 
380
- # Capture list items or general text within the section
381
- # For now, we are quite inclusive of lines within a detected section.
382
- # We could be more strict and only take list_item_regex lines,
383
- # but often text paragraphs under a heading are relevant too.
384
- extracted_lines << stripped_line unless stripped_line.empty?
555
+ missing_tests
556
+ end
557
+
558
+ def extract_requirements_status(requirements_text)
559
+ requirements = []
560
+
561
+ # Split by lines and process each line
562
+ requirements_text.split("\n").each do |line|
563
+ line = line.strip
564
+ next if line.empty?
565
+
566
+ # Parse requirements with status indicators - order matters for regex matching
567
+ if match = line.match(/(✅|⚠️|❌|🔍)?\s*(PARTIALLY\s+IMPLEMENTED|NOT\s+IMPLEMENTED|IMPLEMENTED|UNCLEAR)?:?\s*(.+)/i)
568
+ status_emoji, status_text, description = match.captures
569
+ status = case
570
+ when status_text&.include?('PARTIALLY')
571
+ 'PARTIALLY_IMPLEMENTED'
572
+ when status_text&.include?('NOT')
573
+ 'NOT_IMPLEMENTED'
574
+ when status_emoji == '✅' || (status_text&.include?('IMPLEMENTED') && !status_text&.include?('NOT') && !status_text&.include?('PARTIALLY'))
575
+ 'IMPLEMENTED'
576
+ when status_emoji == '⚠️'
577
+ 'PARTIALLY_IMPLEMENTED'
578
+ when status_emoji == '❌'
579
+ 'NOT_IMPLEMENTED'
580
+ else
581
+ 'UNCLEAR'
582
+ end
583
+
584
+ requirements << {
585
+ 'status' => status,
586
+ 'description' => description.strip,
587
+ 'status_icon' => status_emoji || (status == 'IMPLEMENTED' ? '✅' : status == 'PARTIALLY_IMPLEMENTED' ? '⚠️' : status == 'NOT_IMPLEMENTED' ? '❌' : '🔍')
588
+ }
385
589
  end
386
590
  end
387
591
 
388
- if extracted_lines.empty?
389
- # Fallback: return the entire description if no specific sections found
390
- return description_string.to_s.strip # Handle nil and strip
391
- else
392
- # Join extracted lines and clean up excessive newlines
393
- # Replace 3+ newlines with 2, and 2+ newlines with 2 (effectively max 2 newlines)
394
- # Also, strip leading/trailing whitespace from the final result.
395
- return extracted_lines.join("\n").gsub(/\n{3,}/, "\n\n").gsub(/\n{2,}/, "\n\n").strip
592
+ requirements
593
+ end
594
+
595
+ def extract_git_info
596
+ begin
597
+ if File.exist?('.git')
598
+ branch = `git branch --show-current 2>/dev/null`.strip
599
+ branch = 'unknown' if branch.empty?
600
+
601
+ # Get diff stats
602
+ diff_stats = `git diff --stat HEAD~1 2>/dev/null`.strip
603
+ files_changed = diff_stats.scan(/(\d+) files? changed/).flatten.first || "0"
604
+ lines_added = diff_stats.scan(/(\d+) insertions?/).flatten.first || "0"
605
+ lines_removed = diff_stats.scan(/(\d+) deletions?/).flatten.first || "0"
606
+ elsif File.exist?('.hg')
607
+ branch = `hg branch 2>/dev/null`.strip
608
+ branch = 'default' if branch.empty?
609
+
610
+ # Get diff stats for hg
611
+ diff_stats = `hg diff --stat 2>/dev/null`.strip
612
+ files_changed = diff_stats.lines.count.to_s
613
+ lines_added = "0" # hg diff --stat doesn't show +/- easily
614
+ lines_removed = "0"
615
+ else
616
+ branch = 'unknown'
617
+ files_changed = "0"
618
+ lines_added = "0"
619
+ lines_removed = "0"
620
+ end
621
+ rescue
622
+ branch = 'unknown'
623
+ files_changed = "0"
624
+ lines_added = "0"
625
+ lines_removed = "0"
396
626
  end
627
+
628
+ {
629
+ branch: branch,
630
+ files_changed: files_changed,
631
+ lines_added: lines_added,
632
+ lines_removed: lines_removed
633
+ }
634
+ end
635
+
636
+ def resolve_template_path(template_key, config)
637
+ user_path = config.dig('templates', template_key) if config.is_a?(Hash)
638
+ return user_path if user_path && File.exist?(user_path)
639
+
640
+ File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
641
+ end
642
+
643
+ def get_config(reconfigure: false, advanced_flow: false)
644
+ # This should match the config loading from the main CLI
645
+ # For now, return empty hash - will be enhanced when config system is unified
646
+ {}
647
+ end
648
+
649
+ def convert_markdown_to_adf(markdown_text)
650
+ content = []
651
+ lines = markdown_text.split("\n")
652
+ current_paragraph = []
653
+
654
+ lines.each do |line|
655
+ case line
656
+ when /^\*(.+)\*$/ # Bold headers like *N2B Code Analysis Report*
657
+ # Flush current paragraph
658
+ if current_paragraph.any?
659
+ content << create_paragraph(current_paragraph.join(" "))
660
+ current_paragraph = []
661
+ end
662
+
663
+ content << {
664
+ "type" => "heading",
665
+ "attrs" => { "level" => 2 },
666
+ "content" => [
667
+ {
668
+ "type" => "text",
669
+ "text" => $1.strip,
670
+ "marks" => [{ "type" => "strong" }]
671
+ }
672
+ ]
673
+ }
674
+ when /^=+$/ # Separator lines
675
+ # Skip separator lines
676
+ when /^\{expand:(.+)\}$/ # Jira expand start
677
+ # Flush current paragraph
678
+ if current_paragraph.any?
679
+ content << create_paragraph(current_paragraph.join(" "))
680
+ current_paragraph = []
681
+ end
682
+
683
+ # Create expand section
684
+ expand_title = $1.strip
685
+ content << {
686
+ "type" => "expand",
687
+ "attrs" => { "title" => expand_title },
688
+ "content" => []
689
+ }
690
+ when /^\{expand\}$/ # Jira expand end
691
+ # End of expand section - handled by the expand start
692
+ when /^☐\s+(.+)$/ # Unchecked checkbox
693
+ # Flush current paragraph
694
+ if current_paragraph.any?
695
+ content << create_paragraph(current_paragraph.join(" "))
696
+ current_paragraph = []
697
+ end
698
+
699
+ content << {
700
+ "type" => "taskList",
701
+ "content" => [
702
+ {
703
+ "type" => "taskItem",
704
+ "attrs" => { "state" => "TODO" },
705
+ "content" => [
706
+ create_paragraph($1.strip)
707
+ ]
708
+ }
709
+ ]
710
+ }
711
+ when /^☑\s+(.+)$/ # Checked checkbox
712
+ # Flush current paragraph
713
+ if current_paragraph.any?
714
+ content << create_paragraph(current_paragraph.join(" "))
715
+ current_paragraph = []
716
+ end
717
+
718
+ content << {
719
+ "type" => "taskList",
720
+ "content" => [
721
+ {
722
+ "type" => "taskItem",
723
+ "attrs" => { "state" => "DONE" },
724
+ "content" => [
725
+ create_paragraph($1.strip)
726
+ ]
727
+ }
728
+ ]
729
+ }
730
+ when /^---$/ # Horizontal rule
731
+ # Flush current paragraph
732
+ if current_paragraph.any?
733
+ content << create_paragraph(current_paragraph.join(" "))
734
+ current_paragraph = []
735
+ end
736
+
737
+ content << { "type" => "rule" }
738
+ when "" # Empty line
739
+ # Flush current paragraph
740
+ if current_paragraph.any?
741
+ content << create_paragraph(current_paragraph.join(" "))
742
+ current_paragraph = []
743
+ end
744
+ else # Regular text
745
+ current_paragraph << line
746
+ end
747
+ end
748
+
749
+ # Flush any remaining paragraph
750
+ if current_paragraph.any?
751
+ content << create_paragraph(current_paragraph.join(" "))
752
+ end
753
+
754
+ {
755
+ "type" => "doc",
756
+ "version" => 1,
757
+ "content" => content
758
+ }
759
+ end
760
+
761
+ def create_paragraph(text)
762
+ {
763
+ "type" => "paragraph",
764
+ "content" => [
765
+ {
766
+ "type" => "text",
767
+ "text" => text
768
+ }
769
+ ]
770
+ }
397
771
  end
398
772
 
399
773
  private
400
774
 
401
775
  def format_comment_as_adf(comment_data)
402
- # If comment_data is a string (legacy), convert to simple ADF
776
+ # If comment_data is a string (from template), convert to simple ADF
403
777
  if comment_data.is_a?(String)
404
- return {
405
- "type" => "doc",
406
- "version" => 1,
407
- "content" => [
408
- {
409
- "type" => "paragraph",
410
- "content" => [
411
- {
412
- "type" => "text",
413
- "text" => comment_data
414
- }
415
- ]
416
- }
417
- ]
418
- }
778
+ return convert_markdown_to_adf(comment_data)
419
779
  end
420
780
 
421
781
  # If comment_data is structured (new format), build proper ADF
@@ -32,7 +32,7 @@ module N2M
32
32
  "messages" => [
33
33
  {
34
34
  "role" => "user",
35
- "content" => content
35
+ "content" => content
36
36
  }
37
37
  ]
38
38
  })
@@ -44,29 +44,16 @@ module N2M
44
44
  if response.code != '200'
45
45
  raise N2B::LlmApiError.new("LLM API Error: #{response.code} #{response.message} - #{response.body}")
46
46
  end
47
- answer = JSON.parse(response.body)['content'].first['text']
48
- begin
49
- # The llm_response.json file is likely for debugging and can be kept or removed.
50
- # For this refactoring, I'll keep it as it doesn't affect the error handling logic.
51
- File.open('llm_response.json', 'w') do |f|
52
- f.write(answer)
53
- end
54
- # remove everything before the first { and after the last }
55
-
47
+ answer = JSON.parse(response.body)['content'].first['text']
48
+ begin
56
49
  answer = answer.sub(/.*?\{(.*)\}.*/m, '{\1}') unless answer.start_with?('{')
57
- # gsub all \n with \\n that are inside "
58
- #
59
50
  answer.gsub!(/"([^"]*)"/) { |match| match.gsub(/\n/, "\\n") }
60
- # The llm_response.json file is likely for debugging and can be kept or removed.
61
- File.open('llm_response.json', 'w') do |f|
62
- f.write(answer)
63
- end
64
51
  answer = JSON.parse(answer)
65
52
  rescue JSON::ParserError
66
53
  # This specific JSON parsing error is about the LLM's *response content*, not an API error.
67
54
  # It should probably be handled differently, but the subtask is about LlmApiError.
68
55
  # For now, keeping existing behavior for this part.
69
- puts "Error parsing JSON from LLM response: #{answer}" # Clarified error message
56
+ puts "Error parsing JSON from LLM response: #{answer}"
70
57
  answer = { 'explanation' => answer} # Default fallback
71
58
  end
72
59
  answer
@@ -83,10 +70,10 @@ module N2M
83
70
 
84
71
  request.body = JSON.dump({
85
72
  "model" => get_model_name,
86
- "max_tokens" => @config['max_tokens'] || 1024, # Allow overriding max_tokens from config
73
+ "max_tokens" => @config['max_tokens'] || 1024,
87
74
  "messages" => [
88
75
  {
89
- "role" => "user", # The entire prompt is passed as a single user message
76
+ "role" => "user",
90
77
  "content" => prompt_content
91
78
  }
92
79
  ]
@@ -34,7 +34,10 @@ module N2M
34
34
  "parts" => [{
35
35
  "text" => content
36
36
  }]
37
- }]
37
+ }],
38
+ "generationConfig" => {
39
+ "responseMimeType" => "application/json"
40
+ }
38
41
  })
39
42
 
40
43
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
@@ -82,14 +85,12 @@ module N2M
82
85
  request.body = JSON.dump({
83
86
  "contents" => [{
84
87
  "parts" => [{
85
- "text" => prompt_content # The entire prompt is passed as text
88
+ "text" => prompt_content
86
89
  }]
87
90
  }],
88
- # Gemini specific: Ensure JSON output if possible via generationConfig
89
- # However, the primary method is instructing it within the prompt itself.
90
- # "generationConfig": {
91
- # "responseMimeType": "application/json", # This might be too restrictive or not always work as expected
92
- # }
91
+ "generationConfig" => {
92
+ "responseMimeType" => "application/json"
93
+ }
93
94
  })
94
95
 
95
96
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
@@ -88,7 +88,7 @@ module N2M
88
88
  # The prompt_content for diff analysis should instruct the LLM to return JSON.
89
89
  # For Ollama, you can also try adding "format": "json" to the request if the model supports it.
90
90
  request_body = {
91
- "model" => @config['model'] || MODELS.keys.first,
91
+ "model" => get_model_name,
92
92
  "messages" => [
93
93
  {
94
94
  "role" => "user",