n2b 0.5.1 → 2.0.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.
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'json'
4
4
  require 'base64'
5
+ require_relative 'template_engine'
5
6
 
6
7
  module N2B
7
8
  class JiraClient
@@ -67,19 +68,44 @@ module N2B
67
68
 
68
69
  puts "Updating Jira ticket #{ticket_key} with analysis comment..."
69
70
 
71
+ # Generate comment using template system
72
+ template_comment = generate_templated_comment(comment)
73
+
74
+ if debug_mode?
75
+ puts "🔍 DEBUG: Generated template comment (#{template_comment.length} chars):"
76
+ puts "--- TEMPLATE COMMENT START ---"
77
+ puts template_comment
78
+ puts "--- TEMPLATE COMMENT END ---"
79
+ end
80
+
70
81
  # Prepare the comment body in Jira's Atlassian Document Format (ADF)
71
82
  comment_body = {
72
- "body" => format_comment_as_adf(comment)
83
+ "body" => format_comment_as_adf(template_comment)
73
84
  }
74
85
 
86
+ if debug_mode?
87
+ puts "🔍 DEBUG: Formatted ADF comment body:"
88
+ puts "--- ADF BODY START ---"
89
+ puts JSON.pretty_generate(comment_body)
90
+ puts "--- ADF BODY END ---"
91
+ end
92
+
75
93
  # Make the API call to add a comment
76
94
  path = "/rest/api/3/issue/#{ticket_key}/comment"
95
+ puts "🔍 DEBUG: Making API request to: #{path}" if debug_mode?
96
+
77
97
  _response = make_api_request('POST', path, comment_body)
78
98
 
79
99
  puts "✅ Successfully added comment to Jira ticket #{ticket_key}"
80
100
  true
81
101
  rescue JiraApiError => e
82
102
  puts "❌ Failed to update Jira ticket #{ticket_key}: #{e.message}"
103
+ if debug_mode?
104
+ puts "🔍 DEBUG: Full error details:"
105
+ puts " - Ticket key: #{ticket_key}"
106
+ puts " - Template comment length: #{template_comment&.length || 'nil'}"
107
+ puts " - Comment body keys: #{comment_body&.keys || 'nil'}"
108
+ end
83
109
  false
84
110
  end
85
111
 
@@ -396,28 +422,543 @@ module N2B
396
422
  end
397
423
  end
398
424
 
399
- private
425
+ def generate_templated_comment(comment_data)
426
+ # Handle structured hash data from format_analysis_for_jira
427
+ if comment_data.is_a?(Hash) && comment_data.key?(:implementation_summary)
428
+ return generate_structured_comment(comment_data)
429
+ end
400
430
 
