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

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

Browse files
Files changed (2) hide show
  1. app.py +9 -66
  2. sonicoder/server/__init__.py +80 -139
app.py CHANGED
@@ -1,24 +1,15 @@
1
  """SoniCoder v2 β€” Entry point.
2
 
3
- Optimized AI code writer agent with:
4
- - Event-driven agent protocol (from Gemini CLI)
5
- - ToolBuilder/ToolInvocation pattern (from Gemini CLI)
6
- - MCP support (from both Gemini CLI & Claude Code)
7
- - Multiple smart 1B models (Qwen2.5-Coder-1.5B, DeepSeek-Coder-1.3B, etc.)
8
- - Policy/permission engine (from Gemini CLI)
9
- - Context management
10
- - Professional web UI
11
 
12
- Usage:
13
- python app.py
14
  """
15
 
16
  from __future__ import annotations
17
 
18
- import json
19
  import logging
20
  import os
21
- from pathlib import Path
22
 
23
  # Ensure workspace exists
24
  from sonicoder.config import WORKSPACE_ROOT, CONFIG_DIR
@@ -55,62 +46,14 @@ def main():
55
  else:
56
  logger.info(f"MCP server '{name}' connected")
57
 
58
- # Create the Gradio app with custom HTML
59
- from sonicoder.server import get_app, api_get_config
 
60
 
61
- # Build config JSON for frontend injection
62
- config_json = api_get_config()
63
- config_data = json.loads(config_json)
64
- config_str = json.dumps(config_data, ensure_ascii=False)
65
-
66
- # Read index.html and inject config
67
- index_path = Path(__file__).parent / "static" / "index.html"
68
- custom_html = None
69
- if index_path.exists():
70
- html = index_path.read_text(encoding="utf-8")
71
- html = html.replace(
72
- "__RUNTIME_CONFIG__ || {}",
73
- config_str + " || {}"
74
- )
75
- custom_html = html
76
- logger.info("Custom HTML prepared")
77
-
78
- application = get_app()
79
-
80
- # Register custom root route after app is built
81
- if custom_html:
82
- try:
83
- from fastapi import Response
84
- from fastapi.responses import HTMLResponse
85
-
86
- # Try multiple ways to access the underlying app in Gradio 6.x
87
- underlying = None
88
- for attr in ("app", "server_app", "_app", "app_obj"):
89
- underlying = getattr(application, attr, None)
90
- if underlying is not None:
91
- break
92
-
93
- if underlying is not None and hasattr(underlying, "get"):
94
- @underlying.get("/", response_class=HTMLResponse, include_in_schema=False)
95
- async def serve_index():
96
- return Response(content=custom_html, media_type="text/html")
97
- logger.info("Custom root route registered successfully")
98
- else:
99
- logger.warning("Could not register custom root route β€” using default Gradio UI")
100
- except Exception as e:
101
- logger.warning(f"Custom root route registration failed: {e}")
102
-
103
- # Launch β€” HF Spaces sets PORT env var
104
  port = int(os.environ.get("PORT", 7860))
105
- logger.info(f"Launching web interface on port {port}")
106
-
107
- application.launch(
108
- show_error=True,
109
- server_name="0.0.0.0",
110
- server_port=port,
111
- share=False,
112
- theme=None,
113
- )
114
 
115
 
116
  if __name__ == "__main__":
 
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
10
 
 
11
  import logging
12
  import os
 
13
 
14
  # Ensure workspace exists
15
  from sonicoder.config import WORKSPACE_ROOT, CONFIG_DIR
 
46
  else:
47
  logger.info(f"MCP server '{name}' connected")
48
 
49
+ # Create the server (gradio.Server β€” custom HTML + Gradio backend)
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
 
59
  if __name__ == "__main__":
sonicoder/server/__init__.py CHANGED
@@ -1,12 +1,15 @@
1
- """Server routes β€” FastAPI + Gradio API endpoints.
2
 
3
- All HTTP and Gradio API endpoints for the application.
 
 
4
  """
5
 
6
  from __future__ import annotations
7
 
8
  import json
9
  import logging
 
10
  import uuid
11
  from pathlib import Path
12
  from typing import Any, Generator
@@ -48,14 +51,12 @@ def _get_registry() -> ToolRegistry:
48
  if _registry is None:
49
  _registry = get_tool_registry()
50
  register_builtin_tools(_registry)
51
- # Apply filters
52
  settings = _get_settings()
53
  _registry.set_filters(settings.enabled_tools, settings.disabled_tools)
54
  return _registry
55
 
56
 
