agentscope-runtime 0.1.5b1__py3-none-any.whl → 0.1.6__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 (90) hide show
  1. agentscope_runtime/engine/agents/agentscope_agent.py +447 -0
  2. agentscope_runtime/engine/agents/agno_agent.py +19 -18
  3. agentscope_runtime/engine/agents/autogen_agent.py +13 -8
  4. agentscope_runtime/engine/agents/utils.py +53 -0
  5. agentscope_runtime/engine/deployers/__init__.py +0 -13
  6. agentscope_runtime/engine/deployers/local_deployer.py +501 -356
  7. agentscope_runtime/engine/helpers/helper.py +60 -41
  8. agentscope_runtime/engine/runner.py +11 -36
  9. agentscope_runtime/engine/schemas/agent_schemas.py +2 -70
  10. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  11. agentscope_runtime/engine/services/tablestore_memory_service.py +304 -0
  12. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  13. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  14. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  15. agentscope_runtime/sandbox/__init__.py +2 -0
  16. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  17. agentscope_runtime/sandbox/box/base/base_sandbox.py +4 -3
  18. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  19. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +8 -13
  20. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  21. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  22. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +8 -6
  23. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  24. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +80 -0
  25. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  26. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  27. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  28. agentscope_runtime/sandbox/box/training_box/training_box.py +10 -15
  29. agentscope_runtime/sandbox/build.py +143 -58
  30. agentscope_runtime/sandbox/client/http_client.py +43 -49
  31. agentscope_runtime/sandbox/client/training_client.py +0 -1
  32. agentscope_runtime/sandbox/constant.py +24 -1
  33. agentscope_runtime/sandbox/custom/custom_sandbox.py +5 -5
  34. agentscope_runtime/sandbox/custom/example.py +2 -2
  35. agentscope_runtime/sandbox/enums.py +1 -0
  36. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +11 -6
  37. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +25 -9
  38. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  39. agentscope_runtime/sandbox/manager/container_clients/agentrun_client.py +1098 -0
  40. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +33 -205
  41. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +8 -555
  42. agentscope_runtime/sandbox/manager/sandbox_manager.py +187 -88
  43. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  44. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  45. agentscope_runtime/sandbox/model/container.py +6 -23
  46. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  47. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  48. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  49. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  50. agentscope_runtime/sandbox/utils.py +124 -0
  51. agentscope_runtime/version.py +1 -1
  52. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/METADATA +168 -77
  53. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/RECORD +59 -78
  54. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/entry_points.txt +0 -1
  55. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  56. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  57. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  58. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  59. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +0 -2886
  60. agentscope_runtime/engine/deployers/adapter/responses/response_api_agent_adapter.py +0 -51
  61. agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +0 -314
  62. agentscope_runtime/engine/deployers/cli_fc_deploy.py +0 -143
  63. agentscope_runtime/engine/deployers/kubernetes_deployer.py +0 -265
  64. agentscope_runtime/engine/deployers/modelstudio_deployer.py +0 -626
  65. agentscope_runtime/engine/deployers/utils/deployment_modes.py +0 -14
  66. agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +0 -8
  67. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +0 -429
  68. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +0 -240
  69. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +0 -297
  70. agentscope_runtime/engine/deployers/utils/package_project_utils.py +0 -932
  71. agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +0 -9
  72. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +0 -504
  73. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +0 -157
  74. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +0 -268
  75. agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +0 -75
  76. agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +0 -220
  77. agentscope_runtime/engine/deployers/utils/wheel_packager.py +0 -389
  78. agentscope_runtime/engine/helpers/agent_api_builder.py +0 -651
  79. agentscope_runtime/engine/llms/__init__.py +0 -3
  80. agentscope_runtime/engine/llms/base_llm.py +0 -60
  81. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  82. agentscope_runtime/engine/schemas/embedding.py +0 -37
  83. agentscope_runtime/engine/schemas/modelstudio_llm.py +0 -310
  84. agentscope_runtime/engine/schemas/oai_llm.py +0 -538
  85. agentscope_runtime/engine/schemas/realtime.py +0 -254
  86. /agentscope_runtime/engine/{deployers/adapter/responses → services/utils}/__init__.py +0 -0
  87. /agentscope_runtime/{engine/deployers/utils → sandbox/box/gui/box}/__init__.py +0 -0
  88. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/WHEEL +0 -0
  89. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/licenses/LICENSE +0 -0
  90. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/top_level.txt +0 -0
