R-Kentaren commited on
Commit
e26c426
Β·
verified Β·
1 Parent(s): 963e30d

rewrite: use gradio.Server + @app .api() + custom HTML (proper Gradio 6.x pattern)

Browse files
Files changed (3) hide show
  1. app.py +3 -5
  2. index.html +0 -0
  3. sonicoder/server/__init__.py +78 -34
app.py CHANGED
@@ -1,9 +1,9 @@
1
  """SoniCoder v2 β€” Entry point.
2
 
3
- Uses gradio.Server (Gradio 6.x) to serve a custom HTML frontend
4
  with Gradio's queuing, concurrency, and API infrastructure.
5
 
6
- Read: https://huggingface.co/blog/introducing-gradio-server
7
  """
8
 
9
  from __future__ import annotations
@@ -50,9 +50,7 @@ def main():
50
  from sonicoder.server import create_app
51
  app = create_app()
52
 
53
- # Launch
54
- port = int(os.environ.get("PORT", 7860))
55
- logger.info(f"Launching on port {port}")
56
  app.launch(show_error=True)
57
 
58
 
 
1
  """SoniCoder v2 β€” Entry point.
2
 
3
+ gradio.Server (Gradio 6.x) serves index.html from project root
4
  with Gradio's queuing, concurrency, and API infrastructure.
5
 
6
+ Blog: https://huggingface.co/blog/introducing-gradio-server
7
  """
8
 
9
  from __future__ import annotations
 
50
  from sonicoder.server import create_app
51
  app = create_app()
52
 
53
+ logger.info("Launching...")
 
 
54
  app.launch(show_error=True)
55
 
56
 
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
sonicoder/server/__init__.py CHANGED
@@ -1,8 +1,13 @@
1
- """Server routes β€” using gradio.Server (Gradio 6.x pattern).
2
 
3
- Read the blog: https://huggingface.co/blog/introducing-gradio-server
4
- gradio.Server extends FastAPI. @app.api() gives queuing + concurrency.
5
- @app.get("/") serves custom HTML frontend directly.
 
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -14,12 +19,15 @@ import uuid
14
  from pathlib import Path
15
  from typing import Any, Generator
16
 
 
 
 
17
  from ..config import (
18
  Settings, get_available_models, load_settings,
19
  WORKSPACE_ROOT, CONFIG_DIR,
20
  )
21
  from ..model.loader import (
22
- get_model_status, start_background_load, switch_model,
23
  )
24
  from ..tools import get_tool_registry, ToolRegistry
25
  from ..tools.fs import BUILTIN_TOOLS as FS_TOOLS
@@ -67,14 +75,14 @@ def _get_or_create_session(session_id: str = "") -> tuple[str, AgentSession]:
67
  return session_id, _sessions[session_id]
68
 
69
 
70
- # ── API Handlers ───────────────────────────────────────────────────────
71
 
72
  def api_chat(
73
  message: str,
74
  session_id: str = "",
75
  image_url: str | None = None,
76
  ) -> Generator[str, None, None]:
77
- """Main chat endpoint β€” streams events as JSON lines."""
78
  if not message or not message.strip():
79
  yield json.dumps({"type": "error", "error": "Empty message"})
80
  return
@@ -94,7 +102,7 @@ def api_model_status() -> str:
94
 
95
 
96
  def api_switch_model(model_key: str) -> str:
97
- switch_model(model_key)
98
  return json.dumps({"status": "switching", "model": model_key})
99
 
100
 
@@ -237,22 +245,23 @@ def api_get_config() -> str:
237
  def create_app():
238
  """Create and return a gradio.Server with custom HTML frontend.
239
 
240
- Uses the Gradio 6.x Server pattern:
241
- - @app.api() for backend endpoints (gets queuing + concurrency)
242
- - @app.get("/") to serve custom index.html
243
- - No gr.Blocks, no hidden Textbox hacks
 
 
 
244
  """
245
- import gradio as gr
246
- from fastapi.responses import HTMLResponse
247
 
248
- app = gr.Server()
249
 
250
- # ── Backend API endpoints (with Gradio queuing) ──
251
 
252
- @app.api(name="chat")
253
- def chat(message: str, session_id: str = "", image_url: str = ""):
254
- """Stream chat events as newline-delimited JSON."""
255
- return api_chat(message, session_id, image_url or None)
256
 
257
  @app.api(name="model_status")
258
  def model_status():
@@ -298,24 +307,59 @@ def create_app():
298
  def reset_session(session_id: str = ""):
299
  return api_reset_session(session_id)
300
 
301
- @app.api(name="get_config")
302
- def get_config():
303
- return api_get_config()
304
 
305
- # ── Serve custom HTML frontend ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  @app.get("/", response_class=HTMLResponse)
308
  async def homepage():
309
- """Serve the custom SPA frontend."""
310
- # Build config JSON to inject
311
- config_str = api_get_config()
312
-
313
- html_path = Path(__file__).parent.parent.parent / "static" / "index.html"
314
  if not html_path.exists():
315
- return HTMLResponse("<h1>SoniCoder v2</h1><p>index.html not found</p>")
316
-
317
- html = html_path.read_text(encoding="utf-8")
318
- html = html.replace("__RUNTIME_CONFIG__ || {}", config_str + " || {}")
319
- return HTMLResponse(content=html)
 
320
 
321
  return app
 
1
+ """Server routes β€” using gradio.Server (Gradio 6.x).
2
 
