Class: GitFit::Export::AI

Inherits:
Object
  • Object
show all
Defined in:
lib/git_fit/export/ai.rb

Overview

Builds insights.json — the AI narrative layer on top of stats (#40, git-fit#93). Contract (wiki/AI): generated_at / period / source_digest / model / language / summary_markdown / insights / coaching

Cost guardrails:

  • input is summary-level (~2-5k tokens, ≤200 activity lines)
  • source_digest unchanged vs existing file -> zero LLM calls
  • per-scenario retry 1 -> warn + omit that section (never raises to caller)

Defined Under Namespace

Classes: ValidationError

Constant Summary collapse

SCENARIOS =
%i[monthly_report insights coaching].freeze
ACTIVITY_LIMIT =
200
INSIGHT_TYPES =
%w[trend anomaly pb consistency season].freeze
INSIGHT_SEVERITIES =
%w[info positive warning].freeze
COACHING_PRIORITIES =
%w[low medium high].freeze

Instance Method Summary collapse

Constructor Details

#initialize(db:, config:, client:, output:, period: 'monthly', language: nil) ⇒ AI

Returns a new instance of AI.



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/git_fit/export/ai.rb', line 27

def initialize(db:, config:, client:, output:, period: 'monthly', language: nil)
  @db = db
  @config = config
  @client = client
  @output = output
  unless %w[monthly yearly].include?(period.to_s)
    raise ArgumentError, "AI: unknown period '#{period}'\n  Fix: use --period monthly|yearly"
  end
  @period = period.to_s
  @language = language || config.ai_config['language'] || 'zh'
end

Instance Method Details

#callObject

Returns the written doc hash, or nil when skipped (empty db / digest match).



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/git_fit/export/ai.rb', line 40

def call
  stats = Calculator.new(db: @db).call
  rows = activity_rows
  return warn_skip('no activities in database') if rows.empty?

  digest = source_digest(stats, rows)
  return warn_skip('insights unchanged (source_digest match)') if unchanged?(digest)

  sections = {}
  SCENARIOS.each do |scenario|
    sections[scenario] = generate_scenario(scenario, stats, rows)
  end

  doc = base_doc(digest)
  doc['summary_markdown'] = sections[:monthly_report]['summary_markdown'] if sections[:monthly_report]
  doc['insights'] = sections[:insights]['insights'] if sections[:insights]
  doc['coaching'] = sections[:coaching]['coaching'] if sections[:coaching]

  File.write(@output, "#{::JSON.pretty_generate(doc)}\n")
  doc
end