synapse-sdk 1.0.0a98__py3-none-any.whl → 1.0.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synapse-sdk might be problematic. Click here for more details.

Files changed (65) hide show
  1. synapse_sdk/cli/__init__.py +139 -84
  2. synapse_sdk/cli/code_server.py +169 -0
  3. synapse_sdk/cli/config.py +105 -4
  4. synapse_sdk/cli/devtools.py +54 -34
  5. synapse_sdk/clients/base.py +3 -4
  6. synapse_sdk/devtools/server.py +24 -791
  7. synapse_sdk/devtools/streamlit_app/__init__.py +5 -0
  8. synapse_sdk/devtools/streamlit_app/app.py +128 -0
  9. synapse_sdk/devtools/streamlit_app/services/__init__.py +11 -0
  10. synapse_sdk/devtools/streamlit_app/services/job_service.py +233 -0
  11. synapse_sdk/devtools/streamlit_app/services/plugin_service.py +236 -0
  12. synapse_sdk/devtools/streamlit_app/services/serve_service.py +95 -0
  13. synapse_sdk/devtools/streamlit_app/ui/__init__.py +15 -0
  14. synapse_sdk/devtools/streamlit_app/ui/config_tab.py +76 -0
  15. synapse_sdk/devtools/streamlit_app/ui/deployment_tab.py +66 -0
  16. synapse_sdk/devtools/streamlit_app/ui/http_tab.py +125 -0
  17. synapse_sdk/devtools/streamlit_app/ui/jobs_tab.py +573 -0
  18. synapse_sdk/devtools/streamlit_app/ui/serve_tab.py +346 -0
  19. synapse_sdk/devtools/streamlit_app/ui/status_bar.py +118 -0
  20. synapse_sdk/devtools/streamlit_app/utils/__init__.py +40 -0
  21. synapse_sdk/devtools/streamlit_app/utils/json_viewer.py +197 -0
  22. synapse_sdk/devtools/streamlit_app/utils/log_formatter.py +38 -0
  23. synapse_sdk/devtools/streamlit_app/utils/styles.py +241 -0
  24. synapse_sdk/devtools/streamlit_app/utils/ui_components.py +289 -0
  25. synapse_sdk/devtools/streamlit_app.py +10 -0
  26. synapse_sdk/plugins/categories/upload/actions/upload.py +2 -1
  27. synapse_sdk/utils/converters/coco/from_dm.py +2 -2
  28. synapse_sdk/utils/converters/dm/__init__.py +0 -1
  29. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/METADATA +4 -6
  30. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/RECORD +34 -45
  31. synapse_sdk/devtools/models.py +0 -55
  32. synapse_sdk/devtools/utils.py +0 -52
  33. synapse_sdk/devtools/web/.gitignore +0 -2
  34. synapse_sdk/devtools/web/README.md +0 -34
  35. synapse_sdk/devtools/web/dist/index.html +0 -17
  36. synapse_sdk/devtools/web/index.html +0 -16
  37. synapse_sdk/devtools/web/jsconfig.json +0 -15
  38. synapse_sdk/devtools/web/package-lock.json +0 -2609
  39. synapse_sdk/devtools/web/package.json +0 -27
  40. synapse_sdk/devtools/web/pnpm-lock.yaml +0 -1055
  41. synapse_sdk/devtools/web/src/App.jsx +0 -14
  42. synapse_sdk/devtools/web/src/App.module.css +0 -33
  43. synapse_sdk/devtools/web/src/assets/favicon.ico +0 -0
  44. synapse_sdk/devtools/web/src/components/Breadcrumbs.jsx +0 -42
  45. synapse_sdk/devtools/web/src/components/Layout.jsx +0 -12
  46. synapse_sdk/devtools/web/src/components/LogViewer.jsx +0 -280
  47. synapse_sdk/devtools/web/src/components/MessageViewer.jsx +0 -150
  48. synapse_sdk/devtools/web/src/components/NavigationSidebar.jsx +0 -128
  49. synapse_sdk/devtools/web/src/components/ServerStatusBar.jsx +0 -245
  50. synapse_sdk/devtools/web/src/components/icons.jsx +0 -325
  51. synapse_sdk/devtools/web/src/index.css +0 -470
  52. synapse_sdk/devtools/web/src/index.jsx +0 -15
  53. synapse_sdk/devtools/web/src/logo.svg +0 -1
  54. synapse_sdk/devtools/web/src/router.jsx +0 -34
  55. synapse_sdk/devtools/web/src/utils/api.js +0 -442
  56. synapse_sdk/devtools/web/src/views/ApplicationDetailView.jsx +0 -241
  57. synapse_sdk/devtools/web/src/views/ApplicationsView.jsx +0 -224
  58. synapse_sdk/devtools/web/src/views/HomeView.jsx +0 -197
  59. synapse_sdk/devtools/web/src/views/JobDetailView.jsx +0 -310
  60. synapse_sdk/devtools/web/src/views/PluginView.jsx +0 -914
  61. synapse_sdk/devtools/web/vite.config.js +0 -13
  62. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/WHEEL +0 -0
  63. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/entry_points.txt +0 -0
  64. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/licenses/LICENSE +0 -0
  65. {synapse_sdk-1.0.0a98.dist-info → synapse_sdk-1.0.0b2.dist-info}/top_level.txt +0 -0
