1"""AgentOS deployment — Docker and orchestration helpers."""

2

3 from __future__ import annotations

4

5 from dataclasses import dataclass, field

6 from pathlib import Path

7 from typing import Optional

8

9# ── Dockerfile generator ──────────────────────────────────────────────────────

10

11

12@dataclass

13 class DockerConfig:

14 """Docker 部署配置。"""

15 python_version: str = "3.11"

16 base_image: str = "python:{python_version}-slim"

17 workdir: str = "/app"

18 port: int = 8000

19 entry_module: str = "agentos.cli.serve"

20 extra_packages: list[str] = field(default_factory=list)

21 env_vars: dict[str, str] = field(default_factory=dict)

22 user: str = "appuser"

23

24

25 def generate_dockerfile(config: Optional[DockerConfig] = None) -> str:

26 """Generate a production-ready Dockerfile for an AgentOS project."""

27 cfg = config or DockerConfig()

28 base = cfg.base_image.format(python_version=cfg.python_version)

29

30 lines = [

31 f"FROM {base}",

32 "",

33 "LABEL org.opencontainers.image.source=\"https://github.com/agentos/agentos\"",

34 "",

35 "# System dependencies",

36 "RUN apt-get update && apt-get install -y --no-install-recommends \\",

37 " curl ca-certificates \\",

38 " && rm -rf /var/lib/apt/lists/*",

39 "",

40 "# Create non-root user",

41 f"RUN groupadd -r {cfg.user} && useradd -r -g {cfg.user} {cfg.user}",

42 "",

43 f"WORKDIR {cfg.workdir}",

44 "",

45 "# Install Python dependencies",

46 "COPY requirements.txt .",

47 f"RUN pip install --no-cache-dir -r requirements.txt",

48 "",

49 "# Copy application",

50 "COPY . .",

51 f"RUN pip install --no-cache-dir -e .",

52 "",

53 ]

54

55 if cfg.extra_packages:

56 lines.append(f"RUN pip install --no-cache-dir {' '.join(cfg.extra_packages)}")

57 lines.append("")

58

59 for k, v in cfg.env_vars.items():

60 lines.append(f"ENV {k}={v}")

61 if cfg.env_vars:

62 lines.append("")

63

64 lines += [

65 "# Security: drop root",

66 f"USER {cfg.user}",

67 "",

68 "# Health check",

69 "HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \\",

70 f" CMD curl -f http://localhost:{cfg.port}/health || exit 1",

71 "",

72 f"EXPOSE {cfg.port}",

73 "",

74 f'ENTRYPOINT ["python", "-m", "{cfg.entry_module}"]',

75 ]

76

77 return "\n".join(lines)

78

79

80# ── docker-compose generator ──────────────────────────────────────────────────

81

82

83@dataclass

84 class ComposeService:

85 """Compose 服务定义。"""

86 name: str

87 build_context: str = "."

88 port: int = 8000

89 env_file: str = ".env"

90 volumes: list[str] = field(default_factory=list)

91 depends_on: list[str] = field(default_factory=list)

92 command: str = ""

93

94

95@dataclass

96 class ComposeConfig:

97 """Compose 编排配置。"""

98 services: list[ComposeService] = field(default_factory=list)

99 project_name: str = "agentos"

100 network_name: str = "agentos-net"

101

102

103 def generate_docker_compose(config: ComposeConfig) -> str:

104 """Generate a docker-compose.yml for an AgentOS project."""

105 lines = [f'version: "3.9"', "", "services:"]

106

107 for svc in config.services:

108 lines.append(f" {svc.name}:")

109 lines.append(f" build:")

110 lines.append(f" context: {svc.build_context}")

111 lines.append(f" ports:")

112 lines.append(f' - "{svc.port}:{svc.port}"')

113 if svc.env_file:

114 lines.append(f" env_file:")

115 lines.append(f" - {svc.env_file}")

116 if svc.volumes:

117 lines.append(f" volumes:")

118 for v in svc.volumes:

119 lines.append(f" - {v}")

120 if svc.depends_on:

121 lines.append(f" depends_on:")

122 for d in svc.depends_on:

123 lines.append(f" - {d}")

124 if svc.command:

125 lines.append(f" command: {svc.command}")

126 lines.append("")

127

128 lines += [

129 "networks:",

130 f" default:",

131 f" name: {config.network_name}",

132 ]

133

134 return "\n".join(lines)

135

136

137# ── Deployment helper ─────────────────────────────────────────────────────────

138

139

140 def write_deployment_files(

141 output_dir: str | Path,

142 docker_config: Optional[DockerConfig] = None,

143 compose_config: Optional[ComposeConfig] = None,

144) -> list[Path]:

145 """Write Dockerfile and docker-compose.yml to output_dir. Returns written paths."""

146 out = Path(output_dir)

147 out.mkdir(parents=True, exist_ok=True)

148 written: list[Path] = []

149

150 df_path = out / "Dockerfile"

151 df_path.write_text(generate_dockerfile(docker_config))

152 written.append(df_path)

153

154 compose = compose_config or ComposeConfig(

155 services=[ComposeService(name="agentos")]

156 )

157 dc_path = out / "docker-compose.yml"

158 dc_path.write_text(generate_docker_compose(compose))

159 written.append(dc_path)

160

161 return written