feat: start frontend development

This commit is contained in:
kevin-bruton 2025-09-29 08:18:03 +02:00
parent 9b7b12d14f
commit e9b0ad42f0
13 changed files with 3501 additions and 2837 deletions

17
.env.example Normal file
View File

@ -0,0 +1,17 @@
# Trading Agents Environment Variables
# Copy this file to .env and fill in your values
# Finnhub API Key (required for market data)
FINNHUB_API_KEY=your_finnhub_api_key_here
# OpenAI API Key (required for AI processing)
OPENAI_API_KEY=your_openai_api_key_here
# Reddit API Credentials (required for social media analysis)
REDDIT_CLIENT_ID=your_reddit_client_id_here
REDDIT_CLIENT_SECRET=your_reddit_client_secret_here
REDDIT_USER_AGENT=TradingAgents/1.0
# Optional Configuration
# DEBUG=True
# LOG_LEVEL=INFO

View File

@ -155,6 +155,21 @@ An interface will appear showing results as they load, letting you track the age
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
## Web Frontend (HTMX/FastAPI)
In addition to the CLI, a new web-based frontend is available to visualize the agent communication process in real-time. It allows you to set configuration parameters, start the trading analysis, and observe the step-by-step execution of agents and tools, including their outputs and any errors.
### Running the Web Frontend
1. Ensure you have installed all dependencies using `uv sync`.
2. Navigate to the project root directory in your terminal.
3. Start the FastAPI server:
```bash
uvicorn webapp.main:app --reload
```
4. Open your web browser and go to `http://127.0.0.1:8000`.
5. Enter a company symbol (e.g., `AAPL`) in the configuration form and click "Start Process" to begin the analysis.
## TradingAgents Package
### Implementation Details

View File

