BrianIsaac commited on
Commit
2de8dd4
·
1 Parent(s): 15d570b

refactor(agents): optimize token usage in AdvisoryCouncil and PortfolioBuilderAgent

Browse files
backend/agents/council/advisory_council.py CHANGED
@@ -343,20 +343,20 @@ Bull Score: {consensus['bull_score']:.0f}% • Bear Score: {consensus['bear_scor
343
  if not data:
344
  return {
345
  "specialist": spec["name"],
346
- "analysis": f"No {spec['data_key']} data available for analysis.",
347
  "data_used": spec["data_key"]
348
  }
349
 
350
- prompt = f"""You are a {spec['name']} focusing on {spec['focus']}.
 
351
 
352
- Data: {str(data)[:800]}
 
353
 
354
- Provide:
355
  1. 3 key findings
356
- 2. Bullish/bearish factors
357
- 3. Confidence (0-100)
358
-
359
- Be concise."""
360
 
361
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
362
 
@@ -384,17 +384,17 @@ Be concise."""
384
  f"**{a['specialist']}**: {a['analysis']}" for a in analyses
385
  ])
386
 
387
- prompt = f"""Bull Researcher: Build bullish case from analyses.
 
388
 
389
  {analyses_text[:1500]}
390
 
391
- Provide:
 
392
  1. Thesis (2 sentences)
393
- 2. 3-4 key supporting points
394
- 3. Expected returns
395
- 4. Confidence (0-100)
396
-
397
- Be concise and persuasive."""
398
 
399
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
400
 
@@ -416,17 +416,17 @@ Be concise and persuasive."""
416
  f"**{a['specialist']}**: {a['analysis']}" for a in analyses
417
  ])
418
 
419
- prompt = f"""Bear Researcher: Build bearish case from analyses.
 
420
 
421
  {analyses_text[:1500]}
422
 
423
- Provide:
 
424
  1. Thesis (2 sentences)
425
- 2. 3-4 key warning signs
426
- 3. Downside risks
427
- 4. Confidence (0-100)
428
-
429
- Be concise and critical."""
430
 
431
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
432
 
@@ -608,18 +608,16 @@ Provide:
608
  latest_opponent = opponent_history[-1] if opponent_history else "No argument yet"
609
 
610
  # Build prompt
611
- prompt = f"""You are the {role} Researcher in round {round_num} of a debate.
612
-
613
- Your position: {own_history[0] if own_history else ''}
614
-
615
- {opponent_role}'s latest argument:
616
- {latest_opponent}
617
-
618
- Respond with a counter-argument. Be {'very aggressive' if contentiousness > 7 else 'moderately contentious' if contentiousness > 4 else 'measured'}.
619
-
620
- Include your confidence level (0-100) in your response.
621
- Format: "Confidence: X%"
622
- """
623
  return prompt
624
 
625
  def _extract_confidence(self, text: str) -> float:
@@ -636,3 +634,65 @@ Format: "Confidence: X%"
636
  return float(match.group(1))
637
 
638
  return 50.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  if not data:
344
  return {
345
  "specialist": spec["name"],
346
+ "analysis": f"No {spec['data_key']} data available.",
347
  "data_used": spec["data_key"]
348
  }
349
 
350
+ # Format data specifically for this specialist to save tokens
351
+ formatted_data = self._format_data_for_specialist(spec["name"], data)
352
 
353
+ prompt = f"""Role: {spec['name']} ({spec['focus']}).
354
+ Data: {formatted_data}
355
 
356
+ Output:
357
  1. 3 key findings
358
+ 2. Bull/Bear factors
359
+ 3. Confidence (0-100)"""
 
 
360
 
361
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
362
 
 
384
  f"**{a['specialist']}**: {a['analysis']}" for a in analyses
385
  ])
386
 
387
+ prompt = f"""Role: Bull Researcher.
388
+ Source: Specialist analyses below.
389
 
390
  {analyses_text[:1500]}
391
 
392
+ Task: Build bullish case.
393
+ Output:
394
  1. Thesis (2 sentences)
395
+ 2. 3 key points
396
+ 3. Exp. Return
397
+ 4. Confidence (0-100)"""
 
 
398
 
