fix: webapp status updates
This commit is contained in:
parent
9201f77c9f
commit
df99d7deb2
109
webapp/main.py
109
webapp/main.py
|
|
@ -40,6 +40,15 @@ app_state: Dict[str, Any] = {
|
||||||
"overall_progress": 0 # 0-100
|
"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.
|
# Mount the static directory to serve CSS, JS, etc.
|
||||||
app.mount("/static", StaticFiles(directory="webapp/static"), name="static")
|
app.mount("/static", StaticFiles(directory="webapp/static"), name="static")
|
||||||
|
|
||||||
|
|
@ -145,6 +154,10 @@ def update_execution_state(state: Dict[str, Any]):
|
||||||
if report_data:
|
if report_data:
|
||||||
update_agent_status(agent_info, "completed", report_data, state)
|
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
|
# Update overall progress
|
||||||
execution_tree = app_state["execution_tree"]
|
execution_tree = app_state["execution_tree"]
|
||||||
total_agents = len(agent_state_mapping)
|
total_agents = len(agent_state_mapping)
|
||||||
|
|
@ -276,8 +289,7 @@ def update_agent_status(agent_info: dict, status: str, report_data: any, full_st
|
||||||
messages_node["status"] = "completed"
|
messages_node["status"] = "completed"
|
||||||
messages_node["content"] = extract_agent_messages(full_state, agent_info["agent_id"])
|
messages_node["content"] = extract_agent_messages(full_state, agent_info["agent_id"])
|
||||||
|
|
||||||
# Update phase status if all agents in phase are completed
|
# Phase status recalculated globally in recalc_phase_statuses
|
||||||
update_phase_status_if_complete(agent_info["phase"], execution_tree)
|
|
||||||
|
|
||||||
def find_agent_in_tree(agent_id: str, tree: list):
|
def find_agent_in_tree(agent_id: str, tree: list):
|
||||||
"""Find an agent node in the execution tree."""
|
"""Find an agent node in the execution tree."""
|
||||||
|
|
@ -317,17 +329,27 @@ def extract_agent_messages(state: dict, agent_id: str) -> str:
|
||||||
else:
|
else:
|
||||||
return "💬 Agent Messages\n\nExecution completed without specific message logs"
|
return "💬 Agent Messages\n\nExecution completed without specific message logs"
|
||||||
|
|
||||||
def update_phase_status_if_complete(phase_id: str, execution_tree: list):
|
def recalc_phase_statuses(execution_tree: list):
|
||||||
"""Update phase status to completed if all its agents are completed."""
|
"""Recalculate each phase's status: pending (no started), in_progress (some running/completed but not all), completed (all done), error if any child error."""
|
||||||
phase_node = find_item_by_id(f"{phase_id}_phase", execution_tree)
|
for phase in execution_tree:
|
||||||
if not phase_node:
|
if not phase.get("children"):
|
||||||
return
|
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"
|
||||||
|
|
||||||
# Check if all agents in this phase are completed
|
|
||||||
all_completed = all(agent["status"] == "completed" for agent in phase_node["children"])
|
|
||||||
if all_completed and phase_node["status"] != "completed":
|
|
||||||
phase_node["status"] = "completed"
|
|
||||||
phase_node["content"] = f"✅ {phase_node['name']} - All agents completed successfully"
|
|
||||||
|
|
||||||
def count_completed_agents(execution_tree: list) -> int:
|
def count_completed_agents(execution_tree: list) -> int:
|
||||||
"""Count the number of completed agents across all phases."""
|
"""Count the number of completed agents across all phases."""
|
||||||
|
|
@ -339,6 +361,49 @@ def count_completed_agents(execution_tree: list) -> int:
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
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]):
|
def run_trading_process(company_symbol: str, config: Dict[str, Any]):
|
||||||
"""Runs the TradingAgentsGraph in a separate thread."""
|
"""Runs the TradingAgentsGraph in a separate thread."""
|
||||||
with app_state_lock:
|
with app_state_lock:
|
||||||
|
|
@ -417,8 +482,10 @@ def run_trading_process(company_symbol: str, config: Dict[str, Any]):
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def read_root():
|
async def read_root():
|
||||||
|
from datetime import date
|
||||||
template = jinja_env.get_template("index.html")
|
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)
|
@app.post("/start", response_class=HTMLResponse)
|
||||||
async def start_process(
|
async def start_process(
|
||||||
|
|
@ -481,22 +548,6 @@ async def get_status():
|
||||||
template = jinja_env.get_template("_partials/left_panel.html")
|
template = jinja_env.get_template("_partials/left_panel.html")
|
||||||
return template.render(tree=app_state["execution_tree"], app_state=app_state)
|
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")
|
@app.get("/status-updates")
|
||||||
async def get_status_updates():
|
async def get_status_updates():
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% if tree %}
|
{% 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 %}
|
{% for item in tree %}
|
||||||
{{ render_item(item) }}
|
{{ render_item(item) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,20 @@
|
||||||
|
|
||||||
<label for="quick_think_llm">Quick Think LLM:</label>
|
<label for="quick_think_llm">Quick Think LLM:</label>
|
||||||
<select id="quick_think_llm" name="quick_think_llm" required>
|
<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>
|
</select>
|
||||||
|
|
||||||
<label for="deep_think_llm">Deep Think LLM:</label>
|
<label for="deep_think_llm">Deep Think LLM:</label>
|
||||||
<select id="deep_think_llm" name="deep_think_llm" required>
|
<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>
|
</select>
|
||||||
|
|
||||||
<label for="max_debate_rounds">Max Debate Rounds:</label>
|
<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>
|
<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>
|
<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>
|
<button type="submit">Start Process</button>
|
||||||
<div id="loading" class="htmx-indicator">Starting process...</div>
|
<div id="loading" class="htmx-indicator">Starting process...</div>
|
||||||
|
|
@ -178,28 +186,30 @@
|
||||||
|
|
||||||
// Set current date as default for analysis_date
|
// Set current date as default for analysis_date
|
||||||
function setCurrentDate() {
|
function setCurrentDate() {
|
||||||
|
const el = document.getElementById('analysis_date');
|
||||||
|
if (el && !el.value) { // only set if not already provided by server
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dateString = today.toISOString().split('T')[0];
|
const dateString = today.toISOString().split('T')[0];
|
||||||
document.getElementById('analysis_date').value = dateString;
|
el.value = dateString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle node functionality for collapsible tree
|
// Toggle node functionality for collapsible tree
|
||||||
function toggleNode(button) {
|
function toggleNode(button) {
|
||||||
const children = button.parentElement.parentElement.querySelector('.item-children');
|
const li = button.closest('li.process-item');
|
||||||
if (children) {
|
const children = li ? li.querySelector(':scope > .item-children') : null;
|
||||||
|
if (!children) return;
|
||||||
const isExpanded = children.classList.contains('expanded');
|
const isExpanded = children.classList.contains('expanded');
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
// Collapse
|
|
||||||
children.classList.remove('expanded');
|
children.classList.remove('expanded');
|
||||||
children.classList.add('collapsed');
|
children.classList.add('collapsed');
|
||||||
button.classList.remove('expanded');
|
button.classList.remove('expanded');
|
||||||
|
button.setAttribute('aria-expanded', 'false');
|
||||||
} else {
|
} else {
|
||||||
// Expand
|
|
||||||
children.classList.remove('collapsed');
|
children.classList.remove('collapsed');
|
||||||
children.classList.add('expanded');
|
children.classList.add('expanded');
|
||||||
button.classList.add('expanded');
|
button.classList.add('expanded');
|
||||||
}
|
button.setAttribute('aria-expanded', 'true');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,14 +278,14 @@
|
||||||
|
|
||||||
// Handle HTMX before request to save state
|
// Handle HTMX before request to save state
|
||||||
document.addEventListener('htmx:beforeSwap', function(event) {
|
document.addEventListener('htmx:beforeSwap', function(event) {
|
||||||
if (event.target.id === 'left-panel-content') {
|
if (event.target.id === 'left-panel') {
|
||||||
window.savedExpansionState = saveExpansionState();
|
window.savedExpansionState = saveExpansionState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle HTMX after settle to restore state
|
// Handle HTMX after settle to restore state
|
||||||
document.addEventListener('htmx:afterSettle', function(event) {
|
document.addEventListener('htmx:afterSettle', function(event) {
|
||||||
if (event.target.id === 'left-panel-content' && window.savedExpansionState) {
|
if (event.target.id === 'left-panel' && window.savedExpansionState) {
|
||||||
restoreExpansionState(window.savedExpansionState);
|
restoreExpansionState(window.savedExpansionState);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -319,8 +329,11 @@
|
||||||
|
|
||||||
// Start targeted updates when execution tree is present
|
// Start targeted updates when execution tree is present
|
||||||
function startTargetedUpdates() {
|
function startTargetedUpdates() {
|
||||||
if (document.querySelector('.execution-tree')) {
|
const tree = document.querySelector('.execution-tree');
|
||||||
window.statusUpdateInterval = setInterval(updateStatusIndicators, 2000); // Update every 2 seconds
|
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)
|
// Handle when new content is loaded (like when starting a process)
|
||||||
document.addEventListener('htmx:afterSettle', function(event) {
|
document.addEventListener('htmx:afterSettle', function(event) {
|
||||||
if (event.target.id === 'left-panel-content') {
|
if (event.target.id === 'left-panel') {
|
||||||
if (window.savedExpansionState) {
|
if (window.savedExpansionState) {
|
||||||
restoreExpansionState(window.savedExpansionState);
|
restoreExpansionState(window.savedExpansionState);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue