|
|
|
@ -471,6 +471,20 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
if not query: |
|
|
|
return {**state, "query_type": "unknown", "error": "empty_query"} |
|
|
|
|
|
|
|
# --- Handle capability/help queries first --- |
|
|
|
help_kws = [ |
|
|
|
"what can you do", "what can this do", "what do you do", |
|
|
|
"tell me what you can do", "tell me what i can do", |
|
|
|
"what are your capabilities", "what features", |
|
|
|
"how do you work", "what is this", "what does this do", |
|
|
|
] |
|
|
|
if any(kw in query for kw in help_kws): |
|
|
|
return { |
|
|
|
**state, |
|
|
|
"query_type": "capabilities", |
|
|
|
"_classification_source": "keyword", |
|
|
|
} |
|
|
|
|
|
|
|
# --- Write confirmation replies --- |
|
|
|
pending_write = state.get("pending_write") |
|
|
|
if pending_write: |
|
|
|
@ -643,6 +657,11 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
if any(phrase in query for phrase in full_position_kws) and _extract_ticker(query): |
|
|
|
return {**state, "query_type": "performance+compliance+activity"} |
|
|
|
|
|
|
|
# --- Full portfolio summary (performance only) — before full_report --- |
|
|
|
full_summary_kws = ["full portfolio summary", "give me a full portfolio"] |
|
|
|
if any(phrase in query for phrase in full_summary_kws): |
|
|
|
return {**state, "query_type": "performance"} |
|
|
|
|
|
|
|
# --- Full portfolio report / health check — run all three tools --- |
|
|
|
full_report_kws = [ |
|
|
|
"health check", "complete portfolio", "full portfolio", "portfolio report", |
|
|
|
@ -719,6 +738,10 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
"how long to feel stable", "feel stable after", |
|
|
|
"how long to feel okay after moving", "months until i rebuild", |
|
|
|
"financially stable if i move", |
|
|
|
"actually a raise", "is a raise", "raise from", "raise vs", |
|
|
|
"better salary", "salary comparison", "cost of living raise", |
|
|
|
"col adjusted", "adjusted for cost of living", |
|
|
|
"raise if i move", "effective raise", |
|
|
|
] |
|
|
|
if any(kw in query for kw in relocation_runway_kws): |
|
|
|
return {**state, "query_type": "relocation_runway"} |
|
|
|
@ -788,6 +811,11 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
if any(kw in query for kw in realestate_strategy_kws): |
|
|
|
return {**state, "query_type": "life_decision"} |
|
|
|
|
|
|
|
# --- Afford a house (run regardless of feature flag for correct routing) --- |
|
|
|
afford_house_kws = ["can i afford a house", "afford a house", "afford a home"] |
|
|
|
if any(kw in query for kw in afford_house_kws): |
|
|
|
return {**state, "query_type": "wealth_down_payment"} |
|
|
|
|
|
|
|
# --- Wealth Bridge — down payment, job offer COL, global city data --- |
|
|
|
# Checked before real estate so "can I afford" doesn't fall through to snapshot |
|
|
|
if is_real_estate_enabled(): |
|
|
|
@ -823,6 +851,8 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
"my house", "my home", "my property", "my real estate", |
|
|
|
"about my house", "about my home", "about my property", |
|
|
|
"my home value", "my house value", "my property value", |
|
|
|
"rental yield", "rental income", "yield on", "rental return", |
|
|
|
"rent vs market", "property yield", "rental rate", |
|
|
|
] |
|
|
|
if any(kw in query for kw in property_general_kws): |
|
|
|
if any(v in query for v in ["value", "worth"]): |
|
|
|
@ -987,6 +1017,16 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
"my allocation to", |
|
|
|
"what do i hold", |
|
|
|
"what am i holding", |
|
|
|
"how many shares of", |
|
|
|
"shares do i have", |
|
|
|
"shares of appl do i", |
|
|
|
"shares of aapl do i", |
|
|
|
"how many appl", |
|
|
|
"how many aapl", |
|
|
|
"how many msft", |
|
|
|
"how many nvda", |
|
|
|
"how many tsla", |
|
|
|
"shared of", # typo for "shares of" |
|
|
|
] |
|
|
|
if any(kw in query for kw in portfolio_ticker_kws): |
|
|
|
return {**state, "query_type": "performance"} |
|
|
|
@ -1012,6 +1052,9 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
"what about aapl", "what about msft", "what about nvda", "what about tsla", |
|
|
|
"what about apple", "what about nvidia", "what about tesla", "what about microsoft", |
|
|
|
"aapl number", "msft number", "nvda number", "stock check", |
|
|
|
"prize of", "prise of", "how much is 1 share", "how much is one share", |
|
|
|
"1 share of", "one share of", "cost of 1 share", "cost of one share", |
|
|
|
"price of appl", "price of 1", |
|
|
|
] |
|
|
|
if any(kw in query for kw in stock_price_kws) and _extract_ticker(query): |
|
|
|
return {**state, "query_type": "market"} |
|
|
|
@ -1034,6 +1077,8 @@ async def classify_node(state: AgentState) -> AgentState: |
|
|
|
"show me my holdings", "show my holdings", "list my holdings", |
|
|
|
"biggest holdings", "biggest positions", "largest holdings", |
|
|
|
"top holdings", "top positions", |
|
|
|
"give me a full portfolio", "full portfolio summary", "full portfolio", |
|
|
|
"can i afford a house", "afford a house", "afford a home", |
|
|
|
] |
|
|
|
natural_activity_kws = [ |
|
|
|
"what have i bought", "what have i sold", |
|
|
|
@ -2507,6 +2552,22 @@ async def format_node(state: AgentState) -> AgentState: |
|
|
|
error = state.get("error") |
|
|
|
query_type = state.get("query_type", "") |
|
|
|
|
|
|
|
# Short-circuit: capabilities/help query |
|
|
|
if query_type == "capabilities": |
|
|
|
response = ( |
|
|
|
"I can help you with:\n\n" |
|
|
|
"**Portfolio** — holdings, returns, allocation, performance vs benchmarks\n\n" |
|
|
|
"**Market Data** — live stock prices for any ticker (AAPL, MSFT, NVDA, TSLA, GOOGL, AMZN, META)\n\n" |
|
|
|
"**Tax** — capital gains estimate, tax-loss harvesting opportunities, wash sale warnings\n\n" |
|
|
|
"**Risk & Compliance** — concentration check, diversification analysis\n\n" |
|
|
|
"**Transactions** — trade history, recent buys/sells\n\n" |
|
|
|
"**Real Estate** — add and track properties, equity analysis, net worth including real estate\n\n" |
|
|
|
"**Life Decisions** — job offer comparison, relocation analysis, retirement readiness, family planning costs, real estate strategy\n\n" |
|
|
|
"Just ask naturally — I understand variations like 'check apple', 'how much is AAPL', 'tell me about my portfolio', etc." |
|
|
|
) |
|
|
|
updated_messages = _append_messages(state, user_query, response) |
|
|
|
return {**state, "final_response": response, "messages": updated_messages} |
|
|
|
|
|
|
|
# Short-circuit: agent refused a destructive operation |
|
|
|
if query_type == "write_refused": |
|
|
|
response = ( |
|
|
|
|