pipecatcloud 0.3.0__tar.gz → 0.4.0__tar.gz

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 (66) hide show
  1. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/CHANGELOG.md +57 -0
  2. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/PKG-INFO +1 -1
  3. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/build_utils.py +14 -1
  4. pipecatcloud-0.4.0/src/pipecatcloud/_utils/deploy_utils.py +419 -0
  5. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/api.py +0 -1
  6. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/agent.py +48 -22
  7. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/build.py +10 -5
  8. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/deploy.py +49 -39
  9. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/docker.py +6 -1
  10. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/config.py +5 -1
  11. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/entry_point.py +10 -4
  12. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
  13. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/SOURCES.txt +3 -1
  14. pipecatcloud-0.4.0/tests/conftest.py +17 -0
  15. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_api_client.py +0 -2
  16. pipecatcloud-0.4.0/tests/test_config_file.py +48 -0
  17. pipecatcloud-0.4.0/tests/test_deployment_status.py +478 -0
  18. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_krisp_viva.py +1 -9
  19. pipecatcloud-0.3.0/src/pipecatcloud/_utils/deploy_utils.py +0 -219
  20. pipecatcloud-0.3.0/tests/test_managed_keys.py +0 -426
  21. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/format.yml +0 -0
  22. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
  23. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/publish-test.yml +0 -0
  24. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/tests.yml +0 -0
  25. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.gitignore +0 -0
  26. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.pre-commit-config.yaml +0 -0
  27. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/LICENSE +0 -0
  28. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/README.md +0 -0
  29. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pipecat-cloud.png +0 -0
  30. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pyproject.toml +0 -0
  31. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pytest.ini +0 -0
  32. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/scripts/fix-ruff.sh +0 -0
  33. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/scripts/pre-commit.sh +0 -0
  34. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/setup.cfg +0 -0
  35. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__init__.py +0 -0
  36. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__main__.py +0 -0
  37. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__version__.py +0 -0
  38. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  39. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  40. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  41. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
  42. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/regions.py +0 -0
  43. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/agent.py +0 -0
  44. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/__init__.py +0 -0
  45. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/api.py +0 -0
  46. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  47. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/auth.py +0 -0
  48. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  49. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
  50. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/secrets.py +0 -0
  51. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/config.py +0 -0
  52. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/constants.py +0 -0
  53. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/exception.py +0 -0
  54. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/py.typed +0 -0
  55. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/session.py +0 -0
  56. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  57. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  58. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  59. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
  60. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  61. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/__init__.py +0 -0
  62. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_agent_sessions.py +0 -0
  63. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_agent_stop.py +0 -0
  64. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_docker_commands.py +0 -0
  65. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_regions.py +0 -0
  66. {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/uv.lock +0 -0
@@ -5,6 +5,61 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-03-29
9
+
10
+ ### Added
11
+
12
+ - **`--config-file` flag**: Specify an alternate deploy config file on any
13
+ command that uses a config file (`deploy`, `agent sessions`, `agent start`,
14
+ `agent stop`, `docker build-push`). Enables multi-environment workflows
15
+ without renaming files or setting environment variables.
16
+
17
+ ### Changed
18
+
19
+ - Renamed `--config` to `--show-cli-config` for clarity — it displays internal
20
+ CLI state (credentials, active org), not deploy configuration.
21
+
22
+ ### Removed
23
+
24
+ - Removed `--enable-managed-keys` CLI option from `deploy` command.
25
+
26
+ - Removed `enable_managed_keys` from `pcc-deploy.toml` configuration.
27
+
28
+ - Removed "Managed Keys" status display from `agent status` output.
29
+
30
+ ### Fixed
31
+
32
+ - CLI no longer crashes on a malformed credentials file. It warns and allows
33
+ `auth login` to run so the user can fix it.
34
+
35
+ ## [0.3.1] - 2026-03-13
36
+
37
+ ### Added
38
+
39
+ - Added `.github`, `.claude`, `pcc-deploy.toml`, `.ipynb_checkpoints`, and
40
+ `.cache` to default cloud build context exclusions to reduce upload size and
41
+ avoid including non-runtime files in container images.
42
+
43
+ ### Changed
44
+
45
+ - Increased deploy polling timeout from 90 seconds to 10 minutes to support
46
+ large deployments with big images, high replica counts, and cross-region pulls.
47
+
48
+ - Deploy polling now shows rich, multi-line status using API conditions and
49
+ revision info instead of a static "Waiting for deployment to become ready..."
50
+ message. The headline shows condition state (e.g. "Progressing · Available"),
51
+ with per-revision detail lines showing phase, replica counts, and elapsed time
52
+ for both current and previous deployments during rolling updates.
53
+
54
+ - Deploy timeout now differentiates exit behavior: services that are available
55
+ and serving traffic show a soft warning instead of a hard error, with guidance
56
+ to check status. Hard errors are reserved for services that are truly
57
+ unavailable.
58
+
59
+ - `agent status` now shows per-revision details (phase, deployment ID, replica
60
+ counts) in the health panel when the API provides revision info, replacing the
61
+ binary "Health: Ready/Stopped" display.
62
+
8
63
  ## [0.3.0] - 2026-03-12
9
64
 
10
65
  ### Added
@@ -15,6 +70,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
70
  - `pcc deploy --build-id <id>` to reuse an existing build
16
71
  - `pcc deploy --yes` for non-interactive CI/CD usage
17
72
  - `[build]` section in `pcc-deploy.toml` for build configuration
73
+
18
74
  - New `pcc build` command group for managing cloud builds:
19
75
  - `pcc build list` — list recent builds with status and region filtering
20
76
  - `pcc build status <id>` — get detailed status of a build
@@ -23,6 +79,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
79
  ### Changed
24
80
 
25
81
  - The `image` field in `pcc deploy` is now optional when using cloud builds
82
+
26
83
  - The CLI now uses `desiredDeploymentId` (with fallback to `activeDeploymentId`)
27
84
  and compares it against `reconciledDeploymentId` to determine deployment status.
28
85
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
@@ -26,7 +26,6 @@ from typing import Callable, List, Optional, Set, Tuple
26
26
  import aiohttp
27
27
  from loguru import logger
28
28
 
29
-
30
29
  # Default patterns to exclude from build context
31
30
  DEFAULT_EXCLUSIONS: Set[str] = {
32
31
  # Version control
@@ -68,8 +67,22 @@ DEFAULT_EXCLUSIONS: Set[str] = {
68
67
  "dist",
69
68
  "build",
70
69
  "*.egg-info",
70
+ "*.egg",
71
+ ".eggs",
71
72
  # Node (if present)
72
73
  "node_modules",
74
+ # CI/CD
75
+ ".github",
76
+ # AI tools
77
+ ".claude",
78
+ ".codex",
79
+ ".cursor",
80
+ # Pipecat config
81
+ "pcc-deploy.toml",
82
+ # Jupyter
83
+ ".ipynb_checkpoints",
84
+ # Caches
85
+ ".cache",
73
86
  # Misc
74
87
  ".DS_Store",
75
88
  "Thumbs.db",
@@ -0,0 +1,419 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ import functools
8
+ import os
9
+ from datetime import datetime, timezone
10
+ from enum import Enum
11
+ from typing import Callable, List, Optional
12
+
13
+
14
+ import toml
15
+ import typer
16
+ from attr import dataclass, field
17
+ from loguru import logger
18
+
19
+ from pipecatcloud.cli import PIPECAT_DEPLOY_CONFIG_PATH
20
+ from pipecatcloud.constants import KRISP_VIVA_MODELS, KrispVivaAudioFilter
21
+ from pipecatcloud.exception import ConfigFileError
22
+
23
+ DEPLOY_STATUS_MAP = {
24
+ "Unknown": "[dim]Waiting[/dim]",
25
+ "True": "[green]Ready[/green]",
26
+ "False": "[yellow]Creating[/yellow]",
27
+ }
28
+
29
+
30
+ class DeploymentPhase(Enum):
31
+ WAITING_FOR_OPERATOR = "waiting_for_operator"
32
+ PROGRESSING_AVAILABLE = "progressing_available"
33
+ PROGRESSING_NEW = "progressing_new"
34
+ DEGRADED_AVAILABLE = "degraded_available"
35
+ UNAVAILABLE = "unavailable"
36
+ READY = "ready"
37
+
38
+
39
+ @dataclass
40
+ class DeploymentStatus:
41
+ phase: DeploymentPhase
42
+ status_message: str
43
+ is_available: bool = False
44
+ is_ready: bool = False
45
+ degraded_reason: Optional[str] = None
46
+ current_revision: Optional[dict] = None
47
+ previous_revision: Optional[dict] = None
48
+
49
+
50
+ def _find_condition(conditions: list, condition_type: str) -> Optional[dict]:
51
+ """Find a condition by type from the conditions array."""
52
+ for c in conditions:
53
+ if c.get("type") == condition_type:
54
+ return c
55
+ return None
56
+
57
+
58
+ def _format_elapsed(phase_started_at: Optional[str]) -> str:
59
+ """Format elapsed time since phaseStartedAt as a human-readable string."""
60
+ if not phase_started_at:
61
+ return ""
62
+ try:
63
+ started = datetime.fromisoformat(phase_started_at.replace("Z", "+00:00"))
64
+ elapsed = datetime.now(timezone.utc) - started
65
+ total_seconds = int(elapsed.total_seconds())
66
+ if total_seconds < 0:
67
+ return ""
68
+ if total_seconds < 60:
69
+ return f"{total_seconds}s"
70
+ minutes = total_seconds // 60
71
+ seconds = total_seconds % 60
72
+ return f"{minutes}m {seconds}s"
73
+ except (ValueError, TypeError):
74
+ return ""
75
+
76
+
77
+ def _format_revision_line(label: str, rev: dict) -> str:
78
+ """Format a single revision as an indented detail line."""
79
+ deploy_id = rev.get("deploymentID", "unknown")[:8]
80
+ phase = rev.get("phase", "Unknown")
81
+ ready_replicas = rev.get("readyReplicas")
82
+
83
+ parts = [f" [dim]{label}[/dim] [bold]({deploy_id})[/bold] {phase}"]
84
+ if ready_replicas is not None:
85
+ parts.append(f"[dim]·[/dim] {ready_replicas} agents")
86
+
87
+ elapsed = _format_elapsed(rev.get("phaseStartedAt"))
88
+ if elapsed:
89
+ parts.append(f"[dim]·[/dim] [dim]{elapsed}[/dim]")
90
+
91
+ return " ".join(parts)
92
+
93
+
94
+ def _build_status_message(headline: str, current_rev=None, previous_rev=None) -> str:
95
+ """Build a multi-line status message with optional revision detail lines."""
96
+ lines = [headline]
97
+ if current_rev:
98
+ lines.append(_format_revision_line("Current ", current_rev))
99
+ if previous_rev:
100
+ lines.append(_format_revision_line("Previous", previous_rev))
101
+ return "\n".join(lines)
102
+
103
+
104
+ def interpret_deployment_status(
105
+ agent_status: dict,
106
+ desired_deployment_id: Optional[str] = None,
107
+ ) -> DeploymentStatus:
108
+ """Interpret the raw API response into a structured deployment status.
109
+
110
+ Examines reconciledDeploymentId, conditions array, available/ready booleans,
111
+ and revision info to determine the current deployment phase and build a
112
+ human-readable status message.
113
+ """
114
+ reconciled_id = agent_status.get("reconciledDeploymentId")
115
+ desired_id = desired_deployment_id or agent_status.get(
116
+ "desiredDeploymentId", agent_status.get("activeDeploymentId")
117
+ )
118
+ conditions = agent_status.get("conditions") or []
119
+ available = agent_status.get("available", agent_status.get("ready", False))
120
+ ready = agent_status.get("ready", False)
121
+ active_deployment_ready = agent_status.get("activeDeploymentReady", False)
122
+
123
+ current_rev = agent_status.get("currentRevision")
124
+ previous_rev = agent_status.get("previousRevision")
125
+
126
+ # 1. Check if operator has reconciled
127
+ if desired_id and reconciled_id != desired_id:
128
+ return DeploymentStatus(
129
+ phase=DeploymentPhase.WAITING_FOR_OPERATOR,
130
+ status_message="[dim]Waiting for operator to process deployment...[/dim]",
131
+ current_revision=current_rev,
132
+ previous_revision=previous_rev,
133
+ )
134
+
135
+ # 2. Fully ready
136
+ if available and (ready or active_deployment_ready):
137
+ return DeploymentStatus(
138
+ phase=DeploymentPhase.READY,
139
+ status_message="[green]Deployment is ready[/green]",
140
+ is_available=True,
141
+ is_ready=True,
142
+ current_revision=current_rev,
143
+ previous_revision=previous_rev,
144
+ )
145
+
146
+ # 3. Look at conditions for richer status
147
+ degraded = _find_condition(conditions, "Degraded")
148
+ progressing = _find_condition(conditions, "Progressing")
149
+
150
+ degraded_active = degraded and degraded.get("status") == "True"
151
+ degraded_reason = (
152
+ degraded.get("message", degraded.get("reason", "")) if degraded_active else None
153
+ )
154
+
155
+ # 4. Degraded but available — warning state
156
+ if degraded_active and available:
157
+ headline = f"[yellow]Degraded · Available[/yellow] [dim]— {degraded_reason}[/dim]"
158
+ return DeploymentStatus(
159
+ phase=DeploymentPhase.DEGRADED_AVAILABLE,
160
+ status_message=_build_status_message(headline, current_rev, previous_rev),
161
+ is_available=True,
162
+ degraded_reason=degraded_reason,
163
+ current_revision=current_rev,
164
+ previous_revision=previous_rev,
165
+ )
166
+
167
+ # 5. Available but not ready — rolling update in progress
168
+ if available and not ready:
169
+ headline = "[cyan]Progressing · Available[/cyan]"
170
+ return DeploymentStatus(
171
+ phase=DeploymentPhase.PROGRESSING_AVAILABLE,
172
+ status_message=_build_status_message(headline, current_rev, previous_rev),
173
+ is_available=True,
174
+ current_revision=current_rev,
175
+ previous_revision=previous_rev,
176
+ )
177
+
178
+ # 6. Not available, progressing — new service coming up
179
+ if not available and (progressing and progressing.get("status") == "True"):
180
+ headline = "[dim]Progressing[/dim]"
181
+ return DeploymentStatus(
182
+ phase=DeploymentPhase.PROGRESSING_NEW,
183
+ status_message=_build_status_message(headline, current_rev, previous_rev),
184
+ current_revision=current_rev,
185
+ previous_revision=previous_rev,
186
+ )
187
+
188
+ # 7. Not available, not progressing — broken
189
+ if not available and not ready:
190
+ reason_suffix = f" [dim]— {degraded_reason}[/dim]" if degraded_reason else ""
191
+ headline = f"[red]Unavailable[/red]{reason_suffix}"
192
+ return DeploymentStatus(
193
+ phase=DeploymentPhase.UNAVAILABLE,
194
+ status_message=_build_status_message(headline, current_rev, previous_rev),
195
+ current_revision=current_rev,
196
+ previous_revision=previous_rev,
197
+ )
198
+
199
+ # Fallback — shouldn't normally reach here
200
+ return DeploymentStatus(
201
+ phase=DeploymentPhase.PROGRESSING_NEW,
202
+ status_message="[dim]Waiting for deployment to become ready...[/dim]",
203
+ current_revision=current_rev,
204
+ previous_revision=previous_rev,
205
+ )
206
+
207
+
208
+ @dataclass
209
+ class ScalingParams:
210
+ min_agents: Optional[int] = 0
211
+ max_agents: Optional[int] = None
212
+ # @deprecated
213
+ min_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
214
+ # @deprecated
215
+ max_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
216
+
217
+ def __attrs_post_init__(self):
218
+ # Handle deprecated fields
219
+ if self.min_instances is not None:
220
+ logger.warning("min_instances is deprecated, use min_agents instead")
221
+ self.min_agents = self.min_instances
222
+
223
+ if self.max_instances is not None:
224
+ logger.warning("max_instances is deprecated, use max_agents instead")
225
+ self.max_agents = self.max_instances
226
+
227
+ # Validation
228
+ if self.min_agents is not None:
229
+ if self.min_agents < 0:
230
+ raise ValueError("min_agents must be greater than or equal to 0")
231
+
232
+ if self.max_agents is not None:
233
+ if self.max_agents < 1:
234
+ raise ValueError("max_agents must be greater than 0")
235
+
236
+ if self.min_agents is not None and self.max_agents < self.min_agents:
237
+ raise ValueError("max_agents must be greater than or equal to min_agents")
238
+
239
+ def to_dict(self):
240
+ return {"min_agents": self.min_agents, "max_agents": self.max_agents}
241
+
242
+
243
+ @dataclass
244
+ class KrispVivaConfig:
245
+ audio_filter: Optional[KrispVivaAudioFilter] = None
246
+
247
+ def __attrs_post_init__(self):
248
+ # Validation against known models
249
+ # IMPORTANT: KRISP_VIVA_MODELS must be kept in sync with API configuration
250
+ if self.audio_filter is not None:
251
+ if self.audio_filter not in KRISP_VIVA_MODELS:
252
+ raise ValueError(
253
+ f"audio_filter must be one of {KRISP_VIVA_MODELS}, got '{self.audio_filter}'"
254
+ )
255
+
256
+ def to_dict(self):
257
+ return {"audio_filter": self.audio_filter}
258
+
259
+
260
+ @dataclass
261
+ class BuildConfig:
262
+ """Configuration for cloud builds."""
263
+
264
+ context_dir: str = "."
265
+ dockerfile: str = "Dockerfile"
266
+ exclude_patterns: List[str] = field(factory=list)
267
+
268
+ def to_dict(self):
269
+ return {
270
+ "context_dir": self.context_dir,
271
+ "dockerfile": self.dockerfile,
272
+ "exclude_patterns": self.exclude_patterns,
273
+ }
274
+
275
+
276
+ @dataclass
277
+ class DeployConfigParams:
278
+ agent_name: Optional[str] = None
279
+ image: Optional[str] = None
280
+ build_id: Optional[str] = None # For cloud builds
281
+ image_credentials: Optional[str] = None
282
+ secret_set: Optional[str] = None
283
+ region: Optional[str] = None
284
+ scaling: ScalingParams = ScalingParams()
285
+ enable_krisp: bool = False
286
+ docker_config: dict = field(factory=dict)
287
+ build_config: BuildConfig = field(factory=BuildConfig) # Cloud build configuration
288
+ agent_profile: Optional[str] = None
289
+ krisp_viva: KrispVivaConfig = field(factory=KrispVivaConfig)
290
+
291
+ def __attrs_post_init__(self):
292
+ if self.image is not None and ":" not in self.image:
293
+ raise ValueError("Provided image must include tag e.g. my-image:latest")
294
+ # Cannot specify both image and build_id
295
+ if self.image is not None and self.build_id is not None:
296
+ raise ValueError("Cannot specify both 'image' and 'build_id'")
297
+
298
+ def to_dict(self):
299
+ return {
300
+ "agent_name": self.agent_name,
301
+ "image": self.image,
302
+ "build_id": self.build_id,
303
+ "image_credentials": self.image_credentials,
304
+ "secret_set": self.secret_set,
305
+ "region": self.region,
306
+ "scaling": self.scaling.to_dict() if self.scaling else None,
307
+ "enable_krisp": self.enable_krisp,
308
+ "docker_config": self.docker_config,
309
+ "build_config": self.build_config.to_dict() if self.build_config else None,
310
+ "agent_profile": self.agent_profile,
311
+ "krisp_viva": self.krisp_viva.to_dict() if self.krisp_viva else None,
312
+ }
313
+
314
+
315
+ def load_deploy_config_file() -> Optional[DeployConfigParams]:
316
+ from pipecatcloud.cli.config import deploy_config_path
317
+
318
+ logger.debug(f"Deploy config path: {deploy_config_path}")
319
+ logger.debug(f"Deploy config path exists: {os.path.exists(deploy_config_path)}")
320
+
321
+ try:
322
+ with open(deploy_config_path, "r") as f:
323
+ config_data = toml.load(f)
324
+ except Exception:
325
+ return None
326
+
327
+ try:
328
+ # Extract scaling parameters if present
329
+ scaling_data = config_data.pop("scaling", {})
330
+ scaling_params = ScalingParams(**scaling_data)
331
+
332
+ # Extract docker configuration if present
333
+ docker_data = config_data.pop("docker", {})
334
+
335
+ # Extract krisp_viva configuration if present
336
+ krisp_viva_data = config_data.pop("krisp_viva", {})
337
+ krisp_viva_config = KrispVivaConfig(**krisp_viva_data)
338
+
339
+ # Extract build configuration if present
340
+ build_data = config_data.pop("build", {})
341
+ exclude_data = build_data.pop("exclude", {})
342
+ build_config = BuildConfig(
343
+ context_dir=build_data.get("context_dir", "."),
344
+ dockerfile=build_data.get("dockerfile", "Dockerfile"),
345
+ exclude_patterns=exclude_data.get("patterns", []),
346
+ )
347
+
348
+ # Create DeployConfigParams with validated data
349
+ validated_config = DeployConfigParams(
350
+ **config_data,
351
+ scaling=scaling_params,
352
+ docker_config=docker_data,
353
+ build_config=build_config,
354
+ krisp_viva=krisp_viva_config,
355
+ )
356
+
357
+ # Check for unexpected keys
358
+ expected_keys = {
359
+ "agent_name",
360
+ "image",
361
+ "build_id",
362
+ "image_credentials",
363
+ "secret_set",
364
+ "region",
365
+ "scaling",
366
+ "enable_krisp",
367
+ "docker",
368
+ "build",
369
+ "agent_profile",
370
+ "krisp_viva",
371
+ }
372
+ unexpected_keys = set(config_data.keys()) - expected_keys
373
+ if unexpected_keys:
374
+ raise ConfigFileError(f"Unexpected keys in config file: {unexpected_keys}")
375
+
376
+ return validated_config
377
+
378
+ except Exception as e:
379
+ logger.debug(e)
380
+ raise ConfigFileError(str(e))
381
+
382
+
383
+ CONFIG_FILE_OPTION: Optional[str] = typer.Option(
384
+ None,
385
+ "--config-file",
386
+ help=f"Path to deploy config file (default: {PIPECAT_DEPLOY_CONFIG_PATH})",
387
+ )
388
+
389
+
390
+ def with_deploy_config(func: Callable) -> Callable:
391
+ """
392
+ Decorator that loads the deploy config file and injects it into the function.
393
+ If the config file exists, it will be loaded and passed to the function as `deploy_config`.
394
+
395
+ If the wrapped function receives a `config_file` kwarg (from a --config-file typer option),
396
+ it will override the default deploy config path before loading.
397
+ """
398
+
399
+ @functools.wraps(func)
400
+ def wrapper(*args, **kwargs):
401
+ config_file = kwargs.pop("config_file", None)
402
+ if config_file:
403
+ import pipecatcloud.cli.config as config_module
404
+
405
+ if not os.path.exists(config_file):
406
+ from pipecatcloud._utils.console_utils import console
407
+
408
+ console.error(f"Deploy config file not found: {config_file}")
409
+ raise typer.Exit(1)
410
+ config_module.deploy_config_path = config_file
411
+ try:
412
+ deploy_config = load_deploy_config_file()
413
+ kwargs["deploy_config"] = deploy_config
414
+ except Exception as e:
415
+ logger.error(f"Error loading deploy config: {e}")
416
+ raise ConfigFileError(str(e))
417
+ return func(*args, **kwargs)
418
+
419
+ return wrapper
@@ -336,7 +336,6 @@ class _API:
336
336
  "maxAgents": deploy_config.scaling.max_agents,
337
337
  },
338
338
  "enableKrisp": deploy_config.enable_krisp,
339
- "enableIntegratedKeysProxy": deploy_config.enable_managed_keys, # API expects this field name
340
339
  "agentProfile": deploy_config.agent_profile,
341
340
  "krispViva": {
342
341
  "audioFilter": deploy_config.krisp_viva.audio_filter,
@@ -28,7 +28,11 @@ from pipecatcloud._utils.console_utils import (
28
28
  format_duration,
29
29
  format_timestamp,
30
30
  )
31
- from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
31
+ from pipecatcloud._utils.deploy_utils import (
32
+ CONFIG_FILE_OPTION,
33
+ DeployConfigParams,
34
+ with_deploy_config,
35
+ )
32
36
  from pipecatcloud._utils.regions import get_region_codes, validate_region
33
37
  from pipecatcloud.cli import PIPECAT_CLI_NAME
34
38
  from pipecatcloud.cli.api import API
@@ -206,24 +210,6 @@ async def status(
206
210
  str(data.get("updatedAt", "N/A")),
207
211
  )
208
212
 
209
- # Check for Managed Keys status
210
- # API returns integratedKeysProxy but we display as "Managed Keys"
211
- integrated_keys = (
212
- data.get("deployment", {})
213
- .get("manifest", {})
214
- .get("spec", {})
215
- .get("integratedKeysProxy", {})
216
- )
217
- if isinstance(integrated_keys, dict):
218
- integrated_keys_enabled = integrated_keys.get("enabled", False)
219
- else:
220
- integrated_keys_enabled = bool(integrated_keys)
221
-
222
- deployment_table.add_row(
223
- "[bold]Managed Keys:[/bold]",
224
- "[green]Enabled[/green]" if integrated_keys_enabled else "[dim]Disabled[/dim]",
225
- )
226
-
227
213
  # Check for Krisp VIVA status (reverse-mapped by API)
228
214
  krisp_viva = data.get("krispViva")
229
215
  krisp_viva_status = "[dim]Disabled[/dim]"
@@ -277,7 +263,44 @@ async def status(
277
263
  border_style="red",
278
264
  )
279
265
 
280
- color = "bold green" if data["ready"] else "bold yellow"
266
+ # Build health/status panel with revision info when available
267
+ current_rev = data.get("currentRevision")
268
+ previous_rev = data.get("previousRevision")
269
+
270
+ if current_rev:
271
+ # Rich status with revision details
272
+ health_lines = []
273
+
274
+ rev_phase = current_rev.get("phase", "Unknown")
275
+ rev_id = current_rev.get("deploymentID", "")[:8]
276
+ rev_replicas = current_rev.get("readyReplicas")
277
+
278
+ if data["ready"]:
279
+ health_lines.append("[bold green]Ready[/bold green]")
280
+ else:
281
+ health_lines.append(f"[bold yellow]{rev_phase}[/bold yellow]")
282
+
283
+ current_parts = [f" Current [bold]({rev_id})[/bold] {rev_phase}"]
284
+ if rev_replicas is not None:
285
+ current_parts.append(f"[dim]·[/dim] {rev_replicas} agents")
286
+ health_lines.append(" ".join(current_parts))
287
+
288
+ if previous_rev:
289
+ prev_phase = previous_rev.get("phase", "Unknown")
290
+ prev_id = previous_rev.get("deploymentID", "")[:8]
291
+ prev_replicas = previous_rev.get("readyReplicas")
292
+ prev_parts = [f" Previous [bold]({prev_id})[/bold] {prev_phase}"]
293
+ if prev_replicas is not None:
294
+ prev_parts.append(f"[dim]·[/dim] {prev_replicas} agents")
295
+ health_lines.append(" ".join(prev_parts))
296
+
297
+ health_content = "\n".join(health_lines)
298
+ health_border = "green" if data["ready"] else "yellow"
299
+ else:
300
+ # Fallback: no revision data from API
301
+ health_content = f"[{'bold green' if data['ready'] else 'bold yellow'}]Health: {'Ready' if data['ready'] else 'Stopped'}[/]"
302
+ health_border = "green" if data["ready"] else "yellow"
303
+
281
304
  subtitle = (
282
305
  f"[dim]Start a new active session with[/dim] [bold cyan]{PIPECAT_CLI_NAME} agent start {agent_name}[/bold cyan]"
283
306
  if data["ready"]
@@ -289,8 +312,8 @@ async def status(
289
312
  deployment_table,
290
313
  scaling_panel if scaling_panel else "",
291
314
  Panel(
292
- f"[{color}]Health: {'Ready' if data['ready'] else 'Stopped'}[/]",
293
- border_style="green" if data["ready"] else "yellow",
315
+ health_content,
316
+ border_style=health_border,
294
317
  expand=False,
295
318
  ),
296
319
  error_panel if error_panel else "",
@@ -309,6 +332,7 @@ async def status(
309
332
  @with_deploy_config
310
333
  async def sessions(
311
334
  deploy_config=typer.Option(None, hidden=True),
335
+ config_file: Optional[str] = CONFIG_FILE_OPTION,
312
336
  agent_name: str = typer.Argument(
313
337
  None, help="Name of the agent to list sessions for e.g. 'my-agent'", show_default=False
314
338
  ),
@@ -744,6 +768,7 @@ async def deployments(
744
768
  @with_deploy_config
745
769
  async def start(
746
770
  deploy_config=typer.Option(None, hidden=True),
771
+ config_file: Optional[str] = CONFIG_FILE_OPTION,
747
772
  agent_name: str = typer.Argument(None, help="Name of the agent to start e.g. 'my-agent'"),
748
773
  force: bool = typer.Option(
749
774
  False,
@@ -910,6 +935,7 @@ async def start(
910
935
  @with_deploy_config
911
936
  async def stop(
912
937
  deploy_config=typer.Option(None, hidden=True),
938
+ config_file: Optional[str] = CONFIG_FILE_OPTION,
913
939
  agent_name: str = typer.Argument(None, help="Name of the agent e.g. 'my-agent'"),
914
940
  session_id: str = typer.Option(
915
941
  ...,