BrianIsaac commited on
Commit
6d9f030
·
1 Parent(s): bfaf40f

feat: add roast mode and expand example portfolio gallery

Browse files

Add roast mode toggle that provides brutally honest portfolio analysis
with humour whilst maintaining professional recommendations. Expand
example portfolios from 3 to 5 diverse scenarios showcasing tech growth,
conservative income, balanced allocation, global diversification, and
single stock concentration risk.

Changes:
- Add ROAST_MODE_SYSTEM_PROMPT with entertaining yet educational critique style
- Modify PortfolioAnalystAgent to support roast_mode parameter
- Update workflow to dynamically create agents with correct mode
- Add roast mode checkbox toggle in Gradio UI
- Expand example gallery to 5 portfolios (Tech Growth, Conservative Income,
Balanced 60/40, Global Diversified, Single Stock Risk)
- Wire roast_mode through full analysis pipeline

app.py CHANGED
@@ -124,11 +124,12 @@ def parse_portfolio_input(portfolio_text: str) -> List[Dict[str, Any]]:
124
  return holdings
125
 
126
 
127
- async def run_analysis(portfolio_text: str) -> str:
128
  """Run portfolio analysis workflow.
129
 
130
  Args:
131
  portfolio_text: Raw portfolio input
 
132
 
133
  Returns:
134
  Formatted analysis result
@@ -143,7 +144,7 @@ async def run_analysis(portfolio_text: str) -> str:
143
  if not holdings:
144
  return "❌ Could not parse portfolio. Please use format: TICKER QUANTITY"
145
 
146
- logger.info(f"Starting analysis for {len(holdings)} holdings")
147
 
148
  # Construct initial state
149
  portfolio_id = f"demo_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
@@ -167,8 +168,11 @@ async def run_analysis(portfolio_text: str) -> str:
167
  'mcp_calls': []
168
  }
169
 
 
 
 
170
  # Run workflow
171
- final_state = await workflow.run(initial_state)
172
  LAST_ANALYSIS_STATE = final_state
173
 
174
  if final_state.get("errors"):
@@ -314,6 +318,7 @@ def create_visualisations() -> Tuple:
314
 
315
  async def run_analysis_with_ui_update(
316
  portfolio_text: str,
 
317
  progress=gr.Progress()
318
  ) -> Tuple[str, str, Any, Any, Any, Any, Any]:
319
  """Run analysis and return results with loading progress overlay.
@@ -322,6 +327,7 @@ async def run_analysis_with_ui_update(
322
 
323
  Args:
324
  portfolio_text: Portfolio input
 
325
  progress: Gradio progress tracker
326
 
327
  Returns:
@@ -374,7 +380,10 @@ async def run_analysis_with_ui_update(
374
 
375
  progress(0.2, desc=random.choice(LOADING_MESSAGES))
376
 
377
- final_state = await workflow.run(initial_state)
 
 
 
378
  LAST_ANALYSIS_STATE = final_state
379
 
380
  progress(0.7, desc=random.choice(LOADING_MESSAGES))
@@ -942,18 +951,28 @@ def create_interface() -> gr.Blocks:
942
  lines=8,
943
  info="Enter your holdings below (see examples for format)"
944
  )
 
 
 
 
 
 
 
 
945
  analyse_btn = gr.Button("Analyse Portfolio", variant="primary", size="lg", scale=0)
946
 
947
  # Examples integrated (no separate card)
948
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
949
  gr.Examples(
950
  examples=[
951
- ["AAPL 50\nTSLA 25 shares\nNVDA $5000"],
952
- ["VOO 100 shares\nVTI 75 shares\nSCHD 50 shares"],
953
  ["VTI $25000\nVXUS $15000\nBND $15000\nGLD $5000"],
 
 
954
  ],
955
  inputs=portfolio_input,
956
- label="Tech Growth, Dividend Focus, or Balanced 60/40"
957
  )
958
 
959
  # Right column: Live preview (wider at scale=3)
@@ -1075,7 +1094,7 @@ def create_interface() -> gr.Blocks:
1075
  results_page: gr.update(visible=False)
1076
  }
1077
 
1078
- async def handle_analysis(portfolio_text, progress=gr.Progress()):
1079
  # Show loading page immediately