401
- def format_comment_as_adf(comment_data)
402
- # If comment_data is a string (legacy), convert to simple ADF
431
+ # Prepare template data from the analysis results
432
+ template_data = prepare_template_data(comment_data)
433
+
434
+ # Load and render template
435
+ config = get_config(reconfigure: false, advanced_flow: false)
436
+ template_path = resolve_template_path('jira_comment', config)
437
+ template_content = File.read(template_path)
438
+
439
+ engine = N2B::TemplateEngine.new(template_content, template_data)
440
+ engine.render
441
+ end
442
+
443
+ def generate_structured_comment(data)
444
+ # Generate a properly formatted comment from structured analysis data
445
+ git_info = extract_git_info
446
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M UTC")
447
+
448
+ comment_parts = []
449
+
450
+ # Header
451
+ comment_parts << "*N2B Code Analysis Report*"
452
+ comment_parts << ""
453
+
454
+ # Implementation Summary (always expanded)
455
+ comment_parts << "*Implementation Summary:*"
456
+ comment_parts << (data[:implementation_summary] || "Unknown")
457
+ comment_parts << ""
458
+
459
+ # Custom message if provided (also expanded)
460
+ if data[:custom_analysis_focus] && !data[:custom_analysis_focus].empty?
461
+ comment_parts << "*Custom Analysis Focus:*"
462
+ comment_parts << data[:custom_analysis_focus]
463
+ comment_parts << ""
464
+ end
465
+
466
+ comment_parts << "---"
467
+ comment_parts << ""
468
+
469
+ # Automated Analysis Findings
470
+ comment_parts << "*Automated Analysis Findings:*"
471
+ comment_parts << ""
472
+
473
+ # Critical Issues (collapsed by default)
474
+ critical_issues = classify_issues_by_severity(data[:issues] || [], 'CRITICAL')
475
+ if critical_issues.any?
476
+ comment_parts << "{expand:🚨 Critical Issues (Must Fix Before Merge)}"
477
+ critical_issues.each { |issue| comment_parts << "☐ #{issue}" }
478
+ comment_parts << "{expand}"
479
+ else
480
+ comment_parts << "✅ No critical issues found"
481
+ end
482
+ comment_parts << ""
483
+
484
+ # Important Issues (collapsed by default)
485
+ important_issues = classify_issues_by_severity(data[:issues] || [], 'IMPORTANT')
486
+ if important_issues.any?
487
+ comment_parts << "{expand:⚠️ Important Issues (Should Address)}"
488
+ important_issues.each { |issue| comment_parts << "☐ #{issue}" }
489
+ comment_parts << "{expand}"
490
+ else
491
+ comment_parts << "✅ No important issues found"
492
+ end
493
+ comment_parts << ""
494
+
495
+ # Suggested Improvements (collapsed by default)
496
+ if data[:improvements] && data[:improvements].any?
497
+ comment_parts << "{expand:💡 Suggested Improvements (Nice to Have)}"
498
+ data[:improvements].each { |improvement| comment_parts << "☐ #{improvement}" }
499
+ comment_parts << "{expand}"
500
+ else
501
+ comment_parts << "✅ No specific improvements suggested"
502
+ end
503
+ comment_parts << ""
504
+
505
+ # Test Coverage Assessment
506
+ comment_parts << "{expand:🧪 Test Coverage Assessment}"
507
+ if data[:test_coverage] && !data[:test_coverage].empty?
508
+ comment_parts << "*Overall Assessment:* #{data[:test_coverage]}"
509
+ else
510
+ comment_parts << "*Overall Assessment:* Not assessed"
511
+ end
512
+ comment_parts << "{expand}"
513
+ comment_parts << ""
514
+
515
+ # Missing Test Coverage
516
+ comment_parts << "*Missing Test Coverage:*"
517
+ comment_parts << "☐ No specific missing tests identified"
518
+ comment_parts << ""
519
+
520
+ # Requirements Evaluation
521
+ comment_parts << "*📋 Requirements Evaluation:*"
522
+ if data[:requirements_evaluation] && !data[:requirements_evaluation].empty?
523
+ comment_parts << "#{data[:requirements_evaluation]}"
524
+ else
525
+ comment_parts << "🔍 *UNCLEAR:* Requirements not provided or assessed"
526
+ end
527
+ comment_parts << ""
528
+
529
+ comment_parts << "---"
530
+ comment_parts << ""
531
+
532
+ # Footer with metadata (simplified)
533
+ comment_parts << "Analysis completed on #{timestamp} | Branch: #{git_info[:branch]}"
534
+
535
+ comment_parts.join("\n")
536
+ end
537
+
538
+ def classify_issues_by_severity(issues, target_severity)
539
+ return [] unless issues.is_a?(Array)
540
+
541
+ issues.select do |issue|
542
+ severity = classify_error_severity(issue)
543
+ severity == target_severity
544
+ end
545
+ end
546
+
547
+ def prepare_template_data(comment_data)
548
+ # Handle both string and hash inputs
403
549
  if comment_data.is_a?(String)
550
+ # For simple string comments, create a basic template data structure
551
+ git_info = extract_git_info
404
552
  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
