Class: Assistant::MonthlyBudget
- Inherits:
-
Object
- Object
- Assistant::MonthlyBudget
- Defined in:
- app/services/assistant/monthly_budget.rb
Overview
Per-user monthly spend ceiling for Sunny (the CRM assistant).
Sunny cost is attributed to the human who owns the conversation
(+AssistantConversation#user_id+) — NOT +ai_usage_logs.account_id+, which
is a context/customer field, not the actor. Spend is the sum of
+cost_microdollars+ over the user's +AssistantConversation+-subject usage
in the current calendar month.
The cap is the governor for complexity-aware model escalation: while a user
has budget left they can be auto-upgraded up the model ladder
(flash -> pro -> opus -> opus-1m); once they are over, escalation is
disabled and they soft-degrade to the cheap tier rather than being cut off.
Caps (confirmed 2026-06-03): $50/user/month, $200 for managers. Managers
are +is_manager?+ accounts, which already include admins — the same
power-user split as +Assistant::ChatToolBuilder::POWER_USER_ROLES+.
Pure read: no migration, no persisted counter. Spend is recomputed per turn
(once per user message, not per tool call) against the indexed
+(subject_type, subject_id)+ lookup and memoized for the instance's life.
Constant Summary collapse
- DEFAULT_CAP_USD =
Default monthly cap in USD for a standard user.
50- MANAGER_CAP_USD =
Monthly cap in USD for managers (and admins, via +is_manager?+).
200- WARN_FRACTION =
Fraction of the cap at which a soft warning should be surfaced.
0.80
Class Method Summary collapse
-
.for_user_context(user_context) ⇒ Assistant::MonthlyBudget
Build from the serialized user_context the controller hands the worker.
Instance Method Summary collapse
-
#allows_escalation? ⇒ Boolean
Whether the user still has budget to be auto-upgraded to a more expensive model.
-
#cap_microdollars ⇒ Integer
The monthly cap in microdollars (for exact integer gating).
-
#cap_usd ⇒ Integer
The monthly cap in whole USD for this user: a per-user override (set by an admin on the employee page) wins, otherwise the role default.
-
#fraction_used ⇒ Float
Fraction of the cap consumed.
-
#initialize(party_id:, manager: false, now: Time.current) ⇒ MonthlyBudget
constructor
A new instance of MonthlyBudget.
-
#over? ⇒ Boolean
True once spend has reached or passed the cap.
-
#override_usd ⇒ Integer?
Per-user budget override from the parties column, or nil when unset (→ role default).
-
#remaining_usd ⇒ Float
Remaining budget in USD (never negative).
-
#spent_microdollars ⇒ Integer
Month-to-date Sunny spend in microdollars.
-
#spent_usd ⇒ Float
Month-to-date Sunny spend in USD.
-
#status ⇒ Symbol
:ok, :warn, or :over.
-
#warn? ⇒ Boolean
True once spend has reached the warning threshold.
Constructor Details
#initialize(party_id:, manager: false, now: Time.current) ⇒ MonthlyBudget
Returns a new instance of MonthlyBudget.
53 54 55 56 57 |
# File 'app/services/assistant/monthly_budget.rb', line 53 def initialize(party_id:, manager: false, now: Time.current) @party_id = party_id @manager = manager @now = now end |
Class Method Details
.for_user_context(user_context) ⇒ Assistant::MonthlyBudget
Build from the serialized user_context the controller hands the worker.
42 43 44 45 46 47 48 |
# File 'app/services/assistant/monthly_budget.rb', line 42 def self.for_user_context(user_context) ctx = user_context || {} new( party_id: ctx['party_id'] || ctx[:party_id], manager: ctx['is_manager'] || ctx[:is_manager] || false ) end |
Instance Method Details
#allows_escalation? ⇒ Boolean
Whether the user still has budget to be auto-upgraded to a more
expensive model. Escalation stops at the cap; it never hard-blocks an
in-flight conversation — the caller degrades to the cheap tier instead.
131 132 133 |
# File 'app/services/assistant/monthly_budget.rb', line 131 def allows_escalation? !over? end |
#cap_microdollars ⇒ Integer
Returns the monthly cap in microdollars (for exact integer gating).
68 69 70 |
# File 'app/services/assistant/monthly_budget.rb', line 68 def cap_microdollars cap_usd * AiUsageLog::MICRODOLLARS_PER_DOLLAR end |
#cap_usd ⇒ Integer
The monthly cap in whole USD for this user: a per-user override (set by an
admin on the employee page) wins, otherwise the role default.
63 64 65 |
# File 'app/services/assistant/monthly_budget.rb', line 63 def cap_usd override_usd || (@manager ? MANAGER_CAP_USD : DEFAULT_CAP_USD) end |
#fraction_used ⇒ Float
Fraction of the cap consumed. Float — presentation only (the % label and
progress bar); the gating predicates below use exact integer microdollars
so cap/warn boundaries don't drift on Float division.
102 103 104 105 106 |
# File 'app/services/assistant/monthly_budget.rb', line 102 def fraction_used return 1.0 if cap_microdollars.zero? spent_microdollars.to_f / cap_microdollars end |
#over? ⇒ Boolean
Returns true once spend has reached or passed the cap.
109 110 111 |
# File 'app/services/assistant/monthly_budget.rb', line 109 def over? spent_microdollars >= cap_microdollars end |
#override_usd ⇒ Integer?
Per-user budget override from the parties column, or nil when unset (→ role
default). Admins raise an individual's budget from /employees/:id. Memoized.
76 77 78 79 80 |
# File 'app/services/assistant/monthly_budget.rb', line 76 def override_usd return @override_usd if defined?(@override_usd) @override_usd = @party_id && Party.where(id: @party_id).pick(:monthly_ai_budget_usd) end |
#remaining_usd ⇒ Float
Returns remaining budget in USD (never negative).
93 94 95 |
# File 'app/services/assistant/monthly_budget.rb', line 93 def remaining_usd [cap_microdollars - spent_microdollars, 0].max.to_f / AiUsageLog::MICRODOLLARS_PER_DOLLAR end |
#spent_microdollars ⇒ Integer
Returns month-to-date Sunny spend in microdollars.
83 84 85 |
# File 'app/services/assistant/monthly_budget.rb', line 83 def spent_microdollars @spent_microdollars ||= compute_spent_microdollars end |
#spent_usd ⇒ Float
Returns month-to-date Sunny spend in USD.
88 89 90 |
# File 'app/services/assistant/monthly_budget.rb', line 88 def spent_usd spent_microdollars.to_f / AiUsageLog::MICRODOLLARS_PER_DOLLAR end |
#status ⇒ Symbol
Returns :ok, :warn, or :over.
119 120 121 122 123 124 |
# File 'app/services/assistant/monthly_budget.rb', line 119 def status return :over if over? return :warn if warn? :ok end |
#warn? ⇒ Boolean
Returns true once spend has reached the warning threshold.
114 115 116 |
# File 'app/services/assistant/monthly_budget.rb', line 114 def warn? spent_microdollars >= (cap_microdollars * WARN_FRACTION).to_i end |