ebeardsley's picture
Upload app.py
df8be0a verified
import gradio as gr
import anthropic
import json
# โ”€โ”€ constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
EXPENSE_CATS = [
"Housing", "Food & groceries", "Transport", "Utilities",
"Entertainment", "Health", "Education", "Clothing", "Other"
]
CAT_LIMITS = {
"Housing": 30, "Food & groceries": 15, "Transport": 15,
"Utilities": 10, "Entertainment": 10, "Health": 10,
"Education": 10, "Clothing": 5, "Other": 10
}
# โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def fmt(n: float) -> str:
return f"${round(n):,}"
def pct(part: float, whole: float, decimals: int = 1) -> str:
if whole == 0:
return "0%"
return f"{round(part / whole * 100, decimals)}%"
# โ”€โ”€ core calculation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def calculate_budget(
gross_income: float,
fed_tax_pct: float,
state_tax_pct: float,
fica_pct: float,
medicare_pct: float,
k401_pct: float,
housing: float,
food: float,
transport: float,
utilities: float,
entertainment: float,
health: float,
education: float,
clothing: float,
other: float,
api_key: str,
):
if gross_income <= 0:
return (
"โš ๏ธ Please enter a valid gross income.",
"", "", "", "", "", ""
)
# โ”€โ”€ deductions โ”€โ”€
fed_amt = gross_income * fed_tax_pct / 100
state_amt = gross_income * state_tax_pct / 100
fica_amt = gross_income * fica_pct / 100
medicare_amt = gross_income * medicare_pct / 100
k401_amt = gross_income * k401_pct / 100
total_tax = fed_amt + state_amt + fica_amt + medicare_amt
total_ded = total_tax + k401_amt
takehome = gross_income - total_ded
# โ”€โ”€ income flow summary โ”€โ”€
flow_md = f"""## Income flow
| | Amount | % of gross |
|---|---|---|
| **Gross income** | **{fmt(gross_income)}** | 100% |
| Federal tax | -{fmt(fed_amt)} | {pct(fed_amt, gross_income)} |
| State tax | -{fmt(state_amt)} | {pct(state_amt, gross_income)} |
| Social Security | -{fmt(fica_amt)} | {pct(fica_amt, gross_income)} |
| Medicare | -{fmt(medicare_amt)} | {pct(medicare_amt, gross_income)} |
| 401(k) | -{fmt(k401_amt)} | {pct(k401_amt, gross_income)} |
| **Take-home pay** | **{fmt(takehome)}** | {pct(takehome, gross_income)} |
"""
# โ”€โ”€ expense breakdown โ”€โ”€
expenses = {
"Housing": housing,
"Food & groceries": food,
"Transport": transport,
"Utilities": utilities,
"Entertainment": entertainment,
"Health": health,
"Education": education,
"Clothing": clothing,
"Other": other,
}
used = {k: v for k, v in expenses.items() if v > 0}
total_exp = sum(used.values())
remaining = takehome - total_exp
exp_rows = []
for cat, amt in used.items():
limit = CAT_LIMITS.get(cat, 10)
cat_pct = round(amt / takehome * 100, 1) if takehome > 0 else 0
status = "โœ…" if cat_pct <= limit * 0.8 else ("โš ๏ธ" if cat_pct <= limit else "๐Ÿ”ด")
exp_rows.append(
f"| {status} {cat} | {fmt(amt)} | {cat_pct}% | {limit}% |"
)
exp_md = f"""## Spending breakdown (vs take-home)
| Category | Amount | % of take-home | Limit |
|---|---|---|---|
{''.join(chr(10) + r for r in exp_rows)}
**Total expenses:** {fmt(total_exp)} &nbsp;|&nbsp; **Remaining:** {fmt(remaining)} ({pct(remaining, takehome)} of take-home)
"""
# โ”€โ”€ summary metrics โ”€โ”€
rem_emoji = "โœ…" if remaining >= 0 else "๐Ÿ”ด"
summary_md = f"""## Summary
| | |
|---|---|
| Gross income | {fmt(gross_income)} |
| Total taxes | {fmt(total_tax)} ({pct(total_tax, gross_income)}) |
| 401(k) saved | {fmt(k401_amt)} ({pct(k401_amt, gross_income)}) |
| Take-home | {fmt(takehome)} |
| Total expenses | {fmt(total_exp)} |
| {rem_emoji} Remaining | {fmt(remaining)} |
"""
# โ”€โ”€ AI advice via streaming โ”€โ”€
if not api_key.strip():
ai_advice = "โš ๏ธ Enter your Anthropic API key to get personalized AI advice."
else:
exp_lines = "\n".join(
f"- {cat}: {fmt(amt)} ({round(amt/takehome*100,1) if takehome else 0}% of take-home, limit {CAT_LIMITS.get(cat,10)}%)"
for cat, amt in used.items()
)
prompt = f"""You are a friendly, practical personal finance advisor. Analyze this monthly budget and give 5-6 specific, actionable suggestions. Be direct โ€” use numbered points. Reference the actual dollar amounts.
BUDGET:
Gross income: {fmt(gross_income)}/month
Taxes: {fmt(total_tax)} ({pct(total_tax, gross_income)} of gross)
401(k): {fmt(k401_amt)} ({pct(k401_amt, gross_income)} of gross)
Take-home: {fmt(takehome)}/month
EXPENSES:
{exp_lines}
Total expenses: {fmt(total_exp)}
Remaining: {fmt(remaining)} ({pct(remaining, gross_income)} of gross)
Give honest advice referencing the real numbers. 1-2 sentences per point."""
try:
client = anthropic.Anthropic(api_key=api_key.strip())
ai_advice = ""
with client.messages.stream(
model="claude-sonnet-4-5",
max_tokens=1000,
messages=[{"role": "user", "content": prompt}],
) as stream:
for text in stream.text_stream:
ai_advice += text
yield flow_md, exp_md, summary_md, ai_advice, \
fmt(gross_income), fmt(takehome), fmt(remaining)
return
except anthropic.AuthenticationError:
ai_advice = "โŒ Invalid API key. Check your Anthropic API key and try again."
except Exception as e:
ai_advice = f"โŒ Error calling AI: {str(e)}"
yield flow_md, exp_md, summary_md, ai_advice, \
fmt(gross_income), fmt(takehome), fmt(remaining)
# โ”€โ”€ Gradio UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Blocks(
title="AI Budget Advisor",
theme=gr.themes.Soft(primary_hue="violet"),
css="""
.section-header { font-weight: 600; font-size: 1rem; margin-bottom: 4px; }
.metric-box { text-align: center; padding: 8px; }
"""
) as demo:
gr.Markdown("# ๐Ÿ’ฐ AI Budget Advisor")
gr.Markdown(
"Enter your gross income, pre-tax deductions, and monthly expenses. "
"Add your [Anthropic API key](https://console.anthropic.com/) to get AI-powered personalized advice."
)
with gr.Row():
api_key_input = gr.Textbox(
label="๐Ÿ”‘ Anthropic API key",
placeholder="sk-ant-...",
type="password",
scale=2,
)
gr.Markdown("---")
# โ”€โ”€ INCOME โ”€โ”€
with gr.Row():
with gr.Column():
gr.Markdown("### ๐Ÿ“ฅ Gross income (before taxes)")
gross_income = gr.Number(label="Monthly gross income ($)", value=5000, minimum=0)
# โ”€โ”€ PRE-TAX DEDUCTIONS โ”€โ”€
gr.Markdown("### ๐Ÿ›๏ธ Pre-tax deductions")
with gr.Row():
fed_tax = gr.Slider(0, 50, value=22, step=0.5, label="Federal income tax (%)")
state_tax = gr.Slider(0, 15, value=5, step=0.5, label="State income tax (%)")
with gr.Row():
fica = gr.Slider(0, 10, value=6.2, step=0.1, label="Social Security / FICA (%)")
medicare = gr.Slider(0, 5, value=1.45, step=0.05, label="Medicare (%)")
with gr.Row():
k401 = gr.Slider(0, 30, value=6, step=0.5, label="401(k) contribution (%)")
gr.Markdown("---")
# โ”€โ”€ EXPENSES โ”€โ”€
gr.Markdown("### ๐Ÿ’ธ Monthly expenses")
with gr.Row():
housing = gr.Number(label="Housing ($)", value=1200, minimum=0)
food = gr.Number(label="Food & groceries ($)", value=400, minimum=0)
transport = gr.Number(label="Transport ($)", value=200, minimum=0)
with gr.Row():
utilities = gr.Number(label="Utilities ($)", value=150, minimum=0)
entertainment = gr.Number(label="Entertainment ($)", value=100, minimum=0)
health = gr.Number(label="Health ($)", value=0, minimum=0)
with gr.Row():
education = gr.Number(label="Education ($)", value=0, minimum=0)
clothing = gr.Number(label="Clothing ($)", value=0, minimum=0)
other = gr.Number(label="Other ($)", value=0, minimum=0)
gr.Markdown("---")
calc_btn = gr.Button("๐Ÿงฎ Calculate + get AI advice", variant="primary", size="lg")
# โ”€โ”€ OUTPUTS โ”€โ”€
gr.Markdown("---")
gr.Markdown("## Results")
with gr.Row():
out_gross = gr.Textbox(label="Gross income", interactive=False)
out_takehome = gr.Textbox(label="Take-home pay", interactive=False)
out_remain = gr.Textbox(label="Remaining", interactive=False)
with gr.Row():
with gr.Column():
out_flow = gr.Markdown(label="Income flow")
with gr.Column():
out_summary = gr.Markdown(label="Summary")
out_expenses = gr.Markdown(label="Spending breakdown")
gr.Markdown("### ๐Ÿค– AI budget advisor")
out_ai = gr.Markdown(label="AI advice", value="_AI advice will appear here after you calculate._")
# โ”€โ”€ wire up โ”€โ”€
calc_btn.click(
fn=calculate_budget,
inputs=[
gross_income,
fed_tax, state_tax, fica, medicare, k401,
housing, food, transport, utilities,
entertainment, health, education, clothing, other,
api_key_input,
],
outputs=[out_flow, out_expenses, out_summary, out_ai,
out_gross, out_takehome, out_remain],
)
gr.Markdown(
"---\n"
"_Powered by [Claude](https://anthropic.com). "
"This is a financial planning tool, not professional financial advice._"
)
if __name__ == "__main__":
demo.launch()