- ]
553
+ 'implementation_summary' => comment_data,
554
+ 'critical_errors' => [],
555
+ 'important_errors' => [],
556
+ 'improvements' => [],
557
+ 'missing_tests' => [],
558
+ 'requirements' => [],
559
+ 'test_coverage_summary' => "No specific test coverage analysis available",
560
+ 'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
561
+ 'branch_name' => git_info[:branch],
562
+ 'files_changed' => git_info[:files_changed],
563
+ 'lines_added' => git_info[:lines_added],
564
+ 'lines_removed' => git_info[:lines_removed],
565
+ 'critical_errors_empty' => true,
566
+ 'important_errors_empty' => true,
567
+ 'improvements_empty' => true,
568
+ 'missing_tests_empty' => true
569
+ }
570
+ end
571
+
572
+ # Handle hash input (structured analysis data)
573
+ # Extract and classify errors by severity
574
+ errors = comment_data[:issues] || comment_data['issues'] || []
575
+ critical_errors = []
576
+ important_errors = []
577
+ low_errors = []
578
+
579
+ errors.each do |error|
580
+ severity = classify_error_severity(error)
581
+ file_ref = extract_file_reference(error)
582
+
583
+ error_item = {
584
+ 'file_reference' => file_ref,
585
+ 'description' => clean_error_description(error),
586
+ 'severity' => severity
587
+ }
588
+
589
+ case severity
590
+ when 'CRITICAL'
591
+ critical_errors << error_item
592
+ when 'IMPORTANT'
593
+ important_errors << error_item
594
+ else
595
+ low_errors << error_item
596
+ end
597
+ end
598
+
599
+ # Process improvements
600
+ improvements = (comment_data[:improvements] || comment_data['improvements'] || []).map do |improvement|
601
+ {
602
+ 'file_reference' => extract_file_reference(improvement),
603
+ 'description' => clean_error_description(improvement)
418
604
  }
419
605
  end
420
606
 
