Initial commit of integrated agent system
This commit is contained in:
321
backend/utils/msg_adapter.py
Normal file
321
backend/utils/msg_adapter.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Message Adapter - Converts AgentScope Msg to frontend JSON format
|
||||
Ensures compatibility with existing frontend without modifications
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrontendAdapter:
|
||||
"""
|
||||
Adapter to convert AgentScope messages to frontend-compatible format
|
||||
|
||||
Frontend expects specific message types:
|
||||
- agent: Agent thinking/analysis messages
|
||||
- team_summary: Portfolio summary with equity curves
|
||||
- team_holdings: Current portfolio holdings
|
||||
- team_stats: Portfolio statistics
|
||||
- team_trades: Trade history
|
||||
- team_leaderboard: Agent performance rankings
|
||||
- price_update: Real-time price updates
|
||||
- system: System notifications
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def parse(msg: Msg) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Parse AgentScope Msg to frontend format
|
||||
|
||||
Args:
|
||||
msg: AgentScope Msg object
|
||||
|
||||
Returns:
|
||||
Dictionary in frontend format, or None if message should be skipped
|
||||
"""
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
# Determine message type based on metadata or content
|
||||
msg_type = FrontendAdapter._determine_type(msg)
|
||||
|
||||
if msg_type == "agent":
|
||||
return FrontendAdapter._format_agent_msg(msg)
|
||||
elif msg_type == "portfolio_update":
|
||||
return FrontendAdapter._format_portfolio_msg(msg)
|
||||
elif msg_type == "system":
|
||||
return FrontendAdapter._format_system_msg(msg)
|
||||
else:
|
||||
# Default: treat as agent message
|
||||
return FrontendAdapter._format_agent_msg(msg)
|
||||
|
||||
@staticmethod
|
||||
def _determine_type(msg: Msg) -> str:
|
||||
"""Determine frontend message type from Msg"""
|
||||
# Check metadata for explicit type
|
||||
if hasattr(msg, "metadata") and msg.metadata:
|
||||
if "type" in msg.metadata:
|
||||
return msg.metadata["type"]
|
||||
|
||||
# Check if message contains portfolio update
|
||||
if "portfolio" in msg.metadata:
|
||||
return "portfolio_update"
|
||||
|
||||
# Check message name/role
|
||||
if msg.name == "system":
|
||||
return "system"
|
||||
|
||||
# Default to agent message
|
||||
return "agent"
|
||||
|
||||
@staticmethod
|
||||
def _format_agent_msg(msg: object) -> Dict[str, Any]:
|
||||
"""
|
||||
Format agent message for frontend
|
||||
|
||||
Args:
|
||||
msg: Either AgentScope Msg or dict from pipeline results
|
||||
|
||||
Frontend expects:
|
||||
{
|
||||
"type": "agent",
|
||||
"role_key": "analyst_id",
|
||||
"content": "message text",
|
||||
"timestamp": "ISO timestamp"
|
||||
}
|
||||
"""
|
||||
# Handle dict from pipeline results
|
||||
if isinstance(msg, dict):
|
||||
name = msg.get("agent", "unknown")
|
||||
content = msg.get("content", "")
|
||||
else:
|
||||
# Handle Msg object
|
||||
name = msg.name
|
||||
content = msg.content
|
||||
|
||||
return {
|
||||
"type": "agent",
|
||||
"role_key": name,
|
||||
"content": content
|
||||
if isinstance(content, str)
|
||||
else json.dumps(content),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _format_portfolio_msg(msg: Msg) -> Dict[str, Any]:
|
||||
"""
|
||||
Format portfolio update message
|
||||
|
||||
This typically generates multiple frontend messages:
|
||||
- team_summary
|
||||
- team_holdings
|
||||
- team_stats
|
||||
- team_trades (if trades were executed)
|
||||
"""
|
||||
metadata = msg.metadata or {}
|
||||
portfolio = metadata.get("portfolio", {})
|
||||
|
||||
messages: List[Dict[str, Any]] = []
|
||||
|
||||
# Generate holdings message
|
||||
holdings = FrontendAdapter.build_holdings(portfolio)
|
||||
if holdings:
|
||||
messages.append(
|
||||
{
|
||||
"type": "team_holdings",
|
||||
"data": holdings,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
# Generate stats message
|
||||
stats = FrontendAdapter.build_stats(portfolio)
|
||||
if stats:
|
||||
messages.append(
|
||||
{
|
||||
"type": "team_stats",
|
||||
"data": stats,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
# Generate trades message if execution logs exist
|
||||
execution_logs = metadata.get("execution_logs", [])
|
||||
if execution_logs:
|
||||
trades = FrontendAdapter.build_trades(execution_logs)
|
||||
messages.append(
|
||||
{
|
||||
"type": "team_trades",
|
||||
"mode": "incremental",
|
||||
"data": trades,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
)
|
||||
|
||||
# Return composite message
|
||||
return {
|
||||
"type": "composite",
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _format_system_msg(msg: Msg) -> Dict[str, Any]:
|
||||
"""Format system message"""
|
||||
return {
|
||||
"type": "system",
|
||||
"content": msg.content
|
||||
if isinstance(msg.content, str)
|
||||
else json.dumps(msg.content),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def build_holdings(
|
||||
portfolio: Dict[str, Any],
|
||||
prices: Dict[str, float] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Build holdings array from portfolio state"""
|
||||
holdings = []
|
||||
prices = prices or {}
|
||||
|
||||
positions = portfolio.get("positions", {})
|
||||
cash = portfolio.get("cash", 0.0)
|
||||
|
||||
# Calculate total value using current prices
|
||||
total_value = cash
|
||||
for ticker, position in positions.items():
|
||||
long_shares = position.get("long", 0)
|
||||
short_shares = position.get("short", 0)
|
||||
price = prices.get(ticker) or position.get("avg_price", 0)
|
||||
total_value += (long_shares - short_shares) * price
|
||||
|
||||
# Build holdings for each position
|
||||
for ticker, position in positions.items():
|
||||
long_shares = position.get("long", 0)
|
||||
short_shares = position.get("short", 0)
|
||||
avg_price = position.get("avg_price", 0)
|
||||
current_price = prices.get(ticker) or avg_price
|
||||
|
||||
net_shares = long_shares - short_shares
|
||||
if net_shares == 0:
|
||||
continue
|
||||
|
||||
market_value = net_shares * current_price
|
||||
weight = market_value / total_value if total_value > 0 else 0
|
||||
|
||||
holdings.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"quantity": net_shares,
|
||||
"avg": avg_price,
|
||||
"currentPrice": current_price,
|
||||
"marketValue": market_value,
|
||||
"weight": weight,
|
||||
},
|
||||
)
|
||||
|
||||
# Add cash as a holding
|
||||
if cash > 0:
|
||||
holdings.append(
|
||||
{
|
||||
"ticker": "CASH",
|
||||
"quantity": 1,
|
||||
"avg": cash,
|
||||
"currentPrice": cash,
|
||||
"marketValue": cash,
|
||||
"weight": cash / total_value if total_value > 0 else 0,
|
||||
},
|
||||
)
|
||||
|
||||
return holdings
|
||||
|
||||
@staticmethod
|
||||
def build_stats(
|
||||
portfolio: Dict[str, Any],
|
||||
prices: Dict[str, float] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build stats dictionary from portfolio"""
|
||||
prices = prices or {}
|
||||
positions = portfolio.get("positions", {})
|
||||
cash = portfolio.get("cash", 0.0)
|
||||
margin_used = portfolio.get("margin_used", 0.0)
|
||||
|
||||
# Calculate total value using current prices
|
||||
total_value = cash
|
||||
for ticker, position in positions.items():
|
||||
long_shares = position.get("long", 0)
|
||||
short_shares = position.get("short", 0)
|
||||
price = prices.get(ticker) or position.get("avg_price", 0)
|
||||
total_value += (long_shares - short_shares) * price
|
||||
|
||||
# Calculate ticker weights
|
||||
ticker_weights = {}
|
||||
for ticker, position in positions.items():
|
||||
long_shares = position.get("long", 0)
|
||||
short_shares = position.get("short", 0)
|
||||
price = prices.get(ticker) or position.get("avg_price", 0)
|
||||
|
||||
market_value = (long_shares - short_shares) * price
|
||||
if market_value != 0:
|
||||
ticker_weights[ticker] = (
|
||||
market_value / total_value if total_value > 0 else 0
|
||||
)
|
||||
|
||||
# Calculate total return
|
||||
initial_cash = portfolio.get("initial_cash", 100000.0)
|
||||
total_return = (
|
||||
((total_value - initial_cash) / initial_cash * 100)
|
||||
if initial_cash > 0
|
||||
else 0.0
|
||||
)
|
||||
|
||||
return {
|
||||
"totalAssetValue": round(total_value, 2),
|
||||
"totalReturn": round(total_return, 2),
|
||||
"cashPosition": round(cash, 2),
|
||||
"tickerWeights": ticker_weights,
|
||||
"marginUsed": round(margin_used, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def build_trades(execution_logs: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Build trades array from execution logs
|
||||
|
||||
Frontend expects:
|
||||
[{
|
||||
"ts": 1234567890,
|
||||
"ticker": "AAPL",
|
||||
"side": "LONG",
|
||||
"qty": 100,
|
||||
"price": 150.0,
|
||||
"reason": "Buy signal"
|
||||
}, ...]
|
||||
"""
|
||||
trades = []
|
||||
timestamp = int(datetime.now().timestamp() * 1000)
|
||||
|
||||
for log in execution_logs:
|
||||
# Parse execution log (simplified - should use structured data)
|
||||
if "Executed" in log:
|
||||
# Extract trade details from log string
|
||||
# in real implementation, pass structured data
|
||||
trades.append(
|
||||
{
|
||||
"ts": timestamp,
|
||||
"ticker": "UNKNOWN", # Should parse from log
|
||||
"side": "LONG", # Should parse from log
|
||||
"qty": 0, # Should parse from log
|
||||
"price": 0.0, # Should parse from log
|
||||
"reason": log,
|
||||
},
|
||||
)
|
||||
|
||||
return trades
|
||||
Reference in New Issue
Block a user