fix: webapp status updates
This commit is contained in:
parent
9201f77c9f
commit
df99d7deb2
113
webapp/main.py
113
webapp/main.py
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue