304 lines
12 KiB
Python
304 lines
12 KiB
Python
import json
|
|
import threading
|
|
import uuid
|
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
|
import logging
|
|
import session_logger
|
|
|
|
class HookServerInstance(ThreadingHTTPServer):
|
|
"""Custom HTTPServer that carries a reference to the main App instance."""
|
|
def __init__(self, server_address, RequestHandlerClass, app):
|
|
super().__init__(server_address, RequestHandlerClass)
|
|
self.app = app
|
|
|
|
class HookHandler(BaseHTTPRequestHandler):
|
|
"""Handles incoming HTTP requests for the API hooks."""
|
|
def do_GET(self):
|
|
app = self.server.app
|
|
session_logger.log_api_hook("GET", self.path, "")
|
|
if self.path == '/status':
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
|
|
elif self.path == '/api/project':
|
|
import project_manager
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
flat = project_manager.flat_config(app.project)
|
|
self.wfile.write(json.dumps({'project': flat}).encode('utf-8'))
|
|
elif self.path == '/api/session':
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(
|
|
json.dumps({'session': {'entries': app.disc_entries}}).
|
|
encode('utf-8'))
|
|
elif self.path == '/api/performance':
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
metrics = {}
|
|
if hasattr(app, 'perf_monitor'):
|
|
metrics = app.perf_monitor.get_metrics()
|
|
self.wfile.write(json.dumps({'performance': metrics}).encode('utf-8'))
|
|
elif self.path == '/api/events':
|
|
# Long-poll or return current event queue
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
events = []
|
|
if hasattr(app, '_api_event_queue'):
|
|
with app._api_event_queue_lock:
|
|
events = list(app._api_event_queue)
|
|
app._api_event_queue.clear()
|
|
self.wfile.write(json.dumps({'events': events}).encode('utf-8'))
|
|
elif self.path == '/api/gui/value':
|
|
# POST with {"field": "field_tag"} to get value
|
|
content_length = int(self.headers.get('Content-Length', 0))
|
|
body = self.rfile.read(content_length)
|
|
data = json.loads(body.decode('utf-8'))
|
|
field_tag = data.get("field")
|
|
print(f"[DEBUG] Hook Server: get_value for {field_tag}")
|
|
|
|
event = threading.Event()
|
|
result = {"value": None}
|
|
|
|
def get_val():
|
|
try:
|
|
if field_tag in app._settable_fields:
|
|
attr = app._settable_fields[field_tag]
|
|
val = getattr(app, attr, None)
|
|
print(f"[DEBUG] Hook Server: attr={attr}, val={val}")
|
|
result["value"] = val
|
|
else:
|
|
print(f"[DEBUG] Hook Server: {field_tag} NOT in settable_fields")
|
|
finally:
|
|
event.set()
|
|
|
|
with app._pending_gui_tasks_lock:
|
|
app._pending_gui_tasks.append({
|
|
"action": "custom_callback",
|
|
"callback": get_val
|
|
})
|
|
|
|
if event.wait(timeout=2):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(result).encode('utf-8'))
|
|
else:
|
|
self.send_response(504)
|
|
self.end_headers()
|
|
elif self.path.startswith('/api/gui/value/'):
|
|
# Generic endpoint to get the value of any settable field
|
|
field_tag = self.path.split('/')[-1]
|
|
event = threading.Event()
|
|
result = {"value": None}
|
|
|
|
def get_val():
|
|
try:
|
|
if field_tag in app._settable_fields:
|
|
attr = app._settable_fields[field_tag]
|
|
result["value"] = getattr(app, attr, None)
|
|
finally:
|
|
event.set()
|
|
|
|
with app._pending_gui_tasks_lock:
|
|
app._pending_gui_tasks.append({
|
|
"action": "custom_callback",
|
|
"callback": get_val
|
|
})
|
|
|
|
if event.wait(timeout=2):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(result).encode('utf-8'))
|
|
else:
|
|
self.send_response(504)
|
|
self.end_headers()
|
|
elif self.path == '/api/gui/diagnostics':
|
|
# Safe way to query multiple states at once via the main thread queue
|
|
event = threading.Event()
|
|
result = {}
|
|
|
|
def check_all():
|
|
try:
|
|
# Generic state check based on App attributes (works for both DPG and ImGui versions)
|
|
status = getattr(app, "ai_status", "idle")
|
|
result["thinking"] = status in ["sending...", "running powershell..."]
|
|
result["live"] = status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."]
|
|
result["prior"] = getattr(app, "is_viewing_prior_session", False)
|
|
finally:
|
|
event.set()
|
|
|
|
with app._pending_gui_tasks_lock:
|
|
app._pending_gui_tasks.append({
|
|
"action": "custom_callback",
|
|
"callback": check_all
|
|
})
|
|
|
|
if event.wait(timeout=2):
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(result).encode('utf-8'))
|
|
else:
|
|
self.send_response(504)
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8'))
|
|
else:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
|
|
def do_POST(self):
|
|
app = self.server.app
|
|
content_length = int(self.headers.get('Content-Length', 0))
|
|
body = self.rfile.read(content_length)
|
|
body_str = body.decode('utf-8') if body else ""
|
|
session_logger.log_api_hook("POST", self.path, body_str)
|
|
|
|
try:
|
|
data = json.loads(body_str) if body_str else {}
|
|
if self.path == '/api/project':
|
|
app.project = data.get('project', app.project)
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(
|
|
json.dumps({'status': 'updated'}).encode('utf-8'))
|
|
elif self.path == '/api/session':
|
|
app.disc_entries = data.get('session', {}).get(
|
|
'entries', app.disc_entries)
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(
|
|
json.dumps({'status': 'updated'}).encode('utf-8'))
|
|
elif self.path == '/api/gui':
|
|
with app._pending_gui_tasks_lock:
|
|
app._pending_gui_tasks.append(data)
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(
|
|
json.dumps({'status': 'queued'}).encode('utf-8'))
|
|
elif self.path == '/api/ask':
|
|
request_id = str(uuid.uuid4())
|
|
event = threading.Event()
|
|
|
|
if not hasattr(app, '_pending_asks'):
|
|
app._pending_asks = {}
|
|
if not hasattr(app, '_ask_responses'):
|
|
app._ask_responses = {}
|
|
|
|
app._pending_asks[request_id] = event
|
|
|
|
# Emit event for test/client discovery
|
|
with app._api_event_queue_lock:
|
|
app._api_event_queue.append({
|
|
"type": "ask_received",
|
|
"request_id": request_id,
|
|
"data": data
|
|
})
|
|
|
|
with app._pending_gui_tasks_lock:
|
|
app._pending_gui_tasks.append({
|
|
"type": "ask",
|
|
"request_id": request_id,
|
|
"data": data
|
|
})
|
|
|
|
if event.wait(timeout=60.0):
|
|
response_data = app._ask_responses.get(request_id)
|
|
# Clean up response after reading
|
|
if request_id in app._ask_responses:
|
|
del app._ask_responses[request_id]
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'status': 'ok', 'response': response_data}).encode('utf-8'))
|
|
else:
|
|
if request_id in app._pending_asks:
|
|
del app._pending_asks[request_id]
|
|
self.send_response(504)
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8'))
|
|
|
|
elif self.path == '/api/ask/respond':
|
|
request_id = data.get('request_id')
|
|
response_data = data.get('response')
|
|
|
|
if request_id and hasattr(app, '_pending_asks') and request_id in app._pending_asks:
|
|
app._ask_responses[request_id] = response_data
|
|
event = app._pending_asks[request_id]
|
|
event.set()
|
|
|
|
# Clean up pending ask entry
|
|
del app._pending_asks[request_id]
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
|
|
else:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
else:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
except Exception as e:
|
|
self.send_response(500)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8'))
|
|
|
|
def log_message(self, format, *args):
|
|
logging.info("Hook API: " + format % args)
|
|
|
|
class HookServer:
|
|
def __init__(self, app, port=8999):
|
|
self.app = app
|
|
self.port = port
|
|
self.server = None
|
|
self.thread = None
|
|
|
|
def start(self):
|
|
if not getattr(self.app, 'test_hooks_enabled', False):
|
|
return
|
|
|
|
# Ensure the app has the task queue and lock initialized
|
|
if not hasattr(self.app, '_pending_gui_tasks'):
|
|
self.app._pending_gui_tasks = []
|
|
if not hasattr(self.app, '_pending_gui_tasks_lock'):
|
|
self.app._pending_gui_tasks_lock = threading.Lock()
|
|
|
|
# Initialize ask-related dictionaries
|
|
if not hasattr(self.app, '_pending_asks'):
|
|
self.app._pending_asks = {}
|
|
if not hasattr(self.app, '_ask_responses'):
|
|
self.app._ask_responses = {}
|
|
|
|
# Event queue for test script subscriptions
|
|
if not hasattr(self.app, '_api_event_queue'):
|
|
self.app._api_event_queue = []
|
|
if not hasattr(self.app, '_api_event_queue_lock'):
|
|
self.app._api_event_queue_lock = threading.Lock()
|
|
|
|
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
|
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
|
self.thread.start()
|
|
logging.info(f"Hook server started on port {self.port}")
|
|
|
|
def stop(self):
|
|
if self.server:
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
if self.thread:
|
|
self.thread.join()
|
|
logging.info("Hook server stopped")
|