Alibrown commited on
Commit
d1c8b54
Β·
verified Β·
1 Parent(s): fb43e0b

Update app/app.py

Browse files
Files changed (1) hide show
  1. app/app.py +41 -67
app/app.py CHANGED
@@ -19,12 +19,12 @@
19
  # - Secrets stay in .env β†’ Guardian reads them β†’ never touched by app/*
20
  # =============================================================================
21
 
22
- from quart import Quart, request, jsonify # async Flask β€” required for async providers + Neon DB
23
  import logging
24
- from hypercorn.asyncio import serve
25
- from hypercorn.config import Config
26
- import threading # bank-pattern: each blocking service gets its own thread
27
- import requests # sync HTTP for health check worker
28
  import time
29
  from datetime import datetime
30
  import asyncio
@@ -35,7 +35,7 @@ from typing import Dict, Any, Optional
35
  # Each module reads its own config from app/.pyfun independently.
36
  # NO fundaments passed into these modules!
37
  # =============================================================================
38
- from . import mcp # MCP transport layer (stdio / SSE)
39
  from . import config as app_config # app/.pyfun parser β€” used only in app/*
40
  # from . import providers # API provider registry β€” reads app/.pyfun
41
  # from . import models # Model config + token/rate limits β€” reads app/.pyfun
@@ -66,38 +66,6 @@ logger_config = logging.getLogger('config')
66
  app = Quart(__name__)
67
  START_TIME = datetime.utcnow()
68
 
69
- # =============================================================================
70
- # Background workers
71
- # =============================================================================
72
- def start_mcp_in_thread() -> None:
73
- """
74
- Starts the MCP Hub (stdio or SSE) in its own thread with its own event loop.
75
- Mirrors the bank-thread pattern from the Discord bot architecture.
76
- mcp.py reads its own config from app/.pyfun β€” no fundaments passed in.
77
- """
78
- loop = asyncio.new_event_loop()
79
- asyncio.set_event_loop(loop)
80
- try:
81
- loop.run_until_complete(mcp.start_mcp())
82
- finally:
83
- loop.close()
84
-
85
-
86
- def health_check_worker(port: int) -> None:
87
- """
88
- Periodic self-ping to keep the app alive on hosting platforms (e.g. HuggingFace).
89
- Runs in its own daemon thread β€” does not block the main loop.
90
- Port passed directly β€” no global state needed.
91
- """
92
- while True:
93
- time.sleep(3600)
94
- try:
95
- response = requests.get(f"http://127.0.0.1:{port}/")
96
- logger.info(f"Health check ping: {response.status_code}")
97
- except Exception as e:
98
- logger.error(f"Health check failed: {e}")
99
-
100
-
101
  # =============================================================================
102
  # Quart Routes
103
  # =============================================================================
@@ -139,6 +107,16 @@ async def crypto_endpoint():
139
  return jsonify({"status": "not_implemented"}), 501
140
 
141
 
 
 
 
 
 
 
 
 
 
 
142
  # Future routes (uncomment when ready):
143
  # @app.route("/discord", methods=["POST"])
144
  # async def discord_interactions():
@@ -156,6 +134,19 @@ async def crypto_endpoint():
156
  # pass
157
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  # =============================================================================
160
  # Main entry point β€” called exclusively by Guardian (main.py)
161
  # =============================================================================
@@ -207,41 +198,24 @@ async def start_application(fundaments: Dict[str, Any]) -> None:
207
  # models.initialize() # reads app/.pyfun [MODELS]
208
  # tools.initialize() # reads app/.pyfun [TOOLS]
209
 
 
 
 
210
  # --- Read PORT from app/.pyfun [HUB] ---
211
  port = int(app_config.get_hub().get("HUB_PORT", "7860"))
212
 
213
- # --- Start MCP Hub in its own thread ---
214
- mcp_thread = threading.Thread(target=start_mcp_in_thread, daemon=True)
215
- mcp_thread.start()
216
- logger.info("MCP Hub thread started.")
217
 
218
- await asyncio.sleep(1)
 
219
 
220
- # --- Start health check worker ---
221
- health_thread = threading.Thread(
222
- target=health_check_worker,
223
- args=(port,),
224
- daemon=True
225
  )