@@ -1,429 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # pylint:disable=too-many-branches
3
-
4
- import json
5
- import logging
6
- import os
7
- import subprocess
8
- from typing import Optional, Dict
9
- from pydantic import BaseModel
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class RegistryConfig(BaseModel):
15
- """Container registry configuration"""
16
-
17
- registry_url: str = ""
18
- username: str = None
19
- password: str = None
20
- namespace: str = "agentscope-runtime"
21
- image_pull_secret: str = None
22
-
23
- def get_full_url(self) -> str:
24
- # Handle different registry URL formats
25
- return f"{self.registry_url}/{self.namespace}"
26
-
27
-
28
- class BuildConfig(BaseModel):
29
- """Configuration for Docker image building"""
30
-
31
- no_cache: bool = False
32
- quiet: bool = False
33
- build_args: Dict[str, str] = {}
34
- platform: Optional[str] = None
35
- target: Optional[str] = None
36
- source_updated: bool = False
37
-
38
-
39
- class DockerImageBuilder:
40
- """
41
- Responsible solely for building and managing Docker images.
42
- Separated from project packaging for better separation of concerns.
43
- """
44
-
45
- def __init__(self):
46
- """
47
- Initialize Docker image builder.
48
- """
49
- self._ensure_docker_available()
50
-
51
- @staticmethod
52
- def _ensure_docker_available():
53
- """Ensure Docker is available on the system"""
54
- try:
55
- result = subprocess.run(
56
- ["docker", "--version"],
57
- check=True,
58
- capture_output=True,
59
- text=True,
60
- )
61
- logger.debug(f"Docker available: {result.stdout.strip()}")
62
- except (subprocess.CalledProcessError, FileNotFoundError) as e:
63
- raise RuntimeError(
64
- "Docker is not installed or not available in PATH. "
65
- "Please install Docker to use this functionality.",
66
- ) from e
67
-
68
- @staticmethod
69
- def get_full_name(
70
- image_name: str,
71
- image_tag: str = "latest",
72
- ):
73
- return f"{image_name}:{image_tag}"
74
-
75
- def build_image(
76
- self,
77
- build_context: str,
78
- image_name: str,
79
- image_tag: str = "latest",
80
- dockerfile_path: Optional[str] = None,
81
- config: Optional[BuildConfig] = None,
82
- source_updated: bool = False,
83
- ) -> str:
84
- """
85
- Build Docker image from build context.
86
-
87
- Args:
88
- build_context: Path to build context directory
89
- image_name: Name for the Docker image
90
- image_tag: Tag for the Docker image
91
- dockerfile_path: Optional path to Dockerfile
92
- (defaults to Dockerfile in context)
93
- config: Build configuration
94
- source_updated: Optional flag to determine if source image
95
- should be updated.
96
-
97
- Returns:
98
- str: Full image name with tag
99
-
100
- Raises:
101
- subprocess.CalledProcessError: If docker build fails
102
- ValueError: If build context doesn't exist
103
- """
104
- if not os.path.exists(build_context):
105
- raise ValueError(f"Build context does not exist: {build_context}")
106
-
107
- config = config or BuildConfig()
108
- full_image_name = self.get_full_name(image_name, image_tag)
109
-
110
- if not source_updated:
111
- return full_image_name
112
-
113
- # Prepare docker build command
114
- build_cmd = ["docker", "build", "-t", full_image_name]
115
-
116
- # Add dockerfile path if specified
117
- if dockerfile_path:
118
- if not os.path.isabs(dockerfile_path):
119
- dockerfile_path = os.path.join(build_context, dockerfile_path)
120
- build_cmd.extend(["-f", dockerfile_path])
121
-
122
- # Add build arguments
123
- if config.build_args:
124
- for key, value in config.build_args.items():
125
- build_cmd.extend(["--build-arg", f"{key}={value}"])
126
-
127
- # Add platform if specified
128
- if config.platform:
129
- build_cmd.extend(["--platform", config.platform])
130
-
131
- # Add target if specified
132
- if config.target:
133
- build_cmd.extend(["--target", config.target])
134
-
135
- # Add additional options
136
- if config.no_cache:
137
- build_cmd.append("--no-cache")
138
-
139
- if config.quiet:
140
- build_cmd.append("--quiet")
141
-
142
- # Add build context path
143
- build_cmd.append(build_context)
144
-
145
- try:
146
- if config.quiet:
147
- # Capture output for quiet mode
148
- result = subprocess.run(
149
- build_cmd,
150
- check=True,
151
- capture_output=True,
152
- text=True,
153
- cwd=build_context,
154
- )
155
- logger.info(f"Built image: {full_image_name}")
156
- if result.stdout:
157
- logger.debug(f"Build output: {result.stdout}")
158
- else:
159
- # Stream output for non-quiet mode
160
- logger.info(f"Building image: {full_image_name}")
161
- logger.debug(f"Build command: {' '.join(build_cmd)}")
162
-
163
- with subprocess.Popen(
164
- build_cmd,
165
- stdout=subprocess.PIPE,
166
- stderr=subprocess.STDOUT,
167
- text=True,
168
- bufsize=1,
169
- universal_newlines=True,
170
- cwd=build_context,
171
- ) as process:
172
- # Stream output in real-time
173
- while True:
174
- output = process.stdout.readline()
175
- if output == "" and process.poll() is not None:
176
- break
177
- if output:
178
- print(output.strip())
179
-
180
- process.wait()
181
-
182
- if process.returncode != 0:
183
- raise subprocess.CalledProcessError(
184
- process.returncode,
185
- build_cmd,
186
- "Docker build failed",
187
- )
188
-
189
- logger.info(f"Successfully built image: {full_image_name}")
190
-
191
- return full_image_name
192
-
193
- except subprocess.CalledProcessError as e:
194
- error_msg = f"Docker build failed for image {full_image_name}"
195
- if hasattr(e, "output") and e.output:
196
- error_msg += f"\nError output: {e.output}"
197
- logger.error(error_msg)
198
- raise subprocess.CalledProcessError(
199
- e.returncode,
200
- e.cmd,
201
- error_msg,
202
- ) from e
203
-
204
- def push_image(
205
- self,
206
- image_name: str,
207
- registry_config: Optional[RegistryConfig] = None,
208
- quiet: bool = False,
209
- ) -> str:
210
- """
211
- Push image to registry.
212
-
213
- Args:
214
- image_name: Full image name to push
215
- registry_config: Optional registry config
216
- (uses instance config if None)
217
- quiet: Whether to suppress output
218
-
219
- Returns:
220
- str: Full image name that was pushed
221
-
222
- Raises:
223
- subprocess.CalledProcessError: If push fails
224
- ValueError: If no registry configuration is available
225
- """
226
- config = registry_config
227
- if not config:
228
- raise ValueError("No registry configuration provided")
229
-
230
- # Construct full registry image name
231
- if config.registry_url and not image_name.startswith(
232
- config.registry_url,
233
- ):
234
- registry_image_name = f"{config.get_full_url()}/{image_name}"
235
- # Tag the image with registry prefix
236
- subprocess.run(
237
- ["docker", "tag", image_name, registry_image_name],
238
- check=True,
239
- capture_output=True,
240
- )
241
- else:
242
- registry_image_name = image_name
243
-
244
- try:
245
- push_cmd = ["docker", "push", registry_image_name]
246
-
247
- if quiet:
248
- result = subprocess.run(
249
- push_cmd,
250
- check=True,
251
- capture_output=True,
252
- text=True,
253
- )
254
- logger.info(f"Pushed image: {registry_image_name}")
255
- if result.stdout:
256
- logger.debug(f"Push output: {result.stdout}")
257
- else:
258
- logger.info(f"Pushing image: {registry_image_name}")
259
-
260
- with subprocess.Popen(
261
- push_cmd,
262
- stdout=subprocess.PIPE,
263
- stderr=subprocess.STDOUT,
264
- text=True,
265
- bufsize=1,
266
- universal_newlines=True,
267
- ) as process:
268
- # Stream output in real-time
269
- while True:
270
- output = process.stdout.readline()
271
- if output == "" and process.poll() is not None:
272
- break
273
- if output:
274
- print(output.strip())
275
-
276
- process.wait()
277
-
278
- if process.returncode != 0:
279
- raise subprocess.CalledProcessError(
280
- process.returncode,
281
- push_cmd,
282
- "Docker push failed",
283
- )
284
-
285
- logger.info(
286
- f"Successfully pushed image: {registry_image_name}",
287
- )
288
-
289
- return registry_image_name
290
-
291
- except subprocess.CalledProcessError as e:
292
- error_msg = f"Docker push failed for image {registry_image_name}"
293
- if hasattr(e, "stderr") and e.stderr:
294
- error_msg += f"\nError output: {e.stderr}"
295
- logger.error(error_msg)
296
- raise subprocess.CalledProcessError(
297
- e.returncode,
298
- e.cmd,
299
- error_msg,
300
- ) from e
301
-
302
- def build_and_push(
303
- self,
304
- build_context: str,
305
- image_name: str,
306
- image_tag: str = "latest",
307
- dockerfile_path: Optional[str] = None,
308
- build_config: Optional[BuildConfig] = None,
309
- registry_config: Optional[RegistryConfig] = None,
310
- source_updated: bool = False,
311
- ) -> str:
312
- """
313
- Build and push image in one operation.
314
-
315
- Args:
316
- build_context: Path to build context directory
317
- image_name: Name for the Docker image
318
- image_tag: Tag for the Docker image
319
- dockerfile_path: Optional path to Dockerfile
320
- build_config: Build configuration
321
- registry_config: Registry configuration
322
- source_updated: Whether the source image was updated or not
323
-
324
- Returns:
325
- str: Full registry image name
326
- """
327
- # Build the image
328
- built_image = self.build_image(
329
- build_context=build_context,
330
- image_name=image_name,
331
- image_tag=image_tag,
332
- dockerfile_path=dockerfile_path,
333
- config=build_config,
334
- source_updated=source_updated,
335
- )
336
-
337
- # Push to registry
338
- registry_image = self.push_image(
339
- image_name=built_image,
340
- registry_config=registry_config,
341
- quiet=build_config.quiet if build_config else False,
342
- )
343
-
344
- # make sure return the built name without registry
345
- return registry_image.split("/")[-1]
346
-
347
- def remove_image(
348
- self,
349
- image_name: str,
350
- force: bool = False,
351
- quiet: bool = True,
352
- ) -> bool:
353
- """
354
- Remove Docker image.
355
-
356
- Args:
357
- image_name: Name of image to remove
358
- force: Force removal
359
- quiet: Suppress output
360
-
361
- Returns:
362
- bool: True if successful
363
- """
364
- try:
365
- cmd = ["docker", "rmi"]
366
- if force:
367
- cmd.append("-f")
368
- cmd.append(image_name)
369
-
370
- subprocess.run(
371
- cmd,
372
- check=True,
373
- capture_output=quiet,
374
- text=True,
375
- )
376
-
377
- if not quiet:
378
- logger.info(f"Removed image: {image_name}")
379
-
380
- return True
381
-
382
- except subprocess.CalledProcessError as e:
383
- if not quiet:
384
- logger.warning(f"Failed to remove image {image_name}: {e}")
385
- return False
386
-
387
- def get_image_info(self, image_name: str) -> Dict:
388
- """
389
- Get information about a Docker image.
390
-
391
- Args:
392
- image_name: Name of the Docker image
393
-
394
- Returns:
395
- Dict: Image information from docker inspect
396
-
397
- Raises:
398
- ValueError: If image not found or info invalid
399
- """
400
- try:
401
- result = subprocess.run(
402
- ["docker", "inspect", image_name],
403
- check=True,
404
- capture_output=True,
405
- text=True,
406
- )
407
- image_info = json.loads(result.stdout)[0]
408
- return image_info
409
-
410
- except subprocess.CalledProcessError as e:
411
- raise ValueError(f"Image not found: {image_name}") from e
412
- except (json.JSONDecodeError, IndexError) as e:
413
- raise ValueError(f"Invalid image info for: {image_name}") from e
414
-
415
- def image_exists(self, image_name: str) -> bool:
416
- """
417
- Check if Docker image exists locally.
418
-
419
- Args:
420
- image_name: Name of image to check
421
-
422
- Returns:
423
- bool: True if image exists
424
- """
425
- try:
426
- self.get_image_info(image_name)
427
- return True
428
- except ValueError:
429
- return False
@@ -1,240 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import logging
3
- import os
4
- import tempfile
5
- from typing import Optional, Dict, List
6
-
7
- from pydantic import BaseModel
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
-
12
- class DockerfileConfig(BaseModel):
13
- """Configuration for Dockerfile generation"""
14
-
15
- base_image: str = "python:3.10-slim-bookworm"
16
- port: int = 8000
17
- working_dir: str = "/app"
18
- user: str = "appuser"
19
- additional_packages: List[str] = []
20
- env_vars: Dict[str, str] = {}
21
- startup_command: Optional[str] = None
22
- health_check_endpoint: str = "/health"
23
- custom_template: Optional[str] = None
24
-
25
-
26
- class DockerfileGenerator:
27
- """
28
- Responsible for generating Dockerfiles from templates.
29
- Separated from image building for better modularity.
30
- """
31
-
32
- # Default Dockerfile template for Python applications
33
- DEFAULT_TEMPLATE = """# Use official Python runtime as base image
34
- FROM {base_image}
35
-
36
- # Set working directory in container
37
- WORKDIR /app
38
-
39
- # Set environment variables
40
- ENV PYTHONDONTWRITEBYTECODE=1
41
- ENV PYTHONUNBUFFERED=1
42
-
43
- # Configure package sources for better performance
44
- RUN rm -f /etc/apt/sources.list.d/*.list
45
-
46
- # 替换主源为阿里云
47
- RUN echo "deb https://mirrors.aliyun.com/debian/ bookworm main contrib " \\
48
- "non-free non-free-firmware" > /etc/apt/sources.list && \\
49
- echo "deb https://mirrors.aliyun.com/debian/ bookworm-updates main " \\
50
- "contrib non-free non-free-firmware" >> /etc/apt/sources.list && \\
51
- echo "deb https://mirrors.aliyun.com/debian-security/ " \\
52
- "bookworm-security main contrib non-free " \\
53
- "non-free-firmware" >> /etc/apt/sources.list
54
-
55
- # Clean up package lists
56
- RUN rm -rf /var/lib/apt/lists/*
57
-
58
- # Install system dependencies
59
- RUN apt-get update && apt-get install -y \\
60
- gcc \\
61
- curl \\
62
- {additional_packages_section} && rm -rf /var/lib/apt/lists/*
63
-
64
- # Copy project files
65
- COPY . {working_dir}/
66
-
67
- # Install Python dependencies
68
- RUN pip install --no-cache-dir --upgrade pip
69
- RUN if [ -f requirements.txt ]; then \\
70
- pip install --no-cache-dir -r requirements.txt \\
71
- -i https://pypi.tuna.tsinghua.edu.cn/simple; fi
72
-
73
- # Create non-root user for security
74
- RUN adduser --disabled-password --gecos '' {user} && \\
75
- chown -R {user} {working_dir}
76
- USER {user}
77
-
78
- {env_vars_section}
79
- # Expose port
80
- EXPOSE {port}
81
-
82
- # Health check
83
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
84
- CMD curl -f http://localhost:{port}{health_check_endpoint} || exit 1
85
-
86
- # Command to run the application
87
- {startup_command_section}"""
88
-
89
- def __init__(self):
90
- self.temp_files: List[str] = []
91
-
92
- def generate_dockerfile_content(self, config: DockerfileConfig) -> str:
93
- """
94
- Generate Dockerfile content from configuration.
95
-
96
- Args:
97
- config: Dockerfile configuration
98
-
99
- Returns:
100
- str: Generated Dockerfile content
101
- """
102
- template = config.custom_template or self.DEFAULT_TEMPLATE
103
-
104
- # Prepare additional packages section
105
- additional_packages_section = ""
106
- if config.additional_packages:
107
- packages_line = " \\\n ".join(config.additional_packages)
108
- additional_packages_section = f" {packages_line} \\\n"
109
-
110
- # Prepare environment variables section
111
- env_vars_section = ""
112
- if config.env_vars:
113
- env_vars_section = "\n# Additional environment variables\n"
114
- for key, value in config.env_vars.items():
115
- env_vars_section += f"ENV {key}={value}\n"
116
- env_vars_section += "\n"
117
-
118
- # Prepare startup command section
119
- if config.startup_command:
120
- if config.startup_command.startswith("["):
121
- # JSON array format
122
- startup_command_section = f"CMD {config.startup_command}"
123
- else:
124
- # Shell format
125
- startup_command_section = f'CMD ["{config.startup_command}"]'
126
- else:
127
- # Default uvicorn command
128
- startup_command_section = (
129
- f'CMD ["uvicorn", "main:app", "--host", "0.0.0.0", '
130
- f'"--port", "{config.port}"]'
131
- )
132
-
133
- # Format template with configuration values
134
- content = template.format(
135
- base_image=config.base_image,
136
- working_dir=config.working_dir,
137
- port=config.port,
138
- user=config.user,
139
- health_check_endpoint=config.health_check_endpoint,
140
- additional_packages_section=additional_packages_section,
141
- env_vars_section=env_vars_section,
142
- startup_command_section=startup_command_section,
143
- )
144
-
145
- return content
146
-
147
- def create_dockerfile(
148
- self,
149
- config: DockerfileConfig,
150
- output_dir: Optional[str] = None,
151
- ) -> str:
152
- """
153
- Create Dockerfile in specified directory.
154
-
155
- Args:
156
- config: Dockerfile configuration
157
- output_dir: Directory to create Dockerfile (temp dir if None)
158
-
159
- Returns:
160
- str: Path to created Dockerfile
161
- """
162
- # Create output directory if not provided
163
- if output_dir is None:
164
- output_dir = tempfile.mkdtemp(prefix="dockerfile_")
165
- self.temp_files.append(output_dir)
166
- else:
167
- os.makedirs(output_dir, exist_ok=True)
168
-
169
- # Generate Dockerfile content
170
- dockerfile_content = self.generate_dockerfile_content(config)
171
-
172
- # Write Dockerfile
173
- dockerfile_path = os.path.join(output_dir, "Dockerfile")
174
- try:
175
- with open(dockerfile_path, "w", encoding="utf-8") as f:
176
- f.write(dockerfile_content)
177
-
178
- logger.info(f"Created Dockerfile: {dockerfile_path}")
179
- return dockerfile_path
180
-
181
- except Exception as e:
182
- logger.error(f"Failed to create Dockerfile: {e}")
183
- if output_dir in self.temp_files and os.path.exists(output_dir):
184
- import shutil
185
-
186
- shutil.rmtree(output_dir)
187
- self.temp_files.remove(output_dir)
188
- raise
189
-
190
- def validate_config(self, config: DockerfileConfig) -> bool:
191
- """
192
- Validate Dockerfile configuration.
193
-
194
- Args:
195
- config: Configuration to validate
196
-
197
- Returns:
198
- bool: True if valid
199
-
200
- Raises:
201
- ValueError: If configuration is invalid
202
- """
203
- if not config.base_image:
204
- raise ValueError("Base image cannot be empty")
205
-
206
- if (
207
- not isinstance(config.port, int)
208
- or config.port <= 0
209
- or config.port > 65535
210
- ):
211
- raise ValueError(f"Invalid port: {config.port}")
212
-
213
- if not config.working_dir.startswith("/"):
214
- raise ValueError(
215
- f"Working directory must be absolute path: "
216
- f"{config.working_dir}",
217
- )
218
-
219
- return True
220
-
221
- def cleanup(self):
222
- """Clean up temporary files"""
223
- import shutil
224
-
225
- for temp_path in self.temp_files:
226
- if os.path.exists(temp_path):
227
- try:
228
- shutil.rmtree(temp_path)
229
- logger.debug(f"Cleaned up temp path: {temp_path}")
230
- except OSError as e:
231
- logger.warning(f"Failed to cleanup {temp_path}: {e}")
232
- self.temp_files.clear()
233
-
234
- def __enter__(self):
235
- """Context manager entry"""
236
- return self
237
-
238
- def __exit__(self, exc_type, exc_val, exc_tb):
239
- """Context manager exit with cleanup"""
240
- self.cleanup()