Spaces:
Running
on
Zero
Running
on
Zero
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 +99 -81
- backend/agents/react_agent.py +25 -4
- backend/agents/rehearsal.py +5 -2
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:
|
| 1530 |
overflow-x: visible !important;
|
| 1531 |
-
max-height:
|
| 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-
|
| 1564 |
-
|
|
|
|
| 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("π
|
| 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
|
| 2235 |
-
|
| 2236 |
-
|
| 2237 |
-
|
| 2238 |
-
|
| 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 |
-
|
| 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 |
-
|
| 2687 |
Tuple of UI updates for build results
|
| 2688 |
"""
|
| 2689 |
-
|
| 2690 |
-
|
| 2691 |
-
|
| 2692 |
-
|
| 2693 |
-
|
| 2694 |
-
|
| 2695 |
-
|
| 2696 |
-
|
| 2697 |
-
|
| 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 =
|
| 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 |
-
#
|
| 2724 |
-
|
| 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 |
-
|
| 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 |
-
|
| 2801 |
-
"""Handle the Compare Strategies workflow.
|
| 2802 |
|
| 2803 |
Args:
|
| 2804 |
portfolio_text: Raw portfolio text input
|
| 2805 |
session_state: User session
|
| 2806 |
|
| 2807 |
-
|
| 2808 |
Tuple of UI updates for compare results
|
| 2809 |
"""
|
| 2810 |
-
|
| 2811 |
-
|
| 2812 |
-
|
| 2813 |
-
|
| 2814 |
-
|
| 2815 |
-
|
| 2816 |
-
|
| 2817 |
-
|
| 2818 |
-
|
| 2819 |
-
|
| 2820 |
-
|
| 2821 |
-
|
| 2822 |
-
|
| 2823 |
|
| 2824 |
-
|
| 2825 |
-
|
| 2826 |
|
| 2827 |
-
|
| 2828 |
-
|
| 2829 |
-
|
| 2830 |
-
|
| 2831 |
-
|
| 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 =
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 210 |
) -> dict:
|
| 211 |
"""Assess portfolio risk metrics.
|
| 212 |
|
| 213 |
Args:
|
| 214 |
-
weights: Dictionary mapping tickers to allocation weights
|
| 215 |
-
|
|
|
|
| 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", {
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|