feat: Initialize frontend and backend project structure
- Added frontend directory with initial React/Vite setup including: - Basic component structure - TailwindCSS configuration - Router setup - Added backend directory structure - Added FRONTEND_README.md documentation
This commit is contained in:
parent
13b826a31d
commit
b4c0d46681
|
|
@ -0,0 +1,134 @@
|
|||
# TradingAgents Frontend
|
||||
|
||||
This document describes how to run the TradingAgents web application.
|
||||
|
||||
## Architecture
|
||||
|
||||
The application consists of two parts:
|
||||
1. **Backend** (`backend/`) - FastAPI server that wraps TradingAgentsGraph
|
||||
2. **Frontend** (`frontend/`) - Next.js React application
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ with uv or pip
|
||||
- Node.js 18+ and npm
|
||||
- Environment variables configured (see `.env` file)
|
||||
|
||||
## Setup
|
||||
|
||||
### Backend Setup
|
||||
|
||||
1. Install backend dependencies:
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
# Or if using uv:
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Ensure the main TradingAgents package is installed (it should be in the parent directory)
|
||||
|
||||
3. Set environment variables in `.env`:
|
||||
```
|
||||
OPENAI_API_KEY=your_key_here
|
||||
ALPHA_VANTAGE_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
1. Install frontend dependencies:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Create `.env.local` file:
|
||||
```
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
NEXT_PUBLIC_WS_URL=ws://localhost:8000
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Start the Backend
|
||||
|
||||
From the project root directory:
|
||||
|
||||
```bash
|
||||
# Option 1: Using the run script
|
||||
cd backend
|
||||
python run.py
|
||||
|
||||
# Option 2: Using uvicorn from project root
|
||||
uvicorn backend.api.main:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# Option 3: Using Python module from project root
|
||||
python -m backend.api.main
|
||||
```
|
||||
|
||||
The backend will be available at `http://localhost:8000`
|
||||
API documentation: `http://localhost:8000/docs`
|
||||
|
||||
### Start the Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will be available at `http://localhost:3000`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open `http://localhost:3000` in your browser
|
||||
2. Click "New Analysis" to start a trading analysis
|
||||
3. Fill in the form:
|
||||
- Ticker symbol (e.g., "SPY")
|
||||
- Analysis date
|
||||
- Select analysts
|
||||
- Configure LLM settings
|
||||
4. Click "Start Analysis"
|
||||
5. View real-time progress and results
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Analysis**: Watch agents work in real-time via WebSocket
|
||||
- **Agent Progress**: See status of all agents (Analyst Team, Research Team, etc.)
|
||||
- **Report Viewer**: View generated reports with collapsible sections
|
||||
- **History**: Browse and view previous analyses
|
||||
- **Configuration**: Save and load analysis configurations
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `POST /api/analysis/start` - Start new analysis
|
||||
- `GET /api/analysis/{id}/status` - Get analysis status
|
||||
- `GET /api/analysis/{id}/results` - Get analysis results
|
||||
- `WS /ws/analysis/{id}/stream` - WebSocket stream for updates
|
||||
- `GET /api/history` - List historical analyses
|
||||
- `GET /api/history/{ticker}/{date}` - Get specific historical analysis
|
||||
- `GET /api/config/presets` - List configuration presets
|
||||
- `POST /api/config/save` - Save configuration preset
|
||||
|
||||
## Development
|
||||
|
||||
### Backend Development
|
||||
|
||||
The backend uses FastAPI with automatic reload. Changes to Python files will trigger a reload.
|
||||
|
||||
### Frontend Development
|
||||
|
||||
The frontend uses Next.js with hot module replacement. Changes to React components will update automatically.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Backend won't start**: Check that all dependencies are installed and environment variables are set
|
||||
2. **Frontend can't connect**: Verify `NEXT_PUBLIC_API_URL` matches the backend URL
|
||||
3. **WebSocket connection fails**: Ensure the backend is running and CORS is configured correctly
|
||||
4. **Analysis fails**: Check API keys in `.env` file and verify they're valid
|
||||
|
||||
## Notes
|
||||
|
||||
- The CLI (`cli/main.py`) continues to work independently
|
||||
- All existing TradingAgents code remains unchanged
|
||||
- Results are saved to the `results/` directory (same as CLI)
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from .main import app
|
||||
|
||||
__all__ = ["app"]
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import tradingagents
|
||||
backend_dir = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
from .routes import analysis_router, history_router, config_router
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(
|
||||
title="TradingAgents API",
|
||||
description="API for TradingAgents Multi-Agent Trading Framework",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"http://localhost:3000", # Next.js dev server
|
||||
"http://localhost:3001",
|
||||
os.getenv("FRONTEND_URL", "http://localhost:3000"),
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(analysis_router)
|
||||
app.include_router(history_router)
|
||||
app.include_router(config_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint."""
|
||||
return {
|
||||
"message": "TradingAgents API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from .schemas import (
|
||||
AnalystType,
|
||||
LLMProvider,
|
||||
AnalysisRequest,
|
||||
AgentStatus,
|
||||
MessageUpdate,
|
||||
ToolCallUpdate,
|
||||
ReportSection,
|
||||
AnalysisStatus,
|
||||
StreamUpdate,
|
||||
InvestmentDebateState,
|
||||
RiskDebateState,
|
||||
AnalysisResults,
|
||||
HistoricalAnalysisSummary,
|
||||
ConfigPreset,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AnalystType",
|
||||
"LLMProvider",
|
||||
"AnalysisRequest",
|
||||
"AgentStatus",
|
||||
"MessageUpdate",
|
||||
"ToolCallUpdate",
|
||||
"ReportSection",
|
||||
"AnalysisStatus",
|
||||
"StreamUpdate",
|
||||
"InvestmentDebateState",
|
||||
"RiskDebateState",
|
||||
"AnalysisResults",
|
||||
"HistoricalAnalysisSummary",
|
||||
"ConfigPreset",
|
||||
]
|
||||
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any, Literal
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AnalystType(str, Enum):
|
||||
MARKET = "market"
|
||||
SOCIAL = "social"
|
||||
NEWS = "news"
|
||||
FUNDAMENTALS = "fundamentals"
|
||||
|
||||
|
||||
class LLMProvider(str, Enum):
|
||||
OPENAI = "openai"
|
||||
ANTHROPIC = "anthropic"
|
||||
GOOGLE = "google"
|
||||
OPENROUTER = "openrouter"
|
||||
OLLAMA = "ollama"
|
||||
|
||||
|
||||
class AnalysisRequest(BaseModel):
|
||||
ticker: str = Field(..., description="Stock ticker symbol")
|
||||
analysis_date: str = Field(..., description="Analysis date in YYYY-MM-DD format")
|
||||
analysts: List[AnalystType] = Field(
|
||||
default=[AnalystType.MARKET, AnalystType.SOCIAL, AnalystType.NEWS, AnalystType.FUNDAMENTALS],
|
||||
description="List of analysts to include"
|
||||
)
|
||||
research_depth: int = Field(default=1, ge=1, le=10, description="Number of debate rounds")
|
||||
llm_provider: LLMProvider = Field(default=LLMProvider.OPENAI, description="LLM provider")
|
||||
backend_url: str = Field(default="https://api.openai.com/v1", description="Backend API URL")
|
||||
quick_think_llm: str = Field(default="gpt-4o-mini", description="Quick thinking LLM model")
|
||||
deep_think_llm: str = Field(default="o4-mini", description="Deep thinking LLM model")
|
||||
data_vendors: Optional[Dict[str, str]] = Field(
|
||||
default=None,
|
||||
description="Data vendor configuration"
|
||||
)
|
||||
|
||||
|
||||
class AgentStatus(BaseModel):
|
||||
agent: str
|
||||
status: Literal["pending", "in_progress", "completed", "error"]
|
||||
team: Optional[str] = None
|
||||
|
||||
|
||||
class MessageUpdate(BaseModel):
|
||||
timestamp: str
|
||||
type: str
|
||||
content: str
|
||||
|
||||
|
||||
class ToolCallUpdate(BaseModel):
|
||||
timestamp: str
|
||||
tool_name: str
|
||||
args: Dict[str, Any]
|
||||
|
||||
|
||||
class ReportSection(BaseModel):
|
||||
section_name: str
|
||||
content: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class AnalysisStatus(BaseModel):
|
||||
analysis_id: str
|
||||
status: Literal["pending", "running", "completed", "error"]
|
||||
ticker: str
|
||||
analysis_date: str
|
||||
started_at: Optional[str] = None
|
||||
completed_at: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class StreamUpdate(BaseModel):
|
||||
type: Literal[
|
||||
"status",
|
||||
"message",
|
||||
"tool_call",
|
||||
"report",
|
||||
"agent_status",
|
||||
"debate_update",
|
||||
"risk_debate_update",
|
||||
"final_decision"
|
||||
]
|
||||
data: Dict[str, Any]
|
||||
timestamp: str
|
||||
|
||||
|
||||
class InvestmentDebateState(BaseModel):
|
||||
bull_history: Optional[str] = None
|
||||
bear_history: Optional[str] = None
|
||||
judge_decision: Optional[str] = None
|
||||
count: int = 0
|
||||
|
||||
|
||||
class RiskDebateState(BaseModel):
|
||||
risky_history: Optional[str] = None
|
||||
safe_history: Optional[str] = None
|
||||
neutral_history: Optional[str] = None
|
||||
current_risky_response: Optional[str] = None
|
||||
current_safe_response: Optional[str] = None
|
||||
current_neutral_response: Optional[str] = None
|
||||
judge_decision: Optional[str] = None
|
||||
count: int = 0
|
||||
|
||||
|
||||
class AnalysisResults(BaseModel):
|
||||
analysis_id: str
|
||||
ticker: str
|
||||
analysis_date: str
|
||||
market_report: Optional[str] = None
|
||||
sentiment_report: Optional[str] = None
|
||||
news_report: Optional[str] = None
|
||||
fundamentals_report: Optional[str] = None
|
||||
investment_debate_state: Optional[InvestmentDebateState] = None
|
||||
trader_investment_plan: Optional[str] = None
|
||||
risk_debate_state: Optional[RiskDebateState] = None
|
||||
final_trade_decision: Optional[str] = None
|
||||
processed_signal: Optional[str] = None
|
||||
completed_at: str
|
||||
|
||||
|
||||
class HistoricalAnalysisSummary(BaseModel):
|
||||
ticker: str
|
||||
analysis_date: str
|
||||
completed_at: Optional[str] = None
|
||||
has_results: bool = False
|
||||
|
||||
|
||||
class ConfigPreset(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
config: Dict[str, Any]
|
||||
created_at: str
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .analysis import router as analysis_router
|
||||
from .history import router as history_router
|
||||
from .config import router as config_router
|
||||
|
||||
__all__ = ["analysis_router", "history_router", "config_router"]
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, HTTPException
|
||||
from typing import Dict, Any
|
||||
|
||||
from ..models.schemas import AnalysisRequest, AnalysisStatus, AnalysisResults
|
||||
from ..services.analysis_service import AnalysisService
|
||||
from ..websocket.stream_handler import StreamHandler
|
||||
|
||||
router = APIRouter(prefix="/api/analysis", tags=["analysis"])
|
||||
|
||||
# Initialize services (these would be injected via dependency injection in production)
|
||||
analysis_service = AnalysisService()
|
||||
stream_handler = StreamHandler(analysis_service)
|
||||
|
||||
|
||||
@router.post("/start", response_model=Dict[str, str])
|
||||
async def start_analysis(request: AnalysisRequest):
|
||||
"""Start a new analysis and return analysis_id."""
|
||||
# Start analysis first to get the ID
|
||||
analysis_id = analysis_service.start_analysis(request, None)
|
||||
|
||||
# Create and store update callback that sends to WebSocket
|
||||
async def send_update(update):
|
||||
await stream_handler.send_update(analysis_id, update)
|
||||
|
||||
# Store callback in the analysis data
|
||||
if analysis_id in analysis_service.active_analyses:
|
||||
analysis_service.active_analyses[analysis_id]["update_callback"] = send_update
|
||||
|
||||
return {"analysis_id": analysis_id}
|
||||
|
||||
|
||||
@router.get("/{analysis_id}/status", response_model=AnalysisStatus)
|
||||
async def get_analysis_status(analysis_id: str):
|
||||
"""Get the status of an analysis."""
|
||||
status = analysis_service.get_analysis_status(analysis_id)
|
||||
if not status:
|
||||
raise HTTPException(status_code=404, detail="Analysis not found")
|
||||
return status
|
||||
|
||||
|
||||
@router.get("/{analysis_id}/results", response_model=Dict[str, Any])
|
||||
async def get_analysis_results(analysis_id: str):
|
||||
"""Get the results of a completed analysis."""
|
||||
results = analysis_service.get_analysis_results(analysis_id)
|
||||
if not results:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Analysis not found or not completed"
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
@router.websocket("/{analysis_id}/stream")
|
||||
async def stream_analysis_updates(websocket: WebSocket, analysis_id: str):
|
||||
"""WebSocket endpoint for streaming analysis updates."""
|
||||
await stream_handler.handle_stream(websocket, analysis_id)
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.schemas import ConfigPreset
|
||||
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
|
||||
# Store configs in a simple JSON file (could use a database in production)
|
||||
CONFIG_FILE = Path("backend/config_presets.json")
|
||||
|
||||
|
||||
def load_presets() -> List[Dict[str, Any]]:
|
||||
"""Load configuration presets from file."""
|
||||
if not CONFIG_FILE.exists():
|
||||
return []
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_presets(presets: List[Dict[str, Any]]):
|
||||
"""Save configuration presets to file."""
|
||||
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(presets, f, indent=2)
|
||||
|
||||
|
||||
@router.get("/presets", response_model=List[ConfigPreset])
|
||||
async def list_config_presets():
|
||||
"""List all saved configuration presets."""
|
||||
presets = load_presets()
|
||||
return [ConfigPreset(**preset) for preset in presets]
|
||||
|
||||
|
||||
@router.post("/save", response_model=ConfigPreset)
|
||||
async def save_config_preset(preset: ConfigPreset):
|
||||
"""Save a configuration preset."""
|
||||
presets = load_presets()
|
||||
|
||||
# Check if preset with same name exists
|
||||
for i, existing in enumerate(presets):
|
||||
if existing["name"] == preset.name:
|
||||
presets[i] = preset.model_dump()
|
||||
save_presets(presets)
|
||||
return preset
|
||||
|
||||
# Add new preset
|
||||
presets.append(preset.model_dump())
|
||||
save_presets(presets)
|
||||
return preset
|
||||
|
||||
|
||||
@router.delete("/presets/{name}")
|
||||
async def delete_config_preset(name: str):
|
||||
"""Delete a configuration preset."""
|
||||
presets = load_presets()
|
||||
original_count = len(presets)
|
||||
presets = [p for p in presets if p["name"] != name]
|
||||
|
||||
if len(presets) == original_count:
|
||||
raise HTTPException(status_code=404, detail="Preset not found")
|
||||
|
||||
save_presets(presets)
|
||||
return {"message": "Preset deleted"}
|
||||
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to path if not already there
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from ..models.schemas import HistoricalAnalysisSummary, AnalysisResults
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
|
||||
router = APIRouter(prefix="/api/history", tags=["history"])
|
||||
|
||||
|
||||
def get_results_dir() -> Path:
|
||||
"""Get the results directory path."""
|
||||
results_dir = DEFAULT_CONFIG.get("results_dir", "./results")
|
||||
return Path(results_dir)
|
||||
|
||||
|
||||
@router.get("", response_model=List[HistoricalAnalysisSummary])
|
||||
async def list_historical_analyses():
|
||||
"""List all historical analyses."""
|
||||
results_dir = get_results_dir()
|
||||
if not results_dir.exists():
|
||||
return []
|
||||
|
||||
analyses = []
|
||||
|
||||
# Iterate through ticker directories
|
||||
for ticker_dir in results_dir.iterdir():
|
||||
if not ticker_dir.is_dir():
|
||||
continue
|
||||
|
||||
ticker = ticker_dir.name
|
||||
|
||||
# Iterate through date directories
|
||||
for date_dir in ticker_dir.iterdir():
|
||||
if not date_dir.is_dir():
|
||||
continue
|
||||
|
||||
analysis_date = date_dir.name
|
||||
|
||||
# Check if reports directory exists
|
||||
reports_dir = date_dir / "reports"
|
||||
has_results = reports_dir.exists() and any(reports_dir.glob("*.md"))
|
||||
|
||||
analyses.append(HistoricalAnalysisSummary(
|
||||
ticker=ticker,
|
||||
analysis_date=analysis_date,
|
||||
has_results=has_results,
|
||||
completed_at=None # Could parse from log file if needed
|
||||
))
|
||||
|
||||
# Sort by date (most recent first)
|
||||
analyses.sort(key=lambda x: x.analysis_date, reverse=True)
|
||||
return analyses
|
||||
|
||||
|
||||
@router.get("/{ticker}/{date}", response_model=Dict[str, Any])
|
||||
async def get_historical_analysis(ticker: str, date: str):
|
||||
"""Get a specific historical analysis."""
|
||||
results_dir = get_results_dir()
|
||||
analysis_dir = results_dir / ticker / date
|
||||
|
||||
if not analysis_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="Analysis not found")
|
||||
|
||||
reports_dir = analysis_dir / "reports"
|
||||
|
||||
# Load reports
|
||||
reports = {}
|
||||
report_files = {
|
||||
"market_report": "market_report.md",
|
||||
"sentiment_report": "sentiment_report.md",
|
||||
"news_report": "news_report.md",
|
||||
"fundamentals_report": "fundamentals_report.md",
|
||||
"trader_investment_plan": "trader_investment_plan.md",
|
||||
"final_trade_decision": "final_trade_decision.md",
|
||||
}
|
||||
|
||||
for key, filename in report_files.items():
|
||||
file_path = reports_dir / filename
|
||||
if file_path.exists():
|
||||
with open(file_path, "r") as f:
|
||||
reports[key] = f.read()
|
||||
|
||||
return {
|
||||
"ticker": ticker,
|
||||
"analysis_date": date,
|
||||
"reports": reports,
|
||||
"has_results": len(reports) > 0,
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from .analysis_service import AnalysisService
|
||||
|
||||
__all__ = ["AnalysisService"]
|
||||
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Callable, AsyncGenerator
|
||||
from pathlib import Path
|
||||
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from ..models.schemas import AnalysisRequest, AnalysisStatus, StreamUpdate
|
||||
|
||||
|
||||
class AnalysisService:
|
||||
"""Service that wraps TradingAgentsGraph and handles analysis execution."""
|
||||
|
||||
def __init__(self):
|
||||
self.active_analyses: Dict[str, Dict[str, Any]] = {}
|
||||
self.completed_analyses: Dict[str, Dict[str, Any]] = {}
|
||||
# Track agent statuses for each analysis
|
||||
self.agent_statuses: Dict[str, Dict[str, str]] = {}
|
||||
|
||||
def extract_content_string(self, content):
|
||||
"""Extract string content from various message formats."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Handle Anthropic's list format
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict):
|
||||
if item.get('type') == 'text':
|
||||
text_parts.append(item.get('text', ''))
|
||||
elif item.get('type') == 'tool_use':
|
||||
text_parts.append(f"[Tool: {item.get('name', 'unknown')}]")
|
||||
else:
|
||||
text_parts.append(str(item))
|
||||
return ' '.join(text_parts)
|
||||
else:
|
||||
return str(content)
|
||||
|
||||
def start_analysis(
|
||||
self,
|
||||
request: AnalysisRequest,
|
||||
update_callback: Optional[Callable[[StreamUpdate], None]] = None
|
||||
) -> str:
|
||||
"""Start a new analysis and return analysis_id."""
|
||||
analysis_id = str(uuid.uuid4())
|
||||
|
||||
# Create config from request
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
config["max_debate_rounds"] = request.research_depth
|
||||
config["max_risk_discuss_rounds"] = request.research_depth
|
||||
config["quick_think_llm"] = request.quick_think_llm
|
||||
config["deep_think_llm"] = request.deep_think_llm
|
||||
config["backend_url"] = request.backend_url
|
||||
config["llm_provider"] = request.llm_provider.value
|
||||
|
||||
if request.data_vendors:
|
||||
config["data_vendors"].update(request.data_vendors)
|
||||
|
||||
# Initialize agent statuses
|
||||
self.agent_statuses[analysis_id] = {
|
||||
"Market Analyst": "pending",
|
||||
"Social Analyst": "pending",
|
||||
"News Analyst": "pending",
|
||||
"Fundamentals Analyst": "pending",
|
||||
"Bull Researcher": "pending",
|
||||
"Bear Researcher": "pending",
|
||||
"Research Manager": "pending",
|
||||
"Trader": "pending",
|
||||
"Risky Analyst": "pending",
|
||||
"Neutral Analyst": "pending",
|
||||
"Safe Analyst": "pending",
|
||||
"Portfolio Manager": "pending",
|
||||
}
|
||||
|
||||
# Initialize analysis status
|
||||
self.active_analyses[analysis_id] = {
|
||||
"status": "running",
|
||||
"ticker": request.ticker,
|
||||
"analysis_date": request.analysis_date,
|
||||
"started_at": datetime.now().isoformat(),
|
||||
"config": config,
|
||||
"request": request,
|
||||
"final_state": None,
|
||||
"processed_signal": None,
|
||||
"update_callback": update_callback,
|
||||
}
|
||||
|
||||
# Set first analyst to in_progress (will be sent when analysis starts)
|
||||
first_analyst = request.analysts[0].value.capitalize() + " Analyst"
|
||||
if first_analyst in self.agent_statuses[analysis_id]:
|
||||
self.agent_statuses[analysis_id][first_analyst] = "in_progress"
|
||||
|
||||
# Run analysis in background task
|
||||
asyncio.create_task(
|
||||
self._run_analysis(analysis_id, request, config, update_callback)
|
||||
)
|
||||
|
||||
return analysis_id
|
||||
|
||||
async def _run_analysis(
|
||||
self,
|
||||
analysis_id: str,
|
||||
request: AnalysisRequest,
|
||||
config: Dict[str, Any],
|
||||
update_callback: Optional[Callable] = None
|
||||
):
|
||||
"""Run the analysis and stream updates."""
|
||||
try:
|
||||
# Get update callback from stored analysis if not provided
|
||||
if update_callback is None and analysis_id in self.active_analyses:
|
||||
stored_callback = self.active_analyses[analysis_id].get("update_callback")
|
||||
if stored_callback:
|
||||
update_callback = stored_callback
|
||||
# Initialize the graph
|
||||
graph = TradingAgentsGraph(
|
||||
selected_analysts=[analyst.value for analyst in request.analysts],
|
||||
config=config,
|
||||
debug=True
|
||||
)
|
||||
|
||||
# Create result directory
|
||||
results_dir = Path(config["results_dir"]) / request.ticker / request.analysis_date
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_dir = results_dir / "reports"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Initialize state
|
||||
init_agent_state = graph.propagator.create_initial_state(
|
||||
request.ticker, request.analysis_date
|
||||
)
|
||||
args = graph.propagator.get_graph_args()
|
||||
|
||||
# Send initial agent status for first analyst
|
||||
if update_callback and analysis_id in self.agent_statuses:
|
||||
first_analyst = request.analysts[0].value.capitalize() + " Analyst"
|
||||
if first_analyst in self.agent_statuses[analysis_id]:
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": first_analyst, "status": "in_progress"},
|
||||
timestamp=datetime.now().isoformat()
|
||||
))
|
||||
|
||||
# Stream the analysis
|
||||
trace = []
|
||||
for chunk in graph.graph.stream(init_agent_state, **args):
|
||||
if update_callback:
|
||||
await self._process_chunk(chunk, update_callback, analysis_id, request.analysts)
|
||||
trace.append(chunk)
|
||||
|
||||
# Get final state
|
||||
final_state = trace[-1] if trace else None
|
||||
processed_signal = None
|
||||
|
||||
if final_state:
|
||||
processed_signal = graph.process_signal(final_state.get("final_trade_decision", ""))
|
||||
|
||||
# Save reports
|
||||
self._save_reports(final_state, report_dir)
|
||||
|
||||
# Update analysis status
|
||||
self.active_analyses[analysis_id]["final_state"] = final_state
|
||||
self.active_analyses[analysis_id]["processed_signal"] = processed_signal
|
||||
self.active_analyses[analysis_id]["status"] = "completed"
|
||||
self.active_analyses[analysis_id]["completed_at"] = datetime.now().isoformat()
|
||||
|
||||
# Move to completed
|
||||
self.completed_analyses[analysis_id] = self.active_analyses.pop(analysis_id)
|
||||
|
||||
# Send final update
|
||||
if update_callback:
|
||||
try:
|
||||
await update_callback(StreamUpdate(
|
||||
type="final_decision",
|
||||
data={
|
||||
"final_trade_decision": final_state.get("final_trade_decision", ""),
|
||||
"processed_signal": processed_signal,
|
||||
},
|
||||
timestamp=datetime.now().isoformat()
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"Error sending final update: {e}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self.active_analyses[analysis_id]["status"] = "error"
|
||||
self.active_analyses[analysis_id]["error"] = error_msg
|
||||
self.active_analyses[analysis_id]["completed_at"] = datetime.now().isoformat()
|
||||
|
||||
if update_callback:
|
||||
try:
|
||||
await update_callback(StreamUpdate(
|
||||
type="status",
|
||||
data={"error": error_msg},
|
||||
timestamp=datetime.now().isoformat()
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"Error sending error update: {e}")
|
||||
|
||||
async def _process_chunk(
|
||||
self,
|
||||
chunk: Dict[str, Any],
|
||||
update_callback: Optional[Callable] = None,
|
||||
analysis_id: Optional[str] = None,
|
||||
selected_analysts: Optional[list] = None
|
||||
):
|
||||
"""Process a chunk from the graph stream and send updates."""
|
||||
if not update_callback:
|
||||
return
|
||||
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
# Process messages
|
||||
if "messages" in chunk and len(chunk["messages"]) > 0:
|
||||
last_message = chunk["messages"][-1]
|
||||
|
||||
if hasattr(last_message, "content"):
|
||||
content = self.extract_content_string(last_message.content)
|
||||
msg_type = "Reasoning"
|
||||
else:
|
||||
content = str(last_message)
|
||||
msg_type = "System"
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="message",
|
||||
data={
|
||||
"type": msg_type,
|
||||
"content": content,
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Process tool calls
|
||||
if hasattr(last_message, "tool_calls"):
|
||||
for tool_call in last_message.tool_calls:
|
||||
if isinstance(tool_call, dict):
|
||||
tool_name = tool_call.get("name", "unknown")
|
||||
tool_args = tool_call.get("args", {})
|
||||
else:
|
||||
tool_name = tool_call.name
|
||||
tool_args = tool_call.args
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="tool_call",
|
||||
data={
|
||||
"tool_name": tool_name,
|
||||
"args": tool_args,
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Process reports and update agent statuses
|
||||
report_sections = [
|
||||
("market_report", "Market Analyst"),
|
||||
("sentiment_report", "Social Analyst"),
|
||||
("news_report", "News Analyst"),
|
||||
("fundamentals_report", "Fundamentals Analyst"),
|
||||
]
|
||||
|
||||
for section, agent_name in report_sections:
|
||||
if section in chunk and chunk[section]:
|
||||
# Mark current analyst as completed
|
||||
if analysis_id and agent_name in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][agent_name] = "completed"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": agent_name, "status": "completed"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Set next analyst to in_progress
|
||||
if analysis_id and selected_analysts:
|
||||
analyst_map = {
|
||||
"market": "Market Analyst",
|
||||
"social": "Social Analyst",
|
||||
"news": "News Analyst",
|
||||
"fundamentals": "Fundamentals Analyst",
|
||||
}
|
||||
|
||||
# Find next analyst
|
||||
current_idx = None
|
||||
for i, analyst_type in enumerate(selected_analysts):
|
||||
if analyst_map.get(analyst_type.value) == agent_name:
|
||||
current_idx = i
|
||||
break
|
||||
|
||||
if current_idx is not None and current_idx + 1 < len(selected_analysts):
|
||||
next_analyst_type = selected_analysts[current_idx + 1]
|
||||
next_agent = analyst_map.get(next_analyst_type.value)
|
||||
if next_agent and next_agent in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][next_agent] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": next_agent, "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
elif current_idx is not None and current_idx + 1 == len(selected_analysts):
|
||||
# All analysts done, start research team
|
||||
research_team = ["Bull Researcher", "Bear Researcher", "Research Manager"]
|
||||
for research_agent in research_team:
|
||||
if research_agent in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][research_agent] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": research_agent, "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="report",
|
||||
data={
|
||||
"section_name": section,
|
||||
"content": chunk[section],
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Process trader investment plan
|
||||
if "trader_investment_plan" in chunk and chunk["trader_investment_plan"]:
|
||||
# Mark trader as completed and start risk team
|
||||
if analysis_id:
|
||||
if "Trader" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Trader"] = "completed"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Trader", "status": "completed"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Start risk team
|
||||
risk_team = ["Risky Analyst", "Safe Analyst", "Neutral Analyst"]
|
||||
for agent in risk_team:
|
||||
if agent in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][agent] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": agent, "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="report",
|
||||
data={
|
||||
"section_name": "trader_investment_plan",
|
||||
"content": chunk["trader_investment_plan"],
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Process investment debate state
|
||||
if "investment_debate_state" in chunk and chunk["investment_debate_state"]:
|
||||
debate_state = chunk["investment_debate_state"]
|
||||
|
||||
# Update research team statuses based on debate state
|
||||
if analysis_id:
|
||||
if getattr(debate_state, "bull_history", None) or (isinstance(debate_state, dict) and debate_state.get("bull_history")):
|
||||
if "Bull Researcher" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Bull Researcher"] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Bull Researcher", "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
if getattr(debate_state, "bear_history", None) or (isinstance(debate_state, dict) and debate_state.get("bear_history")):
|
||||
if "Bear Researcher" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Bear Researcher"] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Bear Researcher", "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
if getattr(debate_state, "judge_decision", None) or (isinstance(debate_state, dict) and debate_state.get("judge_decision")):
|
||||
research_team = ["Bull Researcher", "Bear Researcher", "Research Manager"]
|
||||
for agent in research_team:
|
||||
if agent in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][agent] = "completed"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": agent, "status": "completed"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="debate_update",
|
||||
data={
|
||||
"bull_history": getattr(debate_state, "bull_history", None) or debate_state.get("bull_history") if isinstance(debate_state, dict) else None,
|
||||
"bear_history": getattr(debate_state, "bear_history", None) or debate_state.get("bear_history") if isinstance(debate_state, dict) else None,
|
||||
"judge_decision": getattr(debate_state, "judge_decision", None) or debate_state.get("judge_decision") if isinstance(debate_state, dict) else None,
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
# Process risk debate state
|
||||
if "risk_debate_state" in chunk and chunk["risk_debate_state"]:
|
||||
risk_state = chunk["risk_debate_state"]
|
||||
|
||||
# Update risk team statuses
|
||||
if analysis_id:
|
||||
if getattr(risk_state, "current_risky_response", None) or (isinstance(risk_state, dict) and risk_state.get("current_risky_response")):
|
||||
if "Risky Analyst" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Risky Analyst"] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Risky Analyst", "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
if getattr(risk_state, "current_safe_response", None) or (isinstance(risk_state, dict) and risk_state.get("current_safe_response")):
|
||||
if "Safe Analyst" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Safe Analyst"] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Safe Analyst", "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
if getattr(risk_state, "current_neutral_response", None) or (isinstance(risk_state, dict) and risk_state.get("current_neutral_response")):
|
||||
if "Neutral Analyst" in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id]["Neutral Analyst"] = "in_progress"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": "Neutral Analyst", "status": "in_progress"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
if getattr(risk_state, "judge_decision", None) or (isinstance(risk_state, dict) and risk_state.get("judge_decision")):
|
||||
risk_team = ["Risky Analyst", "Safe Analyst", "Neutral Analyst", "Portfolio Manager"]
|
||||
for agent in risk_team:
|
||||
if agent in self.agent_statuses.get(analysis_id, {}):
|
||||
self.agent_statuses[analysis_id][agent] = "completed"
|
||||
await update_callback(StreamUpdate(
|
||||
type="agent_status",
|
||||
data={"agent": agent, "status": "completed"},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
await update_callback(StreamUpdate(
|
||||
type="risk_debate_update",
|
||||
data={
|
||||
"current_risky_response": getattr(risk_state, "current_risky_response", None) or risk_state.get("current_risky_response") if isinstance(risk_state, dict) else None,
|
||||
"current_safe_response": getattr(risk_state, "current_safe_response", None) or risk_state.get("current_safe_response") if isinstance(risk_state, dict) else None,
|
||||
"current_neutral_response": getattr(risk_state, "current_neutral_response", None) or risk_state.get("current_neutral_response") if isinstance(risk_state, dict) else None,
|
||||
"judge_decision": getattr(risk_state, "judge_decision", None) or risk_state.get("judge_decision") if isinstance(risk_state, dict) else None,
|
||||
},
|
||||
timestamp=timestamp
|
||||
))
|
||||
|
||||
def _save_reports(self, final_state: Dict[str, Any], report_dir: Path):
|
||||
"""Save reports to files."""
|
||||
report_sections = [
|
||||
"market_report", "sentiment_report", "news_report",
|
||||
"fundamentals_report", "trader_investment_plan", "final_trade_decision"
|
||||
]
|
||||
|
||||
for section in report_sections:
|
||||
if section in final_state and final_state[section]:
|
||||
file_path = report_dir / f"{section}.md"
|
||||
with open(file_path, "w") as f:
|
||||
f.write(final_state[section])
|
||||
|
||||
def get_analysis_status(self, analysis_id: str) -> Optional[AnalysisStatus]:
|
||||
"""Get the status of an analysis."""
|
||||
analysis = self.active_analyses.get(analysis_id) or self.completed_analyses.get(analysis_id)
|
||||
if not analysis:
|
||||
return None
|
||||
|
||||
return AnalysisStatus(
|
||||
analysis_id=analysis_id,
|
||||
status=analysis["status"],
|
||||
ticker=analysis["ticker"],
|
||||
analysis_date=analysis["analysis_date"],
|
||||
started_at=analysis.get("started_at"),
|
||||
completed_at=analysis.get("completed_at"),
|
||||
error=analysis.get("error"),
|
||||
)
|
||||
|
||||
def get_analysis_results(self, analysis_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get the results of a completed analysis."""
|
||||
analysis = self.completed_analyses.get(analysis_id)
|
||||
if not analysis or analysis["status"] != "completed":
|
||||
return None
|
||||
|
||||
return {
|
||||
"analysis_id": analysis_id,
|
||||
"ticker": analysis["ticker"],
|
||||
"analysis_date": analysis["analysis_date"],
|
||||
"final_state": analysis.get("final_state", {}),
|
||||
"processed_signal": analysis.get("processed_signal"),
|
||||
"completed_at": analysis.get("completed_at"),
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from .stream_handler import StreamHandler, ConnectionManager
|
||||
|
||||
__all__ = ["StreamHandler", "ConnectionManager"]
|
||||
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import json
|
||||
from typing import Dict, Set
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
from ..models.schemas import StreamUpdate
|
||||
from ..services.analysis_service import AnalysisService
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manages WebSocket connections for streaming analysis updates."""
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str, Set[WebSocket]] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, analysis_id: str):
|
||||
"""Accept a WebSocket connection for a specific analysis."""
|
||||
await websocket.accept()
|
||||
if analysis_id not in self.active_connections:
|
||||
self.active_connections[analysis_id] = set()
|
||||
self.active_connections[analysis_id].add(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket, analysis_id: str):
|
||||
"""Remove a WebSocket connection."""
|
||||
if analysis_id in self.active_connections:
|
||||
self.active_connections[analysis_id].discard(websocket)
|
||||
if not self.active_connections[analysis_id]:
|
||||
del self.active_connections[analysis_id]
|
||||
|
||||
async def send_update(self, analysis_id: str, update: StreamUpdate):
|
||||
"""Send an update to all connected clients for an analysis."""
|
||||
if analysis_id in self.active_connections:
|
||||
message = update.model_dump_json()
|
||||
disconnected = set()
|
||||
for connection in self.active_connections[analysis_id]:
|
||||
try:
|
||||
await connection.send_text(message)
|
||||
except Exception:
|
||||
disconnected.add(connection)
|
||||
|
||||
# Remove disconnected clients
|
||||
for conn in disconnected:
|
||||
self.disconnect(conn, analysis_id)
|
||||
|
||||
|
||||
class StreamHandler:
|
||||
"""Handles WebSocket streaming for analysis updates."""
|
||||
|
||||
def __init__(self, analysis_service: AnalysisService):
|
||||
self.analysis_service = analysis_service
|
||||
self.connection_manager = ConnectionManager()
|
||||
|
||||
async def handle_stream(
|
||||
self,
|
||||
websocket: WebSocket,
|
||||
analysis_id: str
|
||||
):
|
||||
"""Handle WebSocket connection for streaming updates."""
|
||||
await self.connection_manager.connect(websocket, analysis_id)
|
||||
|
||||
try:
|
||||
# Send initial connection confirmation
|
||||
await websocket.send_json({
|
||||
"type": "status",
|
||||
"data": {"message": "Connected", "analysis_id": analysis_id},
|
||||
"timestamp": ""
|
||||
})
|
||||
|
||||
# Keep connection alive and forward updates
|
||||
while True:
|
||||
# Wait for any incoming messages (ping/pong or close)
|
||||
try:
|
||||
data = await websocket.receive_text()
|
||||
# Handle ping/pong if needed
|
||||
if data == "ping":
|
||||
await websocket.send_text("pong")
|
||||
except WebSocketDisconnect:
|
||||
break
|
||||
except Exception:
|
||||
# Connection closed or error
|
||||
break
|
||||
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
self.connection_manager.disconnect(websocket, analysis_id)
|
||||
|
||||
async def send_update(self, analysis_id: str, update: StreamUpdate):
|
||||
"""Send an update to all connected clients."""
|
||||
await self.connection_manager.send_update(analysis_id, update)
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.30.0
|
||||
websockets>=13.0
|
||||
pydantic>=2.0.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
"""Run the FastAPI backend server."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to Python path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from api.main import app
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useWebSocket } from "@/hooks/useWebSocket";
|
||||
import { StreamUpdate, AgentStatusType } from "@/lib/types";
|
||||
import { getAnalysisStatus, getAnalysisResults } from "@/lib/api";
|
||||
import AgentProgress from "@/components/AgentProgress";
|
||||
import ReportViewer from "@/components/ReportViewer";
|
||||
|
||||
export default function AnalysisPage() {
|
||||
const params = useParams();
|
||||
const analysisId = params.id as string;
|
||||
|
||||
const [status, setStatus] = useState<string>("running");
|
||||
const [statusData, setStatusData] = useState<{ ticker?: string; analysis_date?: string } | null>(null);
|
||||
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentStatusType>>({});
|
||||
const [reports, setReports] = useState<Record<string, string>>({});
|
||||
const [messages, setMessages] = useState<Array<{ type: string; content: string; timestamp: string }>>([]);
|
||||
const [finalResults, setFinalResults] = useState<any>(null);
|
||||
|
||||
const handleStreamUpdate = useCallback((update: StreamUpdate) => {
|
||||
switch (update.type) {
|
||||
case "status":
|
||||
if (update.data.status) {
|
||||
setStatus(update.data.status);
|
||||
}
|
||||
break;
|
||||
case "message":
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: update.data.type,
|
||||
content: update.data.content,
|
||||
timestamp: update.timestamp,
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "report":
|
||||
setReports((prev) => ({
|
||||
...prev,
|
||||
[update.data.section_name]: update.data.content,
|
||||
}));
|
||||
break;
|
||||
case "agent_status":
|
||||
setAgentStatuses((prev) => ({
|
||||
...prev,
|
||||
[update.data.agent]: update.data.status,
|
||||
}));
|
||||
break;
|
||||
case "final_decision":
|
||||
setStatus("completed");
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isConnected } = useWebSocket(analysisId, handleStreamUpdate);
|
||||
|
||||
useEffect(() => {
|
||||
// Poll for status updates
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const statusData = await getAnalysisStatus(analysisId);
|
||||
setStatus(statusData.status);
|
||||
setStatusData({ ticker: statusData.ticker, analysis_date: statusData.analysis_date });
|
||||
|
||||
if (statusData.status === "completed" && !finalResults) {
|
||||
const results = await getAnalysisResults(analysisId);
|
||||
setFinalResults(results);
|
||||
if (results.final_state) {
|
||||
setReports({
|
||||
market_report: results.final_state.market_report || "",
|
||||
sentiment_report: results.final_state.sentiment_report || "",
|
||||
news_report: results.final_state.news_report || "",
|
||||
fundamentals_report: results.final_state.fundamentals_report || "",
|
||||
trader_investment_plan: results.final_state.trader_investment_plan || "",
|
||||
final_trade_decision: results.final_state.final_trade_decision || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch status:", error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [analysisId, finalResults]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
Analysis: {statusData?.ticker || "Loading..."} - {statusData?.analysis_date || ""}
|
||||
</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className={`px-3 py-1 rounded-full text-sm ${
|
||||
status === "completed" ? "bg-green-100 text-green-800" :
|
||||
status === "running" ? "bg-blue-100 text-blue-800" :
|
||||
"bg-gray-100 text-gray-800"
|
||||
}`}>
|
||||
{status}
|
||||
</span>
|
||||
<span className={`px-3 py-1 rounded-full text-sm ${
|
||||
isConnected ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||
}`}>
|
||||
{isConnected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-1">
|
||||
<AgentProgress agentStatuses={agentStatuses} />
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
{Object.keys(reports).length > 0 && (
|
||||
<div className="mb-6">
|
||||
<ReportViewer reports={reports} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Messages</h2>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{messages.slice(-20).map((msg, idx) => (
|
||||
<div key={idx} className="text-sm border-b pb-2">
|
||||
<span className="text-gray-500">{msg.timestamp}</span>
|
||||
<span className="ml-2 font-medium">{msg.type}:</span>
|
||||
<span className="ml-2">{msg.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import AnalysisForm from "@/components/AnalysisForm";
|
||||
import { AnalysisRequest } from "@/lib/types";
|
||||
import { startAnalysis } from "@/lib/api";
|
||||
|
||||
export default function NewAnalysisPage() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = async (request: AnalysisRequest) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const { analysis_id } = await startAnalysis(request);
|
||||
router.push(`/analysis/${analysis_id}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to start analysis");
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="bg-white rounded-lg shadow p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">New Analysis</h1>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AnalysisForm onSubmit={handleSubmit} isLoading={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,26 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getHistoricalAnalysis } from "@/lib/api";
|
||||
import ReportViewer from "@/components/ReportViewer";
|
||||
|
||||
export default function HistoricalAnalysisPage() {
|
||||
const params = useParams();
|
||||
const ticker = params.ticker as string;
|
||||
const date = params.date as string;
|
||||
|
||||
const [reports, setReports] = useState<Record<string, string>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadAnalysis() {
|
||||
try {
|
||||
const data = await getHistoricalAnalysis(ticker, date);
|
||||
setReports(data.reports || {});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load analysis");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
loadAnalysis();
|
||||
}, [ticker, date]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading analysis...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
{ticker} - {date}
|
||||
</h1>
|
||||
|
||||
{Object.keys(reports).length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||
<p className="text-gray-600">No reports available for this analysis.</p>
|
||||
</div>
|
||||
) : (
|
||||
<ReportViewer reports={reports} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { listHistoricalAnalyses } from "@/lib/api";
|
||||
import { HistoricalAnalysisSummary } from "@/lib/types";
|
||||
|
||||
export default function HistoryPage() {
|
||||
const [analyses, setAnalyses] = useState<HistoricalAnalysisSummary[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadAnalyses() {
|
||||
try {
|
||||
const data = await listHistoricalAnalyses();
|
||||
setAnalyses(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load analyses");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
loadAnalyses();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading analyses...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h1 className="text-3xl font-bold mb-6">Analysis History</h1>
|
||||
|
||||
{analyses.length === 0 ? (
|
||||
<div className="bg-white rounded-lg shadow p-8 text-center">
|
||||
<p className="text-gray-600">No historical analyses found.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{analyses.map((analysis) => (
|
||||
<Link
|
||||
key={`${analysis.ticker}-${analysis.analysis_date}`}
|
||||
href={`/history/${analysis.ticker}/${analysis.analysis_date}`}
|
||||
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{analysis.ticker}</h2>
|
||||
<p className="text-gray-600">{analysis.analysis_date}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{analysis.has_results && (
|
||||
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm">
|
||||
Has Results
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-400">→</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import Link from "next/link";
|
||||
import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "TradingAgents - Multi-Agents LLM Financial Trading Framework",
|
||||
description: "Web interface for TradingAgents trading analysis framework",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<nav className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<Link href="/" className="flex items-center px-2 py-2 text-xl font-bold text-gray-900">
|
||||
TradingAgents
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/" className="px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Home
|
||||
</Link>
|
||||
<Link href="/analysis/new" className="px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
New Analysis
|
||||
</Link>
|
||||
<Link href="/history" className="px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
History
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
TradingAgents
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 mb-8">
|
||||
Multi-Agents LLM Financial Trading Framework
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||
<Link
|
||||
href="/analysis/new"
|
||||
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold mb-2">New Analysis</h2>
|
||||
<p className="text-gray-600">
|
||||
Start a new trading analysis with your selected ticker and configuration
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/history"
|
||||
className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<h2 className="text-2xl font-semibold mb-2">View History</h2>
|
||||
<p className="text-gray-600">
|
||||
Browse and review your previous trading analyses
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center text-gray-500">
|
||||
<p>Workflow: Analyst Team → Research Team → Trader → Risk Management → Portfolio Management</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import { AgentStatusType } from "@/lib/types";
|
||||
|
||||
interface AgentStatus {
|
||||
agent: string;
|
||||
status: AgentStatusType;
|
||||
team: string;
|
||||
}
|
||||
|
||||
interface AgentProgressProps {
|
||||
agentStatuses: Record<string, AgentStatusType>;
|
||||
}
|
||||
|
||||
const TEAMS = {
|
||||
"Analyst Team": ["Market Analyst", "Social Analyst", "News Analyst", "Fundamentals Analyst"],
|
||||
"Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"],
|
||||
"Trading Team": ["Trader"],
|
||||
"Risk Management": ["Risky Analyst", "Neutral Analyst", "Safe Analyst"],
|
||||
"Portfolio Management": ["Portfolio Manager"],
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<AgentStatusType, string> = {
|
||||
pending: "bg-gray-200 text-gray-600",
|
||||
in_progress: "bg-blue-500 text-white animate-pulse",
|
||||
completed: "bg-green-500 text-white",
|
||||
error: "bg-red-500 text-white",
|
||||
};
|
||||
|
||||
export default function AgentProgress({ agentStatuses }: AgentProgressProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Agent Progress</h2>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(TEAMS).map(([teamName, agents]) => (
|
||||
<div key={teamName} className="border-b pb-4 last:border-b-0">
|
||||
<h3 className="font-semibold text-sm text-gray-700 mb-2">{teamName}</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
{agents.map((agent) => {
|
||||
const status = agentStatuses[agent] || "pending";
|
||||
return (
|
||||
<div
|
||||
key={agent}
|
||||
className={`px-3 py-2 rounded text-sm text-center ${STATUS_COLORS[status]}`}
|
||||
>
|
||||
<div className="font-medium">{agent}</div>
|
||||
<div className="text-xs mt-1 capitalize">{status.replace("_", " ")}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { AnalysisRequest, AnalystType, LLMProvider } from "@/lib/types";
|
||||
|
||||
interface AnalysisFormProps {
|
||||
onSubmit: (request: AnalysisRequest) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const LLM_OPTIONS = {
|
||||
[LLMProvider.OPENAI]: {
|
||||
quick: [
|
||||
{ label: "GPT-4o-mini - Fast and efficient", value: "gpt-4o-mini" },
|
||||
{ label: "GPT-4.1-nano - Ultra-lightweight", value: "gpt-4.1-nano" },
|
||||
{ label: "GPT-4.1-mini - Compact model", value: "gpt-4.1-mini" },
|
||||
{ label: "GPT-4o - Standard model", value: "gpt-4o" },
|
||||
],
|
||||
deep: [
|
||||
{ label: "GPT-4.1-nano - Ultra-lightweight", value: "gpt-4.1-nano" },
|
||||
{ label: "GPT-4.1-mini - Compact model", value: "gpt-4.1-mini" },
|
||||
{ label: "GPT-4o - Standard model", value: "gpt-4o" },
|
||||
{ label: "o4-mini - Reasoning model (compact)", value: "o4-mini" },
|
||||
{ label: "o3-mini - Advanced reasoning (lightweight)", value: "o3-mini" },
|
||||
{ label: "o3 - Full advanced reasoning", value: "o3" },
|
||||
{ label: "o1 - Premier reasoning model", value: "o1" },
|
||||
],
|
||||
},
|
||||
[LLMProvider.ANTHROPIC]: {
|
||||
quick: [
|
||||
{ label: "Claude Haiku 3.5 - Fast inference", value: "claude-3-5-haiku-latest" },
|
||||
{ label: "Claude Sonnet 3.5 - Highly capable", value: "claude-3-5-sonnet-latest" },
|
||||
{ label: "Claude Sonnet 3.7 - Exceptional hybrid", value: "claude-3-7-sonnet-latest" },
|
||||
{ label: "Claude Sonnet 4 - High performance", value: "claude-sonnet-4-0" },
|
||||
],
|
||||
deep: [
|
||||
{ label: "Claude Haiku 3.5 - Fast inference", value: "claude-3-5-haiku-latest" },
|
||||
{ label: "Claude Sonnet 3.5 - Highly capable", value: "claude-3-5-sonnet-latest" },
|
||||
{ label: "Claude Sonnet 3.7 - Exceptional hybrid", value: "claude-3-7-sonnet-latest" },
|
||||
{ label: "Claude Sonnet 4 - High performance", value: "claude-sonnet-4-0" },
|
||||
{ label: "Claude Opus 4 - Most powerful", value: "claude-opus-4-0" },
|
||||
],
|
||||
},
|
||||
[LLMProvider.GOOGLE]: {
|
||||
quick: [
|
||||
{ label: "Gemini 2.0 Flash-Lite - Cost efficient", value: "gemini-2.0-flash-lite" },
|
||||
{ label: "Gemini 2.0 Flash - Next generation", value: "gemini-2.0-flash" },
|
||||
{ label: "Gemini 2.5 Flash - Adaptive thinking", value: "gemini-2.5-flash-preview-05-20" },
|
||||
],
|
||||
deep: [
|
||||
{ label: "Gemini 2.0 Flash-Lite - Cost efficient", value: "gemini-2.0-flash-lite" },
|
||||
{ label: "Gemini 2.0 Flash - Next generation", value: "gemini-2.0-flash" },
|
||||
{ label: "Gemini 2.5 Flash - Adaptive thinking", value: "gemini-2.5-flash-preview-05-20" },
|
||||
{ label: "Gemini 2.5 Pro", value: "gemini-2.5-pro-preview-06-05" },
|
||||
],
|
||||
},
|
||||
[LLMProvider.OPENROUTER]: {
|
||||
quick: [
|
||||
{ label: "Meta: Llama 4 Scout", value: "meta-llama/llama-4-scout:free" },
|
||||
{ label: "Meta: Llama 3.3 8B Instruct", value: "meta-llama/llama-3.3-8b-instruct:free" },
|
||||
{ label: "Google Gemini 2.0 Flash", value: "google/gemini-2.0-flash-exp:free" },
|
||||
],
|
||||
deep: [
|
||||
{ label: "DeepSeek V3", value: "deepseek/deepseek-chat-v3-0324:free" },
|
||||
],
|
||||
},
|
||||
[LLMProvider.OLLAMA]: {
|
||||
quick: [
|
||||
{ label: "llama3.1 local", value: "llama3.1" },
|
||||
{ label: "llama3.2 local", value: "llama3.2" },
|
||||
],
|
||||
deep: [
|
||||
{ label: "llama3.1 local", value: "llama3.1" },
|
||||
{ label: "qwen3", value: "qwen3" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const BACKEND_URLS: Record<LLMProvider, string> = {
|
||||
[LLMProvider.OPENAI]: "https://api.openai.com/v1",
|
||||
[LLMProvider.ANTHROPIC]: "https://api.anthropic.com/",
|
||||
[LLMProvider.GOOGLE]: "https://generativelanguage.googleapis.com/v1",
|
||||
[LLMProvider.OPENROUTER]: "https://openrouter.ai/api/v1",
|
||||
[LLMProvider.OLLAMA]: "http://localhost:11434/v1",
|
||||
};
|
||||
|
||||
export default function AnalysisForm({ onSubmit, isLoading }: AnalysisFormProps) {
|
||||
const [ticker, setTicker] = useState("SPY");
|
||||
const [analysisDate, setAnalysisDate] = useState(
|
||||
new Date().toISOString().split("T")[0]
|
||||
);
|
||||
const [selectedAnalysts, setSelectedAnalysts] = useState<AnalystType[]>([
|
||||
AnalystType.MARKET,
|
||||
AnalystType.SOCIAL,
|
||||
AnalystType.NEWS,
|
||||
AnalystType.FUNDAMENTALS,
|
||||
]);
|
||||
const [researchDepth, setResearchDepth] = useState(1);
|
||||
const [llmProvider, setLlmProvider] = useState<LLMProvider>(LLMProvider.OPENAI);
|
||||
const [quickThinkLLM, setQuickThinkLLM] = useState("gpt-4o-mini");
|
||||
const [deepThinkLLM, setDeepThinkLLM] = useState("o4-mini");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const request: AnalysisRequest = {
|
||||
ticker: ticker.toUpperCase(),
|
||||
analysis_date: analysisDate,
|
||||
analysts: selectedAnalysts,
|
||||
research_depth: researchDepth,
|
||||
llm_provider: llmProvider,
|
||||
backend_url: BACKEND_URLS[llmProvider],
|
||||
quick_think_llm: quickThinkLLM,
|
||||
deep_think_llm: deepThinkLLM,
|
||||
};
|
||||
onSubmit(request);
|
||||
};
|
||||
|
||||
const toggleAnalyst = (analyst: AnalystType) => {
|
||||
setSelectedAnalysts((prev) =>
|
||||
prev.includes(analyst)
|
||||
? prev.filter((a) => a !== analyst)
|
||||
: [...prev, analyst]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Ticker Symbol</label>
|
||||
<input
|
||||
type="text"
|
||||
value={ticker}
|
||||
onChange={(e) => setTicker(e.target.value.toUpperCase())}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Analysis Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={analysisDate}
|
||||
onChange={(e) => setAnalysisDate(e.target.value)}
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Analysts</label>
|
||||
<div className="space-y-2">
|
||||
{Object.values(AnalystType).map((analyst) => (
|
||||
<label key={analyst} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAnalysts.includes(analyst)}
|
||||
onChange={() => toggleAnalyst(analyst)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="capitalize">{analyst} Analyst</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Research Depth</label>
|
||||
<select
|
||||
value={researchDepth}
|
||||
onChange={(e) => setResearchDepth(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
>
|
||||
<option value={1}>Shallow - Quick research</option>
|
||||
<option value={3}>Medium - Moderate debate</option>
|
||||
<option value={5}>Deep - Comprehensive research</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">LLM Provider</label>
|
||||
<select
|
||||
value={llmProvider}
|
||||
onChange={(e) => {
|
||||
const provider = e.target.value as LLMProvider;
|
||||
setLlmProvider(provider);
|
||||
const options = LLM_OPTIONS[provider];
|
||||
setQuickThinkLLM(options.quick[0].value);
|
||||
setDeepThinkLLM(options.deep[0].value);
|
||||
}}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
>
|
||||
{Object.values(LLMProvider).map((provider) => (
|
||||
<option key={provider} value={provider}>
|
||||
{provider}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Quick-Thinking LLM</label>
|
||||
<select
|
||||
value={quickThinkLLM}
|
||||
onChange={(e) => setQuickThinkLLM(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
>
|
||||
{LLM_OPTIONS[llmProvider].quick.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Deep-Thinking LLM</label>
|
||||
<select
|
||||
value={deepThinkLLM}
|
||||
onChange={(e) => setDeepThinkLLM(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
>
|
||||
{LLM_OPTIONS[llmProvider].deep.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || selectedAnalysts.length === 0}
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? "Starting Analysis..." : "Start Analysis"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface ReportViewerProps {
|
||||
reports: Record<string, string>;
|
||||
}
|
||||
|
||||
const REPORT_TITLES: Record<string, string> = {
|
||||
market_report: "Market Analysis",
|
||||
sentiment_report: "Social Sentiment",
|
||||
news_report: "News Analysis",
|
||||
fundamentals_report: "Fundamentals Analysis",
|
||||
trader_investment_plan: "Trading Team Plan",
|
||||
final_trade_decision: "Final Trade Decision",
|
||||
};
|
||||
|
||||
export default function ReportViewer({ reports }: ReportViewerProps) {
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpanded((prev) => ({
|
||||
...prev,
|
||||
[section]: !prev[section],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(reports).map(([section, content]) => (
|
||||
<div key={section} className="bg-white rounded-lg shadow">
|
||||
<button
|
||||
onClick={() => toggleSection(section)}
|
||||
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-gray-50 rounded-t-lg"
|
||||
>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{REPORT_TITLES[section] || section}
|
||||
</h3>
|
||||
<span className="text-gray-500">
|
||||
{expanded[section] ? "▼" : "▶"}
|
||||
</span>
|
||||
</button>
|
||||
{expanded[section] && (
|
||||
<div className="px-6 py-4 border-t">
|
||||
<div className="prose max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { createWebSocket } from "@/lib/websocket";
|
||||
import { StreamUpdate } from "@/lib/types";
|
||||
|
||||
export function useWebSocket(
|
||||
analysisId: string | null,
|
||||
onMessage?: (update: StreamUpdate) => void
|
||||
) {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [error, setError] = useState<Event | null>(null);
|
||||
const wsRef = useRef<ReturnType<typeof createWebSocket> | null>(null);
|
||||
const onMessageRef = useRef(onMessage);
|
||||
|
||||
// Keep the ref updated with the latest callback
|
||||
useEffect(() => {
|
||||
onMessageRef.current = onMessage;
|
||||
}, [onMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!analysisId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = createWebSocket(
|
||||
analysisId,
|
||||
(update) => {
|
||||
// Use ref to avoid dependency issues
|
||||
onMessageRef.current?.(update);
|
||||
},
|
||||
(err) => {
|
||||
setError(err);
|
||||
setIsConnected(false);
|
||||
},
|
||||
() => {
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
},
|
||||
() => {
|
||||
setIsConnected(false);
|
||||
}
|
||||
);
|
||||
|
||||
wsRef.current = ws;
|
||||
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
setIsConnected(false);
|
||||
};
|
||||
}, [analysisId]); // Only depend on analysisId, not onMessage
|
||||
|
||||
const send = useCallback((message: string) => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(message);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { isConnected, error, send };
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { AnalysisRequest, AnalysisStatus, AnalysisResults, HistoricalAnalysisSummary, ConfigPreset } from "./types";
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
export async function startAnalysis(request: AnalysisRequest): Promise<{ analysis_id: string }> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/analysis/start`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to start analysis: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getAnalysisStatus(analysisId: string): Promise<AnalysisStatus> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/analysis/${analysisId}/status`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get analysis status: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getAnalysisResults(analysisId: string): Promise<AnalysisResults> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/analysis/${analysisId}/results`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get analysis results: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function listHistoricalAnalyses(): Promise<HistoricalAnalysisSummary[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/history`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list historical analyses: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function getHistoricalAnalysis(
|
||||
ticker: string,
|
||||
date: string
|
||||
): Promise<Record<string, any>> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/history/${ticker}/${date}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get historical analysis: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function listConfigPresets(): Promise<ConfigPreset[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/config/presets`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list config presets: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function saveConfigPreset(preset: ConfigPreset): Promise<ConfigPreset> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/config/save`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save config preset: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteConfigPreset(name: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/config/presets/${name}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete config preset: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// TypeScript types matching backend Pydantic schemas
|
||||
|
||||
export enum AnalystType {
|
||||
MARKET = "market",
|
||||
SOCIAL = "social",
|
||||
NEWS = "news",
|
||||
FUNDAMENTALS = "fundamentals",
|
||||
}
|
||||
|
||||
export enum LLMProvider {
|
||||
OPENAI = "openai",
|
||||
ANTHROPIC = "anthropic",
|
||||
GOOGLE = "google",
|
||||
OPENROUTER = "openrouter",
|
||||
OLLAMA = "ollama",
|
||||
}
|
||||
|
||||
export interface AnalysisRequest {
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
analysts: AnalystType[];
|
||||
research_depth: number;
|
||||
llm_provider: LLMProvider;
|
||||
backend_url: string;
|
||||
quick_think_llm: string;
|
||||
deep_think_llm: string;
|
||||
data_vendors?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type AgentStatusType = "pending" | "in_progress" | "completed" | "error";
|
||||
|
||||
export interface AgentStatus {
|
||||
agent: string;
|
||||
status: AgentStatusType;
|
||||
team?: string;
|
||||
}
|
||||
|
||||
export interface MessageUpdate {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ToolCallUpdate {
|
||||
timestamp: string;
|
||||
tool_name: string;
|
||||
args: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ReportSection {
|
||||
section_name: string;
|
||||
content: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export type AnalysisStatusType = "pending" | "running" | "completed" | "error";
|
||||
|
||||
export interface AnalysisStatus {
|
||||
analysis_id: string;
|
||||
status: AnalysisStatusType;
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type StreamUpdateType =
|
||||
| "status"
|
||||
| "message"
|
||||
| "tool_call"
|
||||
| "report"
|
||||
| "agent_status"
|
||||
| "debate_update"
|
||||
| "risk_debate_update"
|
||||
| "final_decision";
|
||||
|
||||
export interface StreamUpdate {
|
||||
type: StreamUpdateType;
|
||||
data: Record<string, any>;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface InvestmentDebateState {
|
||||
bull_history?: string;
|
||||
bear_history?: string;
|
||||
judge_decision?: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RiskDebateState {
|
||||
risky_history?: string;
|
||||
safe_history?: string;
|
||||
neutral_history?: string;
|
||||
current_risky_response?: string;
|
||||
current_safe_response?: string;
|
||||
current_neutral_response?: string;
|
||||
judge_decision?: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface AnalysisResults {
|
||||
analysis_id: string;
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
market_report?: string;
|
||||
sentiment_report?: string;
|
||||
news_report?: string;
|
||||
fundamentals_report?: string;
|
||||
investment_debate_state?: InvestmentDebateState;
|
||||
trader_investment_plan?: string;
|
||||
risk_debate_state?: RiskDebateState;
|
||||
final_trade_decision?: string;
|
||||
processed_signal?: string;
|
||||
completed_at: string;
|
||||
}
|
||||
|
||||
export interface HistoricalAnalysisSummary {
|
||||
ticker: string;
|
||||
analysis_date: string;
|
||||
completed_at?: string;
|
||||
has_results: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigPreset {
|
||||
name: string;
|
||||
description?: string;
|
||||
config: Record<string, any>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||
import { StreamUpdate } from "./types";
|
||||
|
||||
const WS_BASE_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8000";
|
||||
|
||||
export function createWebSocket(
|
||||
analysisId: string,
|
||||
onMessage: (update: StreamUpdate) => void,
|
||||
onError?: (error: Event) => void,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void
|
||||
): ReconnectingWebSocket {
|
||||
const ws = new ReconnectingWebSocket(
|
||||
`${WS_BASE_URL}/api/analysis/${analysisId}/stream`,
|
||||
[],
|
||||
{
|
||||
connectionTimeout: 4000,
|
||||
maxRetries: 10,
|
||||
maxReconnectionDelay: 10000,
|
||||
minReconnectionDelay: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
try {
|
||||
const update: StreamUpdate = JSON.parse(event.data);
|
||||
onMessage(update);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse WebSocket message:", error);
|
||||
}
|
||||
});
|
||||
|
||||
if (onError) {
|
||||
ws.addEventListener("error", onError);
|
||||
}
|
||||
|
||||
if (onOpen) {
|
||||
ws.addEventListener("open", onOpen);
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
ws.addEventListener("close", onClose);
|
||||
}
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"next": "16.0.3",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
|
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in New Issue