@ -0,0 +1,129 @@
# HTMX Frontend Implementation Plan
This document outlines the architecture and step-by-step plan for building a new HTMX-based frontend for the TradingAgents project.
## 1. General Architecture
The frontend will be a single-page web application served by a lightweight Python backend (FastAPI). This backend will be responsible for serving the HTML, handling user requests to start the agent process, and providing real-time status updates. The frontend and backend code will be housed in a new top-level `webapp` directory to keep it separate from the core agent logic.
### Core Components:
* **FastAPI Backend:** A Python web server that will:
* Serve the main `index.html` file.
* Provide API endpoints for the frontend to interact with.
* Run the `TradingAgentsGraph` in a background thread.
* Maintain and serve the state of the execution process.
* **HTMX Frontend:** The user interface, which will:
* Display the configuration form and start button.
* Show a hierarchical view of the agent execution process.
* Poll the backend for status updates.
* Display the content of selected process steps (reports, messages, errors) on the right side of the screen.
* **Communication:** The frontend will communicate with the backend using a simple polling mechanism. The HTMX frontend will periodically request a status update from a `/status` endpoint. The backend will return a JSON object representing the current state of the execution tree. For displaying detailed content, the frontend will make specific requests to a `/content/{item_id}` endpoint.
## 2. Proposed Project Structure
To maintain separation of concerns, the new frontend code will live in a `webapp` directory.
```
C:\Users\kevin\repo\TradingAgents\
├───... (existing project files)
└───webapp/
├───main.py # FastAPI application
├───static/
│ └───styles.css # CSS for styling
└───templates/
├───index.html # Main HTML file
└───_partials/
├───left_panel.html # HTMX partial for the execution tree
└───right_panel.html # HTMX partial for the content view
```
## 3. Backend Implementation (FastAPI)
The `webapp/main.py` file will define the FastAPI application and its endpoints.
### API Endpoints:
* **`GET /`**: Serves the main `templates/index.html` page.
* **`POST /start`**:
* Accepts a JSON payload with the run configuration (`company_symbol`, etc.).
* Initializes the `TradingAgentsGraph`.
* Starts the `graph.propagate()` method in a background thread.
* Returns an initial response that replaces the config form with the main progress bar.
* **`GET /status`**:
* This is the main polling endpoint for HTMX.
* It will return an HTML partial (`_partials/left_panel.html`) rendered with the current state of the execution tree. The state will be stored in memory.
* **`GET /content/{item_id}`**:
* When a user clicks an item in the left panel, HTMX will call this endpoint.
* It will retrieve the specific content for that `item_id` from the in-memory state.
* It will return an HTML partial (`_partials/right_panel.html`) with the formatted content (e.g., a formatted report, a code block for a message, or a stack trace for an error).
### State Management & Integration:
To get real-time updates from the `TradingAgentsGraph`, we will need to instrument its execution. The plan is to modify the `TradingAgentsGraph` class slightly to accept a callback function.
1. **Modify `TradingAgentsGraph.__init__`**: Add an optional `on_step_end` callback parameter.
2. **Callback Execution**: Inside the graph's execution logic (after each agent or tool runs), this callback will be invoked with the details of the completed step (e.g., node name, output, status).
3. **Update Global State**: The callback function, defined in `webapp/main.py`, will update a global in-memory dictionary that represents the hierarchical execution tree. This tree will store the status, content, and relationships of all steps.
This approach avoids tight coupling and allows the web application to listen to the progress of the core agent logic.
## 4. Frontend Implementation (HTMX)
The frontend will be built using HTMX attributes directly in the HTML templates.
* **`templates/index.html`**:
* Contains the basic page structure: a top bar for the overall progress, a left panel for the execution tree, and a right panel for content.
* Includes the HTMX library.
* Contains the initial configuration form. The form will have an `hx-post="/start"` attribute to trigger the process.
* **Left Panel (`_partials/left_panel.html`)**:
* This partial will be the target of the status polling. The main container will have `hx-get="/status"` and `hx-trigger="load, every 2s"`.
* It will use a template loop (Jinja2) to render the hierarchical tree from the state object provided by the backend.
* Each item in the tree will be a clickable element with an `hx-get="/content/{item_id}"` attribute and an `hx-target="#right-panel"` attribute to load its content on the right side.
* The status of each item (pending, in-progress, completed, error) will be reflected using different CSS classes.
* **Right Panel (`_partials/right_panel.html`)**:
* A simple container (`<div id="right-panel">`) that gets its content replaced by HTMX when a user clicks an item on the left.
* Content will be pre-formatted by the backend (e.g., using Markdown-to-HTML conversion or syntax highlighting for code/errors).
* **Progress Bar**:
* The response from the initial `POST /start` call will replace the configuration form with a global progress bar.
* This progress bar's value will be updated as part of the `/status` polling response, by targeting its element ID with an `hx-swap-oob="true"` (Out of Band swap).
## 5. Detailed Implementation Steps
1. **Setup Environment**:
* Create the `webapp` directory and the file structure outlined above.
* Add `fastapi`, `uvicorn`, and `python-multipart` to the `requirements.txt` file and install them.
2. **Backend - Basic Server**:
* Create the initial FastAPI app in `webapp/main.py`.
* Implement the `GET /` endpoint to serve `templates/index.html`.
* Create a basic `index.html` with the two-panel layout.
3. **Backend - State & Integration**:
* Define the Python data classes for the execution state (e.g., `ProcessStep`, `RunState`).
* Modify `tradingagents/graph/trading_graph.py` to include the `on_step_end` callback mechanism.
* In `webapp/main.py`, implement the callback function that builds the hierarchical state tree in memory.
4. **Backend - Endpoints**:
* Implement the `/start` endpoint to receive configuration and launch the `propagate` method in a background thread, passing the callback function.
* Implement the `/status` endpoint to render and return the `_partials/left_panel.html` partial.
* Implement the `/content/{item_id}` endpoint to render and return the `_partials/right_panel.html` partial.
5. **Frontend - HTMX**:
* Develop the configuration form in `index.html` with `hx-post` to start the process.
* Create the `_partials/left_panel.html` template with the Jinja2 loop and the `hx-get` attributes for clicking on items.
* Add the polling mechanism to the main container in `index.html`.
* Style the different states (pending, completed, error) using CSS in `static/styles.css`.
6. **Error Handling**:
* When the callback receives an error, it will update the corresponding item's status to "error" and store the stack trace.
* The frontend will visually flag the item as an error.
* When clicked, the `/content/{item_id}` endpoint will return the formatted stack trace to be displayed in the right panel.
7. **Refinement**:
* Add a loading indicator for HTMX requests.
* Refine the CSS to ensure the application is visually appealing and user-friendly.
* Ensure the background process is managed correctly, especially in case of errors or server shutdown.

