Module: DailyFocus

Defined in:
app/services/daily_focus.rb,
app/services/daily_focus/prompt.rb,
app/services/daily_focus/run_policy.rb,
app/models/daily_focus/coaching_note.rb,
app/services/daily_focus/chat_runner.rb,
app/services/daily_focus/data_gatherer.rb,
app/services/daily_focus/review_counts.rb,
app/services/daily_focus/stats_section.rb,
app/services/daily_focus/timezone_hints.rb,
app/services/daily_focus/grounding_check.rb,
app/services/daily_focus/coverage_section.rb,
app/services/daily_focus/support_sections.rb,
app/services/daily_focus/follow_up_section.rb,
app/services/daily_focus/work_day_indicator.rb,
app/services/daily_focus/review_data_builder.rb,
app/services/daily_focus/conversation_sharing.rb,
app/services/daily_focus/user_context_builder.rb,
app/services/daily_focus/review_work_day_preloader.rb

Overview

Namespace for Daily Focus automation; see DailyFocus in app/services/daily_focus.rb.

Defined Under Namespace

Modules: ConversationSharing, CoverageSection, FollowUpSection, GroundingCheck, Prompt, StatsSection, SupportSections, TimezoneHints Classes: ChatRunner, CoachingNote, DataGatherer, EmptyBriefingError, FabricatedBriefingError, ReviewCounts, ReviewDataBuilder, ReviewWorkDayPreloader, RunPolicy, UserContextBuilder, WorkDayIndicator

Constant Summary collapse

TOOL_SERVICE_KEYS =

Service keys passed to Assistant::ChatToolBuilder for tool-based generation.

%w[sales_management app_db support_cases content].freeze
CHAT_MODEL_KEY =

Model key for Assistant::ChatService (batch + unified path).

GLM 5.2 via OpenRouter, promoted 2026-08-12 after the model A/B on real
briefings: 0 grounding findings on both audiences, the coverage table
reproduced byte-exact (20/20 rows), and ~$0.08/briefing vs
gemini-flash's ~$0.35 (convs 4741/4745; see
doc/development/OPENROUTER.md). gemini-flash stays registered — a
one-line pin back if the scoreboard (daily_focus_grounding_findings
metadata) regresses, exactly as it caught the 3.5→3.6 swap.

'glm-5.2'
ORPHAN_AFTER =

A briefing left in processing longer than this with no live worker is
treated as orphaned (its worker died — deploy/OOM/hard-kill — before
advancing the status to ready/failed). Generous headroom over the
slowest healthy generation (~7 min) so a merely-slow run is never reaped.

15.minutes

Class Method Summary collapse

Class Method Details

.audience_for(employee) ⇒ String

Which briefing variant an employee gets. Sales wins for dual-role
employees so they keep their original briefing.

Parameters:

Returns:

  • (String)

    'sales' or 'support'



27
28
29
30
31
# File 'app/services/daily_focus.rb', line 27

def self.audience_for(employee)
  return 'sales' if employee.sales_rep?

  employee.tech_rep? ? 'support' : 'sales'
end

.briefing_live?(conversation) ⇒ Boolean

True when a worker is actively generating this briefing. The daily-focus
path runs synchronously through Assistant::ChatService, which — unlike
AssistantChatWorker — does NOT heartbeat the column lock, so a long run's
lock can expire (LOCK_STALE_AFTER = 5 min) while it's still alive. The
PlanOrchestrator rewrites the execution_plan metadata as each step lands,
so a healthy run keeps bumping updated_at; we treat recent activity as
"live" to avoid reclaiming/reaping a slow-but-running briefing.

Parameters:

Returns:

  • (Boolean)


86
87
88
89
# File 'app/services/daily_focus.rb', line 86

def self.briefing_live?(conversation)
  conversation.processing? ||
    (conversation.updated_at.present? && conversation.updated_at > ORPHAN_AFTER.ago)
end

.rep_scope(audience) ⇒ ActiveRecord::Relation<Employee>

Rep population for an audience. Single source for RunPolicy, the review
page, and ReviewCounts so all three always agree on the denominator.

Parameters:

  • audience (String, Symbol)

Returns:



38
39
40
41
42
43
44
45
46
47
48
# File 'app/services/daily_focus.rb', line 38

def self.rep_scope(audience)
  if audience.to_s == 'support'
    # distinct: the role join yields one row per matching role
    # (technical_support_rep + technical_support_manager). Dual-role
    # employees stay in the sales population (see .audience_for).
    Employee.technical_support_reps.active_employees.distinct
            .where.not(id: Employee.sales_reps.select(:id))
  else
    Employee.sales_reps.active_employees
  end
end

.review_stream_for(audience) ⇒ String

Turbo stream name for the audience's review page, so support-row updates
never land on the sales managers' review screen.

Parameters:

  • audience (String, Symbol, nil)

Returns:

  • (String)


55
56
57
# File 'app/services/daily_focus.rb', line 55

def self.review_stream_for(audience)
  audience.to_s == 'support' ? 'daily_focus:review:support' : 'daily_focus:review'
end