BrianIsaac commited on
Commit
d1839af
Β·
1 Parent(s): 3109d12

fix: improve analyse portfolio page UI layout and agent compatibility

Browse files

- Fix button sizing on analyse portfolio page with min_width=290
- Remove scroll constraint on input-card to show example portfolios header
- Fix Decimal type compatibility in rehearsal engine expected return calculation
- Improve ReAct agent tool message handling

app.py CHANGED
@@ -1526,9 +1526,9 @@ def create_interface() -> gr.Blocks:
1526
  flex: 1 1 auto !important;
1527
  display: flex !important;
1528
  flex-direction: column !important;
1529
- overflow-y: auto !important;
1530
  overflow-x: visible !important;
1531
- max-height: 800px !important;
1532
  min-height: 0;
1533
  box-sizing: border-box !important;
1534
  gap: 0 !important;
@@ -1560,8 +1560,9 @@ def create_interface() -> gr.Blocks:
1560
  }
1561
 
1562
  .examples-header {
1563
- margin-bottom: 0.75rem;
1564
- padding-top: 1.5rem;
 
1565
  border-top: 1px solid rgba(255, 255, 255, 0.1);
1566
  color: #048CFC;
1567
  font-weight: 600;
@@ -1796,10 +1797,8 @@ def create_interface() -> gr.Blocks:
1796
  gr.Markdown("## Navigation", elem_classes="sidebar-header")
1797
 
1798
  # Navigation options
1799
- nav_new_analysis = gr.Button("🏠 New Analysis", variant="secondary", size="lg", elem_classes="nav-btn")
1800
  nav_view_history = gr.Button("πŸ“œ View History", variant="secondary", size="lg", elem_classes="nav-btn")
1801
- nav_settings = gr.Button("βš™οΈ Settings", variant="secondary", size="lg", elem_classes="nav-btn")
1802
- nav_help = gr.Button("❓ Help & Documentation", variant="secondary", size="lg", elem_classes="nav-btn")
1803
 
1804
  gr.Markdown("---")
1805
 
@@ -1940,7 +1939,7 @@ def create_interface() -> gr.Blocks:
1940
  with gr.Group(visible=False, elem_classes="task-selection-container") as task_page:
1941
  gr.Markdown("## What would you like to do?", elem_classes="section-title")
1942
 
1943
- with gr.Row():
1944
  # Analyse Portfolio - enabled
1945
  with gr.Column(scale=1, min_width=200):
1946
  task_analyse_btn = gr.Button(
@@ -2231,12 +2230,20 @@ def create_interface() -> gr.Blocks:
2231
  info="Enable brutal honesty mode for portfolio critique (only works with Standard Analysis)"
2232
  )
2233
 
2234
- # Action button
2235
- analyse_btn = gr.Button(
2236
- "Analyse Portfolio",
2237
- variant="primary",
2238
- size="lg"
2239
- )
 
 
 
 
 
 
 
 
2240
 
2241
  # Examples integrated as simple buttons
2242
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
@@ -2673,8 +2680,8 @@ def create_interface() -> gr.Blocks:
2673
  test_page: gr.update(visible=True)
2674
  }
2675
 
2676
- async def handle_build_portfolio(goals, risk_tolerance, constraints, show_reasoning, session_state):
2677
- """Handle the Build Portfolio workflow.
2678
 
2679
  Args:
2680
  goals: List of selected investment goals
@@ -2683,20 +2690,33 @@ def create_interface() -> gr.Blocks:
2683
  show_reasoning: Whether to show reasoning trace
2684
  session_state: User session
2685
 
2686
- Returns:
2687
  Tuple of UI updates for build results
2688
  """
2689
- try:
2690
- if not goals:
2691
- return (
2692
- gr.update(visible=True), # build_results_container
2693
- "Please select at least one investment goal.", # build_status
2694
- gr.update(visible=False), # build_reasoning_container
2695
- [], # build_reasoning_trace
2696
- [], # build_portfolio_table
2697
- "" # build_final_response
2698
- )
 
 
 
 
 
 
 
 
 
 
 
 
2699
 
 
2700
  # Initialise MCP router and workflow router
2701
  from backend.mcp_router import MCPRouter
2702
  from backend.agents.workflow_router import WorkflowRouter
@@ -2705,11 +2725,11 @@ def create_interface() -> gr.Blocks:
2705
  workflow_router = WorkflowRouter(mcp_router)
2706
 
2707
  # Run the build workflow
2708
- result = await workflow_router.route_build(
2709
  goals=goals,
2710
  risk_tolerance=int(risk_tolerance),
2711
  constraints=constraints or ""
2712
- )
2713
 
