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.
- checksums.yaml +4 -4
- data/README.md +374 -23
- data/bin/n2b-diff +5 -0
- data/bin/n2b-test-jira +17 -14
- data/lib/n2b/cli.rb +137 -72
- data/lib/n2b/jira_client.rb +430 -70
- data/lib/n2b/llm/claude.rb +6 -19
- data/lib/n2b/llm/gemini.rb +8 -7
- data/lib/n2b/llm/ollama.rb +1 -1
- data/lib/n2b/llm/open_ai.rb +26 -20
- data/lib/n2b/merge_cli.rb +694 -0
- data/lib/n2b/merge_conflict_parser.rb +70 -0
- data/lib/n2b/template_engine.rb +105 -0
- data/lib/n2b/templates/diff_json_instruction.txt +27 -0
- data/lib/n2b/templates/diff_system_prompt.txt +23 -0
- data/lib/n2b/templates/jira_comment.txt +67 -0
- data/lib/n2b/templates/merge_conflict_prompt.txt +20 -0
- data/lib/n2b/version.rb +1 -1
- data/lib/n2b.rb +3 -0
- metadata +34 -7
data/lib/n2b/jira_client.rb
CHANGED
@@ -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(
|
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
|
331
|
-
|
332
|
-
|
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
|
-
#
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
342
|
-
|
343
|
-
|
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
|
-
|
349
|
-
|
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
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
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
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
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 (
|
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
|
data/lib/n2b/llm/claude.rb
CHANGED
@@ -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}"
|
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,
|
73
|
+
"max_tokens" => @config['max_tokens'] || 1024,
|
87
74
|
"messages" => [
|
88
75
|
{
|
89
|
-
"role" => "user",
|
76
|
+
"role" => "user",
|
90
77
|
"content" => prompt_content
|
91
78
|
}
|
92
79
|
]
|
data/lib/n2b/llm/gemini.rb
CHANGED
@@ -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
|
88
|
+
"text" => prompt_content
|
86
89
|
}]
|
87
90
|
}],
|
88
|
-
|
89
|
-
|
90
|
-
|
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|
|
data/lib/n2b/llm/ollama.rb
CHANGED
@@ -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" =>
|
91
|
+
"model" => get_model_name,
|
92
92
|
"messages" => [
|
93
93
|
{
|
94
94
|
"role" => "user",
|