399
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
400
 
 
416
  f"**{a['specialist']}**: {a['analysis']}" for a in analyses
417
  ])
418
 
419
+ prompt = f"""Role: Bear Researcher.
420
+ Source: Specialist analyses below.
421
 
422
  {analyses_text[:1500]}
423
 
424
+ Task: Build bearish case.
425
+ Output:
426
  1. Thesis (2 sentences)
427
+ 2. 3 key risks
428
+ 3. Downside
429
+ 4. Confidence (0-100)"""
 
 
430
 
431
  response = await self.model.ainvoke([HumanMessage(content=prompt)])
432
 
 
608
  latest_opponent = opponent_history[-1] if opponent_history else "No argument yet"
609
 
610
  # Build prompt
611
+ # Build prompt
612
+ prompt = f"""Role: {role} (Round {round_num}).
613
+ My Stance: {own_history[0] if own_history else ''}
614
+ Opponent ({opponent_role}): {latest_opponent}
615
+
616
+ Task: Counter-argue. Tone: {'Aggressive' if contentiousness > 7 else 'Measured'}.
617
+ Output:
618
+ 1. Counter-points
619
+ 2. Evidence
620
+ 3. Confidence (0-100)"""
 
 
621
  return prompt
622
 
623
  def _extract_confidence(self, text: str) -> float:
 
634
  return float(match.group(1))
635
 
636
  return 50.0
637
+
638
+ def _format_data_for_specialist(self, specialist_name: str, data: Any) -> str:
639
+ """Format data specifically for each specialist to minimize token usage.
640
+
641
+ Instead of dumping raw JSON, extracts only relevant metrics.
642
+ """
643
+ if not data:
644
+ return "No data"
645
+
646
+ try:
647
+ if specialist_name == "Fundamental Analyst":
648
+ # Extract key ratios and metrics
649
+ if isinstance(data, dict):
650
+ # Handle FMP profile data
651
+ if "symbol" in data:
652
+ return (f"Symbol: {data.get('symbol')}, Price: {data.get('price')}, "
653
+ f"Mkt Cap: {data.get('mktCap')}, P/E: {data.get('mktCap')}, "
654
+ f"Beta: {data.get('beta')}, Ind: {data.get('industry')}")
655
+ # Handle general dict
656
+ return str({k: v for k, v in data.items() if k in
657
+ ['pe_ratio', 'eps', 'revenue_growth', 'profit_margin', 'debt_to_equity']})
658
+ return str(data)[:300]
659
+
660
+ elif specialist_name == "Technical Analyst":
661
+ # Extract indicators
662
+ if isinstance(data, dict):
663
+ return str({k: v for k, v in data.items() if k in
664
+ ['rsi', 'macd', 'bollinger', 'sma_50', 'sma_200', 'trend']})
665
+ return str(data)[:300]
666
+
667
+ elif specialist_name == "Sentiment Analyst":
668
+ # Extract sentiment scores and top headlines
669
+ if isinstance(data, dict):
670
+ summary = f"Score: {data.get('sentiment_score', 'N/A')}"
671
+ if 'news' in data and isinstance(data['news'], list):
672
+ headlines = [n.get('title', '') for n in data['news'][:3]]
673
+ summary += f", Headlines: {'; '.join(headlines)}"
674
+ return summary
675
+ return str(data)[:300]
676
+
677
+ elif specialist_name == "Macro Analyst":
678
+ # Extract key economic indicators
679
+ if isinstance(data, dict):
680
+ return str({k: v for k, v in data.items() if k in
681
+ ['gdp_growth', 'inflation_rate', 'interest_rate', 'unemployment']})
682
+ return str(data)[:300]
683
+
684
+ elif specialist_name == "Risk Analyst":
685
+ # Extract risk metrics
686
+ if isinstance(data, dict):
687
+ # Handle nested risk metrics
688
+ metrics = data.get('risk_metrics', data)
689
+ return str({k: v for k, v in metrics.items() if k in
690
+ ['sharpe_ratio', 'volatility', 'var_95', 'max_drawdown']})
691
+ return str(data)[:300]
692
+
693
+ else:
694
+ return str(data)[:300]
695
+
696
+ except Exception as e:
697
+ logger.warning(f"Error formatting data for {specialist_name}: {e}")
698
+ return str(data)[:300]
backend/agents/react_agent.py CHANGED
@@ -303,17 +303,42 @@ class PortfolioBuilderAgent:
303
  """Execute tools and truncate outputs to reduce token usage."""
