agentscope-runtime 0.1.6__py3-none-any.whl → 0.2.0__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.
Files changed (87) hide show
  1. agentscope_runtime/common/container_clients/__init__.py +0 -0
  2. agentscope_runtime/{sandbox/manager → common}/container_clients/kubernetes_client.py +546 -6
  3. agentscope_runtime/engine/__init__.py +12 -0
  4. agentscope_runtime/engine/agents/agentscope_agent.py +130 -10
  5. agentscope_runtime/engine/agents/agno_agent.py +8 -10
  6. agentscope_runtime/engine/agents/langgraph_agent.py +52 -9
  7. agentscope_runtime/engine/app/__init__.py +6 -0
  8. agentscope_runtime/engine/app/agent_app.py +239 -0
  9. agentscope_runtime/engine/app/base_app.py +181 -0
  10. agentscope_runtime/engine/app/celery_mixin.py +92 -0
  11. agentscope_runtime/engine/deployers/__init__.py +13 -0
  12. agentscope_runtime/engine/deployers/adapter/responses/__init__.py +0 -0
  13. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +2890 -0
  14. agentscope_runtime/engine/deployers/adapter/responses/response_api_agent_adapter.py +51 -0
  15. agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +314 -0
  16. agentscope_runtime/engine/deployers/base.py +1 -0
  17. agentscope_runtime/engine/deployers/cli_fc_deploy.py +203 -0
  18. agentscope_runtime/engine/deployers/kubernetes_deployer.py +272 -0
  19. agentscope_runtime/engine/deployers/local_deployer.py +414 -501
  20. agentscope_runtime/engine/deployers/modelstudio_deployer.py +838 -0
  21. agentscope_runtime/engine/deployers/utils/__init__.py +0 -0
  22. agentscope_runtime/engine/deployers/utils/deployment_modes.py +14 -0
  23. agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +8 -0
  24. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +429 -0
  25. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +240 -0
  26. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +306 -0
  27. agentscope_runtime/engine/deployers/utils/package_project_utils.py +1163 -0
  28. agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +9 -0
  29. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +1064 -0
  30. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +157 -0
  31. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +268 -0
  32. agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +75 -0
  33. agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +220 -0
  34. agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
  35. agentscope_runtime/engine/deployers/utils/wheel_packager.py +389 -0
  36. agentscope_runtime/engine/helpers/agent_api_builder.py +651 -0
  37. agentscope_runtime/engine/runner.py +76 -35
  38. agentscope_runtime/engine/schemas/agent_schemas.py +112 -2
  39. agentscope_runtime/engine/schemas/embedding.py +37 -0
  40. agentscope_runtime/engine/schemas/modelstudio_llm.py +310 -0
  41. agentscope_runtime/engine/schemas/oai_llm.py +538 -0
  42. agentscope_runtime/engine/schemas/realtime.py +254 -0
  43. agentscope_runtime/engine/services/tablestore_memory_service.py +4 -1
  44. agentscope_runtime/engine/tracing/__init__.py +9 -3
  45. agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
  46. agentscope_runtime/engine/tracing/base.py +66 -34
  47. agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
  48. agentscope_runtime/engine/tracing/message_util.py +528 -0
  49. agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
  50. agentscope_runtime/engine/tracing/tracing_util.py +130 -0
  51. agentscope_runtime/engine/tracing/wrapper.py +794 -169
  52. agentscope_runtime/sandbox/box/base/base_sandbox.py +2 -1
  53. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +2 -1
  54. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
  55. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +2 -1
  56. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +2 -1
  57. agentscope_runtime/sandbox/box/training_box/training_box.py +0 -42
  58. agentscope_runtime/sandbox/client/http_client.py +52 -18
  59. agentscope_runtime/sandbox/constant.py +3 -0
  60. agentscope_runtime/sandbox/custom/custom_sandbox.py +2 -1
  61. agentscope_runtime/sandbox/custom/example.py +2 -1
  62. agentscope_runtime/sandbox/enums.py +0 -1
  63. agentscope_runtime/sandbox/manager/sandbox_manager.py +29 -22
  64. agentscope_runtime/sandbox/model/container.py +6 -0
  65. agentscope_runtime/sandbox/registry.py +1 -1
  66. agentscope_runtime/sandbox/tools/tool.py +4 -0
  67. agentscope_runtime/version.py +1 -1
  68. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/METADATA +103 -59
  69. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/RECORD +87 -52
  70. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/entry_points.txt +1 -0
  71. /agentscope_runtime/{sandbox/manager/container_clients → common}/__init__.py +0 -0
  72. /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
  73. /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
  74. /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
  75. /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
  76. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_mapping.py +0 -0
  77. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
  78. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
  79. /agentscope_runtime/{sandbox/manager → common}/collections/redis_mapping.py +0 -0
  80. /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
  81. /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
  82. /agentscope_runtime/{sandbox/manager → common}/container_clients/agentrun_client.py +0 -0
  83. /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
  84. /agentscope_runtime/{sandbox/manager → common}/container_clients/docker_client.py +0 -0
  85. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/WHEEL +0 -0
  86. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {agentscope_runtime-0.1.6.dist-info → agentscope_runtime-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,586 +1,499 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # pylint:disable=protected-access
3
+
2
4
  import asyncio
3
- import json
4
5
  import logging
6
+ import os
5
7
  import socket
6
8
  import threading
7
- import time
8
- import uuid
9
- from contextlib import asynccontextmanager
10
- from typing import Optional, Dict, Any, Callable, Type, Tuple, Union
9
+ from typing import Callable, Optional, Type, Any, Dict, Union, List
11
10
 
12
11
  import uvicorn
13
- from fastapi import FastAPI, HTTPException, Request, Response
14
- from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import StreamingResponse
16
- from pydantic import BaseModel
17
12
 
18
- from .base import DeployManager
19
13
  from .adapter.protocol_adapter import ProtocolAdapter
20
- from ..schemas.agent_schemas import AgentRequest, AgentResponse, Error
14
+ from .base import DeployManager
15
+ from .utils.deployment_modes import DeploymentMode
16
+ from .utils.package_project_utils import package_project, PackageConfig
17
+ from .utils.service_utils import (
18
+ FastAPIAppFactory,
19
+ FastAPITemplateManager,
20
+ ProcessManager,
21
+ ServicesConfig,
22
+ )
21
23
 
22
24
 
23
25
  class LocalDeployManager(DeployManager):
24
- def __init__(self, host: str = "localhost", port: int = 8090):
25
- super().__init__()
26
- self.host = host
27
- self.port = port
28
- self._server = None
29
- self._server_task = None
30
- self._server_thread = None # Add thread for server
31
- self._is_running = False
32
- self._logger = logging.getLogger(__name__)
33
- self._app = None
34
- self._startup_timeout = 30 # seconds
35
- self._shutdown_timeout = 10 # seconds
36
- self._setup_logging()
37
-
38
- def _setup_logging(self):
39
- formatter = logging.Formatter(
40
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
41
- )
42
-
43
- app_logger = logging.getLogger("app")
44
- app_logger.setLevel(logging.INFO)
45
-
46
- file_handler = logging.handlers.RotatingFileHandler(
47
- "app.log",
48
- maxBytes=10 * 1024 * 1024, # 10MB
49
- backupCount=5,
50
- )
51
- file_handler.setFormatter(formatter)
52
- app_logger.addHandler(file_handler)
53
- console_handler = logging.StreamHandler()
54
- console_handler.setFormatter(formatter)
55
- app_logger.addHandler(console_handler)
56
-
57
- access_logger = logging.getLogger("access")
58
- access_logger.setLevel(logging.INFO)
59
- access_file_handler = logging.handlers.RotatingFileHandler(
60
- "access.log",
61
- maxBytes=10 * 1024 * 1024,
62
- backupCount=5,
63
- )
64
- access_file_handler.setFormatter(
65
- logging.Formatter("%(asctime)s - %(message)s"),
66
- )
67
- access_logger.addHandler(access_file_handler)
68
-
69
- self.app_logger = app_logger
70
- self.access_logger = access_logger
71
-
72
- def _create_fastapi_app(self) -> FastAPI:
73
- """Create and configure FastAPI application with lifespan
74
- management."""
75
-
76
- @asynccontextmanager
77
- async def lifespan(app: FastAPI) -> Any:
78
- """Manage the application lifespan."""
79
- if hasattr(self, "before_start") and self.before_start:
80
- if asyncio.iscoroutinefunction(self.before_start):
81
- await self.before_start(app, **getattr(self, "kwargs", {}))
82
- else:
83
- self.before_start(app, **getattr(self, "kwargs", {}))
84
- yield
85
- if hasattr(self, "after_finish") and self.after_finish:
86
- if asyncio.iscoroutinefunction(self.after_finish):
87
- await self.after_finish(app, **getattr(self, "kwargs", {}))
88
- else:
89
- self.after_finish(app, **getattr(self, "kwargs", {}))
90
-
91
- app = FastAPI(
92
- title="Agent Service",
93
- version="1.0.0",
94
- description="Production-ready Agent Service API",
95
- lifespan=lifespan,
96
- )
97
-
98
- self._add_middleware(app)
99
- self._add_health_endpoints(app)
100
-
101
- if hasattr(self, "func") and self.func:
102
- self._add_main_endpoint(app)
103
-
104
- return app
105
-
106
- def _add_middleware(self, app: FastAPI) -> None:
107
- """Add middleware to the FastAPI application."""
108
-
109
- @app.middleware("http")
110
- async def log_requests(request: Request, call_next):
111
- start_time = time.time()
112
-
113
- self.app_logger.info(f"Request: {request.method} {request.url}")
114
- response = await call_next(
115
- request,
116
- )
117
- process_time = time.time() - start_time
118
- self.access_logger.info(
119
- f'{request.client.host} - "{request.method} {request.url}" '
120
- f"{response.status_code} - {process_time:.3f}s",
121
- )
122
-
123
- return response
124
-
125
- @app.middleware("http")
126
- async def custom_middleware(
127
- request: Request,
128
- call_next: Callable,
129
- ) -> Response:
130
- """Custom middleware for request processing."""
131
- response: Response = await call_next(request)
132
- return response
133
-
134
- app.add_middleware(
135
- CORSMiddleware,
136
- allow_origins=["*"],
137
- allow_credentials=True,
138
- allow_methods=["*"],
139
- allow_headers=["*"],
140
- )
26
+ """Unified LocalDeployManager supporting multiple deployment modes."""
141
27
 
142
- def _add_health_endpoints(self, app: FastAPI) -> None:
143
- """Add health check endpoints to the FastAPI application."""
144
-
145
- @app.get("/health")
146
- async def health_check():
147
- return {
148
- "status": "healthy",
149
- "timestamp": time.time(),
150
- "service": "agent-service",
151
- }
152
-
153
- @app.get("/readiness")
154
- async def readiness() -> str:
155
- """Check if the application is ready to serve requests."""
156
- if getattr(app.state, "is_ready", True):
157
- return "success"
158
- raise HTTPException(
159
- status_code=500,
160
- detail="Application is not ready",
161
- )
162
-
163
- @app.get("/liveness")
164
- async def liveness() -> str:
165
- """Check if the application is alive and healthy."""
166
- if getattr(app.state, "is_healthy", True):
167
- return "success"
168
- raise HTTPException(
169
- status_code=500,
170
- detail="Application is not healthy",
171
- )
172
-
173
- @app.get("/")
174
- async def root():
175
- return {"message": "Agent Service is running"}
176
-
177
- def _add_main_endpoint(self, app: FastAPI) -> None:
178
- """Add the main processing endpoint to the FastAPI application."""
179
-
180
- async def _get_request_info(request: Request) -> Tuple[Dict, Any, str]:
181
- """Extract request information from the HTTP request."""
182
- body = await request.body()
183
- request_body = json.loads(body.decode("utf-8")) if body else {}
184
-
185
- user_id = request_body.get("user_id", "")
186
-
187
- if hasattr(self, "request_model") and self.request_model:
188
- try:
189
- request_body_obj = self.request_model.model_validate(
190
- request_body,
191
- )
192
- except Exception as e:
193
- raise HTTPException(
194
- status_code=400,
195
- detail=f"Invalid request format: {e}",
196
- ) from e
197
- else:
198
- request_body_obj = request_body
199
-
200
- query_params = dict(request.query_params)
201
- return query_params, request_body_obj, user_id
202
-
203
- def _get_request_id(request_body_obj: Any) -> str:
204
- """Extract or generate a request ID from the request body."""
205
- if hasattr(request_body_obj, "header") and hasattr(
206
- request_body_obj.header,
207
- "request_id",
208
- ):
209
- request_id = request_body_obj.header.request_id
210
- elif (
211
- isinstance(
212
- request_body_obj,
213
- dict,
214
- )
215
- and "request_id" in request_body_obj
216
- ):
217
- request_id = request_body_obj["request_id"]
218
- else:
219
- request_id = str(uuid.uuid4())
220
- return request_id
221
-
222
- @app.post(self.endpoint_path)
223
- async def main_endpoint(request: Request):
224
- """Main endpoint handler for processing requests."""
225
- try:
226
- (
227
- _, # query_params
228
- request_body_obj,
229
- user_id,
230
- ) = await _get_request_info(
231
- request=request,
232
- )
233
- request_id = _get_request_id(request_body_obj)
234
- if (
235
- hasattr(
236
- self,
237
- "response_type",
238
- )
239
- and self.response_type == "sse"
240
- ):
241
- return self._handle_sse_response(
242
- user_id=user_id,
243
- request_body_obj=request_body_obj,
244
- request_id=request_id,
245
- )
246
- else:
247
- return await self._handle_standard_response(
248
- user_id=user_id,
249
- request_body_obj=request_body_obj,
250
- request_id=request_id,
251
- )
252
-
253
- except Exception as e:
254
- self._logger.error(f"Request processing failed: {e}")
255
- raise HTTPException(status_code=500, detail=str(e)) from e
256
-
257
- def _handle_sse_response(
258
- self,
259
- user_id: str,
260
- request_body_obj: Any,
261
- request_id: str,
262
- ) -> StreamingResponse:
263
- """Handle Server-Sent Events response."""
264
-
265
- async def stream_generator():
266
- """Generate streaming response data."""
267
- try:
268
- if asyncio.iscoroutinefunction(self.func):
269
- async for output in self.func(
270
- user_id=user_id,
271
- request=request_body_obj,
272
- request_id=request_id,
273
- ):
274
- _data = self._create_success_result(
275
- output=output,
276
- )
277
- yield f"data: {_data}\n\n"
278
- else:
279
- # For sync functions, we need to handle differently
280
- result = self.func(
281
- user_id=user_id,
282
- request=request_body_obj,
283
- request_id=request_id,
284
- )
285
- if hasattr(result, "__aiter__"):
286
- async for output in result:
287
- _data = self._create_success_result(
288
- output=output,
289
- )
290
- yield f"data: {_data}\n\n"
291
- else:
292
- _data = self._create_success_result(
293
- output=result,
294
- )
295
- yield f"data: {_data}\n\n"
296
- except Exception as e:
297
- _data = self._create_error_response(
298
- request_id=request_id,
299
- error=e,
300
- )
301
- yield f"data: {_data}\n\n"
302
-
303
- return StreamingResponse(
304
- stream_generator(),
305
- media_type="text/event-stream",
306
- headers={
307
- "Cache-Control": "no-cache",
308
- "Connection": "keep-alive",
309
- },
310
- )
311
-
312
- async def _handle_standard_response(
28
+ def __init__(
313
29
  self,
314
- user_id: str,
315
- request_body_obj: Any,
316
- request_id: str,
30
+ host: str = "127.0.0.1",
31
+ port: int = 8000,
32
+ shutdown_timeout: int = 30,
33
+ startup_timeout: int = 30,
34
+ logger: Optional[logging.Logger] = None,
317
35
  ):
318
- """Handle standard JSON response."""
319
- try:
320
- if asyncio.iscoroutinefunction(self.func):
321
- result = await self.func(
322
- user_id=user_id,
323
- request=request_body_obj,
324
- request_id=request_id,
325
- )
326
- else:
327
- result = self.func(
328
- user_id=user_id,
329
- request=request_body_obj,
330
- request_id=request_id,
331
- )
332
-
333
- return self._create_success_result(
334
- output=result,
335
- )
336
- except Exception as e:
337
- return self._create_error_response(request_id=request_id, error=e)
36
+ """Initialize LocalDeployManager.
338
37
 
339
- def _create_success_result(
340
- self,
341
- output: Union[BaseModel, Dict, str],
342
- ) -> str:
343
- """Create a success response."""
344
- if isinstance(output, BaseModel):
345
- return output.model_dump_json()
346
- elif isinstance(output, dict):
347
- return json.dumps(output)
348
- else:
349
- return output
38
+ Args:
39
+ host: Host to bind to
40
+ port: Port to bind to
41
+ shutdown_timeout: Timeout for graceful shutdown
42
+ logger: Logger instance
43
+ """
44
+ super().__init__()
45
+ self.host = host
46
+ self.port = port
47
+ self._shutdown_timeout = shutdown_timeout
48
+ self._startup_timeout = startup_timeout
49
+ self._logger = logger or logging.getLogger(__name__)
50
+
51
+ # State management
52
+ self.is_running = False
53
+
54
+ # Daemon thread mode attributes
55
+ self._server: Optional[uvicorn.Server] = None
56
+ self._server_thread: Optional[threading.Thread] = None
57
+ self._server_task: Optional[asyncio.Task] = None
58
+
59
+ # Detached process mode attributes
60
+ self._detached_process_pid: Optional[int] = None
61
+ self._detached_pid_file: Optional[str] = None
62
+ self.process_manager = ProcessManager(
63
+ shutdown_timeout=shutdown_timeout,
64
+ )
350
65
 
351
- def _create_error_response(
352
- self,
353
- request_id: str,
354
- error: Exception,
355
- ) -> str:
356
- """Create an error response."""
357
- response = AgentResponse(id=request_id)
358
- response.failed(Error(code=str(error), message=str(error)))
359
- return response.model_dump_json()
66
+ # Template manager
67
+ self.template_manager = FastAPITemplateManager()
360
68
 
361
- def deploy_sync(
69
+ async def deploy(
362
70
  self,
363
- func: Callable,
71
+ app=None,
72
+ runner: Optional[Any] = None,
364
73
  endpoint_path: str = "/process",
365
- request_model: Optional[Type] = AgentRequest,
74
+ request_model: Optional[Type] = None,
366
75
  response_type: str = "sse",
76
+ stream: bool = True,
367
77
  before_start: Optional[Callable] = None,
368
78
  after_finish: Optional[Callable] = None,
79
+ mode: DeploymentMode = DeploymentMode.DAEMON_THREAD,
80
+ services_config: Optional[ServicesConfig] = None,
81
+ custom_endpoints: Optional[List[Dict]] = None,
82
+ protocol_adapters: Optional[list[ProtocolAdapter]] = None,
83
+ broker_url: Optional[str] = None,
84
+ backend_url: Optional[str] = None,
85
+ enable_embedded_worker: bool = False,
369
86
  **kwargs: Any,
370
87
  ) -> Dict[str, str]:
371
- """
372
- Deploy the agent as a FastAPI service (synchronous version).
88
+ """Deploy using unified FastAPI architecture.
373
89
 
374
90
  Args:
375
- func: Custom processing function
376
- endpoint_path: API endpoint path for the processing function
91
+ runner: Runner instance (for DAEMON_THREAD mode)
92
+ endpoint_path: API endpoint path
377
93
  request_model: Pydantic model for request validation
378
94
  response_type: Response type - "json", "sse", or "text"
95
+ stream: Enable streaming responses
379
96
  before_start: Callback function called before server starts
380
97
  after_finish: Callback function called after server finishes
381
- **kwargs: Additional keyword arguments passed to callbacks
98
+ mode: Deployment mode
99
+ services_config: Services configuration
100
+ custom_endpoints: Custom endpoints from agent app
101
+ protocol_adapters: Protocol adapters
102
+ broker_url: Celery broker URL for background task processing
103
+ backend_url: Celery backend URL for result storage
104
+ enable_embedded_worker: Whether to run Celery worker
105
+ embedded in the app
106
+ **kwargs: Additional keyword arguments
382
107
 
383
108
  Returns:
384
- Dict[str, str]: Dictionary containing deploy_id and url of the
385
- deployed service
109
+ Dict containing deploy_id and url
386
110
 
387
111
  Raises:
388
112
  RuntimeError: If deployment fails
389
113
  """
390
- return asyncio.run(
391
- self._deploy_async(
392
- func=func,
393
- endpoint_path=endpoint_path,
394
- request_model=request_model,
395
- response_type=response_type,
396
- before_start=before_start,
397
- after_finish=after_finish,
398
- **kwargs,
399
- ),
400
- )
114
+ if self.is_running:
115
+ raise RuntimeError("Service is already running")
401
116
 
402
- async def deploy(
117
+ self._app = app
118
+ if self._app is not None:
119
+ runner = self._app._runner
120
+ endpoint_path = self._app.endpoint_path
121
+ response_type = self._app.response_type
122
+ stream = self._app.stream
123
+ request_model = self._app.request_model
124
+ before_start = self._app.before_start
125
+ after_finish = self._app.after_finish
126
+ backend_url = self._app.backend_url
127
+ broker_url = self._app.broker_url
128
+ custom_endpoints = self._app.custom_endpoints
129
+ protocol_adapters = self._app.protocol_adapters
130
+ try:
131
+ if mode == DeploymentMode.DAEMON_THREAD:
132
+ return await self._deploy_daemon_thread(
133
+ runner=runner,
134
+ endpoint_path=endpoint_path,
135
+ request_model=request_model,
136
+ response_type=response_type,
137
+ stream=stream,
138
+ before_start=before_start,
139
+ after_finish=after_finish,
140
+ services_config=services_config,
141
+ custom_endpoints=custom_endpoints,
142
+ protocol_adapters=protocol_adapters,
143
+ broker_url=broker_url,
144
+ backend_url=backend_url,
145
+ enable_embedded_worker=enable_embedded_worker,
146
+ **kwargs,
147
+ )
148
+ elif mode == DeploymentMode.DETACHED_PROCESS:
149
+ return await self._deploy_detached_process(
150
+ runner=runner,
151
+ endpoint_path=endpoint_path,
152
+ request_model=request_model,
153
+ response_type=response_type,
154
+ stream=stream,
155
+ before_start=before_start,
156
+ after_finish=after_finish,
157
+ services_config=services_config,
158
+ custom_endpoints=custom_endpoints,
159
+ protocol_adapters=protocol_adapters,
160
+ **kwargs,
161
+ )
162
+ else:
163
+ raise ValueError(
164
+ f"Unsupported deployment mode for LocalDeployManager: "
165
+ f"{mode}",
166
+ )
167
+
168
+ except Exception as e:
169
+ self._logger.error(f"Deployment failed: {e}")
170
+ raise RuntimeError(f"Failed to deploy service: {e}") from e
171
+
172
+ async def _deploy_daemon_thread(
403
173
  self,
404
- func: Callable,
405
- endpoint_path: str = "/process",
406
- request_model: Optional[Type] = AgentRequest,
407
- response_type: str = "sse",
408
- before_start: Optional[Callable] = None,
409
- after_finish: Optional[Callable] = None,
174
+ runner: Optional[Any] = None,
410
175
  protocol_adapters: Optional[list[ProtocolAdapter]] = None,
411
- **kwargs: Any,
176
+ broker_url: Optional[str] = None,
177
+ backend_url: Optional[str] = None,
178
+ enable_embedded_worker: bool = False,
179
+ **kwargs,
412
180
  ) -> Dict[str, str]:
413
- """
414
- Deploy the agent as a FastAPI service (asynchronous version).
415
-
416
- Args:
417
- func: Custom processing function
418
- endpoint_path: API endpoint path for the processing function
419
- request_model: Pydantic model for request validation
420
- response_type: Response type - "json", "sse", or "text"
421
- before_start: Callback function called before server starts
422
- after_finish: Callback function called after server finishes
423
- **kwargs: Additional keyword arguments passed to callbacks
424
-
425
- Returns:
426
- Dict[str, str]: Dictionary containing deploy_id and url of the
427
- deployed service
181
+ """Deploy in daemon thread mode."""
182
+ self._logger.info("Deploying FastAPI service in daemon thread mode...")
428
183
 
429
- Raises:
430
- RuntimeError: If deployment fails
431
- """
432
- return await self._deploy_async(
433
- func=func,
434
- endpoint_path=endpoint_path,
435
- request_model=request_model,
436
- response_type=response_type,
437
- before_start=before_start,
438
- after_finish=after_finish,
184
+ # Create FastAPI app using factory with Celery support
185
+ app = FastAPIAppFactory.create_app(
186
+ runner=runner,
187
+ mode=DeploymentMode.DAEMON_THREAD,
439
188
  protocol_adapters=protocol_adapters,
189
+ broker_url=broker_url,
190
+ backend_url=backend_url,
191
+ enable_embedded_worker=enable_embedded_worker,
440
192
  **kwargs,
441
193
  )
442
194
 
443
- async def _deploy_async(
195
+ # Create uvicorn server
196
+ config = uvicorn.Config(
197
+ app=app,
198
+ host=self.host,
199
+ port=self.port,
200
+ loop="asyncio",
201
+ log_level="info",
202
+ )
203
+ self._server = uvicorn.Server(config)
204
+
205
+ # Start server in daemon thread
206
+ def run_server():
207
+ asyncio.run(self._server.serve())
208
+
209
+ self._server_thread = threading.Thread(target=run_server, daemon=True)
210
+ self._server_thread.start()
211
+
212
+ # Wait for server to start
213
+ await self._wait_for_server_ready(self._startup_timeout)
214
+
215
+ self.is_running = True
216
+ self.deploy_id = f"daemon_{self.host}_{self.port}"
217
+
218
+ self._logger.info(
219
+ f"FastAPI service started at http://{self.host}:{self.port}",
220
+ )
221
+
222
+ return {
223
+ "deploy_id": self.deploy_id,
224
+ "url": f"http://{self.host}:{self.port}",
225
+ }
226
+
227
+ async def _deploy_detached_process(
444
228
  self,
445
- func: Callable,
446
- endpoint_path: str = "/process",
447
- request_model: Optional[Type] = None,
448
- response_type: str = "sse",
449
- before_start: Optional[Callable] = None,
450
- after_finish: Optional[Callable] = None,
229
+ runner: Optional[Any] = None,
230
+ services_config: Optional[ServicesConfig] = None,
451
231
  protocol_adapters: Optional[list[ProtocolAdapter]] = None,
452
- **kwargs: Any,
232
+ **kwargs,
453
233
  ) -> Dict[str, str]:
454
- if self._is_running:
455
- raise RuntimeError("Service is already running")
234
+ """Deploy in detached process mode."""
235
+ self._logger.info(
236
+ "Deploying FastAPI service in detached process mode...",
237
+ )
238
+
239
+ # Extract agent from runner
240
+ if not runner or not runner._agent:
241
+ raise ValueError(
242
+ "Detached process mode requires a runner with an agent",
243
+ )
244
+
245
+ agent = runner._agent
246
+ if "agent" in kwargs:
247
+ kwargs.pop("agent")
248
+
249
+ # Create package project for detached deployment
250
+ project_dir = await self.create_detached_project(
251
+ agent=agent,
252
+ services_config=services_config,
253
+ protocol_adapters=protocol_adapters,
254
+ **kwargs,
255
+ )
456
256
 
457
257
  try:
458
- self._logger.info("Starting FastAPI service deployment...")
459
-
460
- # Store callable configuration
461
- self.func = func
462
- self.endpoint_path = endpoint_path
463
- self.request_model = request_model
464
- self.response_type = response_type
465
- self.before_start = before_start
466
- self.after_finish = after_finish
467
- self.kwargs = kwargs
468
-
469
- # Create FastAPI app
470
- self._app = self._create_fastapi_app()
471
-
472
- # Support extension protocol
473
- if protocol_adapters:
474
- for protocol_adapter in protocol_adapters:
475
- protocol_adapter.add_endpoint(app=self._app, func=func)
476
-
477
- # Configure uvicorn server
478
- config = uvicorn.Config(
479
- self._app,
258
+ # Start detached process using the packaged project
259
+ script_path = os.path.join(project_dir, "main.py")
260
+ pid = await self.process_manager.start_detached_process(
261
+ script_path=script_path,
480
262
  host=self.host,
481
263
  port=self.port,
482
- log_level="info",
483
- access_log=False,
484
- timeout_keep_alive=30,
485
264
  )
486
265
 
487
- self._server = uvicorn.Server(config)
488
- # Run the server in a separate thread
489
- self._server_thread = threading.Thread(target=self._server.run)
490
- self._server_thread.daemon = (
491
- True # Ensure thread doesn't block exit
266
+ self._detached_process_pid = pid
267
+ self._detached_pid_file = f"/tmp/agentscope_runtime_{pid}.pid"
268
+
269
+ # Create PID file
270
+ self.process_manager.create_pid_file(pid, self._detached_pid_file)
271
+
272
+ # Wait for service to become available
273
+ service_ready = await self.process_manager.wait_for_port(
274
+ self.host,
275
+ self.port,
276
+ timeout=30,
492
277
  )
493
- self._server_thread.start()
494
-
495
- # Wait for server to start with timeout
496
- start_time = time.time()
497
- while not self._is_server_ready():
498
- if time.time() - start_time > self._startup_timeout:
499
- # Clean up the thread if server fails to start
500
- if self._server:
501
- self._server.should_exit = True
502
- self._server_thread.join(timeout=self._shutdown_timeout)
503
- raise RuntimeError(
504
- f"Server startup timeout after "
505
- f"{self._startup_timeout} seconds",
506
- )
507
- await asyncio.sleep(0.1)
508
-
509
- self._is_running = True
510
- url = f"http://{self.host}:{self.port}"
278
+
279
+ if not service_ready:
280
+ raise RuntimeError("Service did not start within timeout")
281
+
282
+ self.is_running = True
283
+ self.deploy_id = f"detached_{pid}"
284
+
511
285
  self._logger.info(
512
- f"FastAPI service deployed successfully at {url}",
286
+ f"FastAPI service started in detached process (PID: {pid})",
513
287
  )
288
+
514
289
  return {
515
290
  "deploy_id": self.deploy_id,
516
- "url": url,
291
+ "url": f"http://{self.host}:{self.port}",
517
292
  }
518
293
 
519
294
  except Exception as e:
520
- self._logger.error(f"Deployment failed: {e}")
521
- await self._cleanup_server()
522
- raise RuntimeError(f"Failed to deploy FastAPI service: {e}") from e
295
+ # Cleanup on failure
296
+ if os.path.exists(project_dir):
297
+ try:
298
+ import shutil
523
299
 
524
- def _is_server_ready(self) -> bool:
525
- """Check if the server is ready to accept connections."""
526
- try:
527
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
528
- s.settimeout(0.1)
529
- result = s.connect_ex((self.host, self.port))
530
- return result == 0
531
- except Exception:
532
- return False
300
+ shutil.rmtree(project_dir)
301
+ except OSError:
302
+ pass
303
+ raise e
533
304
 
534
- async def stop(self) -> None:
535
- """
536
- Stop the FastAPI service.
305
+ @staticmethod
306
+ async def create_detached_project(
307
+ agent: Any,
308
+ endpoint_path: str = "/process",
309
+ requirements: Optional[Union[str, List[str]]] = None,
310
+ extra_packages: Optional[List[str]] = None,
311
+ services_config: Optional[ServicesConfig] = None,
312
+ protocol_adapters: Optional[list[ProtocolAdapter]] = None,
313
+ custom_endpoints: Optional[
314
+ List[Dict]
315
+ ] = None, # New parameter for custom endpoints
316
+ # Celery parameters
317
+ broker_url: Optional[str] = None,
318
+ backend_url: Optional[str] = None,
319
+ enable_embedded_worker: bool = False,
320
+ **kwargs, # pylint: disable=unused-argument
321
+ ) -> str:
322
+ """Create detached project using package_project method."""
323
+ if requirements is None:
324
+ requirements = []
537
325
 
538
- Raises:
539
- RuntimeError: If stopping fails
540
- """
541
- if not self._is_running:
542
- self._logger.warning("Service is not running")
543
- return
326
+ if isinstance(requirements, str):
327
+ requirements = [requirements]
544
328
 
545
- try:
546
- self._logger.info("Stopping FastAPI service...")
329
+ # Create package configuration for detached deployment
330
+ package_config = PackageConfig(
331
+ endpoint_path=endpoint_path,
332
+ deployment_mode="detached_process",
333
+ extra_packages=extra_packages,
334
+ protocol_adapters=protocol_adapters,
335
+ services_config=services_config,
336
+ custom_endpoints=custom_endpoints, # Add custom endpoints
337
+ # Celery configuration
338
+ broker_url=broker_url,
339
+ backend_url=backend_url,
340
+ enable_embedded_worker=enable_embedded_worker,
341
+ requirements=requirements
342
+ + (
343
+ ["redis"]
344
+ if services_config
345
+ and any(
346
+ getattr(config, "provider", None) == "redis"
347
+ for config in [
348
+ services_config.memory,
349
+ services_config.session_history,
350
+ ]
351
+ if config
352
+ )
353
+ else []
354
+ )
355
+ + (
356
+ [
357
+ "celery",
358
+ "redis",
359
+ ] # Add Celery and Redis if Celery is configured
360
+ if broker_url or backend_url
361
+ else []
362
+ ),
363
+ )
364
+
365
+ # Use package_project to create the detached project
366
+ project_dir, _ = package_project(
367
+ agent=agent,
368
+ config=package_config,
369
+ )
547
370
 
548
- # Stop the server gracefully
549
- if self._server:
550
- self._server.should_exit = True
371
+ return project_dir
551
372
 
552
- # Wait for the server thread to finish
553
- if self._server_thread and self._server_thread.is_alive():
554
- self._server_thread.join(timeout=self._shutdown_timeout)
555
- if self._server_thread.is_alive():
556
- self._logger.warning(
557
- "Server thread did not terminate, "
558
- "potential resource leak",
559
- )
373
+ async def stop(self) -> None:
374
+ """Stop the FastAPI service (unified method for all modes)."""
375
+ if not self.is_running:
376
+ self._logger.warning("Service is not running")
377
+ return
560
378
 
561
- await self._cleanup_server()
562
- self._is_running = False
563
- self._logger.info("FastAPI service stopped successfully")
379
+ try:
380
+ if self._detached_process_pid:
381
+ # Detached process mode
382
+ await self._stop_detached_process()
383
+ else:
384
+ # Daemon thread mode
385
+ await self._stop_daemon_thread()
564
386
 
565
387
  except Exception as e:
566
388
  self._logger.error(f"Failed to stop service: {e}")
567
389
  raise RuntimeError(f"Failed to stop FastAPI service: {e}") from e
568
390
 
569
- async def _cleanup_server(self):
570
- """Clean up server resources."""
391
+ async def _stop_daemon_thread(self):
392
+ """Stop daemon thread mode service."""
393
+ self._logger.info("Stopping FastAPI daemon thread service...")
394
+
395
+ # Stop the server gracefully
396
+ if self._server:
397
+ self._server.should_exit = True
398
+
399
+ # Wait for the server thread to finish
400
+ if self._server_thread and self._server_thread.is_alive():
401
+ self._server_thread.join(timeout=self._shutdown_timeout)
402
+ if self._server_thread.is_alive():
403
+ self._logger.warning(
404
+ "Server thread did not terminate, potential resource leak",
405
+ )
406
+
407
+ await self._cleanup_daemon_thread()
408
+ self.is_running = False
409
+ self._logger.info("FastAPI daemon thread service stopped successfully")
410
+
411
+ async def _stop_detached_process(self):
412
+ """Stop detached process mode service."""
413
+ self._logger.info("Stopping FastAPI detached process service...")
414
+
415
+ if self._detached_process_pid:
416
+ await self.process_manager.stop_process_gracefully(
417
+ self._detached_process_pid,
418
+ )
419
+
420
+ await self._cleanup_detached_process()
421
+ self.is_running = False
422
+ self._logger.info(
423
+ "FastAPI detached process service stopped successfully",
424
+ )
425
+
426
+ async def _cleanup_daemon_thread(self):
427
+ """Clean up daemon thread resources."""
571
428
  self._server = None
572
429
  self._server_task = None
573
430
  self._server_thread = None
574
- self._app = None
575
431
 
576
- @property
577
- def is_running(self) -> bool:
578
- """Check if the service is currently running."""
579
- return self._is_running
432
+ async def _cleanup_detached_process(self):
433
+ """Clean up detached process resources."""
434
+ # Cleanup PID file
435
+ if self._detached_pid_file:
436
+ self.process_manager.cleanup_pid_file(self._detached_pid_file)
437
+
438
+ # Reset state
439
+ self._detached_process_pid = None
440
+ self._detached_pid_file = None
441
+
442
+ def _is_server_ready(self) -> bool:
443
+ """Check if the server is ready to accept connections."""
444
+ try:
445
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
446
+ s.settimeout(0.1)
447
+ result = s.connect_ex((self.host, self.port))
448
+ return result == 0
449
+ except Exception:
450
+ return False
451
+
452
+ async def _wait_for_server_ready(self, timeout: int = 30):
453
+ """Wait for server to become ready."""
454
+ end_time = asyncio.get_event_loop().time() + timeout
455
+
456
+ while asyncio.get_event_loop().time() < end_time:
457
+ if self._is_server_ready():
458
+ return
459
+
460
+ await asyncio.sleep(0.1)
461
+
462
+ raise RuntimeError("Server did not become ready within timeout")
463
+
464
+ def is_service_running(self) -> bool:
465
+ """Check if service is running."""
466
+ if not self.is_running:
467
+ return False
468
+
469
+ if self._detached_process_pid:
470
+ # Check detached process
471
+ return self.process_manager.is_process_running(
472
+ self._detached_process_pid,
473
+ )
474
+ else:
475
+ # Check daemon thread
476
+ return self._server is not None and self._is_server_ready()
477
+
478
+ def get_deployment_info(self) -> Dict[str, Any]:
479
+ """Get deployment information."""
480
+ return {
481
+ "deploy_id": self.deploy_id,
482
+ "host": self.host,
483
+ "port": self.port,
484
+ "is_running": self.is_service_running(),
485
+ "mode": "detached_process"
486
+ if self._detached_process_pid
487
+ else "daemon_thread",
488
+ "pid": self._detached_process_pid,
489
+ "url": f"http://{self.host}:{self.port}"
490
+ if self.is_running
491
+ else None,
492
+ }
580
493
 
581
494
  @property
582
495
  def service_url(self) -> Optional[str]:
583
496
  """Get the current service URL if running."""
584
- if self._is_running and self.port:
497
+ if self.is_running and self.port:
585
498
  return f"http://{self.host}:{self.port}"
586
499
  return None