1080
  yield {
1081
  input_page: gr.update(visible=False),
@@ -1092,7 +1111,7 @@ def create_interface() -> gr.Blocks:
1092
 
1093
  # Run analysis with progress updates
1094
  page, analysis, alloc, risk, perf, corr, opt = await run_analysis_with_ui_update(
1095
- portfolio_text, progress
1096
  )
1097
 
1098
  # Show results or return to input
@@ -1139,7 +1158,7 @@ def create_interface() -> gr.Blocks:
1139
 
1140
  analyse_btn.click(
1141
  handle_analysis,
1142
- inputs=[portfolio_input],
1143
  outputs=[
1144
  input_page,
1145
  loading_page,
 
124
  return holdings
125
 
126
 
127
+ async def run_analysis(portfolio_text: str, roast_mode: bool = False) -> str:
128
  """Run portfolio analysis workflow.
129
 
130
  Args:
131
  portfolio_text: Raw portfolio input
132
+ roast_mode: If True, use brutal honesty mode
133
 
134
  Returns:
135
  Formatted analysis result
 
144
  if not holdings:
145
  return "❌ Could not parse portfolio. Please use format: TICKER QUANTITY"
146
 
147
+ logger.info(f"Starting analysis for {len(holdings)} holdings (Roast Mode: {roast_mode})")
148
 
149
  # Construct initial state
150
  portfolio_id = f"demo_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
 
168
  'mcp_calls': []
169
  }
170
 
171
+ # Create workflow with roast mode if enabled
172
+ analysis_workflow = PortfolioAnalysisWorkflow(mcp_router, roast_mode=roast_mode)
173
+
174
  # Run workflow
175
+ final_state = await analysis_workflow.run(initial_state)
176
  LAST_ANALYSIS_STATE = final_state
177
 
178
  if final_state.get("errors"):
 
318
 
319
  async def run_analysis_with_ui_update(
320
  portfolio_text: str,
321
+ roast_mode: bool = False,
322
  progress=gr.Progress()
323
  ) -> Tuple[str, str, Any, Any, Any, Any, Any]:
324
  """Run analysis and return results with loading progress overlay.
 
327
 
328
  Args:
329
  portfolio_text: Portfolio input
330
+ roast_mode: If True, use brutal honesty mode
331
  progress: Gradio progress tracker
332
 
333
  Returns:
 
380
 
381
  progress(0.2, desc=random.choice(LOADING_MESSAGES))
382
 
383
+ # Create workflow with roast mode if enabled
384
+ analysis_workflow = PortfolioAnalysisWorkflow(mcp_router, roast_mode=roast_mode)
385
+
386
+ final_state = await analysis_workflow.run(initial_state)
387
  LAST_ANALYSIS_STATE = final_state
388
 
389
  progress(0.7, desc=random.choice(LOADING_MESSAGES))
 
951
  lines=8,
952
  info="Enter your holdings below (see examples for format)"
953
  )
954
+
955
+ # Roast Mode Toggle
956
+ roast_mode_toggle = gr.Checkbox(
957
+ label="Roast Mode",
958
+ value=False,
959
+ info="Enable brutal honesty mode for portfolio critique (still gets serious recommendations!)"
960
+ )
961
+
962
  analyse_btn = gr.Button("Analyse Portfolio", variant="primary", size="lg", scale=0)
963
 
964
  # Examples integrated (no separate card)
965
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
966
  gr.Examples(
967
  examples=[
968
+ ["AAPL 50 shares\nTSLA 25 shares\nNVDA 30 shares\nMETA 20 shares"],
969
+ ["VOO 100 shares\nVTI 75 shares\nSCHD 50 shares\nTLT 40 shares\nVXUS 60 shares"],
970
  ["VTI $25000\nVXUS $15000\nBND $15000\nGLD $5000"],
971
+ ["VTI $15000\nVXUS $10000\nVWO $5000\nBND $10000\nGLD $3000\nVNQ $2000"],
972
+ ["TSLA 100 shares"],
973
  ],
974
  inputs=portfolio_input,
975
+ label="Try: Tech Growth | Conservative Income | Balanced 60/40 | Global Diversified | Single Stock Risk"
976
  )
977
 
978
  # Right column: Live preview (wider at scale=3)
 
1094
  results_page: gr.update(visible=False)
1095
  }
1096
 
1097
+ async def handle_analysis(portfolio_text, roast_mode, progress=gr.Progress()):
1098
  # Show loading page immediately
1099
  yield {
1100
  input_page: gr.update(visible=False),
 
1111
 
1112
  # Run analysis with progress updates
1113
  page, analysis, alloc, risk, perf, corr, opt = await run_analysis_with_ui_update(
1114
+ portfolio_text, roast_mode, progress
1115
  )
1116
 
1117
  # Show results or return to input
 
1158
 
1159
  analyse_btn.click(
1160
  handle_analysis,
1161
+ inputs=[portfolio_input, roast_mode_toggle],
1162
  outputs=[
1163
  input_page,
1164
  loading_page,
backend/agents/portfolio_analyst.py CHANGED
@@ -212,15 +212,60 @@ Output a structured analysis with:
212
  - Confidence level in your analysis (0.0-1.0)
213
  """
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  class PortfolioAnalystAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
217
  """Main portfolio analysis agent."""
218
 
219
- def __init__(self):
 
 
 
 
 
 
220
  super().__init__(
221
  output_type=PortfolioAnalysisOutput,
222
- system_prompt=SYSTEM_PROMPT,
223
- agent_name="PortfolioAnalyst",
224
  )
225
 
226
  async def analyze_portfolio(
 
212
  - Confidence level in your analysis (0.0-1.0)
213
  """
214
 
215
+ ROAST_MODE_SYSTEM_PROMPT = """You are a brutally honest portfolio analyst with a sharp wit and no patience for poor investment decisions.
216
+
217
+ Your role is to roast questionable portfolios whilst still providing genuinely useful, actionable advice. Be savage about mistakes but educational in your corrections.
218
+
219
+ Roasting style guidelines:
220
+ 1. Start with brutal honesty about portfolio mistakes (but keep it entertaining, not cruel)
221
+ 2. Use comparisons and metaphors to highlight poor decisions
222
+ 3. Call out specific red flags with humour
223
+ 4. Balance roasting with actual expertise - show you know better
224
+ 5. ALWAYS end with constructive, actionable recommendations
225
+
226
+ Examples of good roasts:
227
+ - "Your portfolio screams 'I read Reddit in 2021.' 54% in meme stocks isn't diversification, it's gambling with extra steps."
228
+ - "Congratulations on creating the world's most expensive way to lose money. This allocation has the risk of a startup but returns of a savings account."
229
+ - "This portfolio is like bringing a knife to a gunfight, except the knife is made of paper and it's raining."
230
+
231
+ CRITICAL: After the roasting, you MUST provide serious, professional advice. The roast is the spoonful of sugar that helps the medicine go down.
232
+
233
+ CRITICAL RESPONSE FORMAT REQUIREMENTS:
234
+ - Return ONLY valid JSON matching the exact schema specified
235
+ - All required fields must be present with correct types
236
+ - summary: must be 50-500 characters long (THIS IS WHERE YOU ROAST)
237
+ - health_score: must be an integer between 0-100
238
+ - risk_level: must be exactly one of "conservative", "moderate", or "aggressive"
239
+ - top_strength: must be at least 10 characters
240
+ - top_concern: must be at least 10 characters (can be roast-style)
241
+ - diversification_score: must be an integer between 0-100
242
+ - recommendations: must be an array of 2-5 strings (serious, actionable advice)
243
+ - reasoning: array of explanation strings (can include roast commentary)
244
+ - confidence: must be a float between 0.0-1.0
245
+
246
+ Output a structured analysis with:
247
+ - A roast-style summary of portfolio health (50-500 chars)
248
+ - The single biggest strength (acknowledge good decisions!) and concern (roast the bad)
249
+ - 2-5 prioritised, SERIOUS, actionable recommendations (no jokes here)
250
+ - Your reasoning process (can include roast commentary)
251
+ - Confidence level in your analysis (0.0-1.0)
252
+ """
253
+
254
 
255
  class PortfolioAnalystAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
256
  """Main portfolio analysis agent."""
257
 
258
+ def __init__(self, roast_mode: bool = False):
259
+ """Initialise portfolio analyst agent.
260
+
261
+ Args:
262
+ roast_mode: If True, use brutal honesty mode for analysis
263
+ """
264
+ system_prompt = ROAST_MODE_SYSTEM_PROMPT if roast_mode else SYSTEM_PROMPT
265
  super().__init__(
266
  output_type=PortfolioAnalysisOutput,
267
+ system_prompt=system_prompt,
268
+ agent_name="PortfolioAnalyst" + (" (Roast Mode)" if roast_mode else ""),
269
  )
270
 
271
  async def analyze_portfolio(
backend/agents/workflow.py CHANGED
@@ -58,14 +58,16 @@ def summarize_fred_data(series_data: Dict[str, Any], indicator_name: str) -> Dic
58
  class PortfolioAnalysisWorkflow:
59
  """LangGraph workflow for portfolio analysis."""
60
 
61
- def __init__(self, mcp_router):
62
  """Initialize the workflow with MCP router.
63
 
64
  Args:
65
  mcp_router: MCP router instance for calling MCP servers
 
66
  """
67
  self.mcp_router = mcp_router
68
- self.analyst_agent = PortfolioAnalystAgent()
 
69
 
70
  # Build the workflow graph
71
  self.workflow = self._build_workflow()
 
58
  class PortfolioAnalysisWorkflow:
59
  """LangGraph workflow for portfolio analysis."""
60
 
61
+ def __init__(self, mcp_router, roast_mode: bool = False):
62
  """Initialize the workflow with MCP router.
63
 
64
  Args:
65
  mcp_router: MCP router instance for calling MCP servers
66
+ roast_mode: If True, use brutal honesty mode for analysis
67
  """
68
  self.mcp_router = mcp_router
69
+ self.analyst_agent = PortfolioAnalystAgent(roast_mode=roast_mode)
70
+ self.roast_mode = roast_mode
71
 
72
  # Build the workflow graph
73
  self.workflow = self._build_workflow()