57
  def _get_or_create_session(session_id: str = "") -> tuple[str, AgentSession]:
58
- """Get existing session or create new one."""
59
  if not session_id:
60
  session_id = str(uuid.uuid4())[:12]
61
  if session_id not in _sessions:
@@ -66,14 +67,14 @@ def _get_or_create_session(session_id: str = "") -> tuple[str, AgentSession]:
66
  return session_id, _sessions[session_id]
67
 
68
 
69
- # ── Gradio API Functions ──────────────────────────────────────────────
70
 
71
  def api_chat(
72
  message: str,
73
  session_id: str = "",
74
  image_url: str | None = None,
75
  ) -> Generator[str, None, None]:
76
- """Main chat/agent endpoint β€” streams events as JSON lines."""
77
  if not message or not message.strip():
78
  yield json.dumps({"type": "error", "error": "Empty message"})
79
  return
@@ -88,27 +89,16 @@ def api_chat(
88
  yield json.dumps({"type": "error", "error": str(e)})
89
 
90
 
91
- def api_chat_sync(message: str, session_id: str = "") -> str:
92
- """Non-streaming chat for simple calls."""
93
- events = list(api_chat(message, session_id))
94
- if not events:
95
- return json.dumps({"type": "error", "error": "No response"})
96
- return events[-1] # Return last event (complete)
97
-
98
-
99
  def api_model_status() -> str:
100
- """Get model loading status."""
101
  return json.dumps(get_model_status())
102
 
103
 
104
  def api_switch_model(model_key: str) -> str:
105
- """Switch to a different model."""
106
  switch_model(model_key)
107
  return json.dumps({"status": "switching", "model": model_key})
108
 
109
 
110
  def api_list_models() -> str:
111
- """List all available models."""
112
  models = get_available_models()
113
  current = get_model_status().get("model_key", "")
114
  result = []
@@ -125,8 +115,6 @@ def api_list_models() -> str:
125
 
126
 
127
  def api_workspace_tree(max_depth: int = 3) -> str:
128
- """Get workspace directory tree."""
129
- import os
130
  tree = []
131
 
132
  def walk(path: Path, prefix: str = "", depth: int = 0):
@@ -136,7 +124,6 @@ def api_workspace_tree(max_depth: int = 3) -> str:
136
  items = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
137
  except PermissionError:
138
  return
139
-
140
  skip = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build", ".sonicoder"}
141
  for i, item in enumerate(items):
142
  if item.name.startswith(".") or item.name in skip:
@@ -160,7 +147,6 @@ def api_workspace_tree(max_depth: int = 3) -> str:
160
 
161
 
162
  def api_workspace_read(path: str, offset: int = 1, limit: int = 500) -> str:
163
- """Read a workspace file."""
164
  from ..tools.fs import _resolve_safe
165
  try:
166
  resolved = _resolve_safe(path)
@@ -183,7 +169,6 @@ def api_workspace_read(path: str, offset: int = 1, limit: int = 500) -> str:
183
 
184
 
185
  def api_workspace_write(path: str, content: str) -> str:
186
- """Write a workspace file."""
187
  from ..tools.fs import _resolve_safe
188
  try:
189
  resolved = _resolve_safe(path)
@@ -195,7 +180,6 @@ def api_workspace_write(path: str, content: str) -> str:
195
 
196
 
197
  def api_workspace_bash(command: str) -> str:
198
- """Execute a bash command in workspace."""
199
  from ..tools.bash import BashBuilder
200
  builder = BashBuilder()
201
  errors = builder.validate({"command": command})
@@ -207,7 +191,6 @@ def api_workspace_bash(command: str) -> str:
207
 
208
 
209
  def api_web_search(query: str) -> str:
210
- """Search the web."""
211
  from ..tools.web import WebSearchBuilder
212
  builder = WebSearchBuilder()
213
  invocation = builder.create_invocation({"query": query})
@@ -216,29 +199,21 @@ def api_web_search(query: str) -> str:
216
 
217
 
218
  def api_mcp_status() -> str:
219
- """Get MCP server status."""
220
- manager = get_mcp_manager()
221
- return json.dumps(manager.get_status())
222
 
223
 
224
  def api_policy_status() -> str:
225
- """Get policy engine status."""
226
  engine = get_policy_engine()
227
- return json.dumps({
228
- "mode": engine.approval_mode,
229
- "rules": engine.get_rules(),
230
- })
231
 
232
 
233
  def api_reset_session(session_id: str = "") -> str:
234
- """Reset a chat session."""
235
  if session_id in _sessions:
236
  _sessions[session_id].clear_history()
237
  return json.dumps({"success": True})
238
 
239
 
240
  def api_get_config() -> str:
241
- """Get runtime configuration for the frontend."""
242
  models = get_available_models()
243
  model_list = []
244
  for key, config in models.items():
@@ -249,7 +224,6 @@ def api_get_config() -> str:
249
  "size_gb": config.size_gb,
250
  "description": config.description,
251
  })
