Files
blackroad-os-pack-finance/agents/budgeteer.py
Alexa Louise 0d1d083485
Some checks failed
Auto Deploy PR / detect-and-deploy (push) Has been cancelled
Deploy to Railway / build (push) Has been cancelled
Deploy to Railway / deploy (push) Has been cancelled
Stale Issue Cleanup / stale (push) Failing after 1m13s
Pi deployment mega-session: 136+ containers deployed
Massive deployment session deploying entire BlackRoad/Lucidia infrastructure to Raspberry Pi 4B:
- Cleaned /tmp space: 595MB → 5.2GB free
- Total containers: 136+ running simultaneously
- Ports: 3067-3200+
- Disk: 25G/29G (92% usage)
- Memory: 3.6Gi/7.9Gi

Deployment scripts created:
- /tmp/continue-deploy.sh (v2-* deployments)
- /tmp/absolute-final-deploy.sh (final-* deployments)
- /tmp/deployment-status.sh (monitoring)

Infrastructure maximized on single Pi 4B (8GB RAM, 32GB SD).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-22 23:10:31 -06:00

215 lines
6.9 KiB
Python

from __future__ import annotations
from decimal import Decimal
from datetime import datetime
from typing import Protocol
class BudgetService(Protocol):
"""Protocol for budget management service."""
def get_budget(self, budget_id: str) -> dict:
"""Retrieve budget by ID."""
...
def update_spent(self, budget_id: str, amount: Decimal) -> None:
"""Update spent amount for a budget."""
...
class Budgeteer:
"""Agent for budget management and tracking."""
def __init__(self, budget_service: BudgetService):
self.budget_service = budget_service
self.agent_id = "agent.budgeteer"
self.display_name = "Budgeteer"
self.pack_id = "pack.finance"
def check_budget(self, budget_id: str, proposed_amount: Decimal) -> dict:
"""
Check if a proposed expense fits within budget.
Args:
budget_id: ID of the budget to check
proposed_amount: Amount of proposed expense
Returns:
Dictionary with approval status and details
"""
budget = self.budget_service.get_budget(budget_id)
allocated = Decimal(budget['allocated'])
spent = Decimal(budget['spent'])
remaining = allocated - spent
approved = proposed_amount <= remaining
return {
'approved': approved,
'budget_id': budget_id,
'proposed_amount': str(proposed_amount),
'remaining': str(remaining),
'utilization': str((spent / allocated * 100) if allocated > 0 else 0),
'timestamp': datetime.now().isoformat(),
}
def allocate_budget(self, name: str, amount: Decimal, period: str) -> dict:
"""
Allocate a new budget.
Args:
name: Budget name
amount: Allocated amount
period: Budget period (monthly, quarterly, yearly)
Returns:
Dictionary with budget details
"""
return {
'name': name,
'allocated': str(amount),
'period': period,
'created_at': datetime.now().isoformat(),
}
def generate_report(self, budget_id: str) -> dict:
"""Generate budget utilization report."""
budget = self.budget_service.get_budget(budget_id)
allocated = Decimal(budget['allocated'])
spent = Decimal(budget['spent'])
return {
'budget_id': budget_id,
'name': budget['name'],
'allocated': str(allocated),
'spent': str(spent),
'remaining': str(allocated - spent),
'utilization_pct': str((spent / allocated * 100) if allocated > 0 else 0),
'period': budget['period'],
'report_generated': datetime.now().isoformat(),
}
# CLI interface
if __name__ == "__main__":
import sys
class MockBudgetService:
"""Mock budget service for testing."""
def get_budget(self, budget_id: str) -> dict:
return {
'id': budget_id,
'name': 'Q1 Operations',
'allocated': '100000.00',
'spent': '45000.00',
'period': 'quarterly',
}
def update_spent(self, budget_id: str, amount: Decimal) -> None:
pass
budgeteer = Budgeteer(MockBudgetService())
if len(sys.argv) > 1 and sys.argv[1] == 'check':
result = budgeteer.check_budget('budget-001', Decimal('5000.00'))
print(f"Budget check result: {result}")
elif len(sys.argv) > 1 and sys.argv[1] == 'report':
report = budgeteer.generate_report('budget-001')
print(f"Budget report: {report}")
else:
print("Usage: python budgeteer.py [check|report]")
from dataclasses import dataclass
from datetime import date, timedelta
from typing import Protocol, Dict, Any
class CostExplorerClient(Protocol):
def get_cost_and_usage(self, **kwargs: Any) -> Dict[str, Any]:
"""Protocol for AWS Cost Explorer client."""
class Reporter(Protocol):
def post(self, channel: str, message: str) -> None:
"""Protocol for posting messages to chat destinations."""
def _default_time_range(days_elapsed: int) -> Dict[str, str]:
today = date.today()
start = today - timedelta(days=days_elapsed)
return {"Start": start.isoformat(), "End": today.isoformat()}
@dataclass
class BudgetForecast:
current_spend: float
burn_rate: float
forecast_monthly: float
percent_of_budget: float
class Budgeteer:
"""Forecasts burn rate and prepares weekly spending reports."""
def __init__(self, budget_limit: float, slack_channel: str = "#finops") -> None:
self.budget_limit = budget_limit
self.slack_channel = slack_channel
def get_month_to_date_spend(
self,
client: CostExplorerClient,
days_elapsed: int,
time_range: Dict[str, str] | None = None,
) -> float:
"""Fetch month-to-date spend from a Cost Explorer compatible client."""
window = time_range or _default_time_range(days_elapsed)
response = client.get_cost_and_usage(
TimePeriod=window,
Granularity="MONTHLY",
Metrics=["UnblendedCost"],
)
total = response.get("ResultsByTime", [{}])[0].get("Total", {})
amount = total.get("UnblendedCost", {}).get("Amount", "0")
try:
return float(amount)
except (TypeError, ValueError):
return 0.0
def forecast(self, current_spend: float, days_elapsed: int, days_in_month: int) -> BudgetForecast:
if days_elapsed <= 0 or days_in_month <= 0:
raise ValueError("days_elapsed and days_in_month must be positive")
burn_rate = current_spend / days_elapsed
forecast_monthly = burn_rate * days_in_month
percent_of_budget = (forecast_monthly / self.budget_limit) * 100 if self.budget_limit else 0.0
return BudgetForecast(
current_spend=current_spend,
burn_rate=burn_rate,
forecast_monthly=forecast_monthly,
percent_of_budget=percent_of_budget,
)
def build_weekly_report(
self,
client: CostExplorerClient,
days_elapsed: int,
days_in_month: int,
reporter: Reporter | None = None,
) -> str:
current_spend = self.get_month_to_date_spend(client, days_elapsed)
forecast = self.forecast(current_spend, days_elapsed, days_in_month)
report = (
f"[finance-budgeteer] Week closeout — MTD spend: ${forecast.current_spend:,.2f}\n"
f"Daily burn: ${forecast.burn_rate:,.2f}\n"
f"Projected month-end: ${forecast.forecast_monthly:,.2f} ({forecast.percent_of_budget:,.1f}% of budget)"
)
if reporter:
reporter.post(self.slack_channel, report)
return report
__all__ = ["Budgeteer", "BudgetForecast", "CostExplorerClient", "Reporter"]