Files
moreminimore-chat/app/jobs/analytics/daily_metrics_job.rb
Moreminimore b86b0c59f6 feat(analytics): M2 self-improving chatbot analytics
Daily LLM classification of every chat (topics + product tags + deal
won/lost/undecided) aggregated into immutable daily metrics, with a
filterable admin report dashboard, product-catalog import (text/CSV/XLSX),
weekly persona summary, and an approval flow (LINE -> Telegram -> webhook)
for applying persona recommendations.

- Llm::AnalyticsClassifier: per-account openai -> Captain fallback cascade
- AccountDailyProcessor + Conversation/CustomerDailyMetric aggregation
- ReportService + DrilldownService (summary + deep filterable drilldown)
- AnalyticsReports.vue + productCatalog import UI (admin-only)
- WeeklyPersonaEvaluator + PersonaApprovalService (LINE/Telegram/webhook)
- Weekly cron (Mon 10:00) + daily cron (02:30)
2026-08-25 15:56:11 +07:00

36 lines
1.3 KiB
Ruby

# Daily analytics aggregation driver (part2b-iii).
#
# Runs once daily (see config/schedule.yml) and pushes each account's closed-day
# conversation metrics through Analytics::AccountDailyProcessor: classify every
# active conversation via the LLM, write the classification tags onto the live
# conversation (re-tagging prior M2-managed tags), and aggregate into the
# immutable daily metrics tables.
#
# One account failing (e.g. an LLM/credential error) never blocks the others.
class Analytics::DailyMetricsJob < ApplicationJob
queue_as :scheduled_jobs
def perform
Account.find_each do |account|
process_account(account)
rescue StandardError => e
Rails.logger.error("[Analytics::DailyMetricsJob] account=#{account.id} #{e.class}: #{e.message}")
end
end
private
def process_account(account)
date = closed_day_in_account_tz(account)
Analytics::AccountDailyProcessor.perform(account: account, date: date)
end
# The most recent fully-completed day in the account's reporting timezone.
def closed_day_in_account_tz(account)
tz = account.reporting_timezone.presence
zone = tz ? ActiveSupport::TimeZone[tz] : Time.zone
current_in_zone = zone ? Time.current.in_time_zone(zone) : Time.current
(current_in_zone - 1.day).to_date
end
end