607
+ # Process missing tests
608
+ missing_tests = extract_missing_tests(comment_data[:test_coverage] || comment_data['test_coverage'] || "")
609
+
610
+ # Process requirements
611
+ requirements = extract_requirements_status(comment_data[:requirements_evaluation] || comment_data['requirements_evaluation'] || "")
612
+
613
+ # Get git/hg info
614
+ git_info = extract_git_info
615
+
616
+ {
617
+ 'implementation_summary' => comment_data[:implementation_summary] || comment_data['implementation_summary'] || "Code analysis completed",
618
+ 'critical_errors' => critical_errors,
619
+ 'important_errors' => important_errors,
620
+ 'improvements' => improvements,
621
+ 'missing_tests' => missing_tests,
622
+ 'requirements' => requirements,
623
+ 'test_coverage_summary' => comment_data[:test_coverage] || comment_data['test_coverage'] || "No specific test coverage analysis available",
624
+ 'timestamp' => Time.now.strftime("%Y-%m-%d %H:%M UTC"),
625
+ 'branch_name' => git_info[:branch],
626
+ 'files_changed' => git_info[:files_changed],
627
+ 'lines_added' => git_info[:lines_added],
628
+ 'lines_removed' => git_info[:lines_removed],
629
+ 'critical_errors_empty' => critical_errors.empty?,
630
+ 'important_errors_empty' => important_errors.empty?,
631
+ 'improvements_empty' => improvements.empty?,
632
+ 'missing_tests_empty' => missing_tests.empty?
633
+ }
634
+ end
635
+
636
+ def classify_error_severity(error_text)
637
+ text = error_text.downcase
638
+ case text
639
+ when /security|sql injection|xss|csrf|vulnerability|exploit|attack/
640
+ 'CRITICAL'
641
+ when /performance|n\+1|timeout|memory leak|slow query|bottleneck/
642
+ 'IMPORTANT'
643
+ when /error|exception|bug|fail|crash|break/
644
+ 'IMPORTANT'
645
+ when /style|convention|naming|format|indent|space/
646
+ 'LOW'
647
+ else
648
+ 'IMPORTANT'
649
+ end
650
+ end
651
+
652
+ def extract_file_reference(text)
653
+ # Parse various file reference formats
654
+ 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)
655
+ file = match[1]
656
+ line = match[2] || match[3] || match[4]
657
+ line ? "*#{file}:#{line}*" : "*#{file}*"
658
+ else
659
+ "*General*"
660
+ end
661
+ end
662
+
663
+ def clean_error_description(text)
664
+ # Remove file references from description to avoid duplication
665
+ 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
666
+ end
667
+
668
+ def extract_missing_tests(test_coverage_text)
669
+ # Extract test-related items from coverage analysis
670
+ missing_tests = []
671
+
672
+ # Look for common patterns indicating missing tests
673
+ test_coverage_text.scan(/(?:missing|need|add|require).*?test.*?(?:\.|$)/i) do |match|
674
+ missing_tests << { 'description' => match.strip }
675
+ end
676
+
677
+ # If no specific missing tests found, create generic ones based on coverage
678
+ if missing_tests.empty? && test_coverage_text.include?('%')
679
+ if coverage_match = test_coverage_text.match(/(\d+)%/)
680
+ coverage = coverage_match[1].to_i
681
+ if coverage < 80
682
+ missing_tests << { 'description' => "Increase test coverage from #{coverage}% to target 80%+" }
683
+ end
684
+ end
685
+ end
686
+
687
+ missing_tests
688
+ end
689
+
690
+ def extract_requirements_status(requirements_text)
691
+ requirements = []
692
+
693
+ # Split by lines and process each line
694
+ requirements_text.split("\n").each do |line|
695
+ line = line.strip
696
+ next if line.empty?
697
+
698
+ # Parse requirements with status indicators - order matters for regex matching
699
+ if match = line.match(/(✅|⚠️|❌|🔍)?\s*(PARTIALLY\s+IMPLEMENTED|NOT\s+IMPLEMENTED|IMPLEMENTED|UNCLEAR)?:?\s*(.+)/i)
700
+ status_emoji, status_text, description = match.captures
701
+ status = case
702
+ when status_text&.include?('PARTIALLY')
703
+ 'PARTIALLY_IMPLEMENTED'
704
+ when status_text&.include?('NOT')
705
+ 'NOT_IMPLEMENTED'
706
+ when status_emoji == '✅' || (status_text&.include?('IMPLEMENTED') && !status_text&.include?('NOT') && !status_text&.include?('PARTIALLY'))
707
+ 'IMPLEMENTED'
708
+ when status_emoji == '⚠️'
709
+ 'PARTIALLY_IMPLEMENTED'
710
+ when status_emoji == '❌'
711
+ 'NOT_IMPLEMENTED'
712
+ else
713
+ 'UNCLEAR'
714
+ end
715
+
716
+ requirements << {
717
+ 'status' => status,
718
+ 'description' => description.strip,
719
+ 'status_icon' => status_emoji || (status == 'IMPLEMENTED' ? '✅' : status == 'PARTIALLY_IMPLEMENTED' ? '⚠️' : status == 'NOT_IMPLEMENTED' ? '❌' : '🔍')
720
+ }
721
+ end
722
+ end
723
+
724
+ requirements
725
+ end
726
+
727
+ def extract_git_info
728
+ begin
729
+ if File.exist?('.git')
730
+ branch = `git branch --show-current 2>/dev/null`.strip
731
+ branch = 'unknown' if branch.empty?
732
+
733
+ # Get diff stats
734
+ diff_stats = `git diff --stat HEAD~1 2>/dev/null`.strip
735
+ files_changed = diff_stats.scan(/(\d+) files? changed/).flatten.first || "0"
736
+ lines_added = diff_stats.scan(/(\d+) insertions?/).flatten.first || "0"
737
+ lines_removed = diff_stats.scan(/(\d+) deletions?/).flatten.first || "0"
738
+ elsif File.exist?('.hg')
739
+ branch = `hg branch 2>/dev/null`.strip
740
+ branch = 'default' if branch.empty?
741
+
742
+ # Get diff stats for hg
743
+ diff_stats = `hg diff --stat 2>/dev/null`.strip
744
+ files_changed = diff_stats.lines.count.to_s
745
+ lines_added = "0" # hg diff --stat doesn't show +/- easily
746
+ lines_removed = "0"
747
+ else
748
+ branch = 'unknown'
749
+ files_changed = "0"
750
+ lines_added = "0"
751
+ lines_removed = "0"
752
+ end
753
+ rescue
754
+ branch = 'unknown'
755
+ files_changed = "0"
756
+ lines_added = "0"
757
+ lines_removed = "0"
758
+ end
759
+
760
+ {
761
+ branch: branch,
762
+ files_changed: files_changed,
763
+ lines_added: lines_added,
764
+ lines_removed: lines_removed
765
+ }
766
+ end
767
+
768
+ def resolve_template_path(template_key, config)
769
+ user_path = config.dig('templates', template_key) if config.is_a?(Hash)
770
+ return user_path if user_path && File.exist?(user_path)
771
+
772
+ File.expand_path(File.join(__dir__, 'templates', "#{template_key}.txt"))
773
+ end
774
+
775
+ def get_config(reconfigure: false, advanced_flow: false)
776
+ # Return the config that was passed during initialization
777
+ # This is used for template resolution and other configuration needs
778
+ @config
779
+ end
780
+
781
+ def convert_markdown_to_adf(markdown_text)
782
+ content = []
783
+ lines = markdown_text.split("\n")
784
+ current_paragraph = []
785
+ current_expand = nil
786
+ expand_content = []
787
+
788
+ lines.each do |line|
789
+ case line
790
+ when /^\*(.+)\*$/ # Bold headers like *N2B Code Analysis Report*
791
+ # Flush current paragraph
792
+ if current_paragraph.any?
793
+ content << create_paragraph(current_paragraph.join(" "))
794
+ current_paragraph = []
795
+ end
796
+
797
+ content << {
798
+ "type" => "heading",
799
+ "attrs" => { "level" => 2 },
800
+ "content" => [
801
+ {
802
+ "type" => "text",
803
+ "text" => $1.strip,
804
+ "marks" => [{ "type" => "strong" }]
805
+ }
806
+ ]
807
+ }
808
+ when /^=+$/ # Separator lines
809
+ # Skip separator lines
810
+ when /^\{expand:(.+)\}$/ # Jira expand start
811
+ # Flush current paragraph
812
+ if current_paragraph.any?
813
+ content << create_paragraph(current_paragraph.join(" "))
814
+ current_paragraph = []
815
+ end
816
+
817
+ # Start collecting expand content
818
+ expand_title = $1.strip
819
+ current_expand = {
820
+ "type" => "expand",
821
+ "attrs" => { "title" => expand_title },
822
+ "content" => []
823
+ }
824
+ expand_content = []
825
+ when /^\{expand\}$/ # Jira expand end
826
+ # End of expand section - add collected content
827
+ if current_expand
828
+ current_expand["content"] = expand_content
829
+ content << current_expand if expand_content.any? # Only add if has content
830
+ current_expand = nil
831
+ expand_content = []
832
+ end
833
+ when /^☐\s+(.+)$/ # Unchecked checkbox
834
+ # Flush current paragraph
835
+ if current_paragraph.any?
836
+ paragraph = create_paragraph(current_paragraph.join(" "))
837
+ if current_expand
838
+ expand_content << paragraph
839
+ else
840
+ content << paragraph
841
+ end
842
+ current_paragraph = []
843
+ end
844
+
845
+ # Convert checkbox to simple paragraph (no bullet points)
846
+ checkbox_paragraph = create_paragraph("☐ " + $1.strip)
847
+
848
+ if current_expand
849
+ expand_content << checkbox_paragraph
850
+ else
851
+ content << checkbox_paragraph
852
+ end
853
+ when /^☑\s+(.+)$/ # Checked checkbox
854
+ # Flush current paragraph
855
+ if current_paragraph.any?
856
+ paragraph = create_paragraph(current_paragraph.join(" "))
857
+ if current_expand
858
+ expand_content << paragraph
859
+ else
860
+ content << paragraph
861
+ end
862
+ current_paragraph = []
863
+ end
864
+
865
+ # Convert checkbox to simple paragraph (no bullet points)
866
+ checkbox_paragraph = create_paragraph("☑ " + $1.strip)
867
+
868
+ if current_expand
869
+ expand_content << checkbox_paragraph
870
+ else
871
+ content << checkbox_paragraph
872
+ end
873
+ when /^---$/ # Horizontal rule
874
+ # Flush current paragraph
875
+ if current_paragraph.any?
876
+ paragraph = create_paragraph(current_paragraph.join(" "))
877
+ if current_expand
878
+ expand_content << paragraph
879
+ else
880
+ content << paragraph
881
+ end
882
+ current_paragraph = []
883
+ end
884
+
885
+ rule = { "type" => "rule" }
886
+ if current_expand
887
+ expand_content << rule
888
+ else
889
+ content << rule
890
+ end
891
+ when "" # Empty line
892
+ # Flush current paragraph
893
+ if current_paragraph.any?
894
+ paragraph = create_paragraph(current_paragraph.join(" "))
895
+ if current_expand
896
+ expand_content << paragraph
897
+ else
898
+ content << paragraph
899
+ end
900
+ current_paragraph = []
901
+ end
902
+ else # Regular text
903
+ # Skip empty or whitespace-only content
904
+ unless line.strip.empty? || line.strip == "{}"
905
+ current_paragraph << line
906
+ end
907
+ end
908
+ end
909
+
910
+ # Flush any remaining paragraph
911
+ if current_paragraph.any?
912
+ paragraph = create_paragraph(current_paragraph.join(" "))
913
+ if current_expand
914
+ expand_content << paragraph
915
+ else
916
+ content << paragraph
917
+ end
918
+ end
919
+
920
+ # Close any remaining expand section
921
+ if current_expand && expand_content.any?
922
+ current_expand["content"] = expand_content
923
+ content << current_expand
924
+ end
925
+
926
+ # Ensure we have at least one content element
927
+ if content.empty?
928
+ content << create_paragraph("Analysis completed.")
929
+ end
930
+
931
+ {
932
+ "type" => "doc",
933
+ "version" => 1,
934
+ "content" => content
935
+ }
936
+ end
937
+
938
+ def create_paragraph(text)
939
+ {
940
+ "type" => "paragraph",
941
+ "content" => [
942
+ {
943
+ "type" => "text",
944
+ "text" => text
945
+ }
946
+ ]
947
+ }
948
+ end
949
+
950
+ private
951
+
952
+ def debug_mode?
953
+ ENV['N2B_DEBUG'] == 'true'
954
+ end
955
+
956
+ def format_comment_as_adf(comment_data)
957
+ # If comment_data is a string (from template), convert to simple ADF
958
+ if comment_data.is_a?(String)
959
+ return convert_markdown_to_adf(comment_data)
960
+ end
961
+
421
962
  # If comment_data is structured (new format), build proper ADF
422
963
  content = []
423
964
 
@@ -736,8 +1277,26 @@ module N2B
736
1277
  request['Content-Type'] = 'application/json'
737
1278
  request['Accept'] = 'application/json'
738
1279
 
1280
+ if debug_mode?
1281
+ puts "🔍 DEBUG: Making #{method} request to: #{full_url}"
1282
+ puts "🔍 DEBUG: Request headers: Content-Type=#{request['Content-Type']}, Accept=#{request['Accept']}"
1283
+ if body
1284
+ puts "🔍 DEBUG: Request body size: #{body.to_json.length} bytes"
1285
+ puts "🔍 DEBUG: Request body preview: #{body.to_json[0..500]}#{'...' if body.to_json.length > 500}"
1286
+ end
1287
+ end
1288
+
739
1289
  response = http.request(request)
740
1290
 
1291
+ if debug_mode?
1292
+ puts "🔍 DEBUG: Response code: #{response.code} #{response.message}"
1293
+ if response.body && !response.body.empty?
1294
+ # Force UTF-8 encoding to handle character encoding issues
1295
+ response_body = response.body.force_encoding('UTF-8')
1296
+ puts "🔍 DEBUG: Response body: #{response_body}"
1297
+ end
1298
+ end
1299
+
741
1300
  unless response.is_a?(Net::HTTPSuccess)
742
1301
  error_message = "Jira API Error: #{response.code} #{response.message}"
743
1302
  error_message += " - #{response.body}" if response.body && !response.body.empty?
@@ -1,6 +1,6 @@
1
1
  require_relative '../model_config'
2
2
 
3
- module N2M
3
+ module N2B
4
4
  module Llm
5
5
  class Claude
6
6
  API_URI = URI.parse('https://api.anthropic.com/v1/messages')
@@ -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
  ]