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.
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/CHANGELOG.md +57 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/PKG-INFO +1 -1
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/build_utils.py +14 -1
- pipecatcloud-0.4.0/src/pipecatcloud/_utils/deploy_utils.py +419 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/api.py +0 -1
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/agent.py +48 -22
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/build.py +10 -5
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/deploy.py +49 -39
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/docker.py +6 -1
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/config.py +5 -1
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/entry_point.py +10 -4
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/SOURCES.txt +3 -1
- pipecatcloud-0.4.0/tests/conftest.py +17 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_api_client.py +0 -2
- pipecatcloud-0.4.0/tests/test_config_file.py +48 -0
- pipecatcloud-0.4.0/tests/test_deployment_status.py +478 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_krisp_viva.py +1 -9
- pipecatcloud-0.3.0/src/pipecatcloud/_utils/deploy_utils.py +0 -219
- pipecatcloud-0.3.0/tests/test_managed_keys.py +0 -426
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.gitignore +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/LICENSE +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/README.md +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pyproject.toml +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/pytest.ini +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/scripts/pre-commit.sh +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/setup.cfg +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/_utils/regions.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/__init__.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_agent_stop.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-0.3.0 → pipecatcloud-0.4.0}/tests/test_regions.py +0 -0
- {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
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
border_style=
|
|
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
|
...,
|