fix: webapp status updates

This commit is contained in:
Kevin Bruton 2025-09-29 20:06:31 +02:00
parent 9201f77c9f
commit df99d7deb2
3 changed files with 123 additions and 59 deletions

View File

@ -40,6 +40,15 @@ app_state: Dict[str, Any] = {
"overall_progress": 0 # 0-100
}
# Define the strict sequential phase execution order
PHASE_SEQUENCE = [
"data_collection_phase",
"research_phase",
"planning_phase",
"execution_phase",
"risk_analysis_phase"
]
# Mount the static directory to serve CSS, JS, etc.
app.mount("/static", StaticFiles(directory="webapp/static"), name="static")
@ -145,12 +154,16 @@ def update_execution_state(state: Dict[str, Any]):
if report_data:
update_agent_status(agent_info, "completed", report_data, state)
# Mark in-progress agent(s) sequentially BEFORE recalculating phase status
mark_in_progress_agents(app_state["execution_tree"])
# Recalculate phase statuses after setting agent in-progress markers
recalc_phase_statuses(app_state["execution_tree"])
# Update overall progress
execution_tree = app_state["execution_tree"]
total_agents = len(agent_state_mapping)
completed_agents = count_completed_agents(execution_tree)
app_state["overall_progress"] = min(100, int((completed_agents / max(total_agents, 1)) * 100))
print(f"📊 Progress updated: {app_state['overall_progress']}% ({completed_agents}/{total_agents} agents)")
def initialize_complete_execution_tree():
@ -276,8 +289,7 @@ def update_agent_status(agent_info: dict, status: str, report_data: any, full_st
messages_node["status"] = "completed"
messages_node["content"] = extract_agent_messages(full_state, agent_info["agent_id"])
# Update phase status if all agents in phase are completed
update_phase_status_if_complete(agent_info["phase"], execution_tree)
# Phase status recalculated globally in recalc_phase_statuses
def find_agent_in_tree(agent_id: str, tree: list):
"""Find an agent node in the execution tree."""
@ -317,17 +329,27 @@ def extract_agent_messages(state: dict, agent_id: str) -> str:
else:
return "💬 Agent Messages\n\nExecution completed without specific message logs"
def update_phase_status_if_complete(phase_id: str, execution_tree: list):
"""Update phase status to completed if all its agents are completed."""
phase_node = find_item_by_id(f"{phase_id}_phase", execution_tree)
if not phase_node:
return
# Check if all agents in this phase are completed
all_completed = all(agent["status"] == "completed" for agent in phase_node["children"])
if all_completed and phase_node["status"] != "completed":
phase_node["status"] = "completed"
phase_node["content"] = f"{phase_node['name']} - All agents completed successfully"
def recalc_phase_statuses(execution_tree: list):
"""Recalculate each phase's status: pending (no started), in_progress (some running/completed but not all), completed (all done), error if any child error."""
for phase in execution_tree:
if not phase.get("children"):
continue
child_statuses = [c["status"] for c in phase["children"]]
if any(s == "error" for s in child_statuses):
phase["status"] = "error"
phase["content"] = f"{phase['name']} - Error in sub-task"
elif all(s == "completed" for s in child_statuses):
phase["status"] = "completed"
phase["content"] = f"{phase['name']} - All agents completed successfully"
elif any(s in ("in_progress", "completed") for s in child_statuses):
# At least one started but not all done
if phase["status"] != "in_progress":
phase["status"] = "in_progress"
phase["content"] = f"{phase['name']} - Running..."
else:
# All pending
phase["status"] = "pending"
def count_completed_agents(execution_tree: list) -> int:
"""Count the number of completed agents across all phases."""
@ -339,6 +361,49 @@ def count_completed_agents(execution_tree: list) -> int:
count += 1
return count
def mark_in_progress_agents(execution_tree: list):
"""Sequentially activate only the earliest phase that still has pending agents.
Rules:
- A phase becomes active when all prior phases are completed.
- Only the first such phase can have an in_progress agent.
- Within that phase, mark exactly one first pending agent as in_progress.
"""
# Build quick lookup by id
phase_map = {p["id"]: p for p in execution_tree}
# Determine which phase should be active
active_phase = None
for phase_id in PHASE_SEQUENCE:
phase = phase_map.get(phase_id)
if not phase:
continue
# If all previous phases completed, and this phase not fully completed, it's the active one
prev_completed = all(
(phase_map.get(prev_id) and all(c["status"] == "completed" for c in phase_map[prev_id].get("children", [])))
for prev_id in PHASE_SEQUENCE[:PHASE_SEQUENCE.index(phase_id)]
)
phase_done = all(c["status"] == "completed" for c in phase.get("children", []))
if prev_completed and not phase_done:
active_phase = phase
break
if not active_phase:
return
# If an agent already in progress in the active phase, leave as-is
if any(a["status"] == "in_progress" for a in active_phase.get("children", [])):
return
# Otherwise pick first pending agent
for agent in active_phase.get("children", []):
if agent["status"] == "pending":
agent["status"] = "in_progress"
agent["content"] = f"{agent['name']} - Running analysis..."
for child in agent.get("children", []):
if child["status"] == "pending":
child["status"] = "in_progress"
break
def run_trading_process(company_symbol: str, config: Dict[str, Any]):
"""Runs the TradingAgentsGraph in a separate thread."""
with app_state_lock:
@ -417,8 +482,10 @@ def run_trading_process(company_symbol: str, config: Dict[str, Any]):
@app.get("/", response_class=HTMLResponse)
async def read_root():
from datetime import date
template = jinja_env.get_template("index.html")
return template.render(app_state=app_state)
today_str = date.today().isoformat()
return template.render(app_state=app_state, default_date=today_str)
@app.post("/start", response_class=HTMLResponse)
async def start_process(
@ -481,22 +548,6 @@ async def get_status():
template = jinja_env.get_template("_partials/left_panel.html")
return template.render(tree=app_state["execution_tree"], app_state=app_state)
@app.get("/status-content", response_class=HTMLResponse)
async def get_status_content():
with app_state_lock:
if not app_state["execution_tree"]:
return HTMLResponse(content="<p>No process running. Start a new one from the configuration.</p>")
template = jinja_env.get_template("_partials/tree_content.html")
tree_content = template.render(tree=app_state["execution_tree"])
# Add out-of-band updates for progress bar
progress_updates = f'''
<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>
'''
return HTMLResponse(content=tree_content + progress_updates)
@app.get("/status-updates")
async def get_status_updates():

View File

@ -41,7 +41,7 @@
</h2>
{% if tree %}
<ul class="execution-tree" id="execution-tree" hx-get="/status-content" hx-trigger="every 5s" hx-swap="innerHTML">
<ul class="execution-tree" id="execution-tree">
{% for item in tree %}
{{ render_item(item) }}
{% endfor %}

View File

@ -34,12 +34,20 @@
<label for="quick_think_llm">Quick Think LLM:</label>
<select id="quick_think_llm" name="quick_think_llm" required>
<!-- Options will be populated by JavaScript -->
{% if not app_state.get('config') %}
<option value="x-ai/grok-4-fast:free">xAI: Grok 4 Fast (free)</option>
<option value="deepseek/deepseek-chat-v3.1:free">DeepSeek: DeepSeek V3.1 (free)</option>
<option value="meta-llama/llama-4-scout:free">Meta: Llama 4 Scout</option>
{% endif %}
</select>
<label for="deep_think_llm">Deep Think LLM:</label>
<select id="deep_think_llm" name="deep_think_llm" required>
<!-- Options will be populated by JavaScript -->
{% if not app_state.get('config') %}
<option value="qwen/qwen3-235b-a22b:free">Qwen: Qwen3 235B A22B (free)</option>
<option value="openai/gpt-oss-120b:free">OpenAI: gpt-oss-120b (free)</option>
<option value="deepseek/deepseek-chat-v3-0324:free">DeepSeek V3 - 685B</option>
{% endif %}
</select>
<label for="max_debate_rounds">Max Debate Rounds:</label>
@ -55,7 +63,7 @@
<input type="number" id="cost_per_trade" name="cost_per_trade" value="2.0" step="0.1" min="0" required>
<label for="analysis_date">Analysis Date:</label>
<input type="date" id="analysis_date" name="analysis_date" required>
<input type="date" id="analysis_date" name="analysis_date" value="{{ default_date }}" required>
<button type="submit">Start Process</button>
<div id="loading" class="htmx-indicator">Starting process...</div>
@ -178,28 +186,30 @@
// Set current date as default for analysis_date
function setCurrentDate() {
const today = new Date();
const dateString = today.toISOString().split('T')[0];
document.getElementById('analysis_date').value = dateString;
const el = document.getElementById('analysis_date');
if (el && !el.value) { // only set if not already provided by server
const today = new Date();
const dateString = today.toISOString().split('T')[0];
el.value = dateString;
}
}
// Toggle node functionality for collapsible tree
function toggleNode(button) {
const children = button.parentElement.parentElement.querySelector('.item-children');
if (children) {
const isExpanded = children.classList.contains('expanded');
if (isExpanded) {
// Collapse
children.classList.remove('expanded');
children.classList.add('collapsed');
button.classList.remove('expanded');
} else {
// Expand
children.classList.remove('collapsed');
children.classList.add('expanded');
button.classList.add('expanded');
}
const li = button.closest('li.process-item');
const children = li ? li.querySelector(':scope > .item-children') : null;
if (!children) return;
const isExpanded = children.classList.contains('expanded');
if (isExpanded) {
children.classList.remove('expanded');
children.classList.add('collapsed');
button.classList.remove('expanded');
button.setAttribute('aria-expanded', 'false');
} else {
children.classList.remove('collapsed');
children.classList.add('expanded');
button.classList.add('expanded');
button.setAttribute('aria-expanded', 'true');
}
}
@ -268,14 +278,14 @@
// Handle HTMX before request to save state
document.addEventListener('htmx:beforeSwap', function(event) {
if (event.target.id === 'left-panel-content') {
if (event.target.id === 'left-panel') {
window.savedExpansionState = saveExpansionState();
}
});
// Handle HTMX after settle to restore state
document.addEventListener('htmx:afterSettle', function(event) {
if (event.target.id === 'left-panel-content' && window.savedExpansionState) {
document.addEventListener('htmx:afterSettle', function(event) {
if (event.target.id === 'left-panel' && window.savedExpansionState) {
restoreExpansionState(window.savedExpansionState);
}
});
@ -319,8 +329,11 @@
// Start targeted updates when execution tree is present
function startTargetedUpdates() {
if (document.querySelector('.execution-tree')) {
window.statusUpdateInterval = setInterval(updateStatusIndicators, 2000); // Update every 2 seconds
const tree = document.querySelector('.execution-tree');
if (!tree) return;
if (!window.statusUpdateInterval) {
console.debug('[status] starting interval');
window.statusUpdateInterval = setInterval(updateStatusIndicators, 2000);
}
}
@ -343,7 +356,7 @@
// Handle when new content is loaded (like when starting a process)
document.addEventListener('htmx:afterSettle', function(event) {
if (event.target.id === 'left-panel-content') {
if (event.target.id === 'left-panel') {
if (window.savedExpansionState) {
restoreExpansionState(window.savedExpansionState);
}