You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

528 lines
21 KiB

"""
Life Decision Advisor
Orchestrates multiple financial tools into a single recommendation
for any major life decision: job offer, relocation, home purchase,
rent vs buy, or general financial guidance.
"""
import sys
import os
import asyncio
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from wealth_bridge import (
calculate_job_offer_affordability,
calculate_down_payment_power,
)
WEALTH_BRIDGE_AVAILABLE = True
except ImportError:
WEALTH_BRIDGE_AVAILABLE = False
try:
from relocation_runway import calculate_relocation_runway
RUNWAY_AVAILABLE = True
except ImportError:
RUNWAY_AVAILABLE = False
try:
from wealth_visualizer import analyze_wealth_position
VISUALIZER_AVAILABLE = True
except ImportError:
VISUALIZER_AVAILABLE = False
try:
from real_estate import get_neighborhood_snapshot
RE_AVAILABLE = True
except ImportError:
RE_AVAILABLE = False
def _run_async(coro):
"""Run an async coroutine from sync context safely."""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
future = pool.submit(asyncio.run, coro)
return future.result(timeout=30)
else:
return loop.run_until_complete(coro)
except Exception:
try:
return asyncio.run(coro)
except Exception as e:
return {"error": str(e)}
def analyze_life_decision(decision_type: str, user_context: dict) -> dict:
"""
Orchestrate all financial tools into a single recommendation.
decision_type: "job_offer" | "relocation" | "home_purchase" |
"rent_or_buy" | "general"
user_context: dict with optional keys:
current_salary, offer_salary, current_city, destination_city,
portfolio_value, age, annual_income, has_family, num_dependents,
timeline_years, priority
"""
ctx = user_context or {}
tools_used = []
data_sources = []
results = {}
# ── Job Offer decision ────────────────────────────────────────────────────
if decision_type == "job_offer":
current_salary = ctx.get("current_salary")
offer_salary = ctx.get("offer_salary")
current_city = ctx.get("current_city", "")
destination_city = ctx.get("destination_city", "")
portfolio_value = ctx.get("portfolio_value", 0)
age = ctx.get("age")
annual_income = ctx.get("annual_income", offer_salary or current_salary or 0)
# COL comparison via wealth_bridge
if (WEALTH_BRIDGE_AVAILABLE and current_salary and offer_salary
and current_city and destination_city):
try:
col_result = _run_async(
calculate_job_offer_affordability(
current_salary=current_salary,
offer_salary=offer_salary,
current_city=current_city,
destination_city=destination_city,
)
)
if col_result and "error" not in col_result:
results["col"] = col_result
tools_used.append("wealth_bridge")
data_sources.append("Cost of living index")
except Exception as e:
results["col"] = {"error": str(e)}
# Relocation runway
if (RUNWAY_AVAILABLE and current_salary and offer_salary
and current_city and destination_city):
try:
runway_result = calculate_relocation_runway(
current_salary=current_salary,
offer_salary=offer_salary,
current_city=current_city,
destination_city=destination_city,
portfolio_value=portfolio_value or 0,
)
if runway_result and "error" not in runway_result:
results["runway"] = runway_result
if "relocation_runway" not in tools_used:
tools_used.append("relocation_runway")
data_sources.append("ACTRIS MLS + Teleport API")
except Exception as e:
results["runway"] = {"error": str(e)}
# Wealth position
if VISUALIZER_AVAILABLE and age and portfolio_value:
try:
wealth_result = analyze_wealth_position(
portfolio_value=portfolio_value,
age=age,
annual_income=annual_income,
)
if wealth_result:
results["wealth"] = wealth_result
tools_used.append("wealth_visualizer")
data_sources.append("Federal Reserve SCF 2022")
except Exception as e:
results["wealth"] = {"error": str(e)}
return _synthesize_job_offer(
ctx, results, tools_used, data_sources
)
# ── Home Purchase decision ────────────────────────────────────────────────
elif decision_type == "home_purchase":
portfolio_value = ctx.get("portfolio_value", 0)
current_city = ctx.get("current_city", "Austin")
age = ctx.get("age")
annual_income = ctx.get("annual_income", 0)
if WEALTH_BRIDGE_AVAILABLE and portfolio_value:
try:
dp_result = calculate_down_payment_power(
portfolio_value=portfolio_value
)
if dp_result:
results["down_payment"] = dp_result
tools_used.append("wealth_bridge")
data_sources.append("ACTRIS MLS Jan 2026")
except Exception as e:
results["down_payment"] = {"error": str(e)}
if VISUALIZER_AVAILABLE and age and annual_income:
try:
wealth_result = analyze_wealth_position(
portfolio_value=portfolio_value,
age=age,
annual_income=annual_income,
)
results["wealth"] = wealth_result
tools_used.append("wealth_visualizer")
except Exception as e:
results["wealth"] = {"error": str(e)}
return _synthesize_home_purchase(ctx, results, tools_used, data_sources)
# ── Rent or Buy decision ──────────────────────────────────────────────────
elif decision_type == "rent_or_buy":
portfolio_value = ctx.get("portfolio_value", 0)
current_city = ctx.get("current_city", "Austin")
annual_income = ctx.get("annual_income", 0)
if WEALTH_BRIDGE_AVAILABLE and portfolio_value:
try:
dp_result = calculate_down_payment_power(
portfolio_value=portfolio_value
)
results["down_payment"] = dp_result
tools_used.append("wealth_bridge")
data_sources.append("ACTRIS MLS Jan 2026")
except Exception as e:
results["down_payment"] = {"error": str(e)}
return _synthesize_rent_or_buy(ctx, results, tools_used, data_sources)
# ── Relocation decision ───────────────────────────────────────────────────
elif decision_type == "relocation":
current_salary = ctx.get("current_salary")
offer_salary = ctx.get("offer_salary", current_salary)
current_city = ctx.get("current_city", "")
destination_city = ctx.get("destination_city", "")
portfolio_value = ctx.get("portfolio_value", 0)
if (RUNWAY_AVAILABLE and current_salary and current_city
and destination_city):
try:
runway_result = calculate_relocation_runway(
current_salary=current_salary,
offer_salary=offer_salary,
current_city=current_city,
destination_city=destination_city,
portfolio_value=portfolio_value,
)
results["runway"] = runway_result
tools_used.append("relocation_runway")
data_sources.append("ACTRIS MLS + Teleport API")
except Exception as e:
results["runway"] = {"error": str(e)}
return _synthesize_relocation(ctx, results, tools_used, data_sources)
# ── General / unknown ─────────────────────────────────────────────────────
else:
return {
"decision_type": "general",
"summary": (
"I can help you with any major financial life decision. "
"Tell me what you're considering and I'll run the numbers."
),
"message": (
"Please share more context. I can help with: "
"(1) Job offer evaluation — is it a real raise after cost of living? "
"(2) Relocation planning — how long until you're financially stable? "
"(3) Home purchase — can your portfolio cover a down payment? "
"(4) Rent vs buy — what makes sense right now? "
"Just describe your situation and I'll analyze it."
),
"recommendation": (
"Share your current salary, any offer details, and your city to get started."
),
"financial_verdict": "Need more context",
"confidence": "low",
"key_numbers": {},
"tradeoffs": [],
"next_steps": [
"Tell me your current salary and city",
"Describe the decision you're facing",
"Share your portfolio value if relevant",
],
"tools_used": [],
"data_sources": [],
}
# ── Synthesis helpers ──────────────────────────────────────────────────────────
def _synthesize_job_offer(ctx, results, tools_used, data_sources):
offer_salary = ctx.get("offer_salary", 0)
current_salary = ctx.get("current_salary", 0)
destination_city = ctx.get("destination_city", "destination city")
current_city = ctx.get("current_city", "current city")
# Extract key numbers
key_numbers = {}
tradeoffs = []
verdict = "Need more context"
confidence = "low"
runway = results.get("runway")
col = results.get("col")
wealth = results.get("wealth")
if runway and "error" not in runway:
dest_monthly = runway.get("destination_monthly", {})
curr_monthly = runway.get("current_monthly", {})
dest_surplus = dest_monthly.get("monthly_surplus", 0)
curr_surplus = curr_monthly.get("monthly_surplus", 0)
surplus_delta = dest_surplus - curr_surplus
key_numbers["destination_monthly_surplus"] = dest_surplus
key_numbers["current_monthly_surplus"] = curr_surplus
key_numbers["surplus_change"] = surplus_delta
milestones = runway.get("milestones_if_you_move", {})
months_down = milestones.get("months_to_down_payment_20pct", 9999)
if months_down < 9999:
key_numbers["months_to_down_payment"] = months_down
if surplus_delta > 500:
verdict = "Take it"
confidence = "high"
tradeoffs.append(f"PRO: ${surplus_delta:,.0f}/mo more surplus than now")
elif surplus_delta > 0:
verdict = "Negotiate"
confidence = "medium"
tradeoffs.append(f"NEUTRAL: Marginal improvement (${surplus_delta:,.0f}/mo more)")
elif dest_monthly.get("monthly_surplus_warning"):
verdict = "Pass"
confidence = "high"
tradeoffs.append("CON: Monthly costs exceed take-home pay at destination")
else:
verdict = "Negotiate"
confidence = "medium"
tradeoffs.append(f"CON: ${abs(surplus_delta):,.0f}/mo less surplus than now")
tradeoffs.append(
f"NEUTRAL: {destination_city} median home ${runway['milestones_if_you_move'].get('destination_median_home_price', 'N/A'):,}"
if isinstance(runway['milestones_if_you_move'].get('destination_median_home_price'), (int, float))
else f"NEUTRAL: Moving to {destination_city}"
)
if col and "error" not in col:
is_real = col.get("is_real_raise", col.get("verdict", {}).get("is_real_raise"))
if is_real is not None:
key_numbers["is_real_raise"] = is_real
if is_real:
tradeoffs.append("PRO: Salary increase beats cost of living difference")
else:
tradeoffs.append("CON: Higher cost of living erodes salary increase")
if wealth and "error" not in wealth:
peer_pos = wealth.get("current_position", {}).get("vs_peers", "")
if peer_pos:
tradeoffs.append(f"NEUTRAL: You are currently {peer_pos} vs peers")
salary_pct = ((offer_salary - current_salary) / current_salary * 100
if current_salary else 0)
key_numbers["salary_increase_pct"] = round(salary_pct, 1)
summary = (
f"You have a {salary_pct:+.1f}% salary offer (${offer_salary:,} vs "
f"${current_salary:,}) with a move from {current_city} to {destination_city}. "
)
if runway and "error" not in runway:
summary += runway.get("verdict", "")
recommendation = (
f"Verdict: {verdict}. "
+ (runway.get("key_insight", "") if runway and "error" not in runway else
f"Consider negotiating to at least ${int(current_salary * 1.15):,} to account for relocation costs.")
)
next_steps = [
f"Calculate your exact take-home after {destination_city} taxes",
"Negotiate relocation assistance and signing bonus",
f"Research {destination_city} neighborhoods within your budget",
]
return {
"decision_type": "job_offer",
"summary": summary,
"financial_verdict": verdict,
"confidence": confidence,
"key_numbers": key_numbers,
"tradeoffs": tradeoffs,
"recommendation": recommendation,
"next_steps": next_steps,
"tools_used": tools_used,
"data_sources": data_sources,
}
def _synthesize_home_purchase(ctx, results, tools_used, data_sources):
portfolio_value = ctx.get("portfolio_value", 0)
current_city = ctx.get("current_city", "Austin")
dp = results.get("down_payment", {})
tradeoffs = []
key_numbers = {}
if dp and "error" not in dp:
summary_data = dp.get("summary", dp)
affordable = summary_data.get("homes_you_can_afford", [])
if affordable:
key_numbers["homes_in_range"] = len(affordable)
tradeoffs.append(f"PRO: Portfolio can fund down payment on {len(affordable)} market segments")
else:
tradeoffs.append("CON: Portfolio may be thin for a down payment right now")
liquid_available = summary_data.get("liquid_available_for_down_payment",
portfolio_value * 0.2)
key_numbers["available_for_down_payment"] = round(liquid_available)
tradeoffs.append("NEUTRAL: Owning builds equity; renting preserves flexibility")
tradeoffs.append("CON: Transaction costs (3-6%) mean you need 3+ year horizon")
verdict = "Consider it" if portfolio_value > 50000 else "Build savings first"
confidence = "medium"
return {
"decision_type": "home_purchase",
"summary": (
f"With ${portfolio_value:,} in your portfolio, "
f"you have options for a down payment in {current_city}."
),
"financial_verdict": verdict,
"confidence": confidence,
"key_numbers": key_numbers,
"tradeoffs": tradeoffs,
"recommendation": (
"A home purchase makes sense if you plan to stay 3+ years. "
"Your portfolio gives you down payment flexibility — consider keeping "
"20% in the market to avoid PMI while maintaining an emergency fund."
),
"next_steps": [
"Get pre-approved to understand your buying power",
f"Research {current_city} neighborhoods in your price range",
"Ensure 6-month emergency fund remains after down payment",
],
"tools_used": tools_used,
"data_sources": data_sources,
}
def _synthesize_rent_or_buy(ctx, results, tools_used, data_sources):
portfolio_value = ctx.get("portfolio_value", 0)
current_city = ctx.get("current_city", "Austin")
annual_income = ctx.get("annual_income", 0)
dp = results.get("down_payment", {})
tradeoffs = []
key_numbers = {}
if dp and "error" not in dp:
summary_data = dp.get("summary", dp)
liquid = summary_data.get("liquid_available_for_down_payment",
portfolio_value * 0.2)
key_numbers["down_payment_available"] = round(liquid)
monthly_income = annual_income / 12 if annual_income else 0
if monthly_income > 0:
key_numbers["monthly_gross_income"] = round(monthly_income)
tradeoffs.extend([
"PRO (buy): Builds equity over time, fixed payment, tax benefits",
"PRO (rent): Flexibility, lower upfront cost, no maintenance",
"CON (buy): Illiquid, high transaction costs, market risk",
"CON (rent): No equity growth, rent increases possible",
])
verdict = (
"Buy if staying 3+ years"
if portfolio_value > 40000
else "Rent and save first"
)
return {
"decision_type": "rent_or_buy",
"summary": (
f"The rent vs buy decision in {current_city} depends on your timeline. "
"With current mortgage rates, buying makes sense only if you plan to stay 3+ years."
),
"financial_verdict": verdict,
"confidence": "medium",
"key_numbers": key_numbers,
"tradeoffs": tradeoffs,
"recommendation": (
"In Austin's current market (rates ~7%), the break-even for buying vs renting "
"is roughly 3-4 years. If you're staying longer, buying locks in your housing cost "
"and builds equity. If uncertain about your timeline, renting preserves flexibility."
),
"next_steps": [
"Calculate your 5-year break-even (buy vs rent)",
"Check if your portfolio can cover 20% down + 6mo emergency fund",
"Compare total cost of ownership vs equivalent rent",
],
"tools_used": tools_used,
"data_sources": data_sources,
}
def _synthesize_relocation(ctx, results, tools_used, data_sources):
destination_city = ctx.get("destination_city", "destination")
current_city = ctx.get("current_city", "current city")
runway = results.get("runway", {})
tradeoffs = []
key_numbers = {}
verdict = "Evaluate carefully"
confidence = "medium"
if runway and "error" not in runway:
dest = runway.get("destination_monthly", {})
curr = runway.get("current_monthly", {})
surplus_delta = dest.get("monthly_surplus", 0) - curr.get("monthly_surplus", 0)
key_numbers["monthly_surplus_change"] = round(surplus_delta)
key_numbers["destination_monthly_surplus"] = dest.get("monthly_surplus", 0)
milestones = runway.get("milestones_if_you_move", {})
key_numbers["months_to_stability"] = milestones.get(
"months_to_6mo_emergency_fund", "N/A"
)
if surplus_delta > 0:
verdict = "Good move financially"
confidence = "high"
tradeoffs.append(f"PRO: ${surplus_delta:,.0f}/mo better financially")
else:
verdict = "Financial step back"
confidence = "medium"
tradeoffs.append(f"CON: ${abs(surplus_delta):,.0f}/mo worse financially")
tradeoffs.append(
f"NEUTRAL: {runway.get('key_insight', '')}"
)
return {
"decision_type": "relocation",
"summary": (
f"Relocating from {current_city} to {destination_city}. "
+ (runway.get("verdict", "") if runway and "error" not in runway else "")
),
"financial_verdict": verdict,
"confidence": confidence,
"key_numbers": key_numbers,
"tradeoffs": tradeoffs,
"recommendation": (
runway.get("key_insight", f"Evaluate the full cost of living in {destination_city} "
"before committing to the relocation.")
if runway and "error" not in runway
else f"Research cost of living in {destination_city} before deciding."
),
"next_steps": [
f"Research specific neighborhoods in {destination_city}",
"Negotiate relocation assistance from employer",
"Build 3-month emergency fund before the move",
],
"tools_used": tools_used,
"data_sources": data_sources,
}