@@ -1,808 +1,41 @@
1
- import json
2
- import logging
3
- from contextlib import asynccontextmanager
4
- from datetime import datetime
5
- from importlib.metadata import version
6
- from pathlib import Path
7
- from typing import Dict, List, Optional
1
+ """
2
+ Legacy server module - DEPRECATED
3
+
4
+ This module is kept for backwards compatibility only.
5
+ The devtools now use Streamlit exclusively.
8
6
 
9
- import requests
10
- import uvicorn
11
- import yaml
12
- from fastapi import FastAPI, HTTPException, Request, WebSocket
13
- from fastapi.middleware.cors import CORSMiddleware
14
- from fastapi.responses import StreamingResponse
15
- from fastapi.staticfiles import StaticFiles
7
+ All functionality has been moved to streamlit_app.py
8
+ """
16
9
 
17
- from synapse_sdk.cli.plugin.publish import _publish
18
- from synapse_sdk.clients.agent import AgentClient
19
- from synapse_sdk.clients.backend import BackendClient
20
- from synapse_sdk.clients.exceptions import ClientError
21
- from synapse_sdk.devtools.config import get_backend_config, load_devtools_config
22
- from synapse_sdk.devtools.models import ConfigResponse, ConfigUpdateRequest
23
- from synapse_sdk.devtools.utils import get_display_host
10
+ import logging
11
+ from pathlib import Path
24
12
 
25
13
  logger = logging.getLogger(__name__)
26
14
 
27
15
 
28
16
  class DevtoolsServer:
17
+ """Legacy DevtoolsServer class - DEPRECATED
18
+
19
+ This class is kept only for backwards compatibility.
20
+ Use streamlit_app.py instead.
21
+ """
22
+
29
23
  def __init__(self, host: str = '0.0.0.0', port: int = 8080, plugin_directory: str = None):
24
+ logger.warning("DevtoolsServer is deprecated. Use 'synapse devtools' command which runs Streamlit instead.")
30
25
  self.host = host
31
26
  self.port = port
32
-
33
- self.agent_id = None
34
- self.plugin_code = None
35
27
  self.plugin_directory = Path(plugin_directory) if plugin_directory else Path.cwd()