304
  result = await base_tool_node.ainvoke(state)
305
 
306
- # Truncate tool message content to prevent excessive token usage
307
- max_tool_output_length = 300 # chars, ~75 tokens
308
  if "messages" in result:
309
  truncated_messages = []
310
  for msg in result["messages"]:
311
  if isinstance(msg, ToolMessage):
312
  content = str(msg.content)
313
- if len(content) > max_tool_output_length:
314
- truncated_content = content[:max_tool_output_length] + f"... (truncated {len(content) - max_tool_output_length} chars)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  msg.content = truncated_content
316
- logger.debug(f"Truncated tool output from {len(content)} to {len(truncated_content)} chars")
317
  truncated_messages.append(msg)
318
  result["messages"] = truncated_messages
319
 
@@ -368,7 +393,15 @@ class PortfolioBuilderAgent:
368
 
369
  return f"""Build portfolio. Goals: {goals}. Risk: {risk}/10. Constraints: {constraints}.
370
 
371
- Use tools efficiently. Output format:
 
 
 
 
 
 
 
 
372
  - TICKER: X% - brief reason
373
  Expected return: X%, Risk: Y%"""
374
 
 
303
  """Execute tools and truncate outputs to reduce token usage."""
304
  result = await base_tool_node.ainvoke(state)
305
 
306
+ # Smart truncation based on tool type
 
307
  if "messages" in result:
308
  truncated_messages = []
309
  for msg in result["messages"]:
310
  if isinstance(msg, ToolMessage):
311
  content = str(msg.content)
312
+ tool_name = msg.name
313
+
314
+ # Smart summarization based on tool name
315
+ if tool_name == "fetch_market_data":
316
+ # Extract just price and change
317
+ try:
318
+ import json
319
+ data = json.loads(content) if isinstance(content, str) else content
320
+ if isinstance(data, dict):
321
+ summary = {k: {"price": v.get("regularMarketPrice"), "change": v.get("regularMarketChangePercent")}
322
+ for k, v in data.items() if isinstance(v, dict)}
323
+ msg.content = str(summary)
324
+ except:
325
+ msg.content = content[:200] + "..."
326
+
327
+ elif tool_name == "get_news_sentiment":
328
+ # Extract just headlines and score
329
+ try:
330
+ import json
331
+ data = json.loads(content) if isinstance(content, str) else content
332
+ if isinstance(data, dict):
333
+ headlines = [n.get('title', '')[:50] for n in data.get('news', [])[:2]]
334
+ msg.content = f"Score: {data.get('sentiment_score')}, News: {headlines}"
335
+ except:
336
+ msg.content = content[:200] + "..."
337
+
338
+ elif len(content) > 300:
339
+ truncated_content = content[:300] + f"... (truncated {len(content) - 300} chars)"
340
  msg.content = truncated_content
341
+
342
  truncated_messages.append(msg)
343
  result["messages"] = truncated_messages
344
 
 
393
 
394
  return f"""Build portfolio. Goals: {goals}. Risk: {risk}/10. Constraints: {constraints}.
395
 
396
+ Tools: fetch_market_data, get_fundamentals, get_historical_prices, calculate_technicals, optimise_allocation, assess_risk, get_news_sentiment.
397
+
398
+ Process:
399
+ 1. Get data for candidates
400
+ 2. Filter by fundamentals/technicals
401
+ 3. Optimise weights
402
+ 4. Output portfolio
403
+
404
+ Output format:
405
  - TICKER: X% - brief reason
406
  Expected return: X%, Risk: Y%"""
407