Binary file not shown.

View File

@ -33,4 +33,8 @@ dependencies = [
"tushare>=1.4.21",
"typing-extensions>=4.14.0",
"yfinance>=0.2.63",
"fastapi",
"uvicorn",
"python-multipart",
"jinja2",
]

View File

@ -24,3 +24,7 @@ rich
questionary
langchain_anthropic
langchain-google-genai
fastapi
uvicorn
python-multipart
jinja2

View File

@ -192,7 +192,7 @@ class TradingAgentsGraph:
),
}
def propagate(self, company_name, trade_date, user_position="none", cost_per_trade=0.0):
def propagate(self, company_name, trade_date, user_position="none", cost_per_trade=0.0, on_step_callback=None):
"""Run the trading agents graph for a company on a specific date."""
self.ticker = company_name
@ -206,17 +206,17 @@ class TradingAgentsGraph:
if self.debug:
# Debug mode with tracing
trace = []
for chunk in self.graph.stream(init_agent_state, **args):
if len(chunk["messages"]) == 0:
pass
else:
chunk["messages"][-1].pretty_print()
trace.append(chunk)
for s in self.graph.stream(init_agent_state, **args):
trace.append(s)
if on_step_callback:
on_step_callback(s)
final_state = trace[-1]
else:
# Standard mode without tracing
final_state = self.graph.invoke(init_agent_state, **args)
# If not in debug mode, we still want to call the callback for the final state
if on_step_callback:
on_step_callback(final_state)
# Store current state for reflection
self.curr_state = final_state

5666
uv.lock

File diff suppressed because it is too large Load Diff

202
webapp/main.py Normal file
View File