2714
  # Extract portfolio data for table
2715
  portfolio_data = []
@@ -2720,29 +2740,19 @@ def create_interface() -> gr.Blocks:
2720
  item.get("reasoning", "")
2721
  ])
2722
 
2723
- # If no portfolio extracted, show the final response
2724
- if not portfolio_data and result.get("final_response"):
2725
- return (
2726
- gr.update(visible=True),
2727
- "Portfolio built successfully!",
2728
- gr.update(visible=show_reasoning),
2729
- result.get("reasoning_trace", []),
2730
- [],
2731
- result.get("final_response", "")
2732
- )
2733
-
2734
- return (
2735
  gr.update(visible=True),
2736
  "Portfolio built successfully!",
2737
  gr.update(visible=show_reasoning),
2738
  result.get("reasoning_trace", []),
2739
- portfolio_data,
2740
  result.get("final_response", "")
2741
  )
2742
 
2743
  except Exception as e:
2744
  logger.error(f"Build portfolio error: {e}")
2745
- return (
2746
  gr.update(visible=True),
2747
  f"Error building portfolio: {str(e)}",
2748
  gr.update(visible=False),
@@ -2751,10 +2761,6 @@ def create_interface() -> gr.Blocks:
2751
  ""
2752
  )
2753
 
2754
- def sync_handle_build_portfolio(goals, risk_tolerance, constraints, show_reasoning, session_state):
2755
- """Synchronous wrapper for handle_build_portfolio."""
2756
- return asyncio.run(handle_build_portfolio(goals, risk_tolerance, constraints, show_reasoning, session_state))
2757
-
2758
  def handle_build_accept(portfolio_table):
2759
  """Accept built portfolio and populate input for analysis.
2760
 
@@ -2797,40 +2803,49 @@ def create_interface() -> gr.Blocks:
2797
  gr.update(visible=False) # test_page
2798
  )
2799
 
2800
- async def handle_compare_portfolio(portfolio_text, session_state):
2801
- """Handle the Compare Strategies workflow.
2802
 
2803
  Args:
2804
  portfolio_text: Raw portfolio text input
2805
  session_state: User session
2806
 
2807
- Returns:
2808
  Tuple of UI updates for compare results
2809
  """
2810
- try:
2811
- if not portfolio_text or not portfolio_text.strip():
2812
- return (
2813
- gr.update(visible=True), # compare_results_container
2814
- "Please enter your portfolio holdings.", # compare_status
2815
- "", # compare_bull_case
2816
- 0, # compare_bull_confidence
2817
- "", # compare_bear_case
2818
- 0, # compare_bear_confidence
2819
- "", # compare_consensus
2820
- "", # compare_stance
2821
- [] # compare_debate_transcript
2822
- )
2823
 
2824
- # Parse portfolio
2825
- holdings = parse_portfolio_input(portfolio_text)
2826
 
2827
- if not holdings:
2828
- return (
2829
- gr.update(visible=True),
2830
- "Could not parse portfolio. Please check format.",
2831
- "", 0, "", 0, "", "", []
2832
- )
 
2833
 
 
 
 
 
 
 
 
 
2834
  # Initialise MCP router and workflow router
2835
  from backend.mcp_router import MCPRouter
2836
  from backend.agents.workflow_router import WorkflowRouter
@@ -2839,14 +2854,15 @@ def create_interface() -> gr.Blocks:
2839
  workflow_router = WorkflowRouter(mcp_router)
2840
 
2841
  # Run the compare workflow
2842
- result = await workflow_router.route_compare(holdings=holdings)
2843
 
2844
  council_result = result.get("council_result", {})
2845
  bull_case = council_result.get("bull_case", {})
2846
  bear_case = council_result.get("bear_case", {})
2847
  consensus = council_result.get("consensus", {})
2848
 
2849
- return (
 
2850
  gr.update(visible=True),
2851
  "Analysis complete!",
2852
  bull_case.get("thesis", ""),
@@ -2860,16 +2876,12 @@ def create_interface() -> gr.Blocks:
2860
 
2861
  except Exception as e:
2862
  logger.error(f"Compare portfolio error: {e}")
2863
- return (
2864
  gr.update(visible=True),
2865
  f"Error: {str(e)}",
2866
  "", 0, "", 0, "", "", []
2867
  )
2868
 
2869
- def sync_handle_compare_portfolio(portfolio_text, session_state):
2870
- """Synchronous wrapper for handle_compare_portfolio."""
2871
- return asyncio.run(handle_compare_portfolio(portfolio_text, session_state))
2872
-
2873
  async def handle_test_changes(portfolio_text, changes_text, portfolio_value, session_state):
2874
  """Handle the Test Changes workflow.