3
+ Blog: https://huggingface.co/blog/introducing-gradio-server
4
+
5
+ Pattern:
6
+ - from gradio import Server (NOT gradio.Server())
7
+ - @app.api(name="...") for backend endpoints β†’ gets Gradio queuing + concurrency
8
+ - @app.post("/chat") raw FastAPI route for SSE streaming
9
+ - @app.get("/", response_class=HTMLResponse) serves index.html from project root
10
+ - Frontend uses @gradio/client JS for non-streaming, raw fetch for streaming
11
  """
12
 
13
  from __future__ import annotations
 
19
  from pathlib import Path
20
  from typing import Any, Generator
21
 
22
+ from fastapi import Request
23
+ from fastapi.responses import HTMLResponse, StreamingResponse
24
+
25
  from ..config import (
26
  Settings, get_available_models, load_settings,
27
  WORKSPACE_ROOT, CONFIG_DIR,
28
  )
29
  from ..model.loader import (
30
+ get_model_status, start_background_load, switch_model as switch_model_fn,
31
  )
32
  from ..tools import get_tool_registry, ToolRegistry
33
  from ..tools.fs import BUILTIN_TOOLS as FS_TOOLS
 
75
  return session_id, _sessions[session_id]
76
 
77
 
78
+ # ── API Handlers (pure functions, called by @app.api or raw routes) ────
79
 
80
  def api_chat(
81
  message: str,
82
  session_id: str = "",
83
  image_url: str | None = None,
84
  ) -> Generator[str, None, None]:
85
+ """Main chat endpoint β€” streams events as newline-delimited JSON."""
86
  if not message or not message.strip():
87
  yield json.dumps({"type": "error", "error": "Empty message"})
88
  return
 
102
 
103
 
104
  def api_switch_model(model_key: str) -> str:
105
+ switch_model_fn(model_key)
106
  return json.dumps({"status": "switching", "model": model_key})
107
 
108
 
 
245
  def create_app():
246
  """Create and return a gradio.Server with custom HTML frontend.
247
 
248
+ Follows the blog pattern exactly:
249
+ https://huggingface.co/blog/introducing-gradio-server
250
+
251
+ - @app.api(name="...") for backend endpoints (gets Gradio queuing + concurrency)
252
+ - @app.post("/chat") raw FastAPI StreamingResponse for SSE streaming
253
+ - @app.get("/") serves index.html from project root
254
+ - Frontend uses @gradio/client for non-streaming, raw fetch for streaming
255
  """
256
+ from gradio import Server
 
257
 
258
+ app = Server()
259
 
260
+ # ── Backend API endpoints via @app.api() (Gradio queuing) ─────────
261
 
262
+ @app.api(name="get_config")
263
+ def get_config():
264
+ return api_get_config()
 
265
 
266
  @app.api(name="model_status")
267
  def model_status():
 
307
  def reset_session(session_id: str = ""):
308
  return api_reset_session(session_id)
309
 
310
+ # ── Streaming chat: raw FastAPI route (SSE) ───────────────────────
 
 
311
 
312
+ @app.post("/chat")
313
+ async def chat_stream(request: Request):
314
+ """Streaming chat via raw FastAPI β€” returns newline-delimited JSON."""
315
+ try:
316
+ body = await request.json()
317
+ except Exception:
318
+ body = {}
319
+
320
+ message = body.get("message", "")
321
+ session_id = body.get("session_id", "")
322
+ image_url = body.get("image_url")
323
+
324
+ if not message or not message.strip():
325
+ return StreamingResponse(
326
+ iter([json.dumps({"type": "error", "error": "Empty message"}) + "\n"]),
327
+ media_type="text/event-stream",
328
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
329
+ )
330
+
331
+ sid, session = _get_or_create_session(session_id)
332
+
333
+ def event_generator():
334
+ try:
335
+ for event in session.run(message, session_id=sid, image_url=image_url):
336
+ yield json.dumps(event, ensure_ascii=False) + "\n"
337
+ except Exception as e:
338
+ logger.error(f"Chat stream error: {e}", exc_info=True)
339
+ yield json.dumps({"type": "error", "error": str(e)}) + "\n"
340
+
341
+ return StreamingResponse(
342
+ event_generator(),
343
+ media_type="text/event-stream",
344
+ headers={
345
+ "Cache-Control": "no-cache",
346
+ "X-Accel-Buffering": "no",
347
+ "Connection": "keep-alive",
348
+ },
349
+ )
350
+
351
+ # ── Serve custom HTML frontend from project root ──────────────────
352
 
353
  @app.get("/", response_class=HTMLResponse)
354
  async def homepage():
355
+ """Serve the custom SPA frontend β€” index.html at project root."""
356
+ html_path = Path(__file__).parent.parent.parent / "index.html"
 
 
 
357
  if not html_path.exists():
358
+ return HTMLResponse(
359
+ "<h1>SoniCoder v2</h1><p>index.html not found at project root</p>",
360
+ status_code=500,
361
+ )
362
+ with open(html_path, "r", encoding="utf-8") as f:
363
+ return f.read()
364
 
365
  return app