checkpoint: mma_orchestrator track
This commit is contained in:
205
gui_2.py
205
gui_2.py
@@ -24,9 +24,14 @@ import events
|
||||
import numpy as np
|
||||
import api_hooks
|
||||
import mcp_client
|
||||
import orchestrator_pm
|
||||
from performance_monitor import PerformanceMonitor
|
||||
from log_registry import LogRegistry
|
||||
from log_pruner import LogPruner
|
||||
import conductor_tech_lead
|
||||
import multi_agent_conductor
|
||||
from models import Track, Ticket
|
||||
from file_cache import ASTParser
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException, Security
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
@@ -181,6 +186,9 @@ class App:
|
||||
self.ui_ai_input = ""
|
||||
self.ui_disc_new_name_input = ""
|
||||
self.ui_disc_new_role_input = ""
|
||||
self.ui_epic_input = ""
|
||||
self.proposed_tracks = []
|
||||
self._show_track_proposal_modal = False
|
||||
|
||||
# Last Script popup variables
|
||||
self.ui_last_script_text = ""
|
||||
@@ -238,6 +246,11 @@ class App:
|
||||
self._mma_approval_edit_mode = False
|
||||
self._mma_approval_payload = ""
|
||||
|
||||
# Orchestration State
|
||||
self.ui_epic_input = ""
|
||||
self.proposed_tracks: list[dict] = []
|
||||
self._show_track_proposal_modal = False
|
||||
|
||||
self._tool_log: list[tuple[str, str]] = []
|
||||
self._comms_log: list[dict] = []
|
||||
|
||||
@@ -706,6 +719,28 @@ class App:
|
||||
agent_tools_cfg = proj.get("agent", {}).get("tools", {})
|
||||
self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES}
|
||||
|
||||
# Restore MMA state
|
||||
mma_sec = proj.get("mma", {})
|
||||
self.ui_epic_input = mma_sec.get("epic", "")
|
||||
at_data = mma_sec.get("active_track")
|
||||
if at_data:
|
||||
try:
|
||||
tickets = []
|
||||
for t_data in at_data.get("tickets", []):
|
||||
tickets.append(Ticket(**t_data))
|
||||
self.active_track = Track(
|
||||
id=at_data.get("id"),
|
||||
description=at_data.get("description"),
|
||||
tickets=tickets
|
||||
)
|
||||
self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table
|
||||
except Exception as e:
|
||||
print(f"Failed to deserialize active track: {e}")
|
||||
self.active_track = None
|
||||
else:
|
||||
self.active_track = None
|
||||
self.active_tickets = []
|
||||
|
||||
def _save_active_project(self):
|
||||
if self.active_project_path:
|
||||
try:
|
||||
@@ -856,6 +891,10 @@ class App:
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
|
||||
elif action == "show_track_proposal":
|
||||
self.proposed_tracks = task.get("payload", [])
|
||||
self._show_track_proposal_modal = True
|
||||
|
||||
elif action == "mma_state_update":
|
||||
payload = task.get("payload", {})
|
||||
self.mma_status = payload.get("status", "idle")
|
||||
@@ -1291,6 +1330,17 @@ class App:
|
||||
disc_sec["active"] = self.active_discussion
|
||||
disc_sec["auto_add"] = self.ui_auto_add_history
|
||||
|
||||
# Save MMA State
|
||||
mma_sec = proj.setdefault("mma", {})
|
||||
mma_sec["epic"] = self.ui_epic_input
|
||||
if self.active_track:
|
||||
# We only persist the basic metadata if full serialization is too complex
|
||||
# For now, let's try full serialization via asdict
|
||||
from dataclasses import asdict
|
||||
mma_sec["active_track"] = asdict(self.active_track)
|
||||
else:
|
||||
mma_sec["active_track"] = None
|
||||
|
||||
def _flush_to_config(self):
|
||||
self.config["ai"] = {
|
||||
"provider": self.current_provider,
|
||||
@@ -1408,6 +1458,8 @@ class App:
|
||||
# Process GUI task queue
|
||||
self._process_pending_gui_tasks()
|
||||
|
||||
self._render_track_proposal_modal()
|
||||
|
||||
# Auto-save (every 60s)
|
||||
now = time.time()
|
||||
if now - self._last_autosave >= self._autosave_interval:
|
||||
@@ -1887,6 +1939,126 @@ class App:
|
||||
if ch:
|
||||
self.ui_agent_tools[t_name] = val
|
||||
|
||||
imgui.separator()
|
||||
imgui.text_colored(C_LBL, 'MMA Orchestration')
|
||||
_, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80))
|
||||
if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)):
|
||||
self._cb_plan_epic()
|
||||
|
||||
def _cb_plan_epic(self):
|
||||
def _bg_task():
|
||||
try:
|
||||
self.ai_status = "Planning Epic (Tier 1)..."
|
||||
history = orchestrator_pm.get_track_history_summary()
|
||||
|
||||
proj = project_manager.load_project(self.active_project_path)
|
||||
flat = project_manager.flat_config(proj)
|
||||
file_items = aggregate.build_file_items(Path("."), flat.get("files", {}).get("paths", []))
|
||||
|
||||
tracks = orchestrator_pm.generate_tracks(self.ui_epic_input, flat, file_items, history_summary=history)
|
||||
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({
|
||||
"action": "show_track_proposal",
|
||||
"payload": tracks
|
||||
})
|
||||
self.ai_status = "Epic tracks generated."
|
||||
except Exception as e:
|
||||
self.ai_status = f"Epic plan error: {e}"
|
||||
print(f"ERROR in _cb_plan_epic background task: {e}")
|
||||
|
||||
threading.Thread(target=_bg_task, daemon=True).start()
|
||||
|
||||
def _cb_accept_tracks(self):
|
||||
def _bg_task():
|
||||
try:
|
||||
self.ai_status = "Generating tickets (Tier 2)..."
|
||||
|
||||
# 1. Get skeletons for context
|
||||
parser = ASTParser(language="python")
|
||||
skeletons = ""
|
||||
for file_path in self.files:
|
||||
try:
|
||||
abs_path = Path(self.ui_files_base_dir) / file_path
|
||||
if abs_path.exists() and abs_path.suffix == ".py":
|
||||
with open(abs_path, "r", encoding="utf-8") as f:
|
||||
code = f.read()
|
||||
skeletons += f"\nFile: {file_path}\n{parser.get_skeleton(code)}\n"
|
||||
except Exception as e:
|
||||
print(f"Error parsing skeleton for {file_path}: {e}")
|
||||
|
||||
# 2. For each proposed track, generate and sort tickets
|
||||
for track_data in self.proposed_tracks:
|
||||
goal = track_data.get("goal", "")
|
||||
title = track_data.get("title", "Untitled Track")
|
||||
|
||||
raw_tickets = conductor_tech_lead.generate_tickets(goal, skeletons)
|
||||
if not raw_tickets:
|
||||
print(f"Warning: No tickets generated for track: {title}")
|
||||
continue
|
||||
|
||||
try:
|
||||
sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets)
|
||||
except ValueError as e:
|
||||
print(f"Dependency error in track '{title}': {e}")
|
||||
# Fallback to unsorted if sort fails? Or skip?
|
||||
sorted_tickets_data = raw_tickets
|
||||
|
||||
# 3. Create Track and Ticket objects
|
||||
tickets = []
|
||||
for t_data in sorted_tickets_data:
|
||||
ticket = Ticket(
|
||||
id=t_data["id"],
|
||||
description=t_data["description"],
|
||||
status=t_data.get("status", "todo"),
|
||||
assigned_to=t_data.get("assigned_to", "unassigned"),
|
||||
depends_on=t_data.get("depends_on", []),
|
||||
step_mode=t_data.get("step_mode", False)
|
||||
)
|
||||
tickets.append(ticket)
|
||||
|
||||
track_id = f"track_{uuid.uuid4().hex[:8]}"
|
||||
track = Track(id=track_id, description=title, tickets=tickets)
|
||||
|
||||
# 4. Initialize ConductorEngine and run_linear loop
|
||||
engine = multi_agent_conductor.ConductorEngine(track, self.event_queue)
|
||||
|
||||
# Schedule the coroutine on the internal event loop
|
||||
asyncio.run_coroutine_threadsafe(engine.run_linear(), self._loop)
|
||||
|
||||
self.ai_status = "Tracks accepted and execution started."
|
||||
except Exception as e:
|
||||
self.ai_status = f"Track acceptance error: {e}"
|
||||
print(f"ERROR in _cb_accept_tracks background task: {e}")
|
||||
|
||||
threading.Thread(target=_bg_task, daemon=True).start()
|
||||
|
||||
def _render_track_proposal_modal(self):
|
||||
if self._show_track_proposal_modal:
|
||||
imgui.open_popup("Track Proposal")
|
||||
|
||||
if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]:
|
||||
imgui.text_colored(C_IN, "Proposed Implementation Tracks")
|
||||
imgui.separator()
|
||||
|
||||
if not self.proposed_tracks:
|
||||
imgui.text("No tracks generated.")
|
||||
else:
|
||||
for idx, track in enumerate(self.proposed_tracks):
|
||||
imgui.text_colored(C_LBL, f"Track {idx+1}: {track.get('title', 'Untitled')}")
|
||||
imgui.text_wrapped(f"Goal: {track.get('goal', 'N/A')}")
|
||||
imgui.separator()
|
||||
|
||||
if imgui.button("Accept", imgui.ImVec2(120, 0)):
|
||||
self._cb_accept_tracks()
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.same_line()
|
||||
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
|
||||
self._show_track_proposal_modal = False
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
def _render_log_management(self):
|
||||
exp, self.show_windows["Log Management"] = imgui.begin("Log Management", self.show_windows["Log Management"])
|
||||
if not exp:
|
||||
@@ -2371,6 +2543,26 @@ class App:
|
||||
if is_blinking:
|
||||
imgui.pop_style_color(2)
|
||||
|
||||
def _cb_ticket_retry(self, ticket_id):
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'todo'
|
||||
break
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("mma_retry", {"ticket_id": ticket_id}),
|
||||
self._loop
|
||||
)
|
||||
|
||||
def _cb_ticket_skip(self, ticket_id):
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
t['status'] = 'skipped'
|
||||
break
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("mma_skip", {"ticket_id": ticket_id}),
|
||||
self._loop
|
||||
)
|
||||
|
||||
def _render_mma_dashboard(self):
|
||||
# 1. Global Controls
|
||||
changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode)
|
||||
@@ -2404,16 +2596,18 @@ class App:
|
||||
|
||||
# 3. Ticket Queue
|
||||
imgui.text("Ticket Queue")
|
||||
if imgui.begin_table("mma_tickets", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
|
||||
if imgui.begin_table("mma_tickets", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.resizable):
|
||||
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||
imgui.table_setup_column("Target", imgui.TableColumnFlags_.width_stretch)
|
||||
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
||||
imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 120)
|
||||
imgui.table_headers_row()
|
||||
|
||||
for t in self.active_tickets:
|
||||
tid = t.get('id', '??')
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text(str(t.get('id', '??')))
|
||||
imgui.text(str(tid))
|
||||
|
||||
imgui.table_next_column()
|
||||
imgui.text(str(t.get('target_file', 'general')))
|
||||
@@ -2435,6 +2629,13 @@ class App:
|
||||
if status in ['RUNNING', 'COMPLETE', 'BLOCKED', 'ERROR', 'PAUSED']:
|
||||
imgui.pop_style_color()
|
||||
|
||||
imgui.table_next_column()
|
||||
if imgui.button(f"Retry##{tid}"):
|
||||
self._cb_ticket_retry(tid)
|
||||
imgui.same_line()
|
||||
if imgui.button(f"Skip##{tid}"):
|
||||
self._cb_ticket_skip(tid)
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
def _render_tool_calls_panel(self):
|
||||
|
||||
Reference in New Issue
Block a user