2875
 
@@ -3694,6 +3706,12 @@ Please try again with different parameters.
3694
  outputs=export_csv_btn
3695
  )
3696
 
 
 
 
 
 
 
3697
  # Auto-load history when History tab is selected
3698
  def load_history_if_selected(evt: gr.SelectData, session):
3699
  """Load history only when History tab is selected."""
@@ -4152,7 +4170,7 @@ Please try again with different parameters.
4152
  )
4153
 
4154
  build_submit_btn.click(
4155
- sync_handle_build_portfolio,
4156
  inputs=[build_goals, build_risk_tolerance, build_constraints, build_show_reasoning, session_state],
4157
  outputs=[
4158
  build_results_container,
@@ -4177,7 +4195,7 @@ Please try again with different parameters.
4177
  )
4178
 
4179
  build_regenerate_btn.click(
4180
- sync_handle_build_portfolio,
4181
  inputs=[build_goals, build_risk_tolerance, build_constraints, build_show_reasoning, session_state],
4182
  outputs=[
4183
  build_results_container,
@@ -4196,7 +4214,7 @@ Please try again with different parameters.
4196
  )
4197
 
4198
  compare_submit_btn.click(
4199
- sync_handle_compare_portfolio,
4200
  inputs=[compare_portfolio_input, session_state],
4201
  outputs=[
4202
  compare_results_container,
 
1526
  flex: 1 1 auto !important;
1527
  display: flex !important;
1528
  flex-direction: column !important;
1529
+ overflow-y: visible !important;
1530
  overflow-x: visible !important;
1531
+ max-height: none !important;
1532
  min-height: 0;
1533
  box-sizing: border-box !important;
1534
  gap: 0 !important;
 
1560
  }
1561
 
1562
  .examples-header {
1563
+ margin-top: 0.25rem;
1564
+ margin-bottom: 0.25rem;
1565
+ padding-top: 0.5rem;
1566
  border-top: 1px solid rgba(255, 255, 255, 0.1);
1567
  color: #048CFC;
1568
  font-weight: 600;
 
1797
  gr.Markdown("## Navigation", elem_classes="sidebar-header")
1798
 
1799
  # Navigation options
1800
+ nav_new_analysis = gr.Button("🏠 Home", variant="secondary", size="lg", elem_classes="nav-btn")
1801
  nav_view_history = gr.Button("πŸ“œ View History", variant="secondary", size="lg", elem_classes="nav-btn")
 
 
1802
 
1803
  gr.Markdown("---")
1804
 
 
1939
  with gr.Group(visible=False, elem_classes="task-selection-container") as task_page:
1940
  gr.Markdown("## What would you like to do?", elem_classes="section-title")
1941
 
1942
+ with gr.Row(equal_height=True):
1943
  # Analyse Portfolio - enabled
1944
  with gr.Column(scale=1, min_width=200):
1945
  task_analyse_btn = gr.Button(
 
2230
  info="Enable brutal honesty mode for portfolio critique (only works with Standard Analysis)"
2231
  )
2232
 
2233
+ # Action buttons
2234
+ with gr.Row():
2235
+ analyse_btn = gr.Button(
2236
+ "Analyse Portfolio",
2237
+ variant="primary",
2238
+ size="lg",
2239
+ min_width=290
2240
+ )
2241
+ input_back_btn = gr.Button(
2242
+ "Back",
2243
+ variant="secondary",
2244
+ size="lg",
2245
+ min_width=290
2246
+ )
2247
 
2248
  # Examples integrated as simple buttons
2249
  gr.Markdown("### Example Portfolios", elem_classes="examples-header")
 
2680
  test_page: gr.update(visible=True)
2681
  }
2682
 
2683
+ def handle_build_portfolio(goals, risk_tolerance, constraints, show_reasoning, session_state):
2684
+ """Handle the Build Portfolio workflow with loading state.
2685
 
2686
  Args:
2687
  goals: List of selected investment goals
 
2690
  show_reasoning: Whether to show reasoning trace
2691
  session_state: User session
2692
 
2693
+ Yields:
2694
  Tuple of UI updates for build results
2695
  """
2696
+ import time
2697
+
2698
+ if not goals:
2699
+ yield (
2700
+ gr.update(visible=True), # build_results_container
2701
+ "Please select at least one investment goal.", # build_status
2702
+ gr.update(visible=False), # build_reasoning_container
2703
+ [], # build_reasoning_trace
2704
+ [], # build_portfolio_table
2705
+ "" # build_final_response
2706
+ )
2707
+ return
2708
+
2709
+ # First yield: Show loading state
2710
+ yield (
2711
+ gr.update(visible=True),
2712
+ "Building your portfolio... This may take a minute.",
2713
+ gr.update(visible=False),
2714
+ [],
2715
+ [],
2716
+ ""
2717
+ )
2718
 
2719
+ try:
2720
  # Initialise MCP router and workflow router
2721
  from backend.mcp_router import MCPRouter
2722
  from backend.agents.workflow_router import WorkflowRouter
 
2725
  workflow_router = WorkflowRouter(mcp_router)
2726
 
2727
  # Run the build workflow
2728
+ result = asyncio.run(workflow_router.route_build(
2729
  goals=goals,
2730
  risk_tolerance=int(risk_tolerance),
2731
  constraints=constraints or ""
2732
+ ))
2733
 
2734
  # Extract portfolio data for table
2735
  portfolio_data = []
 
2740
  item.get("reasoning", "")
2741
  ])
2742
 
2743
+ # Final yield: Show results
2744
+ yield (
 
 
 
 
 
 
 
 
 
 
2745
  gr.update(visible=True),
2746
  "Portfolio built successfully!",
2747
  gr.update(visible=show_reasoning),
2748
  result.get("reasoning_trace", []),
2749
+ portfolio_data if portfolio_data else [],
2750
  result.get("final_response", "")
2751
  )
2752
 
2753
  except Exception as e:
2754
  logger.error(f"Build portfolio error: {e}")
2755
+ yield (
2756
  gr.update(visible=True),
2757
  f"Error building portfolio: {str(e)}",
2758
  gr.update(visible=False),
 
2761
  ""
2762
  )
2763
 
 
 
 
 
2764
  def handle_build_accept(portfolio_table):
2765
  """Accept built portfolio and populate input for analysis.
2766
 
 
2803
  gr.update(visible=False) # test_page
2804
  )
2805
 
2806
+ def handle_compare_portfolio(portfolio_text, session_state):
2807
+ """Handle the Compare Strategies workflow with loading state.
2808
 
2809
  Args:
2810
  portfolio_text: Raw portfolio text input
2811
  session_state: User session
2812
 
2813
+ Yields:
2814
  Tuple of UI updates for compare results
2815
  """
2816
+ if not portfolio_text or not portfolio_text.strip():
2817
+ yield (
2818
+ gr.update(visible=True), # compare_results_container
2819
+ "Please enter your portfolio holdings.", # compare_status
2820
+ "", # compare_bull_case
2821
+ 0, # compare_bull_confidence
2822
+ "", # compare_bear_case
2823
+ 0, # compare_bear_confidence
2824
+ "", # compare_consensus
2825
+ "", # compare_stance
2826
+ [] # compare_debate_transcript
2827
+ )
2828
+ return
2829
 
2830
+ # Parse portfolio
2831
+ holdings = parse_portfolio_input(portfolio_text)
2832
 
2833
+ if not holdings:
2834
+ yield (
2835
+ gr.update(visible=True),
2836
+ "Could not parse portfolio. Please check format.",
2837
+ "", 0, "", 0, "", "", []
2838
+ )
2839
+ return
2840
 
2841
+ # First yield: Show loading state
2842
+ yield (
2843
+ gr.update(visible=True),
2844
+ "Running advisory council debate... This may take a minute.",
2845
+ "", 0, "", 0, "", "", []
2846
+ )
2847
+
2848
+ try:
2849
  # Initialise MCP router and workflow router
2850
  from backend.mcp_router import MCPRouter
2851
  from backend.agents.workflow_router import WorkflowRouter
 
2854
  workflow_router = WorkflowRouter(mcp_router)
2855
 
2856
  # Run the compare workflow
2857
+ result = asyncio.run(workflow_router.route_compare(holdings=holdings))
2858
 
2859
  council_result = result.get("council_result", {})
2860
  bull_case = council_result.get("bull_case", {})
2861
  bear_case = council_result.get("bear_case", {})
2862
  consensus = council_result.get("consensus", {})
2863
 
2864
+ # Final yield: Show results
2865
+ yield (
2866
  gr.update(visible=True),
2867
  "Analysis complete!",
2868
  bull_case.get("thesis", ""),
 
2876
 
2877
  except Exception as e:
2878
  logger.error(f"Compare portfolio error: {e}")
2879
+ yield (
2880
  gr.update(visible=True),
2881
  f"Error: {str(e)}",
2882
  "", 0, "", 0, "", "", []
2883
  )
2884
 
 
 
 
 
2885
  async def handle_test_changes(portfolio_text, changes_text, portfolio_value, session_state):
2886
  """Handle the Test Changes workflow.
2887
 
 
3706
  outputs=export_csv_btn
3707
  )
3708
 
3709
+ # Back button from input page to task page
3710
+ input_back_btn.click(
3711
+ show_task_page,
3712
+ outputs=[task_page, input_page, results_page, history_page, build_page, compare_page, test_page]
3713
+ )
3714
+
3715
  # Auto-load history when History tab is selected
3716
  def load_history_if_selected(evt: gr.SelectData, session):
3717
  """Load history only when History tab is selected."""
 
4170
  )
4171
 
4172
  build_submit_btn.click(
4173
+ handle_build_portfolio,
4174
  inputs=[build_goals, build_risk_tolerance, build_constraints, build_show_reasoning, session_state],
4175
  outputs=[
4176
  build_results_container,
 
4195
  )
4196
 
4197
  build_regenerate_btn.click(
4198
+ handle_build_portfolio,
4199
  inputs=[build_goals, build_risk_tolerance, build_constraints, build_show_reasoning, session_state],
4200
  outputs=[
4201
  build_results_container,
 
4214
  )
4215
 
4216
  compare_submit_btn.click(
4217
+ handle_compare_portfolio,
4218
  inputs=[compare_portfolio_input, session_state],
4219
  outputs=[
4220
  compare_results_container,
backend/agents/react_agent.py CHANGED
@@ -206,20 +206,41 @@ class PortfolioBuilderAgent:
206
  @tool
207
  async def assess_risk(
208
  weights: dict[str, float],
209
- returns_data: dict[str, list[float]]
 
210
  ) -> dict:
211
  """Assess portfolio risk metrics.
212
 
213
  Args:
214
- weights: Dictionary mapping tickers to allocation weights
215
- returns_data: Dictionary mapping tickers to their return series
 
216
 
217
  Returns:
218
  Risk metrics including VaR, CVaR, volatility, etc.
219
  """
220
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  return await mcp_router.call_risk_analyzer_mcp(
222
- "analyze_risk", {"weights": weights, "returns": returns_data}
 
 
 
223
  )
224
  except Exception as e:
225
  logger.error(f"Error assessing risk: {e}")
 
206
  @tool
207
  async def assess_risk(
208
  weights: dict[str, float],
209
+ historical_prices: dict[str, list[float]],
210
+ portfolio_value: float = 10000.0
211
  ) -> dict:
212
  """Assess portfolio risk metrics.
213
 
214
  Args:
215
+ weights: Dictionary mapping tickers to allocation weights (0-1)
216
+ historical_prices: Dictionary mapping tickers to their historical prices
217
+ portfolio_value: Total portfolio value in dollars
218
 
219
  Returns:
220
  Risk metrics including VaR, CVaR, volatility, etc.
221
  """
222
  try:
223
+ from decimal import Decimal
224
+
225
+ # Construct portfolio in the required format
226
+ portfolio = []
227
+ for ticker, weight in weights.items():
228
+ if ticker in historical_prices:
229
+ prices = historical_prices[ticker]
230
+ portfolio.append({
231
+ "ticker": ticker,
232
+ "weight": Decimal(str(weight)),
233
+ "prices": [Decimal(str(p)) for p in prices]
234
+ })
235
+
236
+ if not portfolio:
237
+ return {"error": "No valid portfolio data to analyse"}
238
+
239
  return await mcp_router.call_risk_analyzer_mcp(
240
+ "analyze_risk", {
241
+ "portfolio": portfolio,
242
+ "portfolio_value": Decimal(str(portfolio_value))
243
+ }
244
  )
245
  except Exception as e:
246
  logger.error(f"Error assessing risk: {e}")
backend/agents/rehearsal.py CHANGED
@@ -262,8 +262,11 @@ class PortfolioRehearsalEngine:
262
  if ticker in historical_data:
263
  prices = historical_data[ticker].get("close_prices", [])
264
  if len(prices) >= 2:
265
- annual_return = ((prices[-1] - prices[0]) / prices[0]) * 100
266
- weight = data["weight"] / 100
 
 
 
267
  total_return += annual_return * weight
268
  total_weight += weight
269
 
 
262
  if ticker in historical_data:
263
  prices = historical_data[ticker].get("close_prices", [])
264
  if len(prices) >= 2:
265
+ # Convert Decimal prices to float for arithmetic compatibility
266
+ price_start = float(prices[0])
267
+ price_end = float(prices[-1])
268
+ annual_return = ((price_end - price_start) / price_start) * 100
269
+ weight = float(data["weight"]) / 100
270
  total_return += annual_return * weight
271
  total_weight += weight
272