mirror of https://github.com/ghostfolio/ghostfolio
2 changed files with 600 additions and 0 deletions
@ -0,0 +1,72 @@ |
|||||
|
import sys |
||||
|
import os |
||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools')) |
||||
|
from life_decision_advisor import analyze_life_decision |
||||
|
|
||||
|
|
||||
|
def test_job_offer_returns_complete_structure(): |
||||
|
result = analyze_life_decision( |
||||
|
"job_offer", |
||||
|
{ |
||||
|
"current_salary": 120000, |
||||
|
"offer_salary": 180000, |
||||
|
"current_city": "Austin", |
||||
|
"destination_city": "Seattle", |
||||
|
"portfolio_value": 94000, |
||||
|
"age": 34, |
||||
|
} |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
assert "financial_verdict" in result |
||||
|
assert "recommendation" in result |
||||
|
assert "tradeoffs" in result |
||||
|
assert isinstance(result["tradeoffs"], list) |
||||
|
|
||||
|
|
||||
|
def test_home_purchase_decision(): |
||||
|
result = analyze_life_decision( |
||||
|
"home_purchase", |
||||
|
{ |
||||
|
"portfolio_value": 94000, |
||||
|
"current_city": "Austin", |
||||
|
"age": 34, |
||||
|
"annual_income": 120000, |
||||
|
} |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert "recommendation" in result |
||||
|
|
||||
|
|
||||
|
def test_rent_or_buy_decision(): |
||||
|
result = analyze_life_decision( |
||||
|
"rent_or_buy", |
||||
|
{ |
||||
|
"portfolio_value": 94000, |
||||
|
"current_city": "Austin", |
||||
|
"annual_income": 120000, |
||||
|
} |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert "recommendation" in result |
||||
|
|
||||
|
|
||||
|
def test_minimal_context_does_not_crash(): |
||||
|
result = analyze_life_decision("general", {}) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
has_content = ( |
||||
|
"summary" in result |
||||
|
or "recommendation" in result |
||||
|
or "message" in result |
||||
|
) |
||||
|
assert has_content |
||||
|
|
||||
|
|
||||
|
def test_missing_fields_handled_gracefully(): |
||||
|
result = analyze_life_decision( |
||||
|
"job_offer", |
||||
|
{"current_salary": 120000, "offer_salary": 180000} |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
@ -0,0 +1,528 @@ |
|||||
|
""" |
||||
|
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, |
||||
|
} |
||||
Loading…
Reference in new issue