feat(mma): Decouple UI from API calls using UserRequestEvent and AsyncEventQueue
This commit is contained in:
155
gui_2.py
155
gui_2.py
@@ -1,6 +1,7 @@
|
||||
# gui_2.py
|
||||
import tomli_w
|
||||
import threading
|
||||
import asyncio
|
||||
import time
|
||||
import math
|
||||
import json
|
||||
@@ -10,6 +11,7 @@ import uuid
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, Tk
|
||||
from typing import Optional, Callable
|
||||
import aggregate
|
||||
import ai_client
|
||||
from ai_client import ProviderError
|
||||
@@ -109,11 +111,15 @@ class ConfirmDialog:
|
||||
return self._approved, self._script
|
||||
|
||||
|
||||
class App:
|
||||
class ManualSlopGUI:
|
||||
"""The main ImGui interface orchestrator for Manual Slop."""
|
||||
|
||||
def __init__(self):
|
||||
self.config = load_config()
|
||||
self.event_queue = events.AsyncEventQueue()
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._loop_thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
||||
self._loop_thread.start()
|
||||
|
||||
ai_cfg = self.config.get("ai", {})
|
||||
self._current_provider: str = ai_cfg.get("provider", "gemini")
|
||||
@@ -806,6 +812,21 @@ class App:
|
||||
if action == "refresh_api_metrics":
|
||||
self._refresh_api_metrics(task.get("payload", {}))
|
||||
|
||||
elif action == "handle_ai_response":
|
||||
payload = task.get("payload", {})
|
||||
self.ai_response = payload.get("text", "")
|
||||
self.ai_status = payload.get("status", "done")
|
||||
self._trigger_blink = True
|
||||
if self.ui_auto_add_history:
|
||||
role = payload.get("role", "AI")
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({
|
||||
"role": role,
|
||||
"content": self.ai_response,
|
||||
"collapsed": False,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
|
||||
elif action == "set_value":
|
||||
item = task.get("item")
|
||||
value = task.get("value")
|
||||
@@ -944,61 +965,89 @@ class App:
|
||||
|
||||
def _handle_generate_send(self):
|
||||
"""Logic for the 'Gen + Send' action."""
|
||||
send_busy = False
|
||||
with self._send_thread_lock:
|
||||
if self.send_thread and self.send_thread.is_alive():
|
||||
send_busy = True
|
||||
try:
|
||||
md, path, file_items, stable_md, disc_text = self._do_generate()
|
||||
self.last_md = md
|
||||
self.last_md_path = path
|
||||
self.last_file_items = file_items
|
||||
except Exception as e:
|
||||
self.ai_status = f"generate error: {e}"
|
||||
return
|
||||
|
||||
self.ai_status = "sending..."
|
||||
user_msg = self.ui_ai_input
|
||||
base_dir = self.ui_files_base_dir
|
||||
|
||||
if not send_busy:
|
||||
try:
|
||||
md, path, file_items, stable_md, disc_text = self._do_generate()
|
||||
self.last_md = md
|
||||
self.last_md_path = path
|
||||
self.last_file_items = file_items
|
||||
except Exception as e:
|
||||
self.ai_status = f"generate error: {e}"
|
||||
return
|
||||
# Prepare event payload
|
||||
event_payload = events.UserRequestEvent(
|
||||
prompt=user_msg,
|
||||
stable_md=stable_md,
|
||||
file_items=file_items,
|
||||
disc_text=disc_text,
|
||||
base_dir=base_dir
|
||||
)
|
||||
|
||||
# Push to async queue
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("user_request", event_payload),
|
||||
self._loop
|
||||
)
|
||||
|
||||
self.ai_status = "sending..."
|
||||
user_msg = self.ui_ai_input
|
||||
base_dir = self.ui_files_base_dir
|
||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
ai_client.set_agent_tools(self.ui_agent_tools)
|
||||
send_md = stable_md
|
||||
send_disc = disc_text
|
||||
def _run_event_loop(self):
|
||||
"""Runs the internal asyncio event loop."""
|
||||
asyncio.set_event_loop(self._loop)
|
||||
self._loop.create_task(self._process_event_queue())
|
||||
self._loop.run_forever()
|
||||
|
||||
def do_send():
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()})
|
||||
try:
|
||||
resp = ai_client.send(send_md, user_msg, base_dir, self.last_file_items, send_disc)
|
||||
self.ai_response = resp
|
||||
self.ai_status = "done"
|
||||
self._trigger_blink = True
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({"role": "AI", "content": resp, "collapsed": False, "ts": project_manager.now_ts()})
|
||||
except ProviderError as e:
|
||||
self.ai_response = e.ui_message()
|
||||
self.ai_status = "error"
|
||||
self._trigger_blink = True
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({"role": "Vendor API", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()})
|
||||
except Exception as e:
|
||||
self.ai_response = f"ERROR: {e}"
|
||||
self.ai_status = "error"
|
||||
self._trigger_blink = True
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({"role": "System", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()})
|
||||
async def _process_event_queue(self):
|
||||
"""Listens for and processes events from the AsyncEventQueue."""
|
||||
while True:
|
||||
event_name, payload = await self.event_queue.get()
|
||||
|
||||
if event_name == "user_request":
|
||||
# Handle the request (simulating what was previously in do_send thread)
|
||||
self._handle_request_event(payload)
|
||||
elif event_name == "response":
|
||||
# Handle AI response event
|
||||
with self._pending_gui_tasks_lock:
|
||||
self._pending_gui_tasks.append({
|
||||
"action": "handle_ai_response",
|
||||
"payload": payload
|
||||
})
|
||||
|
||||
def _handle_request_event(self, event: events.UserRequestEvent):
|
||||
"""Processes a UserRequestEvent by calling the AI client."""
|
||||
if self.ui_auto_add_history:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({
|
||||
"role": "User",
|
||||
"content": event.prompt,
|
||||
"collapsed": False,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
|
||||
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
|
||||
ai_client.set_custom_system_prompt("\n\n".join(csp))
|
||||
ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit)
|
||||
ai_client.set_agent_tools(self.ui_agent_tools)
|
||||
|
||||
with self._send_thread_lock:
|
||||
self.send_thread = threading.Thread(target=do_send, daemon=True)
|
||||
self.send_thread.start()
|
||||
try:
|
||||
resp = ai_client.send(event.stable_md, event.prompt, event.base_dir, event.file_items, event.disc_text)
|
||||
# Emit response event
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": resp, "status": "done"}),
|
||||
self._loop
|
||||
)
|
||||
except ProviderError as e:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": e.ui_message(), "status": "error", "role": "Vendor API"}),
|
||||
self._loop
|
||||
)
|
||||
except Exception as e:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.event_queue.put("response", {"text": f"ERROR: {e}", "status": "error", "role": "System"}),
|
||||
self._loop
|
||||
)
|
||||
|
||||
def _test_callback_func_write_to_file(self, data: str):
|
||||
"""A dummy function that a custom_callback would execute for testing."""
|
||||
@@ -2546,7 +2595,7 @@ class App:
|
||||
session_logger.close_session()
|
||||
|
||||
def main():
|
||||
app = App()
|
||||
app = ManualSlopGUI()
|
||||
app.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user