Class: Aireview::ContextBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/aireview/context_builder.rb

Defined Under Namespace

Classes: Context

Constant Summary collapse

GENERATE_PROMPT_TEMPLATE =
File.read(File.expand_path('prompts/generate.txt', __dir__)).strip.freeze
CRITIQUE_PROMPT_TEMPLATE =
File.read(File.expand_path('prompts/critique.txt', __dir__)).strip.freeze
LANGUAGE_NAMES =
{
  'ru' => 'Russian',
  'en' => 'English'
}.freeze
CHANGES_HEADER =
"Changes:\n"
CANDIDATES_HEADER =
"\n\nCandidates JSON from Generate:\n"
CANDIDATES_RESERVE_CHARS =

Room for the candidates in the Critique prompt: three candidates of ~1,500 characters. An estimate, not a guarantee; the actual size is checked before sending.

4_500

Instance Method Summary collapse

Constructor Details

#initialize(config:, logger: Logger.new($stderr)) ⇒ ContextBuilder

Returns a new instance of ContextBuilder.



29
30
31
32
33
34
35
36
37
38
# File 'lib/aireview/context_builder.rb', line 29

def initialize(config:, logger: Logger.new($stderr))
  @config = config
  @logger = logger
  @secret_scrubber = SecretScrubber.new(
    secret_patterns: config.secret_patterns,
    secret_files: config.secret_files,
    logger: logger
  )
  @diff_fetcher = DiffFetcher.new(ignore_paths: [], logger: logger)
end

Instance Method Details

#build_critique_prompt(context, candidates_json:) ⇒ Object



61
62
63
64
# File 'lib/aireview/context_builder.rb', line 61

def build_critique_prompt(context, candidates_json:)
  user = "#{context.user_prompt}#{CANDIDATES_HEADER}#{scrub_text(candidates_json)}"
  check_stage_size!('critique', system_prompt('critique'), user)
end

#build_generate_prompt(context) ⇒ Object



57
58
59
# File 'lib/aireview/context_builder.rb', line 57

def build_generate_prompt(context)
  check_stage_size!('generate', system_prompt('generate'), context.user_prompt)
end

#check_stage_size!(stage, system, user) ⇒ Object

A check before sending: when the candidates exceed the reserve and the request does not fit, that is an error, not a reason to silently cut the context Generate has already seen.



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/aireview/context_builder.rb', line 80

def check_stage_size!(stage, system, user)
  stage = stage.to_s
  limit = @config.max_prompt_chars(stage)
  total = system.length + user.length
  if total > limit
    raise ContextBudgetError,
          "#{stage.capitalize} request is #{total} chars, over llm.#{stage}.max_prompt_chars=#{limit} " \
          "(system prompt #{system.length}, context #{user.length})"
  end

  {system_prompt: system, user_prompt: user}
end

#prepare(merge_request:, changes:, jira_issue: nil, critique: true) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/aireview/context_builder.rb', line 40

def prepare(merge_request:, changes:, jira_issue: nil, critique: true)
  coverage = ContextBudget::Coverage.empty
  budget = context_budget(critique: critique)
  sections = merge_request_sections(merge_request, coverage: coverage)
  sections << jira_section(jira_issue, coverage: coverage) if jira_issue
  fixed = "#{sections.join("\n\n")}\n\n#{CHANGES_HEADER}"

  diff_budget = [budget - fixed.length, @config.max_diff_chars].min
  entries = @diff_fetcher.entries(@secret_scrubber.scrub_changes(changes))
  packed = ContextBudget.pack_entries(entries, budget: diff_budget, coverage: coverage)

  sizes = context_sizes(fixed: fixed, packed: packed, budget: budget, diff_budget: diff_budget, critique: critique)
  log_sizes(sizes)
  Context.new(user_prompt: fixed + packed.text, sections: sections.join("\n\n"), diff_text: packed.text,
              coverage: coverage, sizes: sizes)
end

#scrub_text(text) ⇒ Object



93
94
95
# File 'lib/aireview/context_builder.rb', line 93

def scrub_text(text)
  @secret_scrubber.scrub_text(text.to_s)
end

#system_prompt(stage) ⇒ Object



66
67
68
69
70
71
72
73
74
75
# File 'lib/aireview/context_builder.rb', line 66

def system_prompt(stage)
  template = stage.to_s == 'critique' ? CRITIQUE_PROMPT_TEMPLATE : GENERATE_PROMPT_TEMPLATE
  extras = []
  if Aireview::Utils.present?(@config.review_instructions)
    extras << "Additional project instructions:\n#{scrub_text(@config.review_instructions.strip)}"
  end
  extras << "Response language: #{language_name(@config.review_language)}."

  [template, *extras].join("\n\n")
end