From c3bb2e3d772ad8daf167d8af525b207ab4612673 Mon Sep 17 00:00:00 2001 From: Serena Date: Sat, 21 Jun 2025 21:25:16 +0200 Subject: [PATCH] Change dashboard to use Flask instead of fastapi --- pyproject.toml | 2 +- src/serena/dashboard.py | 49 +++++++++++++++++++------- src/serena/process_isolated_agent.py | 25 +++++--------- uv.lock | 51 +++++++++++++++++++++++----- 4 files changed, 87 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c7bc40a4..9740f792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", - "fastapi>=0.115.12", + "flask>=3.0.0", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 22e34956..fec3cdaf 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -4,9 +4,7 @@ import socket import threading from collections.abc import Callable -import uvicorn -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles +from flask import Flask, request, send_from_directory from pydantic import BaseModel from sensai.util import logging @@ -74,7 +72,7 @@ class SerenaDashboardAPI: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._shutdown_callback = shutdown_callback - self._app = FastAPI(title="Serena Dashboard") + self._app = Flask(__name__) self._setup_routes() @property @@ -82,21 +80,46 @@ class SerenaDashboardAPI: return self._memory_log_handler def _setup_routes(self) -> None: - self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") + # Static files + @self._app.route("/dashboard/") + def serve_dashboard(filename): + return send_from_directory(SERENA_DASHBOARD_DIR, filename) - self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog) - self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames) - self._app.add_api_route("/shutdown", self._shutdown, methods=["PUT"]) + @self._app.route("/dashboard/") + def serve_dashboard_index(): + return send_from_directory(SERENA_DASHBOARD_DIR, "index.html") - async def _get_log_messages(self, request: RequestLog) -> ResponseLog: + # API routes + @self._app.route("/get_log_messages", methods=["POST"]) + def get_log_messages(): + request_data = request.get_json() + if not request_data: + request_log = RequestLog() + else: + request_log = RequestLog.model_validate(request_data) + + result = self._get_log_messages(request_log) + return result.model_dump() + + @self._app.route("/get_tool_names", methods=["GET"]) + def get_tool_names(): + result = self._get_tool_names() + return result.model_dump() + + @self._app.route("/shutdown", methods=["PUT"]) + def shutdown(): + self._shutdown() + return {"status": "shutting down"} + + def _get_log_messages(self, request_log: RequestLog) -> ResponseLog: all_messages = self._memory_log_handler.get_log_messages() - requested_messages = all_messages[request.start_idx :] if request.start_idx <= len(all_messages) else [] + requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else [] return ResponseLog(messages=requested_messages, max_idx=len(all_messages) - 1) - async def _get_tool_names(self) -> ResponseToolNames: + def _get_tool_names(self) -> ResponseToolNames: return ResponseToolNames(tool_names=self._tool_names) - async def _shutdown(self) -> None: + def _shutdown(self) -> None: log.info("Shutting down Serena") if self._shutdown_callback: self._shutdown_callback() @@ -125,7 +148,7 @@ class SerenaDashboardAPI: """ Runs the dashboard on the given host and port and returns the port number. """ - uvicorn.run(self._app, host=host, port=port, workers=1, log_config=None, log_level="critical") + self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) return port def run_in_thread(self) -> tuple[threading.Thread, int]: diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 48d5dc8c..b606c942 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -3,6 +3,7 @@ import contextlib import logging import multiprocessing import os +import threading import traceback import webbrowser from enum import StrEnum @@ -12,7 +13,6 @@ from multiprocessing.sharedctypes import Synchronized from multiprocessing.synchronize import Event as EventClass from typing import Any, Literal, Self -import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry @@ -79,30 +79,21 @@ def _dashboard_worker( port = api._find_first_free_port(0x5EDA) port_value.value = port - # Start server first, then log processing - config = uvicorn.Config( - app=api._app, - host="0.0.0.0", - port=port, - workers=1, - log_config=None, - log_level="critical", - ) - server = uvicorn.Server(config) + # Start Flask server in a thread + def run_flask_server(): + api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) + + server_thread = threading.Thread(target=run_flask_server, daemon=True) + server_thread.start() - # Serve and await shutdown signal - server_task = asyncio.create_task(server.serve()) shutdown_task = asyncio.create_task(_monitor_shutdown()) logging_loop_task = asyncio.create_task(_process_logs(api)) dashboard_ready_event.set() done, pending = await asyncio.wait( - [server_task, shutdown_task, logging_loop_task], + [shutdown_task, logging_loop_task], return_when=asyncio.FIRST_COMPLETED, ) - if shutdown_task in done: - server.should_exit = True - await server_task # Cancel remaining tasks for task in pending: diff --git a/uv.lock b/uv.lock index 05834d9c..c7318117 100644 --- a/uv.lock +++ b/uv.lock @@ -101,6 +101,15 @@ jupyter = [ { name = "tokenize-rt" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -210,17 +219,20 @@ wheels = [ ] [[package]] -name = "fastapi" -version = "0.115.12" +name = "flask" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305 }, ] [[package]] @@ -394,6 +406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -959,7 +980,7 @@ source = { editable = "." } dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, - { name = "fastapi" }, + { name = "flask" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -1007,7 +1028,7 @@ requires-dist = [ { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, - { name = "fastapi", specifier = ">=0.115.12" }, + { name = "flask", specifier = ">=3.0.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1299,3 +1320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +]