Class: Teri::AIIntegration

Inherits:
Object
  • Object
show all
Defined in:
lib/teri/ai_integration.rb

Overview

Handles AI integration with OpenAI

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options, logger, log_file) ⇒ AIIntegration

Returns a new instance of AIIntegration.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/teri/ai_integration.rb', line 6

def initialize(options, logger, log_file)
  @options = options
  @logger = logger
  @log_file = log_file
  @previous_codings = {}
  @counterparty_hints = {}

  # Initialize OpenAI client if API key is provided
  if @options[:use_ai_suggestions] && (@options[:openai_api_key] || ENV.fetch('OPENAI_API_KEY', nil))
    begin
      @openai_client = OpenAIClient.new(api_key: @options[:openai_api_key], log_file: @log_file)
      @logger&.info('OpenAI client initialized')
    rescue StandardError => e
      @logger&.error("Failed to initialize OpenAI client: #{e.message}")
      puts "Warning: Failed to initialize OpenAI client: #{e.message}"
      @openai_client = nil
    end
  end
end

Instance Attribute Details

#counterparty_hintsObject (readonly)

Returns the value of attribute counterparty_hints.



4
5
6
# File 'lib/teri/ai_integration.rb', line 4

def counterparty_hints
  @counterparty_hints
end

#openai_clientObject (readonly)

Returns the value of attribute openai_client.



4
5
6
# File 'lib/teri/ai_integration.rb', line 4

def openai_client
  @openai_client
end

#previous_codingsObject (readonly)

Returns the value of attribute previous_codings.



4
5
6
# File 'lib/teri/ai_integration.rb', line 4

def previous_codings
  @previous_codings
end

Instance Method Details

#load_previous_codings(file_adapter) ⇒ Object

Load previous codings from coding.ledger for AI suggestions



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/teri/ai_integration.rb', line 65

def load_previous_codings(file_adapter)
  @previous_codings = {}
  @counterparty_hints = {}

  # Check if coding.ledger exists
  return unless file_adapter.exist?('coding.ledger')

  # Parse coding.ledger to extract transaction descriptions and categories
  begin
    ledger = Ledger.parse('coding.ledger', file_adapter)

    # Process each transaction
    ledger.transactions.each do |transaction|
      next unless transaction[:description] && transaction[:entries] && !transaction[:entries].empty?

      # Find entries that are not Assets or Liabilities (likely the categorization)
      categorization_entries = transaction[:entries].reject do |entry|
        entry[:account].start_with?('Assets:', 'Liabilities:')
      end

      # Use the first categorization entry as the category
      next if categorization_entries.empty?

      counterparty = transaction[:counterparty]

      # Get hints if available
      hints = transaction[:metadata]&.select { |m| m[:key] == 'Hint' }&.map { |m| m[:value] } || []

      # Update previous codings with this transaction
      update_previous_codings(
        transaction[:description],
        categorization_entries.first[:account],
        counterparty,
        hints
      )
    end

    @logger&.info("Loaded #{@previous_codings.size - (@previous_codings[:by_counterparty] ? 1 : 0)} previous codings with hints for #{@counterparty_hints.size} counterparties")
  rescue StandardError => e
    @logger&.error("Failed to load previous codings: #{e.message}")
    file_adapter.warning("Failed to load previous codings: #{e.message}") if file_adapter.respond_to?(:warning)
  end
end

#suggest_category(transaction, available_categories) ⇒ Object

Delegate suggest_category to OpenAI client



27
28
29
30
31
32
# File 'lib/teri/ai_integration.rb', line 27

def suggest_category(transaction, available_categories)
  return nil unless @openai_client

  # Make self respond to previous_codings and counterparty_hints
  @openai_client.suggest_category(transaction, self)
end

#update_previous_codings(description, category, counterparty, hints) ⇒ Object

Update previous codings with a new transaction



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/teri/ai_integration.rb', line 35

def update_previous_codings(description, category, counterparty, hints)
  # Store by description for backward compatibility with tests
  @previous_codings[description] = {
    category: category,
    counterparty: counterparty,
    hints: hints,
  }

  # Also store by counterparty for the new functionality
  return unless counterparty

  @previous_codings[:by_counterparty] ||= {}
  @previous_codings[:by_counterparty][counterparty] ||= {
    transactions: [],
    hints: @counterparty_hints[counterparty] || [],
  }

  @previous_codings[:by_counterparty][counterparty][:transactions] << {
    description: description,
    category: category,
  }

  # Store hints by counterparty
  if counterparty && !hints.empty?
    @counterparty_hints[counterparty] ||= []
    @counterparty_hints[counterparty].concat(hints)
  end
end