522 lines
22 KiB
Python
522 lines
22 KiB
Python
from fastapi import FastAPI, Request, Form, BackgroundTasks, HTTPException
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
import jinja2
|
|
import os
|
|
from typing import Dict, Any
|
|
import threading
|
|
import time
|
|
from dotenv import load_dotenv
|
|
|
|
# Load environment variables from .env file
|
|
load_dotenv()
|
|
|
|
# Check required environment variables
|
|
required_env_vars = [
|
|
'FINNHUB_API_KEY',
|
|
'OPENAI_API_KEY',
|
|
#'REDDIT_CLIENT_ID',
|
|
#'REDDIT_CLIENT_SECRET',
|
|
#'REDDIT_USER_AGENT'
|
|
]
|
|
|
|
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
|
if missing_vars:
|
|
print(f"Error: Missing required environment variables: {', '.join(missing_vars)}")
|
|
print("Please create a .env file with these variables or set them in your environment.")
|
|
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
|
|
app = FastAPI()
|
|
|
|
# In-memory storage for the process state
|
|
# Using a lock for thread-safe access to app_state
|
|
app_state_lock = threading.Lock()
|
|
app_state: Dict[str, Any] = {
|
|
"process_running": False,
|
|
"company_symbol": None,
|
|
"execution_tree": [],
|
|
"overall_status": "idle", # idle, in_progress, completed, error
|
|
"overall_progress": 0 # 0-100
|
|
}
|
|
|
|
# Mount the static directory to serve CSS, JS, etc.
|
|
app.mount("/static", StaticFiles(directory="webapp/static"), name="static")
|
|
|
|
# Setup Jinja2 for templating
|
|
template_dir = os.path.join(os.path.dirname(__file__), "templates")
|
|
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))
|
|
|
|
def update_execution_state(state: Dict[str, Any]):
|
|
"""Callback function to update the app_state based on LangGraph's state."""
|
|
print(f"📡 Callback received state keys: {list(state.keys())}")
|
|
|
|
with app_state_lock:
|
|
# Initialize the complete execution tree structure if not exists
|
|
if not app_state["execution_tree"] or (
|
|
len(app_state["execution_tree"]) == 1 and
|
|
app_state["execution_tree"][0]["id"] == "initialization"
|
|
):
|
|
app_state["execution_tree"] = initialize_complete_execution_tree()
|
|
|
|
# Map LangGraph node names to our tracking system
|
|
agent_state_mapping = {
|
|
"Market Analyst": {
|
|
"phase": "data_collection",
|
|
"agent_id": "market_analyst",
|
|
"report_key": "market_report",
|
|
"report_name": "Market Analysis Report"
|
|
},
|
|
"Social Analyst": {
|
|
"phase": "data_collection",
|
|
"agent_id": "social_analyst",
|
|
"report_key": "sentiment_report",
|
|
"report_name": "Sentiment Analysis Report"
|
|
},
|
|
"News Analyst": {
|
|
"phase": "data_collection",
|
|
"agent_id": "news_analyst",
|
|
"report_key": "news_report",
|
|
"report_name": "News Analysis Report"
|
|
},
|
|
"Fundamentals Analyst": {
|
|
"phase": "data_collection",
|
|
"agent_id": "fundamentals_analyst",
|
|
"report_key": "fundamentals_report",
|
|
"report_name": "Fundamentals Report"
|
|
},
|
|
"Bull Researcher": {
|
|
"phase": "research",
|
|
"agent_id": "bull_researcher",
|
|
"report_key": "investment_debate_state.bull_history",
|
|
"report_name": "Bull Case Analysis"
|
|
},
|
|
"Bear Researcher": {
|
|
"phase": "research",
|
|
"agent_id": "bear_researcher",
|
|
"report_key": "investment_debate_state.bear_history",
|
|
"report_name": "Bear Case Analysis"
|
|
},
|
|
"Research Manager": {
|
|
"phase": "research",
|
|
"agent_id": "research_manager",
|
|
"report_key": "investment_debate_state.judge_decision",
|
|
"report_name": "Research Synthesis"
|
|
},
|
|
"Trade Planner": {
|
|
"phase": "planning",
|
|
"agent_id": "trade_planner",
|
|
"report_key": "trader_investment_plan",
|
|
"report_name": "Trading Plan"
|
|
},
|
|
"Trader": {
|
|
"phase": "execution",
|
|
"agent_id": "trader",
|
|
"report_key": "investment_plan",
|
|
"report_name": "Execution Report"
|
|
},
|
|
"Risky Analyst": {
|
|
"phase": "risk_analysis",
|
|
"agent_id": "risky_analyst",
|
|
"report_key": "risk_debate_state.risky_history",
|
|
"report_name": "Risk Assessment (Aggressive)"
|
|
},
|
|
"Neutral Analyst": {
|
|
"phase": "risk_analysis",
|
|
"agent_id": "neutral_analyst",
|
|
"report_key": "risk_debate_state.neutral_history",
|
|
"report_name": "Risk Assessment (Neutral)"
|
|
},
|
|
"Safe Analyst": {
|
|
"phase": "risk_analysis",
|
|
"agent_id": "safe_analyst",
|
|
"report_key": "risk_debate_state.safe_history",
|
|
"report_name": "Risk Assessment (Conservative)"
|
|
},
|
|
"Risk Judge": {
|
|
"phase": "risk_analysis",
|
|
"agent_id": "risk_judge",
|
|
"report_key": "final_trade_decision",
|
|
"report_name": "Final Risk Decision"
|
|
}
|
|
}
|
|
|
|
# Update agent statuses based on available reports
|
|
for agent_name, agent_info in agent_state_mapping.items():
|
|
# Check if this agent has completed (has report data)
|
|
report_data = get_nested_value(state, agent_info["report_key"])
|
|
if report_data:
|
|
update_agent_status(agent_info, "completed", report_data, state)
|
|
|
|
# Update overall progress
|
|
root_node = app_state["execution_tree"][0]
|
|
total_agents = len(agent_state_mapping)
|
|
completed_agents = count_completed_agents(root_node)
|
|
app_state["overall_progress"] = min(100, int((completed_agents / max(total_agents, 1)) * 100))
|
|
|
|
print(f"📊 Progress updated: {app_state['overall_progress']}% ({completed_agents}/{total_agents} agents)")
|
|
|
|
def initialize_complete_execution_tree():
|
|
"""Initialize the complete execution tree with all agents in pending state."""
|
|
return [{
|
|
"id": "root",
|
|
"name": f"📈 Trading Analysis for {app_state['company_symbol']}",
|
|
"status": "in_progress",
|
|
"content": f"Comprehensive trading analysis for {app_state['company_symbol']}",
|
|
"children": [
|
|
{
|
|
"id": "data_collection_phase",
|
|
"name": "📊 Data Collection Phase",
|
|
"status": "pending",
|
|
"content": "Collecting market data and analysis from various sources",
|
|
"children": [
|
|
create_agent_node("market_analyst", "📈 Market Analyst"),
|
|
create_agent_node("social_analyst", "📱 Social Media Analyst"),
|
|
create_agent_node("news_analyst", "📰 News Analyst"),
|
|
create_agent_node("fundamentals_analyst", "📊 Fundamentals Analyst")
|
|
]
|
|
},
|
|
{
|
|
"id": "research_phase",
|
|
"name": "🔍 Research Phase",
|
|
"status": "pending",
|
|
"content": "Research and debate investment perspectives",
|
|
"children": [
|
|
create_agent_node("bull_researcher", "🐂 Bull Researcher"),
|
|
create_agent_node("bear_researcher", "🐻 Bear Researcher"),
|
|
create_agent_node("research_manager", "🔍 Research Manager")
|
|
]
|
|
},
|
|
{
|
|
"id": "planning_phase",
|
|
"name": "📋 Planning Phase",
|
|
"status": "pending",
|
|
"content": "Develop trading strategy and execution plan",
|
|
"children": [
|
|
create_agent_node("trade_planner", "📋 Trade Planner")
|
|
]
|
|
},
|
|
{
|
|
"id": "execution_phase",
|
|
"name": "⚡ Execution Phase",
|
|
"status": "pending",
|
|
"content": "Execute trades based on analysis and planning",
|
|
"children": [
|
|
create_agent_node("trader", "⚡ Trader")
|
|
]
|
|
},
|
|
{
|
|
"id": "risk_analysis_phase",
|
|
"name": "⚠️ Risk Management Phase",
|
|
"status": "pending",
|
|
"content": "Assess and manage investment risks",
|
|
"children": [
|
|
create_agent_node("risky_analyst", "🚨 Aggressive Risk Analyst"),
|
|
create_agent_node("neutral_analyst", "⚖️ Neutral Risk Analyst"),
|
|
create_agent_node("safe_analyst", "🛡️ Conservative Risk Analyst"),
|
|
create_agent_node("risk_judge", "⚠️ Risk Judge")
|
|
]
|
|
}
|
|
],
|
|
"timestamp": time.time()
|
|
}]
|
|
|
|
def create_agent_node(agent_id: str, agent_name: str):
|
|
"""Create a standardized agent node with report and messages sub-items."""
|
|
return {
|
|
"id": agent_id,
|
|
"name": agent_name,
|
|
"status": "pending",
|
|
"content": f"Agent: {agent_name} - Awaiting execution",
|
|
"children": [
|
|
{
|
|
"id": f"{agent_id}_report",
|
|
"name": "📄 Report",
|
|
"status": "pending",
|
|
"content": "Report not yet generated",
|
|
"children": [],
|
|
"timestamp": time.time()
|
|
},
|
|
{
|
|
"id": f"{agent_id}_messages",
|
|
"name": "💬 Messages",
|
|
"status": "pending",
|
|
"content": "No messages yet",
|
|
"children": [],
|
|
"timestamp": time.time()
|
|
}
|
|
],
|
|
"timestamp": time.time()
|
|
}
|
|
|
|
def get_nested_value(data: dict, key_path: str):
|
|
"""Get value from nested dict using dot notation (e.g., 'investment_debate_state.bull_history')."""
|
|
keys = key_path.split('.')
|
|
value = data
|
|
for key in keys:
|
|
if isinstance(value, dict) and key in value:
|
|
value = value[key]
|
|
else:
|
|
return None
|
|
return value
|
|
|
|
def update_agent_status(agent_info: dict, status: str, report_data: any, full_state: dict):
|
|
"""Update an agent's status and content in the execution tree."""
|
|
root_node = app_state["execution_tree"][0]
|
|
|
|
# Find the agent in the tree
|
|
agent_node = find_agent_in_tree(agent_info["agent_id"], root_node)
|
|
if not agent_node:
|
|
return
|
|
|
|
# Update agent status
|
|
if agent_node["status"] != "completed":
|
|
agent_node["status"] = status
|
|
agent_node["content"] = f"✅ {agent_node['name']} - Analysis completed"
|
|
|
|
# Update report sub-item
|
|
report_node = find_item_by_id(f"{agent_info['agent_id']}_report", agent_node["children"])
|
|
if report_node:
|
|
report_node["status"] = "completed"
|
|
report_node["content"] = format_report_content(agent_info["report_name"], report_data)
|
|
|
|
# Update messages sub-item (extract from state if available)
|
|
messages_node = find_item_by_id(f"{agent_info['agent_id']}_messages", agent_node["children"])
|
|
if messages_node:
|
|
messages_node["status"] = "completed"
|
|
messages_node["content"] = extract_agent_messages(full_state, agent_info["agent_id"])
|
|
|
|
# Update phase status if all agents in phase are completed
|
|
update_phase_status_if_complete(agent_info["phase"], root_node)
|
|
|
|
def find_agent_in_tree(agent_id: str, root_node: dict):
|
|
"""Find an agent node in the execution tree."""
|
|
for phase in root_node["children"]:
|
|
for agent in phase["children"]:
|
|
if agent["id"] == agent_id:
|
|
return agent
|
|
return None
|
|
|
|
def find_item_by_id(item_id: str, items: list):
|
|
"""Find an item by ID in a list of items."""
|
|
for item in items:
|
|
if item["id"] == item_id:
|
|
return item
|
|
return None
|
|
|
|
def format_report_content(report_name: str, report_data: any) -> str:
|
|
"""Format report data for display."""
|
|
if isinstance(report_data, str):
|
|
return f"📄 {report_name}\n\n{report_data}"
|
|
elif isinstance(report_data, dict):
|
|
return f"📄 {report_name}\n\n{str(report_data)}"
|
|
elif isinstance(report_data, list) and report_data:
|
|
# For debate histories, show the latest message
|
|
latest = report_data[-1] if report_data else "No data"
|
|
return f"📄 {report_name}\n\n{str(latest)}"
|
|
else:
|
|
return f"📄 {report_name}\n\nReport generated successfully"
|
|
|
|
def extract_agent_messages(state: dict, agent_id: str) -> str:
|
|
"""Extract relevant messages for an agent from the state."""
|
|
# This is a simplified version - could be enhanced to extract actual messages
|
|
messages = state.get("messages", [])
|
|
if messages:
|
|
return f"💬 Agent Messages\n\n{len(messages)} messages exchanged during execution"
|
|
else:
|
|
return "💬 Agent Messages\n\nExecution completed without specific message logs"
|
|
|
|
def update_phase_status_if_complete(phase_id: str, root_node: dict):
|
|
"""Update phase status to completed if all its agents are completed."""
|
|
phase_node = find_item_by_id(f"{phase_id}_phase", root_node["children"])
|
|
if not phase_node:
|
|
return
|
|
|
|
# Check if all agents in this phase are completed
|
|
all_completed = all(agent["status"] == "completed" for agent in phase_node["children"])
|
|
if all_completed and phase_node["status"] != "completed":
|
|
phase_node["status"] = "completed"
|
|
phase_node["content"] = f"✅ {phase_node['name']} - All agents completed successfully"
|
|
|
|
def count_completed_agents(root_node: dict) -> int:
|
|
"""Count the number of completed agents across all phases."""
|
|
count = 0
|
|
for phase in root_node["children"]:
|
|
for agent in phase["children"]:
|
|
if agent["status"] == "completed":
|
|
count += 1
|
|
return count
|
|
|
|
def run_trading_process(company_symbol: str, config: Dict[str, Any]):
|
|
"""Runs the TradingAgentsGraph in a separate thread."""
|
|
with app_state_lock:
|
|
app_state["overall_status"] = "in_progress"
|
|
app_state["overall_progress"] = 0
|
|
|
|
try:
|
|
# Import and create custom config
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
|
|
# Create custom configuration with user selections
|
|
custom_config = DEFAULT_CONFIG.copy()
|
|
custom_config["llm_provider"] = config["llm_provider"]
|
|
custom_config["max_debate_rounds"] = config["max_debate_rounds"]
|
|
custom_config["cost_per_trade"] = config["cost_per_trade"]
|
|
|
|
# Set the appropriate LLM models based on provider
|
|
if config["llm_provider"] == "google":
|
|
custom_config["gemini_quick_think_llm"] = config["quick_think_llm"]
|
|
custom_config["gemini_deep_think_llm"] = config["deep_think_llm"]
|
|
else:
|
|
custom_config["quick_think_llm"] = config["quick_think_llm"]
|
|
custom_config["deep_think_llm"] = config["deep_think_llm"]
|
|
|
|
# Set backend URL based on provider
|
|
if config["llm_provider"] == "openrouter":
|
|
custom_config["backend_url"] = "https://openrouter.ai/api/v1"
|
|
elif config["llm_provider"] == "google":
|
|
custom_config["backend_url"] = "https://generativelanguage.googleapis.com/v1"
|
|
elif config["llm_provider"] == "anthropic":
|
|
custom_config["backend_url"] = "https://api.anthropic.com/"
|
|
elif config["llm_provider"] == "ollama":
|
|
custom_config["backend_url"] = f"http://{os.getenv('OLLAMA_HOST', 'localhost')}:11434/v1"
|
|
else: # openai
|
|
custom_config["backend_url"] = "https://api.openai.com/v1"
|
|
|
|
print(f"🚀 Initializing TradingAgentsGraph for {company_symbol}")
|
|
graph = TradingAgentsGraph(config=custom_config)
|
|
analysis_date = config["analysis_date"] # Use user-selected date
|
|
print(f"🔄 Starting propagation for {company_symbol} on {analysis_date}")
|
|
|
|
# The propagate method now accepts the callback and trade_date
|
|
final_state, processed_signal = graph.propagate(company_symbol, trade_date=analysis_date, on_step_callback=update_execution_state)
|
|
print(f"✅ Propagation completed for {company_symbol}")
|
|
|
|
with app_state_lock:
|
|
app_state["overall_status"] = "completed"
|
|
app_state["overall_progress"] = 100
|
|
# Update the root node status to completed
|
|
if app_state["execution_tree"]:
|
|
app_state["execution_tree"][0]["status"] = "completed"
|
|
app_state["execution_tree"][0]["content"] = f"✅ Analysis completed successfully!\n\nFinal Decision: {processed_signal}\n\nFull State: {str(final_state)}"
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
error_detail = traceback.format_exc()
|
|
with app_state_lock:
|
|
app_state["overall_status"] = "error"
|
|
app_state["overall_progress"] = 100
|
|
if app_state["execution_tree"]:
|
|
app_state["execution_tree"][0]["status"] = "error"
|
|
app_state["execution_tree"][0]["content"] = f"Error during execution: {str(e)}\n\n{error_detail}"
|
|
# Add a specific error item to the tree
|
|
app_state["execution_tree"].append({
|
|
"id": "error",
|
|
"name": "Process Error",
|
|
"status": "error",
|
|
"content": f"Error during execution: {str(e)}\n\n{error_detail}",
|
|
"children": [],
|
|
"timestamp": time.time()
|
|
})
|
|
finally:
|
|
with app_state_lock:
|
|
app_state["process_running"] = False
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def read_root():
|
|
template = jinja_env.get_template("index.html")
|
|
return template.render(app_state=app_state)
|
|
|
|
@app.post("/start", response_class=HTMLResponse)
|
|
async def start_process(
|
|
background_tasks: BackgroundTasks,
|
|
company_symbol: str = Form(...),
|
|
llm_provider: str = Form(...),
|
|
quick_think_llm: str = Form(...),
|
|
deep_think_llm: str = Form(...),
|
|
max_debate_rounds: int = Form(...),
|
|
cost_per_trade: float = Form(...),
|
|
analysis_date: str = Form(...)
|
|
):
|
|
# Check if all required environment variables are set
|
|
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
|
if missing_vars:
|
|
app_state["overall_status"] = "error"
|
|
app_state["execution_tree"] = [{
|
|
"id": "error",
|
|
"name": "Configuration Error",
|
|
"status": "error",
|
|
"content": f"Missing required environment variables: {', '.join(missing_vars)}. Please check .env.example file.",
|
|
"children": [],
|
|
"timestamp": time.time()
|
|
}]
|
|
template = jinja_env.get_template("_partials/left_panel.html")
|
|
return template.render(tree=app_state["execution_tree"], app_state=app_state)
|
|
|
|
with app_state_lock:
|
|
if app_state["process_running"]:
|
|
# Optionally, return an error or a message that a process is already running
|
|
template = jinja_env.get_template("_partials/left_panel.html")
|
|
return template.render(tree=app_state["execution_tree"], app_state=app_state)
|
|
|
|
app_state["process_running"] = True
|
|
app_state["company_symbol"] = company_symbol
|
|
app_state["overall_status"] = "in_progress"
|
|
app_state["overall_progress"] = 5 # Show initial progress
|
|
|
|
# Store all configuration parameters
|
|
app_state["config"] = {
|
|
"llm_provider": llm_provider,
|
|
"quick_think_llm": quick_think_llm,
|
|
"deep_think_llm": deep_think_llm,
|
|
"max_debate_rounds": max_debate_rounds,
|
|
"cost_per_trade": cost_per_trade,
|
|
"analysis_date": analysis_date
|
|
}
|
|
|
|
# Initialize execution tree with startup message
|
|
app_state["execution_tree"] = [{
|
|
"id": "initialization",
|
|
"name": f"🚀 Initializing Trading Analysis for {company_symbol}",
|
|
"status": "in_progress",
|
|
"content": f"Starting comprehensive trading analysis for {company_symbol}...\n\nConfiguration:\n• LLM Provider: {llm_provider}\n• Quick Think Model: {quick_think_llm}\n• Deep Think Model: {deep_think_llm}\n• Max Debate Rounds: {max_debate_rounds}\n• Cost Per Trade: ${cost_per_trade}\n• Analysis Date: {analysis_date}\n\nInitializing trading agents and preparing analysis pipeline...",
|
|
"children": [],
|
|
"timestamp": time.time()
|
|
}]
|
|
|
|
background_tasks.add_task(run_trading_process, company_symbol, app_state["config"])
|
|
|
|
template = jinja_env.get_template("_partials/left_panel.html")
|
|
return template.render(tree=app_state["execution_tree"], app_state=app_state)
|
|
|
|
@app.get("/status", response_class=HTMLResponse)
|
|
async def get_status():
|
|
with app_state_lock:
|
|
template = jinja_env.get_template("_partials/left_panel.html")
|
|
return template.render(tree=app_state["execution_tree"], app_state=app_state)
|
|
|
|
def find_item_in_tree(item_id: str, tree: list) -> Dict[str, Any] | None:
|
|
"""Recursively searches the execution tree for an item by its ID."""
|
|
for item in tree:
|
|
if item["id"] == item_id:
|
|
return item
|
|
if item["children"]:
|
|
found_child = find_item_in_tree(item_id, item["children"])
|
|
if found_child:
|
|
return found_child
|
|
return None
|
|
|
|
@app.get("/content/{item_id}", response_class=HTMLResponse)
|
|
async def get_item_content(item_id: str):
|
|
with app_state_lock:
|
|
item = find_item_in_tree(item_id, app_state["execution_tree"])
|
|
if item:
|
|
template = jinja_env.get_template("_partials/right_panel.html")
|
|
return template.render(content=item.get("content", "No content available."))
|
|
else:
|
|
return HTMLResponse(content="<p>Item not found.</p>", status_code=404)
|
|
|
|
# To run this app:
|
|
# uvicorn webapp.main:app --reload
|