252
-
253
  return json.dumps({
254
  "models": model_list,
255
  "default_model": _get_settings().default_model,
@@ -258,123 +232,90 @@ def api_get_config() -> str:
258
  }, ensure_ascii=False)
259
 
260
 
261
- # ── Create Gradio App ──────────────────────────────────────────────────
 
 
 
262
 
263
- def get_app():
264
- """Create and return the Gradio application."""
 
 
 
265
  import gradio as gr
266
- from fastapi.responses import FileResponse
267
- from pathlib import Path
268
-
269
- # Create the Gradio app
270
- app = gr.Blocks(
271
- title="SoniCoder v2 β€” AI Code Agent",
272
- )
273
-
274
- with app:
275
- # Hidden components for API
276
- api_chat_output = gr.Textbox(visible=False)
277
- api_status_output = gr.Textbox(visible=False)
278
- api_json_output = gr.Textbox(visible=False)
279
-
280
- # API endpoints
281
- gr.Interface(
282
- fn=api_chat,
283
- inputs=[
284
- gr.Textbox(label="message"),
285
- gr.Textbox(label="session_id", value=""),
286
- gr.Textbox(label="image_url", value=""),
287
- ],
288
- outputs=[api_chat_output],
289
- api_name="chat",
290
- )
291
 
292
- gr.Interface(
293
- fn=api_model_status,
294
- inputs=[],
295
- outputs=[api_json_output],
296
- api_name="model_status",
297
- )
298
 
299
- gr.Interface(
300
- fn=api_switch_model,
301
- inputs=[gr.Textbox(label="model_key")],
302
- outputs=[api_json_output],
303
- api_name="switch_model",
304
- )
305
 
306
- gr.Interface(
307
- fn=api_list_models,
308
- inputs=[],
309
- outputs=[api_json_output],
310
- api_name="list_models",
311
- )
312
 
313
- gr.Interface(
314
- fn=api_workspace_tree,
315
- inputs=[gr.Number(label="max_depth", value=3)],
316
- outputs=[api_json_output],
317
- api_name="workspace_tree",
318
- )
319
 
320
- gr.Interface(
321
- fn=api_workspace_read,
322
- inputs=[
323
- gr.Textbox(label="path"),
324
- gr.Number(label="offset", value=1),
325
- gr.Number(label="limit", value=500),
326
- ],
327
- outputs=[api_json_output],
328
- api_name="workspace_read",
329
- )
330
 
331
- gr.Interface(
332
- fn=api_workspace_write,
333
- inputs=[gr.Textbox(label="path"), gr.Textbox(label="content", lines=10)],
334
- outputs=[api_json_output],
335
- api_name="workspace_write",
336
- )
337
 
338
- gr.Interface(
339
- fn=api_workspace_bash,
340
- inputs=[gr.Textbox(label="command")],
341
- outputs=[api_json_output],
342
- api_name="workspace_bash",
343
- )
344
 
345
- gr.Interface(
346
- fn=api_web_search,
347
- inputs=[gr.Textbox(label="query")],
348
- outputs=[api_json_output],
349
- api_name="web_search",
350
- )
351
 
352
- gr.Interface(
353
- fn=api_mcp_status,
354
- inputs=[],
355
- outputs=[api_json_output],
356
- api_name="mcp_status",
357
- )
358
 
359
- gr.Interface(
360
- fn=api_policy_status,
361
- inputs=[],
362
- outputs=[api_json_output],
363
- api_name="policy_status",
364
- )
365
 
366
- gr.Interface(
367
- fn=api_reset_session,
368
- inputs=[gr.Textbox(label="session_id", value="")],
369
- outputs=[api_json_output],
370
- api_name="reset_session",
371
- )
372
 
