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:
_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.
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
# ---------------------------------------------------------------------------
# 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)
# ---------------------------------------------------------------------------
@ -1624,7 +1724,66 @@ async def tools_node(state: AgentState) -> AgentState:
# ── Life Decision Advisor ─────────────────────────────────────────────────
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"))
portfolio_value = 94000.0
if perf_result.get("success"):
@ -1644,14 +1803,13 @@ async def tools_node(state: AgentState) -> AgentState:
dest_city = candidate.title()
break
# Determine decision type from query
q = user_query.lower()
if any(kw in q for kw in ["job offer", "salary", "raise", "accept"]):
if any(kw in q_lower for kw in ["job offer", "salary", "raise", "accept"]):
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"
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"
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"
else:
decision_type = "general"

Loading…
Cancel
Save