diff --git a/agent/graph.py b/agent/graph.py index b8570cd01..2b6519a7c 100644 --- a/agent/graph.py +++ b/agent/graph.py @@ -108,28 +108,89 @@ def llm_classify_intent(query: str) -> str: Returns a valid query_type string.""" client = anthropic.Anthropic() - prompt = f"""You are a routing classifier for a finance AI agent. -Given a user query, return ONLY one of these exact labels — nothing else: - -market - user wants stock price, market data, or info about a specific ticker -performance - user wants their portfolio summary, holdings, returns, or allocation -compliance - user asks about risk, diversification, concentration, or regulatory concerns -tax - user asks about taxes, capital gains, tax-loss harvesting, wash sales -activity - user asks about trades, buys, sells, transaction history -property_list - user wants to add, view, update, or remove a property they own -property_net_worth - user asks about total net worth including real estate -equity_unlock - user asks about home equity, cash-out refinance, HELOC options -relocation_runway - user asks about moving cities, cost of living comparison, runway -wealth_gap - user asks about retirement, savings rate, financial benchmarks, age -life_decision - user asks about job offers, major life decisions, affordability, buying multiple properties, rental strategy -family_planner - user asks about kids, childcare, family planning costs -market_overview - user asks about general market trends, not a specific stock -unknown - cannot determine intent + prompt = f"""You are a routing classifier for a personal finance AI agent. +The user has a portfolio of stocks and may own properties. + +Classify the user query into EXACTLY ONE label. +Respond with only the label — no explanation, no punctuation, just the label. + +LABELS AND WHEN TO USE THEM: + +market + User wants current stock price or market data for a specific company or ticker symbol. + Examples: + - "check apple for me" + - "what is AAPL at" + - "give me the nvidia number" + - "apple stock check" + - "what about my nvidia" (asking about price) + - "how is tesla doing" (price/market performance) + - "what is AAPL worth today" + KEY SIGNAL: company name or ticker + price/check/at/worth + +performance + User wants to see their own portfolio holdings, returns, allocation, or overall financial picture. + Examples: + - "show me my money" + - "how am I doing financially" + - "what does my investment look like" + - "pull up my stocks" (their holdings) + - "run the numbers on my portfolio" + KEY SIGNAL: "my" + portfolio/investments/stocks/money + +property_list + User wants info about real estate they own. + Examples: + - "tell me about my house" + - "what is my home worth" + - "my property value" + KEY SIGNAL: my house, my home, my property + +wealth_gap + User asks about retirement readiness, savings rate, or financial benchmarks. + Examples: + - "can I afford to retire" + - "am I on track financially" + - "when can I retire" + KEY SIGNAL: retire, retirement, savings rate + +life_decision + User asks about major life decisions like job offers, moving cities, buying a home. + Examples: + - "should I take this job offer" + - "can I afford to move to Seattle" + KEY SIGNAL: should I, can I afford, job offer + +compliance + User asks about portfolio risk or diversification. + Examples: + - "am I too concentrated" + - "how diversified am I" + KEY SIGNAL: diversified, concentrated, risk + +tax + User asks about taxes or capital gains. + Examples: + - "what are my capital gains" + - "do I owe taxes" + KEY SIGNAL: tax, capital gains, wash sale + +activity + User asks about trade history. + Examples: + - "show my recent trades" + - "what did I buy this year" + KEY SIGNAL: trades, bought, sold, transactions + +IMPORTANT DISTINCTION: +"what about my nvidia" = market (price question) +"my nvidia position" = performance (holding question) +"check apple" = market (price check) +"my apple shares" = performance (holding question) User query: {query} -Respond with exactly one label from the list above. -No explanation. No punctuation. Just the label.""" +Respond with exactly one label.""" valid_types = { "market", "performance", "compliance", "tax", "activity", @@ -672,6 +733,9 @@ async def classify_node(state: AgentState) -> AgentState: "wealth percentile", "net worth percentile", "federal reserve", "median wealth", "peer comparison", "how does my net worth compare", "retirement projection", + "can i afford to retire", "afford to retire", "retirement plan", + "on track to retire", "retirement savings", "retire early", + "when can i retire", ] if any(kw in query for kw in wealth_gap_kws): return {**state, "query_type": "wealth_gap"} @@ -754,6 +818,17 @@ async def classify_node(state: AgentState) -> AgentState: if any(kw in query for kw in wealth_net_worth_kws): return {**state, "query_type": "wealth_portfolio_summary"} + # --- Property queries (run regardless of feature flag for correct routing) --- + property_general_kws = [ + "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", + ] + if any(kw in query for kw in property_general_kws): + if any(v in query for v in ["value", "worth"]): + return {**state, "query_type": "property_net_worth"} + return {**state, "query_type": "property_list"} + # --- Property Tracker (feature-flagged) — checked BEFORE general real estate # so "add my property" doesn't fall through to real_estate_snapshot --- if is_property_tracking_enabled(): @@ -767,6 +842,8 @@ async def classify_node(state: AgentState) -> AgentState: "my properties", "list my properties", "show my properties", "my real estate holdings", "properties i own", "my property portfolio", "what properties", "show my homes", + "my house", "my home", "my property", "my real estate", + "about my house", "about my home", "about my property", ] property_net_worth_kws = [ "net worth including", "net worth with real estate", @@ -775,6 +852,7 @@ async def classify_node(state: AgentState) -> AgentState: "everything i own", "show my total net worth", "complete financial picture", "net worth including my home", "net worth including my investment", + "my home value", "my house value", "my property value", ] property_update_kws = [ "update my home", "update my property", "update my house", @@ -927,6 +1005,13 @@ async def classify_node(state: AgentState) -> AgentState: "what is googl", "what is amzn", "what is meta", "what is vti", "trading at", "price today", "how much is", "ticker", "quote", "what's the stock price", "whats the stock price", + "check apple", "check aapl", "check msft", "check nvda", "check tsla", + "check googl", "check amzn", "check meta", + "apple stock", "msft stock", "nvda stock", "tsla stock", "aapl stock", + "apple price", "nvidia price", "microsoft price", "tesla price", "amazon price", + "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", ] if any(kw in query for kw in stock_price_kws) and _extract_ticker(query): return {**state, "query_type": "market"} @@ -935,7 +1020,7 @@ async def classify_node(state: AgentState) -> AgentState: # These are common phrasings that don't match the terse keyword lists above. natural_performance_kws = [ "how am i doing", "how have i done", "how is my money", - "how are my investments", "how are my stocks", + "show me my money", "how are my investments", "how are my stocks", "am i making money", "am i losing money", "what is my portfolio worth", "what's my portfolio worth", "show me my portfolio", "give me a summary",