Spaces:
Running
on
Zero
Running
on
Zero
Commit
·
bc45c22
1
Parent(s):
cea2220
fix: resolve MCP tool exposure and persona agent signature issues
Browse files- Add sentiment_data parameter to PersonaAnalysisAgent.analyze_portfolio()
to match PortfolioAnalystAgent signature (fixes Phase 3 TypeError)
- Add show_api=False to 28 lambda event handlers to hide UI-only
functions from MCP tool exposure (reduces tools from 91 to 63)
- Integrate sentiment data into persona-based analysis prompts
Fixes "No analysis content available" warning caused by method
signature mismatch between agent classes.
- app.py +56 -28
- backend/agents/personas.py +10 -1
app.py
CHANGED
|
@@ -2366,23 +2366,28 @@ def create_interface() -> gr.Blocks:
|
|
| 2366 |
# Add click handlers for example buttons
|
| 2367 |
tech_btn.click(
|
| 2368 |
fn=lambda: "AAPL 50 shares\nTSLA 25 shares\nNVDA 30 shares\nMETA 20 shares",
|
| 2369 |
-
outputs=portfolio_input
|
|
|
|
| 2370 |
)
|
| 2371 |
conservative_btn.click(
|
| 2372 |
fn=lambda: "VOO 100 shares\nVTI 75 shares\nSCHD 50 shares\nTLT 40 shares\nVXUS 60 shares",
|
| 2373 |
-
outputs=portfolio_input
|
|
|
|
| 2374 |
)
|
| 2375 |
balanced_btn.click(
|
| 2376 |
fn=lambda: "VTI $25000\nVXUS $15000\nBND $15000\nGLD $5000",
|
| 2377 |
-
outputs=portfolio_input
|
|
|
|
| 2378 |
)
|
| 2379 |
global_btn.click(
|
| 2380 |
fn=lambda: "VTI $15000\nVXUS $10000\nVWO $5000\nBND $10000\nGLD $3000\nVNQ $2000",
|
| 2381 |
-
outputs=portfolio_input
|
|
|
|
| 2382 |
)
|
| 2383 |
risk_btn.click(
|
| 2384 |
fn=lambda: "TSLA 100 shares",
|
| 2385 |
-
outputs=portfolio_input
|
|
|
|
| 2386 |
)
|
| 2387 |
|
| 2388 |
# Feature highlights grid (1x4 compact layout)
|
|
@@ -4247,92 +4252,110 @@ Please try again with different parameters.
|
|
| 4247 |
history_search.change(
|
| 4248 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4249 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4250 |
-
outputs=[history_table, history_page_info]
|
|
|
|
| 4251 |
).then(
|
| 4252 |
lambda: 1, # Reset to page 1 on search
|
| 4253 |
-
outputs=[history_current_page]
|
|
|
|
| 4254 |
)
|
| 4255 |
|
| 4256 |
history_date_filter.change(
|
| 4257 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4258 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4259 |
-
outputs=[history_table, history_page_info]
|
|
|
|
| 4260 |
).then(
|
| 4261 |
lambda: 1, # Reset to page 1 on filter change
|
| 4262 |
-
outputs=[history_current_page]
|
|
|
|
| 4263 |
)
|
| 4264 |
|
| 4265 |
history_refresh_btn.click(
|
| 4266 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4267 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4268 |
-
outputs=[history_table, history_page_info]
|
|
|
|
| 4269 |
)
|
| 4270 |
|
| 4271 |
# Pagination handlers (standalone)
|
| 4272 |
history_prev_btn.click(
|
| 4273 |
lambda page: max(1, page - 1),
|
| 4274 |
inputs=[history_current_page],
|
| 4275 |
-
outputs=[history_current_page]
|
|
|
|
| 4276 |
).then(
|
| 4277 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4278 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4279 |
-
outputs=[history_table, history_page_info]
|
|
|
|
| 4280 |
)
|
| 4281 |
|
| 4282 |
history_next_btn.click(
|
| 4283 |
lambda page: page + 1,
|
| 4284 |
inputs=[history_current_page],
|
| 4285 |
-
outputs=[history_current_page]
|
|
|
|
| 4286 |
).then(
|
| 4287 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4288 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4289 |
-
outputs=[history_table, history_page_info]
|
|
|
|
| 4290 |
)
|
| 4291 |
|
| 4292 |
# History search and filter handlers (results page tab)
|
| 4293 |
history_search_results.change(
|
| 4294 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4295 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4296 |
-
outputs=[history_table_results, history_page_info_results]
|
|
|
|
| 4297 |
).then(
|
| 4298 |
lambda: 1, # Reset to page 1 on search
|
| 4299 |
-
outputs=[history_current_page_results]
|
|
|
|
| 4300 |
)
|
| 4301 |
|
| 4302 |
history_date_filter_results.change(
|
| 4303 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4304 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4305 |
-
outputs=[history_table_results, history_page_info_results]
|
|
|
|
| 4306 |
).then(
|
| 4307 |
lambda: 1, # Reset to page 1 on filter change
|
| 4308 |
-
outputs=[history_current_page_results]
|
|
|
|
| 4309 |
)
|
| 4310 |
|
| 4311 |
history_refresh_btn_results.click(
|
| 4312 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4313 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4314 |
-
outputs=[history_table_results, history_page_info_results]
|
|
|
|
| 4315 |
)
|
| 4316 |
|
| 4317 |
# Pagination handlers (results page)
|
| 4318 |
history_prev_btn_results.click(
|
| 4319 |
lambda page: max(1, page - 1),
|
| 4320 |
inputs=[history_current_page_results],
|
| 4321 |
-
outputs=[history_current_page_results]
|
|
|
|
| 4322 |
).then(
|
| 4323 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4324 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4325 |
-
outputs=[history_table_results, history_page_info_results]
|
|
|
|
| 4326 |
)
|
| 4327 |
|
| 4328 |
history_next_btn_results.click(
|
| 4329 |
lambda page: page + 1,
|
| 4330 |
inputs=[history_current_page_results],
|
| 4331 |
-
outputs=[history_current_page_results]
|
|
|
|
| 4332 |
).then(
|
| 4333 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4334 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4335 |
-
outputs=[history_table_results, history_page_info_results]
|
|
|
|
| 4336 |
)
|
| 4337 |
|
| 4338 |
# Tax load holdings button
|
|
@@ -4597,17 +4620,20 @@ Please try again with different parameters.
|
|
| 4597 |
# Password reset handlers
|
| 4598 |
forgot_password_btn.click(
|
| 4599 |
lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 4600 |
-
outputs=[login_container, password_reset_modal]
|
|
|
|
| 4601 |
)
|
| 4602 |
|
| 4603 |
reset_cancel_btn.click(
|
| 4604 |
lambda: (gr.update(visible=True), gr.update(visible=False), ""),
|
| 4605 |
-
outputs=[login_container, password_reset_modal, reset_message]
|
|
|
|
| 4606 |
)
|
| 4607 |
|
| 4608 |
update_cancel_btn.click(
|
| 4609 |
lambda: (gr.update(visible=True), gr.update(visible=False), ""),
|
| 4610 |
-
outputs=[login_container, password_update_modal, update_password_message]
|
|
|
|
| 4611 |
)
|
| 4612 |
|
| 4613 |
reset_submit_btn.click(
|
|
@@ -4623,7 +4649,8 @@ Please try again with different parameters.
|
|
| 4623 |
gr.update(visible=True) if token else gr.update(visible=False)
|
| 4624 |
),
|
| 4625 |
inputs=[recovery_token_hash],
|
| 4626 |
-
outputs=[login_container, password_update_modal]
|
|
|
|
| 4627 |
)
|
| 4628 |
|
| 4629 |
# Password update handler (separate modal after email link click)
|
|
@@ -4638,7 +4665,8 @@ Please try again with different parameters.
|
|
| 4638 |
gr.update(visible=True) if "✅" in msg else gr.update(visible=False)
|
| 4639 |
),
|
| 4640 |
inputs=[update_password_message],
|
| 4641 |
-
outputs=[password_update_modal, login_container]
|
|
|
|
| 4642 |
)
|
| 4643 |
|
| 4644 |
def handle_demo_mode(current_session: Dict):
|
|
|
|
| 2366 |
# Add click handlers for example buttons
|
| 2367 |
tech_btn.click(
|
| 2368 |
fn=lambda: "AAPL 50 shares\nTSLA 25 shares\nNVDA 30 shares\nMETA 20 shares",
|
| 2369 |
+
outputs=portfolio_input,
|
| 2370 |
+
show_api=False
|
| 2371 |
)
|
| 2372 |
conservative_btn.click(
|
| 2373 |
fn=lambda: "VOO 100 shares\nVTI 75 shares\nSCHD 50 shares\nTLT 40 shares\nVXUS 60 shares",
|
| 2374 |
+
outputs=portfolio_input,
|
| 2375 |
+
show_api=False
|
| 2376 |
)
|
| 2377 |
balanced_btn.click(
|
| 2378 |
fn=lambda: "VTI $25000\nVXUS $15000\nBND $15000\nGLD $5000",
|
| 2379 |
+
outputs=portfolio_input,
|
| 2380 |
+
show_api=False
|
| 2381 |
)
|
| 2382 |
global_btn.click(
|
| 2383 |
fn=lambda: "VTI $15000\nVXUS $10000\nVWO $5000\nBND $10000\nGLD $3000\nVNQ $2000",
|
| 2384 |
+
outputs=portfolio_input,
|
| 2385 |
+
show_api=False
|
| 2386 |
)
|
| 2387 |
risk_btn.click(
|
| 2388 |
fn=lambda: "TSLA 100 shares",
|
| 2389 |
+
outputs=portfolio_input,
|
| 2390 |
+
show_api=False
|
| 2391 |
)
|
| 2392 |
|
| 2393 |
# Feature highlights grid (1x4 compact layout)
|
|
|
|
| 4252 |
history_search.change(
|
| 4253 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4254 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4255 |
+
outputs=[history_table, history_page_info],
|
| 4256 |
+
show_api=False
|
| 4257 |
).then(
|
| 4258 |
lambda: 1, # Reset to page 1 on search
|
| 4259 |
+
outputs=[history_current_page],
|
| 4260 |
+
show_api=False
|
| 4261 |
)
|
| 4262 |
|
| 4263 |
history_date_filter.change(
|
| 4264 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4265 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4266 |
+
outputs=[history_table, history_page_info],
|
| 4267 |
+
show_api=False
|
| 4268 |
).then(
|
| 4269 |
lambda: 1, # Reset to page 1 on filter change
|
| 4270 |
+
outputs=[history_current_page],
|
| 4271 |
+
show_api=False
|
| 4272 |
)
|
| 4273 |
|
| 4274 |
history_refresh_btn.click(
|
| 4275 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4276 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4277 |
+
outputs=[history_table, history_page_info],
|
| 4278 |
+
show_api=False
|
| 4279 |
)
|
| 4280 |
|
| 4281 |
# Pagination handlers (standalone)
|
| 4282 |
history_prev_btn.click(
|
| 4283 |
lambda page: max(1, page - 1),
|
| 4284 |
inputs=[history_current_page],
|
| 4285 |
+
outputs=[history_current_page],
|
| 4286 |
+
show_api=False
|
| 4287 |
).then(
|
| 4288 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4289 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4290 |
+
outputs=[history_table, history_page_info],
|
| 4291 |
+
show_api=False
|
| 4292 |
)
|
| 4293 |
|
| 4294 |
history_next_btn.click(
|
| 4295 |
lambda page: page + 1,
|
| 4296 |
inputs=[history_current_page],
|
| 4297 |
+
outputs=[history_current_page],
|
| 4298 |
+
show_api=False
|
| 4299 |
).then(
|
| 4300 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4301 |
inputs=[session_state, history_search, history_date_filter, history_current_page],
|
| 4302 |
+
outputs=[history_table, history_page_info],
|
| 4303 |
+
show_api=False
|
| 4304 |
)
|
| 4305 |
|
| 4306 |
# History search and filter handlers (results page tab)
|
| 4307 |
history_search_results.change(
|
| 4308 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4309 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4310 |
+
outputs=[history_table_results, history_page_info_results],
|
| 4311 |
+
show_api=False
|
| 4312 |
).then(
|
| 4313 |
lambda: 1, # Reset to page 1 on search
|
| 4314 |
+
outputs=[history_current_page_results],
|
| 4315 |
+
show_api=False
|
| 4316 |
)
|
| 4317 |
|
| 4318 |
history_date_filter_results.change(
|
| 4319 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4320 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4321 |
+
outputs=[history_table_results, history_page_info_results],
|
| 4322 |
+
show_api=False
|
| 4323 |
).then(
|
| 4324 |
lambda: 1, # Reset to page 1 on filter change
|
| 4325 |
+
outputs=[history_current_page_results],
|
| 4326 |
+
show_api=False
|
| 4327 |
)
|
| 4328 |
|
| 4329 |
history_refresh_btn_results.click(
|
| 4330 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4331 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4332 |
+
outputs=[history_table_results, history_page_info_results],
|
| 4333 |
+
show_api=False
|
| 4334 |
)
|
| 4335 |
|
| 4336 |
# Pagination handlers (results page)
|
| 4337 |
history_prev_btn_results.click(
|
| 4338 |
lambda page: max(1, page - 1),
|
| 4339 |
inputs=[history_current_page_results],
|
| 4340 |
+
outputs=[history_current_page_results],
|
| 4341 |
+
show_api=False
|
| 4342 |
).then(
|
| 4343 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4344 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4345 |
+
outputs=[history_table_results, history_page_info_results],
|
| 4346 |
+
show_api=False
|
| 4347 |
)
|
| 4348 |
|
| 4349 |
history_next_btn_results.click(
|
| 4350 |
lambda page: page + 1,
|
| 4351 |
inputs=[history_current_page_results],
|
| 4352 |
+
outputs=[history_current_page_results],
|
| 4353 |
+
show_api=False
|
| 4354 |
).then(
|
| 4355 |
lambda sess, query, date_filter, page: sync_filter_history(sess, query, date_filter, page),
|
| 4356 |
inputs=[session_state, history_search_results, history_date_filter_results, history_current_page_results],
|
| 4357 |
+
outputs=[history_table_results, history_page_info_results],
|
| 4358 |
+
show_api=False
|
| 4359 |
)
|
| 4360 |
|
| 4361 |
# Tax load holdings button
|
|
|
|
| 4620 |
# Password reset handlers
|
| 4621 |
forgot_password_btn.click(
|
| 4622 |
lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 4623 |
+
outputs=[login_container, password_reset_modal],
|
| 4624 |
+
show_api=False
|
| 4625 |
)
|
| 4626 |
|
| 4627 |
reset_cancel_btn.click(
|
| 4628 |
lambda: (gr.update(visible=True), gr.update(visible=False), ""),
|
| 4629 |
+
outputs=[login_container, password_reset_modal, reset_message],
|
| 4630 |
+
show_api=False
|
| 4631 |
)
|
| 4632 |
|
| 4633 |
update_cancel_btn.click(
|
| 4634 |
lambda: (gr.update(visible=True), gr.update(visible=False), ""),
|
| 4635 |
+
outputs=[login_container, password_update_modal, update_password_message],
|
| 4636 |
+
show_api=False
|
| 4637 |
)
|
| 4638 |
|
| 4639 |
reset_submit_btn.click(
|
|
|
|
| 4649 |
gr.update(visible=True) if token else gr.update(visible=False)
|
| 4650 |
),
|
| 4651 |
inputs=[recovery_token_hash],
|
| 4652 |
+
outputs=[login_container, password_update_modal],
|
| 4653 |
+
show_api=False
|
| 4654 |
)
|
| 4655 |
|
| 4656 |
# Password update handler (separate modal after email link click)
|
|
|
|
| 4665 |
gr.update(visible=True) if "✅" in msg else gr.update(visible=False)
|
| 4666 |
),
|
| 4667 |
inputs=[update_password_message],
|
| 4668 |
+
outputs=[password_update_modal, login_container],
|
| 4669 |
+
show_api=False
|
| 4670 |
)
|
| 4671 |
|
| 4672 |
def handle_demo_mode(current_session: Dict):
|
backend/agents/personas.py
CHANGED
|
@@ -239,6 +239,7 @@ class PersonaAnalysisAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
|
|
| 239 |
optimization_results: Dict[str, Any],
|
| 240 |
risk_analysis: Dict[str, Any],
|
| 241 |
ensemble_forecasts: Optional[Dict[str, Any]] = None,
|
|
|
|
| 242 |
risk_tolerance: str = "moderate",
|
| 243 |
) -> "AgentResult[PortfolioAnalysisOutput]":
|
| 244 |
"""Analyse a complete portfolio through the persona's lens.
|
|
@@ -254,7 +255,8 @@ class PersonaAnalysisAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
|
|
| 254 |
economic_data: Macroeconomic indicators
|
| 255 |
optimization_results: Portfolio optimisation outputs
|
| 256 |
risk_analysis: VaR, CVaR, and risk metrics
|
| 257 |
-
ensemble_forecasts: ML-based price forecasts
|
|
|
|
| 258 |
risk_tolerance: Investor's risk tolerance
|
| 259 |
|
| 260 |
Returns:
|
|
@@ -265,6 +267,7 @@ class PersonaAnalysisAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
|
|
| 265 |
format_economic_summary,
|
| 266 |
format_risk_analysis_xml,
|
| 267 |
format_optimisation_results_xml,
|
|
|
|
| 268 |
)
|
| 269 |
|
| 270 |
# Construct comprehensive analysis prompt (same as standard analyst)
|
|
@@ -277,6 +280,9 @@ class PersonaAnalysisAgent(BasePortfolioAgent[PortfolioAnalysisOutput]):
|
|
| 277 |
if ensemble_forecasts:
|
| 278 |
forecasts_section = f"\n\nML FORECASTS (30-day predictions):\n{ensemble_forecasts}"
|
| 279 |
|
|
|
|
|
|
|
|
|
|
| 280 |
# Add persona-specific context to the prompt
|
| 281 |
prompt = f"""Analyse this investment portfolio from {self.persona.name}'s perspective:
|
| 282 |
|
|
@@ -301,6 +307,9 @@ OPTIMISATION ANALYSIS:
|
|
| 301 |
RISK ANALYSIS:
|
| 302 |
{risk_xml}{forecasts_section}
|
| 303 |
|
|
|
|
|
|
|
|
|
|
| 304 |
INVESTOR RISK TOLERANCE: {risk_tolerance}
|
| 305 |
|
| 306 |
Provide a comprehensive analysis as {self.persona.name} would, with health score, key insights, and actionable recommendations
|
|
|
|
| 239 |
optimization_results: Dict[str, Any],
|
| 240 |
risk_analysis: Dict[str, Any],
|
| 241 |
ensemble_forecasts: Optional[Dict[str, Any]] = None,
|
| 242 |
+
sentiment_data: Optional[Dict[str, Any]] = None,
|
| 243 |
risk_tolerance: str = "moderate",
|
| 244 |
) -> "AgentResult[PortfolioAnalysisOutput]":
|
| 245 |
"""Analyse a complete portfolio through the persona's lens.
|
|
|
|
| 255 |
economic_data: Macroeconomic indicators
|
| 256 |
optimization_results: Portfolio optimisation outputs
|
| 257 |
risk_analysis: VaR, CVaR, and risk metrics
|
| 258 |
+
ensemble_forecasts: ML-based price forecasts (Chronos + statistical models)
|
| 259 |
+
sentiment_data: News sentiment analysis for each ticker
|
| 260 |
risk_tolerance: Investor's risk tolerance
|
| 261 |
|
| 262 |
Returns:
|
|
|
|
| 267 |
format_economic_summary,
|
| 268 |
format_risk_analysis_xml,
|
| 269 |
format_optimisation_results_xml,
|
| 270 |
+
format_sentiment_data,
|
| 271 |
)
|
| 272 |
|
| 273 |
# Construct comprehensive analysis prompt (same as standard analyst)
|
|
|
|
| 280 |
if ensemble_forecasts:
|
| 281 |
forecasts_section = f"\n\nML FORECASTS (30-day predictions):\n{ensemble_forecasts}"
|
| 282 |
|
| 283 |
+
# Format sentiment data if available
|
| 284 |
+
sentiment_table = format_sentiment_data(sentiment_data) if sentiment_data else "No sentiment data available"
|
| 285 |
+
|
| 286 |
# Add persona-specific context to the prompt
|
| 287 |
prompt = f"""Analyse this investment portfolio from {self.persona.name}'s perspective:
|
| 288 |
|
|
|
|
| 307 |
RISK ANALYSIS:
|
| 308 |
{risk_xml}{forecasts_section}
|
| 309 |
|
| 310 |
+
NEWS SENTIMENT (7-day analysis):
|
| 311 |
+
{sentiment_table}
|
| 312 |
+
|
| 313 |
INVESTOR RISK TOLERANCE: {risk_tolerance}
|
| 314 |
|
| 315 |
Provide a comprehensive analysis as {self.persona.name} would, with health score, key insights, and actionable recommendations
|