@ -0,0 +1,202 @@
from fastapi import FastAPI, Request, Form, BackgroundTasks, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
import jinja2
import os
from typing import Dict, Any
import threading
import time
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Check required environment variables
required_env_vars = [
'FINNHUB_API_KEY',
'OPENAI_API_KEY',
#'REDDIT_CLIENT_ID',
#'REDDIT_CLIENT_SECRET',
#'REDDIT_USER_AGENT'
]
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
if missing_vars:
print(f"Error: Missing required environment variables: {', '.join(missing_vars)}")
print("Please create a .env file with these variables or set them in your environment.")
from tradingagents.graph.trading_graph import TradingAgentsGraph
app = FastAPI()
# In-memory storage for the process state
# Using a lock for thread-safe access to app_state
app_state_lock = threading.Lock()
app_state: Dict[str, Any] = {
"process_running": False,
"company_symbol": None,
"execution_tree": [],
"overall_status": "idle", # idle, in_progress, completed, error
"overall_progress": 0 # 0-100
}
# Mount the static directory to serve CSS, JS, etc.
app.mount("/static", StaticFiles(directory="webapp/static"), name="static")
# Setup Jinja2 for templating
template_dir = os.path.join(os.path.dirname(__file__), "templates")
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir))
def update_execution_state(state: Dict[str, Any]):
"""Callback function to update the app_state based on LangGraph's state."""
with app_state_lock:
current_step_name = None
# LangGraph state typically has a single key for the current node's output
# We need to find which agent just ran
for key, value in state.items():
if key != "__end__": # Ignore the special __end__ key
current_step_name = key
break
if current_step_name:
# Find the root node or create it if it doesn't exist
if not app_state["execution_tree"]:
app_state["execution_tree"].append({
"id": "root",
"name": f"Trading Analysis for {app_state['company_symbol']}",
"status": "in_progress",
"content": "",
"children": [],
"timestamp": time.time()
})
root_node = app_state["execution_tree"][0]
# Check if this step already exists (e.g., if an agent runs multiple times)
# For simplicity, we'll just append for now. A more robust solution would update existing.
new_item = {
"id": f"{current_step_name}-{len(root_node['children'])}", # Simple unique ID
"name": current_step_name,
"status": "completed", # Assume completed for now
"content": str(state.get(current_step_name, "No specific output")), # Store the agent's output
"children": [],
"timestamp": time.time()
}
root_node["children"].append(new_item)
root_node["status"] = "in_progress" # Keep root in progress until final
# Update overall progress (very basic, just increments)
# In a real scenario, you'd have a predefined number of steps
app_state["overall_progress"] = min(100, app_state["overall_progress"] + 5)
def run_trading_process(company_symbol: str):
"""Runs the TradingAgentsGraph in a separate thread."""
with app_state_lock:
app_state["overall_status"] = "in_progress"
app_state["overall_progress"] = 0
try:
graph = TradingAgentsGraph()
current_date = time.strftime("%Y-%m-%d") # Use current date for analysis
# The propagate method now accepts the callback and trade_date
final_state = graph.propagate(company_symbol, trade_date=current_date, on_step_callback=update_execution_state)
with app_state_lock:
app_state["overall_status"] = "completed"
app_state["overall_progress"] = 100
# Update the root node status to completed
if app_state["execution_tree"]:
app_state["execution_tree"][0]["status"] = "completed"
app_state["execution_tree"][0]["content"] = str(final_state)
except Exception as e:
import traceback
error_detail = traceback.format_exc()
with app_state_lock:
app_state["overall_status"] = "error"
app_state["overall_progress"] = 100
if app_state["execution_tree"]:
app_state["execution_tree"][0]["status"] = "error"
app_state["execution_tree"][0]["content"] = f"Error during execution: {str(e)}\n\n{error_detail}"
# Add a specific error item to the tree
app_state["execution_tree"].append({
"id": "error",
"name": "Process Error",
"status": "error",
"content": f"Error during execution: {str(e)}\n\n{error_detail}",
"children": [],
"timestamp": time.time()
})
finally:
with app_state_lock:
app_state["process_running"] = False
@app.get("/", response_class=HTMLResponse)
async def read_root():
template = jinja_env.get_template("index.html")
return template.render(app_state=app_state)
@app.post("/start", response_class=HTMLResponse)
async def start_process(background_tasks: BackgroundTasks, company_symbol: str = Form(...)):
# Check if all required environment variables are set
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
if missing_vars:
app_state["overall_status"] = "error"
app_state["execution_tree"] = [{
"id": "error",
"name": "Configuration Error",
"status": "error",
"content": f"Missing required environment variables: {', '.join(missing_vars)}. Please check .env.example file.",
"children": [],
"timestamp": time.time()
}]
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
with app_state_lock:
if app_state["process_running"]:
# Optionally, return an error or a message that a process is already running
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
app_state["process_running"] = True
app_state["company_symbol"] = company_symbol
app_state["execution_tree"] = [] # Clear for new run
app_state["overall_status"] = "in_progress"
app_state["overall_progress"] = 0
background_tasks.add_task(run_trading_process, company_symbol)
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
@app.get("/status", response_class=HTMLResponse)
async def get_status():
with app_state_lock:
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
def find_item_in_tree(item_id: str, tree: list) -> Dict[str, Any] | None:
"""Recursively searches the execution tree for an item by its ID."""
for item in tree:
if item["id"] == item_id:
return item
if item["children"]:
found_child = find_item_in_tree(item_id, item["children"])
if found_child:
return found_child
return None
@app.get("/content/{item_id}", response_class=HTMLResponse)
async def get_item_content(item_id: str):
with app_state_lock:
item = find_item_in_tree(item_id, app_state["execution_tree"])
if item:
template = jinja_env.get_template("_partials/right_panel.html")
return template.render(content=item.get("content", "No content available."))
else:
return HTMLResponse(content="<p>Item not found.</p>", status_code=404)
# To run this app:
# uvicorn webapp.main:app --reload

219
webapp/static/styles.css Normal file
View File

