feat: add Chainlit web UI + Dockerfile for Railway deployment
Adds a Chainlit-based web interface that wraps TradingAgentsGraph, streaming analyst reports, research debates, and final decisions to the browser in real-time. Configured for Anthropic Claude models. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
48ef57715e
commit
eade96f1c9
|
|
@ -0,0 +1,16 @@
|
|||
[project]
|
||||
enable_telemetry = false
|
||||
|
||||
[UI]
|
||||
name = "TradingAgents"
|
||||
description = "Multi-Agent LLM Trading Analysis"
|
||||
default_collapse_content = true
|
||||
hide_cot = false
|
||||
|
||||
[UI.theme.light]
|
||||
primary = "#1a73e8"
|
||||
background = "#ffffff"
|
||||
|
||||
[UI.theme.dark]
|
||||
primary = "#00f0ff"
|
||||
background = "#09090b"
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# System deps for building Python packages
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc g++ && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Chainlit listens on $PORT (Railway sets this automatically)
|
||||
CMD chainlit run app.py --host 0.0.0.0 --port ${PORT:-8000}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
"""Chainlit web UI for TradingAgents — deployed on Railway."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
import chainlit as cl
|
||||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from cli.stats_handler import StatsCallbackHandler
|
||||
|
||||
|
||||
def parse_ticker_date(text: str):
|
||||
"""Extract ticker symbol and optional date from user message.
|
||||
|
||||
Examples:
|
||||
"NVDA" -> ("NVDA", today)
|
||||
"Analyze AAPL 2024-12-01" -> ("AAPL", "2024-12-01")
|
||||
"What about TSLA?" -> ("TSLA", today)
|
||||
"""
|
||||
# Try to find a date (YYYY-MM-DD)
|
||||
date_match = re.search(r"(\d{4}-\d{2}-\d{2})", text)
|
||||
trade_date = date_match.group(1) if date_match else str(date.today())
|
||||
|
||||
# Find uppercase 1-5 letter words as candidate tickers
|
||||
candidates = re.findall(r"\b([A-Z]{1,5})\b", text)
|
||||
# Filter out common English words
|
||||
skip = {"I", "A", "THE", "AND", "OR", "FOR", "TO", "IN", "ON", "AT", "IS",
|
||||
"IT", "OF", "BY", "AS", "AN", "BE", "IF", "SO", "DO", "MY", "UP",
|
||||
"NO", "NOT", "ALL", "BUT", "HOW", "GET", "HAS", "HAD", "CAN",
|
||||
"WHAT", "ABOUT", "BUY", "SELL", "HOLD"}
|
||||
tickers = [c for c in candidates if c not in skip]
|
||||
|
||||
ticker = tickers[0] if tickers else None
|
||||
return ticker, trade_date
|
||||
|
||||
|
||||
def build_config():
|
||||
"""Build TradingAgents config for Anthropic/Claude."""
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["llm_provider"] = "anthropic"
|
||||
config["deep_think_llm"] = os.getenv("DEEP_THINK_MODEL", "claude-sonnet-4-5-20241022")
|
||||
config["quick_think_llm"] = os.getenv("QUICK_THINK_MODEL", "claude-haiku-4-5-20251001")
|
||||
config["backend_url"] = None
|
||||
config["max_debate_rounds"] = 1
|
||||
config["max_risk_discuss_rounds"] = 1
|
||||
config["data_vendors"] = {
|
||||
"core_stock_apis": "yfinance",
|
||||
"technical_indicators": "yfinance",
|
||||
"fundamental_data": "yfinance",
|
||||
"news_data": "yfinance",
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
# Report field -> display name
|
||||
REPORT_NAMES = {
|
||||
"market_report": "Market Analyst",
|
||||
"sentiment_report": "Sentiment Analyst",
|
||||
"news_report": "News Analyst",
|
||||
"fundamentals_report": "Fundamentals Analyst",
|
||||
}
|
||||
|
||||
|
||||
@cl.on_chat_start
|
||||
async def on_chat_start():
|
||||
await cl.Message(
|
||||
content=(
|
||||
"**TradingAgents** — Multi-Agent LLM Trading Analysis\n\n"
|
||||
"Send a ticker symbol to analyze. Examples:\n"
|
||||
"- `NVDA`\n"
|
||||
"- `Analyze AAPL 2024-12-01`\n"
|
||||
"- `What's the outlook for TSLA?`\n\n"
|
||||
"I'll run a team of AI analysts, researchers, traders, and risk managers "
|
||||
"to produce a trading decision."
|
||||
)
|
||||
).send()
|
||||
|
||||
|
||||
@cl.on_message
|
||||
async def on_message(message: cl.Message):
|
||||
ticker, trade_date = parse_ticker_date(message.content)
|
||||
|
||||
if not ticker:
|
||||
await cl.Message(
|
||||
content="I couldn't find a ticker symbol. Try something like `NVDA` or `Analyze AAPL 2024-12-01`."
|
||||
).send()
|
||||
return
|
||||
|
||||
# Status message
|
||||
status_msg = cl.Message(content=f"Analyzing **{ticker}** for **{trade_date}**...")
|
||||
await status_msg.send()
|
||||
|
||||
# Build graph
|
||||
config = build_config()
|
||||
stats = StatsCallbackHandler()
|
||||
|
||||
try:
|
||||
graph = TradingAgentsGraph(
|
||||
selected_analysts=["market", "social", "news", "fundamentals"],
|
||||
debug=False,
|
||||
config=config,
|
||||
callbacks=[stats],
|
||||
)
|
||||
except Exception as e:
|
||||
await cl.Message(content=f"Failed to initialize agents: {e}").send()
|
||||
return
|
||||
|
||||
# Create initial state and stream
|
||||
init_state = graph.propagator.create_initial_state(ticker, trade_date)
|
||||
args = graph.propagator.get_graph_args(callbacks=[stats])
|
||||
|
||||
# Track which reports/phases we've already shown
|
||||
seen_reports = set()
|
||||
seen_debate = False
|
||||
seen_risk = False
|
||||
seen_trader = False
|
||||
final_state = None
|
||||
|
||||
try:
|
||||
async for chunk in graph.graph.astream(init_state, **args):
|
||||
final_state = chunk
|
||||
|
||||
# --- Analyst reports ---
|
||||
for field, name in REPORT_NAMES.items():
|
||||
if field not in seen_reports and chunk.get(field):
|
||||
seen_reports.add(field)
|
||||
report = chunk[field]
|
||||
# Show as a collapsible Step
|
||||
async with cl.Step(name=f"{name} Report", type="tool") as step:
|
||||
step.output = report[:3000] if len(report) > 3000 else report
|
||||
|
||||
# --- Investment debate (Bull vs Bear) ---
|
||||
debate = chunk.get("investment_debate_state")
|
||||
if debate and not seen_debate and debate.get("judge_decision"):
|
||||
seen_debate = True
|
||||
async with cl.Step(name="Research Debate", type="tool") as step:
|
||||
step.output = (
|
||||
f"**Judge Decision:**\n{debate['judge_decision']}"
|
||||
)
|
||||
|
||||
# --- Trader plan ---
|
||||
if not seen_trader and chunk.get("trader_investment_plan"):
|
||||
seen_trader = True
|
||||
async with cl.Step(name="Trader Plan", type="tool") as step:
|
||||
step.output = chunk["trader_investment_plan"][:3000]
|
||||
|
||||
# --- Risk debate ---
|
||||
risk = chunk.get("risk_debate_state")
|
||||
if risk and not seen_risk and risk.get("judge_decision"):
|
||||
seen_risk = True
|
||||
async with cl.Step(name="Risk Assessment", type="tool") as step:
|
||||
step.output = f"**Risk Decision:**\n{risk['judge_decision']}"
|
||||
|
||||
except Exception as e:
|
||||
await cl.Message(content=f"Error during analysis: {e}").send()
|
||||
return
|
||||
|
||||
if not final_state:
|
||||
await cl.Message(content="Analysis produced no results.").send()
|
||||
return
|
||||
|
||||
# Process final decision
|
||||
decision_text = final_state.get("final_trade_decision", "No decision reached.")
|
||||
signal = graph.process_signal(decision_text)
|
||||
|
||||
# Stats summary
|
||||
s = stats.get_stats()
|
||||
stats_line = (
|
||||
f"*{s['llm_calls']} LLM calls · {s['tool_calls']} tool calls · "
|
||||
f"{s['tokens_in']:,} tokens in · {s['tokens_out']:,} tokens out*"
|
||||
)
|
||||
|
||||
await cl.Message(
|
||||
content=(
|
||||
f"## {ticker} — Trading Decision\n\n"
|
||||
f"**Signal: {signal}**\n\n"
|
||||
f"---\n\n"
|
||||
f"{decision_text}\n\n"
|
||||
f"---\n{stats_line}"
|
||||
)
|
||||
).send()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
[build]
|
||||
builder = "DOCKERFILE"
|
||||
dockerfilePath = "Dockerfile"
|
||||
|
||||
[deploy]
|
||||
healthcheckPath = "/"
|
||||
restartPolicyType = "ON_FAILURE"
|
||||
restartPolicyMaxRetries = 3
|
||||
Loading…
Reference in New Issue