Browse Source

feat: extract user assumptions from strategy queries — appreciation, interval, years, price

Add _extract_strategy_params() helper to graph.py that parses
appreciation rate, buy interval, total years, home price, rent yield,
and annual income from natural language messages.

Update life_decision branch to detect strategy queries and route them
to simulate_real_estate_strategy() with extracted params instead of
calling the general life_decision_advisor. Supports conservative /
moderate / optimistic presets. Falls back to life_decision_advisor
for non-strategy queries.

Made-with: Cursor
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
0726b4e5f5
  1. 170
      agent/graph.py

170
agent/graph.py

@ -70,6 +70,12 @@ try:
except ImportError: except ImportError:
_FAMILY_PLANNER_AVAILABLE = False _FAMILY_PLANNER_AVAILABLE = False
try:
from tools.realestate_strategy import simulate_real_estate_strategy
_RE_STRATEGY_AVAILABLE = True
except ImportError:
_RE_STRATEGY_AVAILABLE = False
SYSTEM_PROMPT = """You are a portfolio analysis assistant integrated with Ghostfolio wealth management software. SYSTEM_PROMPT = """You are a portfolio analysis assistant integrated with Ghostfolio wealth management software.
REASONING PROTOCOL silently reason through these four steps BEFORE writing your response. REASONING PROTOCOL silently reason through these four steps BEFORE writing your response.
@ -1267,6 +1273,100 @@ def _extract_current_city(query: str) -> str | None:
return None return None
# ---------------------------------------------------------------------------
# Strategy param extraction
# ---------------------------------------------------------------------------
def _extract_strategy_params(message: str) -> dict:
"""Extract user-provided assumptions from a real estate strategy message."""
params = {}
# Extract appreciation rate
# matches: "3% appreciation", "appreciation of 4%", "3 percent appreciation"
appr_match = re.search(
r'(\d+(?:\.\d+)?)\s*%\s*appreciation|'
r'appreciation\s+(?:of\s+)?(\d+(?:\.\d+)?)\s*%|'
r'(\d+(?:\.\d+)?)\s*percent\s+appreciation',
message, re.IGNORECASE
)
if appr_match:
val = appr_match.group(1) or appr_match.group(2) or appr_match.group(3)
params["annual_appreciation"] = float(val) / 100
# Extract buy interval
# matches: "every 2 years", "every two years"
interval_match = re.search(
r'every\s+(\d+|one|two|three|four|five)\s+years?',
message, re.IGNORECASE
)
if interval_match:
word_to_num = {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5}
val = interval_match.group(1)
params["buy_interval_years"] = word_to_num.get(val.lower(), int(val))
# Extract total years
# matches: "for 10 years", "over 15 years"
years_match = re.search(
r'(?:for|over)\s+(\d+)\s+years',
message, re.IGNORECASE
)
if years_match:
params["total_years"] = int(years_match.group(1))
# Extract home price
# matches: "$400k", "$400,000", "400000"
price_match = re.search(
r'\$(\d+(?:,\d+)*(?:\.\d+)?)\s*k\b|'
r'\$(\d+(?:,\d+)*(?:\.\d+)?)\b',
message, re.IGNORECASE
)
if price_match:
val = price_match.group(1) or price_match.group(2)
val = val.replace(",", "")
price = float(val)
if price_match.group(1): # was in thousands (e.g. $400k)
price *= 1000
if 50000 < price < 5000000:
params["first_home_price"] = price
# Extract rent yield
rent_match = re.search(
r'(\d+(?:\.\d+)?)\s*%\s*(?:rent\s*yield|rental\s*yield)',
message, re.IGNORECASE
)
if rent_match:
params["annual_rent_yield"] = float(rent_match.group(1)) / 100
# Extract annual income
income_match = re.search(
r'(?:make|earn|income|salary)\s+\$?(\d+(?:,\d+)*)\s*k?\b',
message, re.IGNORECASE
)
if income_match:
val = income_match.group(1).replace(",", "")
income = float(val)
if income < 10000:
income *= 1000
if 20000 < income < 2000000:
params["annual_income"] = income
# Conservative / moderate / optimistic presets
if "conservative" in message.lower():
params.setdefault("annual_appreciation", 0.02)
params.setdefault("annual_rent_yield", 0.06)
params.setdefault("annual_market_return", 0.05)
elif "optimistic" in message.lower():
params.setdefault("annual_appreciation", 0.06)
params.setdefault("annual_rent_yield", 0.10)
params.setdefault("annual_market_return", 0.09)
elif "moderate" in message.lower():
params.setdefault("annual_appreciation", 0.04)
params.setdefault("annual_rent_yield", 0.08)
params.setdefault("annual_market_return", 0.07)
return params
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tools node (read-path) # Tools node (read-path)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -1624,7 +1724,66 @@ async def tools_node(state: AgentState) -> AgentState:
# ── Life Decision Advisor ───────────────────────────────────────────────── # ── Life Decision Advisor ─────────────────────────────────────────────────
elif query_type == "life_decision": elif query_type == "life_decision":
if _LIFE_ADVISOR_AVAILABLE: # Check if this is a real estate strategy simulation query
q_lower = user_query.lower()
is_strategy_query = any(kw in q_lower for kw in [
"buy a house every", "buy every", "keep buying houses",
"property every", "buy and rent", "rental portfolio strategy",
"what if i keep buying", "real estate strategy",
"buy one every", "buy a property every",
"keep buying properties", "buy a home every",
])
if is_strategy_query and _RE_STRATEGY_AVAILABLE:
# Extract user-provided assumptions from the message
strategy_params = _extract_strategy_params(user_query)
# Get portfolio value from Ghostfolio (fallback to 94k)
perf_result = await portfolio_analysis(token=state.get("bearer_token"))
portfolio_value = 94000.0
if perf_result.get("success"):
portfolio_value = (
perf_result.get("result", {}).get("summary", {})
.get("total_current_value_usd", 94000.0)
)
# Allow message to override portfolio value
port_match = re.search(
r'(?:have|invested|portfolio)\s+\$?(\d+(?:,\d+)*)\s*k?\b',
user_query, re.IGNORECASE
)
if port_match:
val = port_match.group(1).replace(",", "")
v = float(val)
if v < 10000:
v *= 1000
if 1000 < v < 50000000:
portfolio_value = v
annual_income = strategy_params.pop("annual_income", 120000.0)
first_home_price = strategy_params.pop("first_home_price", 400000.0)
try:
result = simulate_real_estate_strategy(
initial_portfolio_value=portfolio_value,
annual_income=annual_income,
first_home_price=first_home_price,
**strategy_params,
)
tool_results.append({
"tool_name": "realestate_strategy",
"success": True,
"tool_result_id": "realestate_strategy_result",
"result": result,
})
except Exception as e:
tool_results.append({
"tool_name": "realestate_strategy",
"success": False,
"error": {"code": "STRATEGY_ERROR", "message": str(e)},
})
elif _LIFE_ADVISOR_AVAILABLE:
perf_result = await portfolio_analysis(token=state.get("bearer_token")) perf_result = await portfolio_analysis(token=state.get("bearer_token"))
portfolio_value = 94000.0 portfolio_value = 94000.0
if perf_result.get("success"): if perf_result.get("success"):
@ -1644,14 +1803,13 @@ async def tools_node(state: AgentState) -> AgentState:
dest_city = candidate.title() dest_city = candidate.title()
break break
# Determine decision type from query # Determine decision type from query
q = user_query.lower() if any(kw in q_lower for kw in ["job offer", "salary", "raise", "accept"]):
if any(kw in q for kw in ["job offer", "salary", "raise", "accept"]):
decision_type = "job_offer" decision_type = "job_offer"
elif any(kw in q for kw in ["move", "reloc", "relocat"]): elif any(kw in q_lower for kw in ["move", "reloc", "relocat"]):
decision_type = "relocation" decision_type = "relocation"
elif any(kw in q for kw in ["buy", "purchase", "home", "house"]): elif any(kw in q_lower for kw in ["buy", "purchase", "home", "house"]):
decision_type = "home_purchase" decision_type = "home_purchase"
elif any(kw in q for kw in ["rent or buy", "rent vs buy"]): elif any(kw in q_lower for kw in ["rent or buy", "rent vs buy"]):
decision_type = "rent_or_buy" decision_type = "rent_or_buy"
else: else:
decision_type = "general" decision_type = "general"

Loading…
Cancel
Save