Browse Source

fix: update classifier prompt and routing map — all 9 tool categories now correctly routed

- Expand classify_node keyword lists for relocation_runway, wealth_gap,
  equity_unlock, and family_planner to cover more natural phrasings
- Add realestate_strategy_kws block that routes multi-property strategy
  queries ("buy a house every N years", "rental portfolio strategy") to
  the life_decision executor (home_purchase decision type)
- Expand real_estate_kws with MLS/market-data terms and add "show me"
  to _location_intent_kws so city + intent triggers correctly
- Update _route_after_classify with explicit routing map comment showing
  all 9 non-write categories → "tools" (only existing node names used)
- Add tool categories block to SYSTEM_PROMPT so LLM knows to use
  non-portfolio tool results instead of defaulting to portfolio framing

Made-with: Cursor
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
1879169f34
  1. 84
      agent/graph.py

84
agent/graph.py

@ -122,7 +122,25 @@ CRITICAL RULES — never violate these under any circumstances:
9. Low confidence responses (confidence < 0.6) must note that some data may be incomplete. 9. Low confidence responses (confidence < 0.6) must note that some data may be incomplete.
10. Cite the tool_result_id once per sentence place it at the end of the sentence, not 10. Cite the tool_result_id once per sentence place it at the end of the sentence, not
after each individual number. Format: [tool_result_id]""" after each individual number. Format: [tool_result_id]
IMPORTANT: You have access to tools beyond portfolio analysis.
When the classifier routes to a non-portfolio tool,
use that tool's result to answer the user.
Do not default back to portfolio analysis.
Available tool categories:
- Real estate market data (Austin MLS + global cities): use when tool_name is "real_estate" or "neighborhood_snapshot"
- Property tracking (add/update/remove owned properties): use when tool_name is "property_tracker"
- Wealth bridge (down payment power, job offer analysis): use when tool_name is "wealth_bridge" or "teleport_api"
- Relocation runway (financial stability timeline): use when tool_name is "relocation_runway"
- Wealth visualizer (retirement projection, peer comparison): use when tool_name is "wealth_visualizer"
- Life decision advisor (job offers, relocation decisions, home purchase strategy): use when tool_name is "life_decision_advisor"
- Equity unlock advisor (home equity options, refinance): use when tool_name is "equity_advisor"
- Family financial planner (childcare costs, family budget): use when tool_name is "family_planner"
Use the appropriate tool based on what the user asks.
Only use portfolio analysis for questions about investment holdings and portfolio performance."""
LARGE_ORDER_THRESHOLD = 100_000 LARGE_ORDER_THRESHOLD = 100_000
@ -447,6 +465,8 @@ async def classify_node(state: AgentState) -> AgentState:
"how long until", "runway", "financially stable", "how long until", "runway", "financially stable",
"if i move", "relocation timeline", "stable if", "if i move", "relocation timeline", "stable if",
"how long to feel stable", "feel stable after", "how long to feel stable", "feel stable after",
"how long to feel okay after moving", "months until i rebuild",
"financially stable if i move",
] ]
if any(kw in query for kw in relocation_runway_kws): if any(kw in query for kw in relocation_runway_kws):
return {**state, "query_type": "relocation_runway"} return {**state, "query_type": "relocation_runway"}
@ -457,6 +477,10 @@ async def classify_node(state: AgentState) -> AgentState:
"how am i doing financially", "ahead or behind", "how am i doing financially", "ahead or behind",
"net worth compared", "am i ahead", "net worth compared", "am i ahead",
"am i behind for my age", "retirement on track", "am i behind for my age", "retirement on track",
"am i on track for retirement", "am i ahead for my age",
"wealth percentile", "net worth percentile",
"federal reserve", "median wealth", "peer comparison",
"how does my net worth compare", "retirement projection",
] ]
if any(kw in query for kw in wealth_gap_kws): if any(kw in query for kw in wealth_gap_kws):
return {**state, "query_type": "wealth_gap"} return {**state, "query_type": "wealth_gap"}
@ -466,6 +490,7 @@ async def classify_node(state: AgentState) -> AgentState:
"should i take", "help me decide", "what should i do", "should i take", "help me decide", "what should i do",
"is it worth it", "advise me", "what do you think", "is it worth it", "advise me", "what do you think",
"should i move", "should i accept", "should i move", "should i accept",
"should i take this job", "should i accept the offer",
] ]
if any(kw in query for kw in life_decision_kws): if any(kw in query for kw in life_decision_kws):
return {**state, "query_type": "life_decision"} return {**state, "query_type": "life_decision"}
@ -474,6 +499,7 @@ async def classify_node(state: AgentState) -> AgentState:
equity_unlock_kws = [ equity_unlock_kws = [
"home equity", "refinance", "cash out", "home equity", "refinance", "cash out",
"equity options", "what should i do with my equity", "equity options", "what should i do with my equity",
"what to do with my equity", "rental property from equity",
] ]
if any(kw in query for kw in equity_unlock_kws): if any(kw in query for kw in equity_unlock_kws):
return {**state, "query_type": "equity_unlock"} return {**state, "query_type": "equity_unlock"}
@ -483,11 +509,30 @@ async def classify_node(state: AgentState) -> AgentState:
"afford a family", "afford a baby", "afford kids", "afford a family", "afford a baby", "afford kids",
"childcare costs", "financial impact of children", "childcare costs", "financial impact of children",
"can i afford to have", "family planning", "can i afford to have", "family planning",
"having kids", "having kids", "having a baby", "having children",
"can i afford kids", "afford to have children",
"financial impact of kids", "cost of having kids",
"cost of a baby", "childcare budget",
] ]
if any(kw in query for kw in family_planner_kws): if any(kw in query for kw in family_planner_kws):
return {**state, "query_type": "family_planner"} return {**state, "query_type": "family_planner"}
# --- Real Estate Strategy Simulator ---
# Checked BEFORE real_estate_kws so multi-property strategy queries
# get routed to the life_decision advisor (home_purchase type) rather
# than a plain snapshot.
realestate_strategy_kws = [
"buy a house every", "buy every", "keep buying houses",
"property every 2 years", "property every 3 years",
"property every 5 years", "property every 10 years",
"property every n years", "buy and rent the previous",
"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 any(kw in query for kw in realestate_strategy_kws):
return {**state, "query_type": "life_decision"}
# --- Wealth Bridge — down payment, job offer COL, global city data --- # --- Wealth Bridge — down payment, job offer COL, global city data ---
# Checked before real estate so "can I afford" doesn't fall through to snapshot # Checked before real estate so "can I afford" doesn't fall through to snapshot
if is_real_estate_enabled(): if is_real_estate_enabled():
@ -568,11 +613,14 @@ async def classify_node(state: AgentState) -> AgentState:
"investment property", "cap rate", "days on market", "price per sqft", "investment property", "cap rate", "days on market", "price per sqft",
"neighborhood", "housing", "mortgage", "home search", "neighborhood", "housing", "mortgage", "home search",
"compare neighborhoods", "compare cities", "compare neighborhoods", "compare cities",
# New triggers from spec # Bedrooms / search filters
"homes", "houses", "bedroom", "bedrooms", "bathroom", "bathrooms", "homes", "houses", "bedroom", "bedrooms", "bathroom", "bathrooms",
"3 bed", "2 bed", "4 bed", "1 bed", "3br", "2br", "4br", "3 bed", "2 bed", "4 bed", "1 bed", "3br", "2br", "4br",
"under $", "rent estimate", "for sale", "open house", "under $", "rent estimate", "for sale", "open house",
"property search", "find homes", "home value", "property search", "find homes", "home value",
# Market data keywords
"mls", "median price", "home purchase", "inventory",
"property value", "rental market",
] ]
# Location-based routing: known city/county + a real estate intent signal # Location-based routing: known city/county + a real estate intent signal
# (avoids misrouting portfolio queries that happen to mention a city name) # (avoids misrouting portfolio queries that happen to mention a city name)
@ -580,6 +628,7 @@ async def classify_node(state: AgentState) -> AgentState:
"compare", "vs ", "versus", "market", "county", "neighborhood", "compare", "vs ", "versus", "market", "county", "neighborhood",
"tell me about", "how is", "what about", "what's the", "whats the", "tell me about", "how is", "what about", "what's the", "whats the",
"area", "prices in", "homes in", "housing in", "rent in", "area", "prices in", "homes in", "housing in", "rent in",
"show me", "housing costs", "cost to buy",
] ]
has_known_location = any(city in query for city in _KNOWN_CITIES) has_known_location = any(city in query for city in _KNOWN_CITIES)
has_location_re_intent = has_known_location and any(kw in query for kw in _location_intent_kws) has_location_re_intent = has_known_location and any(kw in query for kw in _location_intent_kws)
@ -956,6 +1005,8 @@ _KNOWN_CITIES = [
"austin", "san francisco", "new york", "new york city", "nyc", "austin", "san francisco", "new york", "new york city", "nyc",
"denver", "seattle", "miami", "chicago", "phoenix", "nashville", "dallas", "denver", "seattle", "miami", "chicago", "phoenix", "nashville", "dallas",
"brooklyn", "manhattan", "sf", "atx", "dfw", "brooklyn", "manhattan", "sf", "atx", "dfw",
# International cities — real estate tool supports these
"tokyo", "berlin", "london", "sydney", "toronto", "paris",
# ACTRIS / Greater Austin locations # ACTRIS / Greater Austin locations
"travis county", "travis", "travis county", "travis",
"williamson county", "williamson", "round rock", "cedar park", "georgetown", "leander", "williamson county", "williamson", "round rock", "cedar park", "georgetown", "leander",
@ -2019,19 +2070,40 @@ def _append_messages(state: AgentState, user_query: str, answer: str) -> list:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _route_after_classify(state: AgentState) -> str: def _route_after_classify(state: AgentState) -> str:
"""Decides which node to go to after classify.""" """Decides which node to go to after classify.
All read-path query_types (portfolio, real estate, family, wealth, etc.)
route to the single "tools" node which dispatches by query_type internally.
Only write intents and control flow have dedicated branches.
Routing map (all non-write categories "tools"):
real_estate_snapshot / real_estate_search /
real_estate_compare / real_estate_detail tools
property_add / property_remove /
property_update / property_list /
property_net_worth tools
wealth_down_payment / wealth_job_offer /
wealth_global_city / wealth_portfolio_summary tools
relocation_runway tools
wealth_gap tools
life_decision tools
equity_unlock tools
family_planner tools
performance / activity / compliance /
tax / market / market_overview /
categorize / context_followup tools
"""
qt = state.get("query_type", "performance") qt = state.get("query_type", "performance")
write_intents = {"buy", "sell", "dividend", "cash", "transaction"} write_intents = {"buy", "sell", "dividend", "cash", "transaction"}
if qt == "write_refused": if qt == "write_refused":
return "format" # Refuse message already baked into final_response via format_node return "format"
if qt in write_intents: if qt in write_intents:
return "write_prepare" return "write_prepare"
if qt == "write_confirmed": if qt == "write_confirmed":
return "write_execute" return "write_execute"
if qt == "write_cancelled": if qt == "write_cancelled":
return "format" return "format"
# Real estate types route through the normal tools → verify → format path
return "tools" return "tools"

Loading…
Cancel
Save