373
- gr.Interface(
374
- fn=api_get_config,
375
- inputs=[],
376
- outputs=[api_json_output],
377
- api_name="get_config",
378
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  return app
 
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
9
 
10
  import json
11
  import logging
12
+ import os
13
  import uuid
14
  from pathlib import Path
15
  from typing import Any, Generator
 
51
  if _registry is None:
52
  _registry = get_tool_registry()
53
  register_builtin_tools(_registry)
 
54
  settings = _get_settings()
55
  _registry.set_filters(settings.enabled_tools, settings.disabled_tools)
56
  return _registry
57
 
58
 
59
  def _get_or_create_session(session_id: str = "") -> tuple[str, AgentSession]:
 
60
  if not session_id:
61
  session_id = str(uuid.uuid4())[:12]
62
  if session_id not in _sessions:
 
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
 
89
  yield json.dumps({"type": "error", "error": str(e)})
90
 
91
 
 
 
 
 
 
 
 
 
92
  def api_model_status() -> str:
 
93
  return json.dumps(get_model_status())
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
 
101
  def api_list_models() -> str:
 
102
  models = get_available_models()
103
  current = get_model_status().get("model_key", "")
104
  result = []
 
115
 
116
 
117
  def api_workspace_tree(max_depth: int = 3) -> str:
 
 
118
  tree = []
119
 
120
  def walk(path: Path, prefix: str = "", depth: int = 0):
 
124
  items = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
125
  except PermissionError:
126
  return
 
127
  skip = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build", ".sonicoder"}
128
  for i, item in enumerate(items):
129
  if item.name.startswith(".") or item.name in skip:
 
147
 
148
 
149
  def api_workspace_read(path: str, offset: int = 1, limit: int = 500) -> str:
 
150
  from ..tools.fs import _resolve_safe
151
  try:
152
  resolved = _resolve_safe(path)
 
169
 
170
 
171
  def api_workspace_write(path: str, content: str) -> str:
 
172
  from ..tools.fs import _resolve_safe
173
  try:
174
  resolved = _resolve_safe(path)
 
180
 
181
 
182
  def api_workspace_bash(command: str) -> str:
 
183
  from ..tools.bash import BashBuilder
184
  builder = BashBuilder()
185
  errors = builder.validate({"command": command})
 
191
 
192
 
193
  def api_web_search(query: str) -> str:
 
194
  from ..tools.web import WebSearchBuilder
195
  builder = WebSearchBuilder()
196
  invocation = builder.create_invocation({"query": query})
 
199
 
200
 
201
  def api_mcp_status() -> str:
202
+ return json.dumps(get_mcp_manager().get_status())
 
 
203
 
204
 
205
  def api_policy_status() -> str:
 
206
  engine = get_policy_engine()
207
+ return json.dumps({"mode": engine.approval_mode, "rules": engine.get_rules()})
 
 
 
208
 
209
 
210
  def api_reset_session(session_id: str = "") -> str:
 
211
  if session_id in _sessions:
212
  _sessions[session_id].clear_history()
213
  return json.dumps({"success": True})
214
 
215
 
216
  def api_get_config() -> str:
 
217
  models = get_available_models()
218
  model_list = []
219
  for key, config in models.items():
 
224
  "size_gb": config.size_gb,
225
  "description": config.description,
226
  })
 
227
  return json.dumps({
228
  "models": model_list,
229
  "default_model": _get_settings().default_model,
 
232
  }, ensure_ascii=False)
233
 
234
 
235
+ # ── Build App (gradio.Server) ─────────────────────────────────────────
236
+
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():
259
+ return api_model_status()
 
 
 
260
 
261
+ @app.api(name="switch_model")
262
+ def switch_model_api(model_key: str):
263
+ return api_switch_model(model_key)
 
 
 
 
 
 
 
264
 
265
+ @app.api(name="list_models")
266
+ def list_models():
267
+ return api_list_models()
 
 
 
268
 
269
+ @app.api(name="workspace_tree")
270
+ def workspace_tree(max_depth: int = 3):
271
+ return api_workspace_tree(max_depth)
 
 
 
272
 
273
+ @app.api(name="workspace_read")
274
+ def workspace_read(path: str, offset: int = 1, limit: int = 500):
275
+ return api_workspace_read(path, offset, limit)
 
 
 
276
 
277
+ @app.api(name="workspace_write")
278
+ def workspace_write(path: str, content: str):
279
+ return api_workspace_write(path, content)
 
 
 
280
 
281
+ @app.api(name="workspace_bash")
282
+ def workspace_bash(command: str):
283
+ return api_workspace_bash(command)
 
 
 
284
 
285
+ @app.api(name="web_search")
286
+ def web_search(query: str):
287
+ return api_web_search(query)
 
 
 
288
 
289
+ @app.api(name="mcp_status")
290
+ def mcp_status():
291
+ return api_mcp_status()
292
+
293
+ @app.api(name="policy_status")
294
+ def policy_status():
295
+ return api_policy_status()
296
+
297
+ @app.api(name="reset_session")
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