36
- self.websocket_connections: List[WebSocket] = []
37
- self.app = FastAPI(title='Synapse Devtools', version='1.0.0', lifespan=self.lifespan)
38
- self._cached_validation_result = None # Cache validation result from startup
39
-
40
- self.web_build_dir = Path(__file__).parent / 'web' / 'dist'
41
- self.web_public_dir = Path(__file__).parent / 'web' / 'public'
42
-
43
- self.setup_middleware()
44
- self.setup_static_files()
45
- self.setup_routes()
46
-
47
- # Initialize clients if config is available
48
- self.backend_client = self._init_backend_client()
49
- self.agent_client = self._init_agent_client()
50
-
51
- @asynccontextmanager
52
- async def lifespan(self, app: FastAPI):
53
- # Startup
54
- logger.info('DevTools server starting up...')
55
- yield
56
- # Shutdown
57
- logger.info('DevTools server shutting down...')
58
- # Close all WebSocket connections
59
- if self.websocket_connections:
60
- logger.info(f'Closing {len(self.websocket_connections)} WebSocket connections')
61
- for connection in self.websocket_connections[:]:
62
- try:
63
- await connection.close(code=1000, reason='Server shutdown')
64
- except Exception as e:
65
- logger.error(f'Error closing WebSocket connection: {e}')
66
- self.websocket_connections.clear()
67
- logger.info('DevTools server shutdown complete')
68
-
69
- def _init_backend_client(self) -> Optional[BackendClient]:
70
- config = get_backend_config()
71
- if config:
72
- return BackendClient(config['host'], access_token=config['token'])
73
- return None
74
-
75
- def _init_agent_client(self) -> Optional[AgentClient]:
76
- devtools_config = load_devtools_config()
77
- agent_config = devtools_config.get('agent', {})
78
- if agent_config and 'url' in agent_config:
79
- self.agent_id = agent_config['id']
80
- # Use shorter timeouts for better UX in devtools
81
- timeout = {
82
- 'connect': 3, # 3 second connection timeout
83
- 'read': 10, # 10 second read timeout
84
- }
85
- return AgentClient(agent_config['url'], agent_config.get('token'), timeout=timeout)
86
- return None
87
-
88
- def _validate_plugin_config(self) -> Dict:
89
- """Validate plugin config.yaml in the plugin directory"""
90
-
91
- return {'valid': True, 'errors': None, 'schema_version': 'unknown', 'validated_fields': 0}
92
-
93
- async def _publish_plugin_internal(self, host: str, access_token: str, debug: bool, source_path: Path) -> Dict:
94
- """Internal method to publish plugin using CLI logic"""
95
- import os
96
-
97
- original_cwd = os.getcwd()
98
- try:
99
- os.chdir(str(source_path))
100
-
101
- debug_modules = os.getenv('SYNAPSE_DEBUG_MODULES', '')
102
- plugin_release = _publish(host, access_token, debug, debug_modules)
103
-
104
- return {
105
- 'success': True,
106
- 'message': f'Successfully published "{plugin_release.name}" '
107
- f'({plugin_release.code}) to synapse backend!',
108
- 'plugin_code': plugin_release.code,
109
- 'version': plugin_release.version,
110
- 'name': plugin_release.name,
111
- 'result': '',
112
- }
113
-
114
- finally:
115
- os.chdir(original_cwd)
116
-
117
- async def _execute_plugin_http_request(
118
- self,
119
- action: str,
120
- params: Dict,
121
- debug: bool,
122
- access_token: str,
123
- ) -> Dict:
124
- """Execute an HTTP request to the plugin endpoint"""
125
- import asyncio
126
- import time
127
-
128
- import aiohttp
129
-
130
- plugin_url = f'{self.backend_client.base_url}/plugins/{self.plugin_code}/run/'
131
-
132
- headers = {
133
- 'Accept': 'application/json; indent=4',
134
- 'Content-Type': 'application/json',
135
- 'Synapse-Access-Token': f'Token {access_token}',
136
- }
137
-
138
- payload = {'agent': self.agent_id, 'action': action, 'params': params, 'debug': debug}
139
-
140
- start_time = time.time()
141
-
142
- try:
143
- timeout = aiohttp.ClientTimeout(total=30) # 30 second timeout
144
- async with aiohttp.ClientSession(timeout=timeout) as session:
145
- async with session.post(plugin_url, json=payload, headers=headers) as response:
146
- execution_time = int((time.time() - start_time) * 1000)
147
-
148
- response_text = await response.text()
149
-
150
- # Try to parse as JSON, fallback to text
151
- try:
152
- response_data = await response.json()
153
- except Exception:
154
- response_data = response_text
155
-
156
- return {
157
- 'success': response.status < 400,
158
- 'status_code': response.status,
159
- 'response_data': response_data,
160
- 'execution_time': execution_time,
161
- 'url': plugin_url,
162
- 'method': 'POST',
163
- 'headers': headers,
164
- 'payload': payload,
165
- }
166
-
167
- except asyncio.TimeoutError:
168
- execution_time = int((time.time() - start_time) * 1000)
169
- return {
170
- 'success': False,
171
- 'status_code': 408,
172
- 'error': 'Request timeout',
173
- 'execution_time': execution_time,
174
- 'url': plugin_url,
175
- }
176
- except Exception as e:
177
- execution_time = int((time.time() - start_time) * 1000)
178
- return {
179
- 'success': False,
180
- 'status_code': 500,
181
- 'error': str(e),
182
- 'execution_time': execution_time,
183
- 'url': plugin_url,
184
- }
185
-
186
- def _get_action_example_params(self, action: str) -> Dict:
187
- """Get pre-filled example parameters for different actions based on test.http"""
188
- examples = {
189
- 'tune': {
190
- 'num_cpus': 2,
191
- 'num_gpus': 1,
192
- 'name': 'tune-test-example',
193
- 'description': 'Tuning example',
194
- 'experiment': 37,
195
- 'dataset': 55,
196
- 'checkpoint': 163,
197
- 'tune_config': {'metric': 'metrics/mAP50-95(B)', 'mode': 'max', 'num_samples': 5},
198
- 'hyperparameter': [
199
- {'name': 'learning_rate', 'type': 'loguniform', 'min': 1e-4, 'max': 1e-1},
200
- {'name': 'batch_size', 'type': 'choice', 'options': [8, 16, 32]},
201
- {'name': 'epochs', 'type': 'randint', 'min': 1, 'max': 2},
202
- ],
203
- },
204
- 'deployment': {'num_cpus': 4, 'num_gpus': 1},
205
- 'train': {
206
- 'num_cpus': 2,
207
- 'num_gpus': 1,
208
- 'name': 'train-test-example',
209
- 'description': 'Training example',
210
- 'experiment': 32,
211
- 'dataset': 55,
212
- 'checkpoint': 8,
213
- 'hyperparameter': {'epochs': 10, 'batch_size': 8, 'learning_rate': 0.001, 'imgsz': 640},
214
- },
215
- 'gradio': {'num_cpus': 4},
216
- 'inference': {'num_cpus': 2, 'num_gpus': 1},
217
- 'test': {'num_cpus': 1},
218
- }
219
-
220
- return examples.get(action, {})
221
-
222
- def setup_middleware(self):
223
- self.app.add_middleware(
224
- CORSMiddleware,
225
- allow_origins=['*'],
226
- allow_credentials=True,
227
- allow_methods=['*'],
228
- allow_headers=['*'],
229
- )
230
-
231
- def setup_static_files(self):
232
- # Serve built Vue.js assets from web/dist/
233
- if self.web_build_dir.exists():
234
- # Serve Vue.js built assets
235
- assets_dir = self.web_build_dir / 'assets'
236
- if assets_dir.exists():
237
- self.app.mount('/assets', StaticFiles(directory=assets_dir), name='assets')
238
- else:
239
- logger.warning('Vue.js dist directory not found, devtools may not work properly')
240
-
241
- # Serve public files (like logo.png) during development
242
- if self.web_public_dir.exists():
243
- self.app.mount('/public', StaticFiles(directory=self.web_public_dir), name='public')
244
-
245
- def _inject_env_vars(self, html_content: str) -> str:
246
- """Inject environment variables into HTML"""
247
- # Use display host for frontend access (resolves 0.0.0.0 to actual IP)
248
- display_host = get_display_host(self.host)
249
- env_script = f"""<script>
250
- window.VITE_API_PORT = {self.port};
251
- window.VITE_API_HOST = '{display_host}';
252
- </script>"""
253
-
254
- # Insert before closing head tag
255
- return html_content.replace('</head>', f'{env_script}</head>')
256
-
257
- def setup_routes(self):
258
- @self.app.get('/')
259
- async def read_root():
260
- index_file = self.web_build_dir / 'index.html'
261
-
262
- if index_file.exists():
263
- # Read and modify HTML content to inject environment variables
264
- with open(index_file, 'r') as f:
265
- html_content = f.read()
266
-
267
- modified_html = self._inject_env_vars(html_content)
268
-
269
- from fastapi.responses import HTMLResponse
270
-
271
- return HTMLResponse(content=modified_html)
272
- return {'message': 'Synapse Devtools API', 'docs': '/docs'}
273
-
274
- @self.app.get('/status')
275
- async def get_status():
276
- from synapse_sdk.devtools.config import load_devtools_config
277
-
278
- backend_config = get_backend_config()
279
- devtools_config = load_devtools_config()
280
- agent_config = devtools_config.get('agent', {})
281
-
282
- status = {
283
- 'backend': {
284
- 'host': self.backend_client.base_url,
285
- 'status': 'configured' if backend_config else 'not_configured',
286
- },
287
- 'agent': {
288
- 'id': agent_config.get('id'),
289
- 'name': agent_config.get('name'),
290
- },
291
- 'devtools': {'version': version('synapse-sdk')},
292
- }
293
-
294
- return status
295
-
296
- @self.app.get('/auth/token')
297
- async def get_auth_token():
298
- """Get the current authentication token from configuration"""
299
- backend_config = get_backend_config()
300
-
301
- if backend_config and 'token' in backend_config:
302
- return {'token': backend_config['token'], 'host': backend_config['host']}
303
-
304
- return {'token': None, 'host': None}
305
-
306
- @self.app.get('/jobs')
307
- async def get_jobs():
308
- """Get the list of jobs from the backend"""
309
- if not self.agent_client:
310
- raise HTTPException(status_code=503, detail='Agent client not configured')
311
-
312
- try:
313
- jobs = self.agent_client.list_jobs()
314
- jobs.sort(key=lambda job: job.get('start_time', 0), reverse=True)
315
- return jobs
316
- except ClientError as e:
317
- logger.warning(f'Agent client error while fetching jobs: {e.reason}')
318
- if e.status in [408, 503]:
319
- raise HTTPException(status_code=503, detail=f'Agent unavailable: {e.reason}')
320
- else:
321
- raise HTTPException(status_code=e.status, detail=e.reason)
322
- except Exception as e:
323
- logger.error(f'Unexpected error fetching jobs: {e}')
324
- raise HTTPException(status_code=500, detail='Failed to fetch jobs')
325
-
326
- @self.app.get('/jobs/{job_id}')
327
- async def get_job(job_id: str):
328
- """Get details of a specific job by ID"""
329
- if not self.agent_client:
330
- raise HTTPException(status_code=503, detail='Agent client not configured')
331
-
332
- try:
333
- job = self.agent_client.get_job(job_id)
334
- return job
335
- except Exception as e:
336
- logger.error(f'Failed to fetch job {job_id}: {e}')
337
- raise HTTPException(status_code=500, detail=f'Failed to fetch job {job_id}')
338
-
339
- @self.app.get('/jobs/{job_id}/logs')
340
- async def get_job_logs(job_id: str, request: Request):
341
- """Get logs for a specific job by ID"""
342
- if not self.agent_client:
343
- raise HTTPException(status_code=503, detail='Agent client not configured')
344
-
345
- async def log_generator():
346
- import asyncio
347
-
348
- try:
349
- # Run the synchronous generator in a thread to avoid blocking
350
- loop = asyncio.get_event_loop()
351
- log_iter = self.agent_client.tail_job_logs(job_id, stream_timeout=3)
352
-
353
- while True:
354
- # Check if client disconnected before getting next log
355
- if await request.is_disconnected():
356
- logger.info(f'Client disconnected, stopping log stream for job {job_id}')
357
- break
358
-
359
- try:
360
- # Get next log line with timeout to allow periodic disconnect checks
361
- log_line = await loop.run_in_executor(None, lambda: next(log_iter, None))
362
-
363
- if log_line is None:
364
- break
365
-
366
- # Check again before yielding
367
- if await request.is_disconnected():
368
- logger.info(f'Client disconnected before yield, stopping log stream for job {job_id}')
369
- break
370
-
371
- # Convert plain text to SSE format
372
- if log_line.strip():
373
- yield f'data: {log_line.strip()}\n\n'
374
-
375
- except StopIteration:
376
- break
377
-
378
- except ClientError as e:
379
- logger.warning(f'Agent client error in log streaming for job {job_id}: {e.reason}')
380
- if e.status == 408:
381
- yield 'data: Log stream timeout - agent may be unresponsive\n\n'
382
- elif e.status == 503:
383
- yield 'data: Agent connection error - service may be unavailable\n\n'
384
- else:
385
- yield f'data: Agent error: {e.reason}\n\n'
386
- except Exception as e:
387
- logger.error(f'Unexpected error in log streaming for job {job_id}: {e}')
388
- yield f'data: Unexpected error: {str(e)}\n\n'
389
- finally:
390
- logger.info(f'Log streaming ended for job {job_id}')
391
-
392
- try:
393
- return StreamingResponse(log_generator(), media_type='text/event-stream')
394
- except Exception as e:
395
- logger.error(f'Failed to setup log stream for job {job_id}: {e}')
396
- raise HTTPException(status_code=500, detail=f'Failed to setup log stream for job {job_id}')
397
-
398
- @self.app.get('/serve_applications')
399
- async def get_serve_applications(request: Request):
400
- """Get the list of serve applications from the agent or serve SPA"""
401
- # Check if this is an API request (AJAX call) vs browser navigation
402
- accept_header = request.headers.get('accept', '')
403
-
404
- # If browser navigation (HTML request), serve the SPA
405
- if 'text/html' in accept_header and 'application/json' not in accept_header:
406
- index_file = self.web_build_dir / 'index.html'
407
- if index_file.exists():
408
- # Read and modify HTML content to inject environment variables
409
- with open(index_file, 'r') as f:
410
- html_content = f.read()
411
-
412
- modified_html = self._inject_env_vars(html_content)
413
-
414
- from fastapi.responses import HTMLResponse
415
-
416
- return HTMLResponse(content=modified_html)
417
- return {'message': 'Frontend not built'}
418
-
419
- # Otherwise, handle as API request
420
- if not self.agent_client:
421
- raise HTTPException(status_code=503, detail='Agent client not configured')
422
-
423
- try:
424
- applications = self.agent_client.list_serve_applications()
425
- # Sort by creation time if available, otherwise alphabetically by name
426
- if applications:
427
- applications.sort(key=lambda app: app.get('created_at', app.get('name', '')), reverse=True)
428
- return applications
429
- except ClientError as e:
430
- logger.warning(f'Agent client error while fetching applications: {e.reason}')
431
- if e.status in [408, 503]:
432
- raise HTTPException(status_code=503, detail=f'Agent unavailable: {e.reason}')
433
- else:
434
- raise HTTPException(status_code=e.status, detail=e.reason)
435
- except Exception as e:
436
- logger.error(f'Unexpected error fetching applications: {e}')
437
- raise HTTPException(status_code=500, detail='Failed to fetch applications')
438
-
439
- @self.app.get('/serve_applications/{app_id}')
440
- async def get_serve_application(app_id: str):
441
- """Get details of a specific application by ID"""
442
- if not self.agent_client:
443
- raise HTTPException(status_code=503, detail='Agent client not configured')
444
-
445
- try:
446
- application = self.agent_client.get_serve_application(app_id)
447
- return application
448
- except Exception as e:
449
- logger.error(f'Failed to fetch application {app_id}: {e}')
450
- raise HTTPException(status_code=500, detail=f'Failed to fetch application {app_id}')
451
-
452
- @self.app.delete('/serve_applications/{app_id}')
453
- async def delete_serve_application(app_id: str):
454
- """Delete a serve application by ID"""
455
- if not self.agent_client:
456
- raise HTTPException(status_code=503, detail='Agent client not configured')
457
-
458
- try:
459
- result = self.agent_client.delete_serve_application(app_id)
460
- return {'message': f'Application {app_id} deleted successfully', 'result': result}
461
- except Exception as e:
462
- logger.error(f'Failed to delete application {app_id}: {e}')
463
- raise HTTPException(status_code=500, detail=f'Failed to delete application {app_id}')
464
-
465
- @self.app.get('/health/backend')
466
- async def check_backend_health():
467
- """Check backend health status"""
468
- if not self.backend_client:
469
- return {'status': 'not_configured', 'error': 'Backend client not configured', 'lastCheck': None}
470
-
471
- import time
472
-
473
- start_time = time.time()
474
-
475
- try:
476
- # Use the backend client to check health with shorter timeout
477
- response = requests.get(f'{self.backend_client.base_url}/health', timeout=3)
478
- latency = int((time.time() - start_time) * 1000)
479
-
480
- if response.status_code == 200:
481
- return {
482
- 'status': 'healthy',
483
- 'latency': latency,
484
- 'lastCheck': time.time(),
485
- 'url': f'{self.backend_client.base_url}',
486
- 'httpStatus': response.status_code,
487
- }
488
- else:
489
- status_map = {
490
- 401: 'auth_error',
491
- 403: 'forbidden',
492
- 404: 'not_found',
493
- 500: 'down',
494
- 502: 'down',
495
- 503: 'down',
496
- 504: 'down',
497
- }
498
-
499
- return {
500
- 'status': status_map.get(response.status_code, 'down'),
501
- 'lastCheck': time.time(),
502
- 'url': f'{self.backend_client.host}/health',
503
- 'httpStatus': response.status_code,
504
- 'error': f'HTTP {response.status_code}',
505
- }
506
-
507
- except requests.exceptions.Timeout:
508
- return {
509
- 'status': 'timeout',
510
- 'lastCheck': time.time(),
511
- 'url': f'{self.backend_client.base_url}/health',
512
- 'error': 'Request timeout (>3s)',
513
- }
514
- except requests.exceptions.RequestException as e:
515
- return {
516
- 'status': 'down',
517
- 'lastCheck': time.time(),
518
- 'url': f'{self.backend_client.base_url}/health',
519
- 'error': str(e),
520
- }
521
-
522
- @self.app.get('/health/agent')
523
- async def check_agent_health():
524
- """Check agent health status"""
525
- if not self.agent_client:
526
- return {'status': 'not_configured', 'error': 'Agent client not configured', 'lastCheck': None}
527
-
528
- import time
529
-
530
- start_time = time.time()
531
-
532
- try:
533
- # Use the agent client to check health with shorter timeout
534
- response = requests.get(
535
- f'{self.agent_client.base_url}/health',
536
- timeout=3,
537
- headers={'Authorization': f'Token {self.agent_client.agent_token}'},
538
- )
539
- latency = int((time.time() - start_time) * 1000)
540
-
541
- if response.status_code == 200:
542
- return {
543
- 'status': 'healthy',
544
- 'latency': latency,
545
- 'lastCheck': time.time(),
546
- 'url': f'{self.agent_client.base_url}/health',
547
- 'httpStatus': response.status_code,
548
- }
549
- else:
550
- status_map = {
551
- 401: 'auth_error',
552
- 403: 'forbidden',
553
- 404: 'not_found',
554
- 500: 'down',
555
- 502: 'down',
556
- 503: 'down',
557
- 504: 'down',
558
- }
559
-
560
- return {
561
- 'status': status_map.get(response.status_code, 'down'),
562
- 'lastCheck': time.time(),
563
- 'url': f'{self.agent_client.base_url}/health',
564
- 'httpStatus': response.status_code,
565
- 'error': f'HTTP {response.status_code}',
566
- }
567
-
568
- except requests.exceptions.Timeout:
569
- return {
570
- 'status': 'timeout',
571
- 'lastCheck': time.time(),
572
- 'url': f'{self.agent_client.base_url}/health',
573
- 'error': 'Request timeout (>3s)',
574
- }
575
- except requests.exceptions.RequestException as e:
576
- return {
577
- 'status': 'down',
578
- 'lastCheck': time.time(),
579
- 'url': f'{self.agent_client.base_url}/health',
580
- 'error': str(e),
581
- }
582
-
583
- @self.app.get('/config')
584
- async def get_config():
585
- """Get the current config.yaml content with validation status"""
586
- config_path = self.plugin_directory / 'config.yaml'
587
-
588
- if not config_path.exists():
589
- raise HTTPException(
590
- status_code=404,
591
- detail='config.yaml not found in plugin directory: '
592
- '{self.plugin_directory}. DevTools server expects to run from within a plugin directory.',
593
- )
594
-
595
- try:
596
- with open(config_path, 'r', encoding='utf-8') as f:
597
- config_data = yaml.safe_load(f)
598
-
599
- stat = config_path.stat()
600
- last_modified = datetime.fromtimestamp(stat.st_mtime).isoformat()
601
-
602
- validation_result = self._validate_plugin_config()
603
-
604
- response = ConfigResponse(config=config_data, file_path=str(config_path), last_modified=last_modified)
605
- response_dict = response.model_dump()
606
- response_dict['validation'] = validation_result
607
-
608
- # variables
609
- self.plugin_code = response_dict['config'].get('code')
610
-
611
- return response_dict
612
- except yaml.YAMLError as e:
613
- raise HTTPException(status_code=422, detail=f'Invalid YAML format: {str(e)}')
614
- except Exception as e:
615
- logger.error(f'Failed to read config.yaml: {e}')
616
- raise HTTPException(status_code=500, detail='Failed to read configuration file')
617
-
618
- @self.app.put('/config')
619
- async def update_config(request: ConfigUpdateRequest):
620
- """Update the config.yaml file"""
621
- config_path = self.plugin_directory / 'config.yaml'
622
-
623
- try:
624
- # Create backup of existing config
625
- if config_path.exists():
626
- backup_path = config_path.with_suffix('.yaml.bak')
627
- config_path.rename(backup_path)
628
-
629
- # Write new config
630
- with open(config_path, 'w', encoding='utf-8') as f:
631
- yaml.dump(request.config, f, default_flow_style=False, allow_unicode=True, indent=2)
632
-
633
- # Return updated config
634
- stat = config_path.stat()
635
- last_modified = datetime.fromtimestamp(stat.st_mtime).isoformat()
636
-
637
- return ConfigResponse(config=request.config, file_path=str(config_path), last_modified=last_modified)
638
- except Exception as e:
639
- # Restore backup if write failed
640
- backup_path = config_path.with_suffix('.yaml.bak')
641
- if backup_path.exists():
642
- backup_path.rename(config_path)
643
-
644
- logger.error(f'Failed to update config.yaml: {e}')
645
- raise HTTPException(status_code=500, detail='Failed to update configuration file')
646
-
647
- @self.app.post('/config/validate')
648
- async def validate_config(request: ConfigUpdateRequest):
649
- """Validate config.yaml structure using JSON schema"""
650
- return self._validate_plugin_config()
651
-
652
- @self.app.post('/plugin/http')
653
- async def execute_plugin_http(request: Request):
654
- """Execute a plugin HTTP request with specified parameters"""
655
- try:
656
- body = await request.json()
657
-
658
- # Extract request parameters
659
- action = body.get('action')
660
- params = body.get('params', {})
661
- debug = body.get('debug', True)
662
- access_token = body.get('access_token', None)
663
-
664
- # Validate plugin config before executing
665
- validation_result = self._validate_plugin_config()
666
- if not validation_result['valid']:
667
- return {
668
- 'success': False,
669
- 'error': 'Plugin configuration is invalid',
670
- 'validation_errors': validation_result['errors'],
671
- 'message': 'Please fix config.yaml before executing HTTP requests',
672
- }
673
-
674
- # Execute the HTTP request
675
- result = await self._execute_plugin_http_request(
676
- action=action, params=params, debug=debug, access_token=access_token
677
- )
678
-
679
- return result
680
-
681
- except HTTPException:
682
- raise
683
- except Exception as e:
684
- logger.error(f'Failed to execute plugin HTTP request: {e}')
685
- raise HTTPException(status_code=500, detail=f'Failed to execute HTTP request: {str(e)}')
686
-
687
- @self.app.get('/plugin/http/params/{action}')
688
- async def get_action_params(action: str):
689
- """Get pre-filled example parameters for a specific action"""
690
- example_params = self._get_action_example_params(action)
691
-
692
- return {'action': action, 'example_params': example_params, 'has_example': len(example_params) > 0}
693
-
694
- @self.app.post('/plugin/publish')
695
- async def publish_plugin(request: Request):
696
- """Publish a plugin to the Synapse platform using the same logic as CLI publish"""
697
- try:
698
- body = await request.json()
699
-
700
- # Extract publish configuration
701
- host = body.get('host')
702
- access_token = body.get('access_token') # This is the synapse access token
703
- debug = body.get('debug', True)
704
-
705
- # Validate required fields
706
- if not all([host, access_token]):
707
- raise HTTPException(status_code=400, detail='Missing required fields: host or access_token')
708
-
709
- # Validate plugin config before publishing
710
- validation_result = self._validate_plugin_config()
711
- if not validation_result['valid']:
712
- return {
713
- 'success': False,
714
- 'error': 'Plugin configuration is invalid',
715
- 'validation_errors': validation_result['errors'],
716
- 'message': 'Please fix config.yaml before publishing',
717
- }
718
-
719
- # Use the existing CLI publish logic
720
- result = await self._publish_plugin_internal(
721
- host=host, access_token=access_token, debug=debug, source_path=self.plugin_directory
722
- )
723
-
724
- return result
725
-
726
- except HTTPException:
727
- raise
728
- except Exception as e:
729
- logger.error(f'Failed to publish plugin: {e}')
730
- raise HTTPException(status_code=500, detail=f'Failed to publish plugin: {str(e)}')
731
-
732
- @self.app.get('/{path:path}')
733
- async def spa_handler(path: str):
734
- """Serve index.html for all non-API routes (SPA routing)"""
735
- # Don't handle API routes, static assets, or docs
736
- if (
737
- path.startswith('api/')
738
- or path.startswith('docs')
739
- or path.startswith('redoc')
740
- or path.startswith('assets/')
741
- or path.startswith('public/')
742
- or path.startswith('jobs')
743
- or path.startswith('health/')
744
- or path.startswith('config')
745
- or path.startswith('status')
746
- or path.startswith('auth/')
747
- or path.startswith('plugin/')
748
- or path.endswith('.js')
749
- or path.endswith('.css')
750
- or path.endswith('.ico')
751
- or path.endswith('.png')
752
- or path.endswith('.svg')
753
- or path.endswith('.jpg')
754
- or path.endswith('.jpeg')
755
- or path.endswith('.woff')
756
- or path.endswith('.woff2')
757
- or path.endswith('.ttf')
758
- ):
759
- raise HTTPException(status_code=404, detail='Not found')
760
-
761
- index_file = self.web_build_dir / 'index.html'
762
-
763
- if index_file.exists():
764
- # Read and modify HTML content to inject environment variables
765
- with open(index_file, 'r') as f:
766
- html_content = f.read()
767
-
768
- modified_html = self._inject_env_vars(html_content)
769
-
770
- from fastapi.responses import HTMLResponse
771
-
772
- return HTMLResponse(content=modified_html)
773
-
774
- # Fallback if no built frontend
775
- return {'message': 'Synapse Devtools API', 'docs': '/docs', 'path': path}
776
-
777
- async def broadcast_update(self, data: Dict):
778
- """Broadcast updates to all connected WebSocket clients"""
779
- if self.websocket_connections:
780
- message = json.dumps(data)
781
- disconnected_connections = []
782
-
783
- for connection in self.websocket_connections[:]:
784
- try:
785
- await connection.send_text(message)
786
- except Exception as e:
787
- logger.error(f'Error sending WebSocket message: {e}')
788
- disconnected_connections.append(connection)
789
-
790
- # Remove disconnected connections
791
- for connection in disconnected_connections:
792
- if connection in self.websocket_connections:
793
- self.websocket_connections.remove(connection)
794
- logger.info('Removed disconnected WebSocket connection')
795
28
 
796
29
  def start_server(self):
797
- """Start the devtools server"""
798
- # Use display host for browser opening and URL display
799
- display_host = get_display_host(self.host)
800
- url = f'http://{display_host}:{self.port}'
801
- print(f'Open devtools on your browser: {url}')
802
-
803
- uvicorn.run(self.app, host=self.host, port=self.port, log_level='warning', access_log=False)
30
+ """Legacy method - DEPRECATED"""
31
+ logger.error("FastAPI server is no longer supported. Use 'synapse devtools' command to run Streamlit app.")
32
+ raise RuntimeError("FastAPI server is deprecated. Use 'synapse devtools' command to run the Streamlit app.")
804
33
 
805
34
 
806
35
  def create_devtools_server(host: str = '0.0.0.0', port: int = 8080, plugin_directory: str = None) -> DevtoolsServer:
807
- """Factory function to create a devtools server instance"""
36
+ """Legacy function - DEPRECATED
37
+
38
+ This function is kept only for backwards compatibility.
39
+ Use 'synapse devtools' command instead.
40
+ """
808
41
  return DevtoolsServer(host=host, port=port, plugin_directory=plugin_directory)