226
- health_thread.start()
227
-
228
- # --- Start Quart via Waitress in its own thread ---
229
- def run_server():
230
- serve(app, host="0.0.0.0", port=port)
231
-
232
- server_thread = threading.Thread(target=run_server, daemon=True)
233
- server_thread.start()
234
- logger.info(f"HTTP server started on port {port}.")
235
-
236
- logger.info("All services running. Entering heartbeat loop...")
237
-
238
- # --- Heartbeat loop β€” keeps Guardian's async context alive ---
239
- try:
240
- while True:
241
- await asyncio.sleep(60)
242
- logger.debug("Heartbeat.")
243
- except KeyboardInterrupt:
244
- logger.info("Shutdown signal received.")
245
 
246
 
247
  # =============================================================================
 
19
  # - Secrets stay in .env β†’ Guardian reads them β†’ never touched by app/*
20
  # =============================================================================
21
 
22
+ from quart import Quart, request, jsonify # async Flask β€” ASGI compatible
23
  import logging
24
+ from hypercorn.asyncio import serve # ASGI server β€” async native, replaces waitress
25
+ from hypercorn.config import Config # hypercorn config
26
+ import threading # for future tools that need own threads
27
+ import requests # sync HTTP for future tool workers
28
  import time
29
  from datetime import datetime
30
  import asyncio
 
35
  # Each module reads its own config from app/.pyfun independently.
36
  # NO fundaments passed into these modules!
37
  # =============================================================================
38
+ from . import mcp # MCP transport layer (SSE via Quart route)
39
  from . import config as app_config # app/.pyfun parser β€” used only in app/*
40
  # from . import providers # API provider registry β€” reads app/.pyfun
41
  # from . import models # Model config + token/rate limits β€” reads app/.pyfun
 
66
  app = Quart(__name__)
67
  START_TIME = datetime.utcnow()
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  # =============================================================================
70
  # Quart Routes
71
  # =============================================================================
 
107
  return jsonify({"status": "not_implemented"}), 501
108
 
109
 
110
+ @app.route("/mcp", methods=["GET", "POST"])
111
+ async def mcp_endpoint():
112
+ """
113
+ MCP SSE Transport endpoint β€” routed through Quart/hypercorn.
114
+ All MCP traffic passes through here β€” enables interception, logging,
115
+ auth checks, rate limiting, payload transformation before reaching MCP.
116
+ """
117
+ return await mcp.handle_request(request)
118
+
119
+
120
  # Future routes (uncomment when ready):
121
  # @app.route("/discord", methods=["POST"])
122
  # async def discord_interactions():
 
134
  # pass
135
 
136
 
137
+ # =============================================================================
138
+ # Heartbeat β€” runs parallel to hypercorn via asyncio.gather()
139
+ # =============================================================================
140
+ async def heartbeat() -> None:
141
+ """
142
+ Periodic heartbeat log to confirm the application is alive.
143
+ Runs parallel to hypercorn in the same event loop via asyncio.gather().
144
+ """
145
+ while True:
146
+ await asyncio.sleep(60)
147
+ logger.debug("Heartbeat.")
148
+
149
+
150
  # =============================================================================
151
  # Main entry point β€” called exclusively by Guardian (main.py)
152
  # =============================================================================
 
198
  # models.initialize() # reads app/.pyfun [MODELS]
199
  # tools.initialize() # reads app/.pyfun [TOOLS]
200
 
201
+ # --- Initialize MCP (registers tools, prepares SSE handler) ---
202
+ await mcp.initialize()
203
+
204
  # --- Read PORT from app/.pyfun [HUB] ---
205
  port = int(app_config.get_hub().get("HUB_PORT", "7860"))
206
 
207
+ # --- Configure hypercorn ---
208
+ config = Config()
209
+ config.bind = [f"0.0.0.0:{port}"]
 
210
 
211
+ logger.info(f"Starting hypercorn on port {port}...")
212
+ logger.info("All services running.")
213
 
214
+ # --- Run hypercorn + heartbeat in parallel ---
215
+ await asyncio.gather(
216
+ serve(app, config), # hypercorn β€” blocks until shutdown
217
+ heartbeat() # heartbeat β€” runs parallel in same event loop
 
218
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
 
221
  # =============================================================================