@ -0,0 +1,219 @@
/* Dark theme variables */
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #242424;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent-color: #4CAF50;
--border-color: #333;
--input-bg: #2a2a2a;
--hover-color: #333;
}
/* Basic styles for the webapp */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
}
#left-panel {
width: 30%;
border-right: 1px solid var(--border-color);
padding: 20px;
overflow-y: auto;
background-color: var(--bg-secondary);
}
#right-panel {
width: 70%;
padding: 20px;
overflow-y: auto;
background-color: var(--bg-primary);
}
/* Overall Progress Bar */
#overall-progress-container {
width: 100%;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
padding: 10px 20px;
box-sizing: border-box;
}
#overall-progress-bar {
height: 6px;
background-color: var(--accent-color);
width: 0%; /* Initial width */
transition: width 0.5s ease-in-out, background-color 0.3s ease;
border-radius: 3px;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.3);
}
#overall-progress-text {
margin-left: 15px;
font-size: 0.9em;
color: var(--text-secondary);
font-weight: 500;
}
/* Main Content Layout */
#main-content {
display: flex;
flex-grow: 1;
height: calc(100vh - 46px); /* Adjust for progress bar height */
background-color: var(--bg-primary);
}
/* Left Panel - Configuration and Controls */
#left-panel {
width: 30%;
border-right: 1px solid var(--border-color);
padding: 20px;
overflow-y: auto;
box-sizing: border-box;
background-color: var(--bg-secondary);
}
#left-panel h2 {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-primary);
font-size: 1.5em;
font-weight: 600;
}
#config-form {
background-color: var(--bg-primary);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
#config-form label {
display: block;
margin-bottom: 8px;
color: var(--text-secondary);
font-size: 0.9em;
}
#config-form input {
width: 100%;
padding: 10px;
margin-bottom: 15px;
background-color: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-size: 1em;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
#config-form input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
#config-form button {
width: 100%;
padding: 12px;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
}
#config-form button:hover {
background-color: #45a049;
transform: translateY(-1px);
}
#config-form button:active {
transform: translateY(0);
}
#left-panel ul {
list-style: none;
padding-left: 15px;
margin-top: 20px;
}
#left-panel li {
margin-bottom: 8px;
cursor: pointer;
transition: transform 0.2s ease;
}
#left-panel li .item-name {
padding: 8px 12px;
border-radius: 4px;
display: inline-block;
transition: background-color 0.2s ease;
}
#left-panel li .item-name:hover {
background-color: var(--hover-color);
}
/* Status Indicators */
.status-pending .item-name {
color: var(--text-secondary);
border-left: 3px solid var(--text-secondary);
padding-left: 10px;
}
.status-in_progress .item-name {
color: #3b82f6;
font-weight: 600;
border-left: 3px solid #3b82f6;
padding-left: 10px;
animation: pulse 2s infinite;
}
.status-completed .item-name {
color: var(--accent-color);
border-left: 3px solid var(--accent-color);
padding-left: 10px;
}
.status-error .item-name {
color: #ef4444;
font-weight: 600;
border-left: 3px solid #ef4444;
padding-left: 10px;
}
/* Right Panel Styles */
#right-panel {
padding: 30px;
}
#right-panel p {
color: var(--text-secondary);
line-height: 1.6;
font-size: 1.1em;
}
/* Animations */
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}

View File

@ -0,0 +1,28 @@
{% macro render_item(item) %}
<li class="process-item status-{{ item.status }}">
<span hx-get="/content/{{ item.id }}" hx-target="#right-panel" hx-swap="innerHTML" class="item-name">{{ item.name }}</span>
{% if item.children %}
<ul>
{% for child in item.children %}
{{ render_item(child) }}
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
<div id="overall-progress-bar" hx-swap-oob="true" style="width:{{ app_state.overall_progress }}%;"></div>
<span id="overall-progress-text" hx-swap-oob="true">{{ app_state.overall_progress }}% ({{ app_state.overall_status }})</span>
<div id="left-panel-content" hx-get="/status" hx-trigger="every 2s" hx-swap="innerHTML">
<h2>Execution Status</h2>
{% if tree %}
<ul>
{% for item in tree %}
{{ render_item(item) }}
{% endfor %}
</ul>
{% else %}
<p>No process running. Start a new one from the configuration.</p>
{% endif %}
</div>

View File

@ -0,0 +1,4 @@
<div>
<h3>Content Details</h3>
<pre>{{ content }}</pre>
</div>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TradingAgents</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/styles.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
<div id="overall-progress-container">
<div id="overall-progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<span id="overall-progress-text">0%</span>
</div>
<div id="main-content">
<div id="left-panel">
<h2>Configuration</h2>
<div id="config-form">
<form hx-post="/start" hx-target="#left-panel" hx-swap="innerHTML">
<label for="company_symbol">Company Symbol:</label>
<input type="text" id="company_symbol" name="company_symbol" value="AAPL" required>
<button type="submit">Start Process</button>
</form>
</div>
</div>
<div id="right-panel">
<p>Welcome! Please set your configuration and start the process.</p>
</div>
</div>
</body>
</html>