pipecatcloud 0.2.10__tar.gz → 0.2.12__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.2.10 → pipecatcloud-0.2.12}/.github/workflows/publish-pypi.yml +2 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/CHANGELOG.md +58 -0
- {pipecatcloud-0.2.10/src/pipecatcloud.egg-info → pipecatcloud-0.2.12}/PKG-INFO +2 -2
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/pyproject.toml +1 -1
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/deploy_utils.py +3 -0
- pipecatcloud-0.2.12/src/pipecatcloud/_utils/regions.py +70 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/api.py +61 -10
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/__init__.py +1 -1
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/agent.py +34 -9
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/deploy.py +29 -1
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/docker.py +73 -9
- pipecatcloud-0.2.12/src/pipecatcloud/cli/commands/regions.py +40 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/secrets.py +75 -4
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/entry_point.py +2 -2
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/config.py +1 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/constants.py +7 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12/src/pipecatcloud.egg-info}/PKG-INFO +2 -2
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/SOURCES.txt +6 -3
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/requires.txt +1 -1
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/test_agent_sessions.py +25 -21
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/test_agent_stop.py +18 -14
- pipecatcloud-0.2.12/tests/test_api_client.py +272 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/test_docker_commands.py +66 -5
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/test_krisp_viva.py +14 -9
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/test_managed_keys.py +11 -6
- pipecatcloud-0.2.12/tests/test_regions.py +219 -0
- pipecatcloud-0.2.12/uv.lock +2212 -0
- pipecatcloud-0.2.10/src/pipecatcloud/_utils/local_runner.py +0 -35
- pipecatcloud-0.2.10/src/pipecatcloud/cli/commands/run.py +0 -36
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/.gitignore +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/LICENSE +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/README.md +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/pytest.ini +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/setup.cfg +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.2.10 → pipecatcloud-0.2.12}/tests/__init__.py +0 -0
|
@@ -5,6 +5,64 @@ 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.2.12] - 2025-11-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added regional support for secrets and services/agents. Services and secrets
|
|
13
|
+
must be in the same region. Available regions are fetched dynamically from
|
|
14
|
+
the API and can be viewed with `pcc regions list`.
|
|
15
|
+
|
|
16
|
+
- Added `pcc regions list` command to display available regions with their
|
|
17
|
+
display names (e.g., "US West (Oregon)", "Europe (Frankfurt)",
|
|
18
|
+
"Asia Pacific (Mumbai)").
|
|
19
|
+
|
|
20
|
+
- Added `--region` / `-r` option to the `secrets set` command to specify the
|
|
21
|
+
region when creating or updating secret sets.
|
|
22
|
+
|
|
23
|
+
- Added `--region` / `-r` filter option to the `secrets list` command to filter
|
|
24
|
+
secret sets by region. The Region column is now displayed in the output
|
|
25
|
+
table.
|
|
26
|
+
|
|
27
|
+
- Added `--region` / `-r` option to the `secrets image-pull-secret` command to
|
|
28
|
+
specify the region for image pull secrets.
|
|
29
|
+
|
|
30
|
+
- Added `--region` / `-r` option to the `deploy` command to specify the region
|
|
31
|
+
for service deployments. Region can also be configured via `pcc-deploy.toml`
|
|
32
|
+
by adding the `region` attribute. The region is displayed in the deployment
|
|
33
|
+
review panel.
|
|
34
|
+
|
|
35
|
+
- Added `--region` / `-r` filter option to the `agent list` command to filter
|
|
36
|
+
agents by region. The Region column is now displayed in the output table.
|
|
37
|
+
|
|
38
|
+
- Added region validation that checks user-provided region codes against the
|
|
39
|
+
live API region list, with helpful error messages showing available regions.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- When the `--region` option is not specified for create/update operations
|
|
44
|
+
(secrets, deployments), the CLI now defaults to `us-west` with a deprecation
|
|
45
|
+
warning. Users are encouraged to explicitly specify the region as this
|
|
46
|
+
default will be required in a future version.
|
|
47
|
+
|
|
48
|
+
- All error messages reference the Pipecat CLI (e.g. `pipecat cloud`) in place
|
|
49
|
+
of `pipecatcloud` or `pcc`.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Improved error handling when building a docker image on an x86 machine.
|
|
54
|
+
|
|
55
|
+
## [0.2.11] - 2025-11-12
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Added an option to retrieve logs by session ID using `--session-id` / `-s`
|
|
60
|
+
with the `agent logs` command.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- Bumped the `fastapi` dependency's upperbound to `<1.0.0`.
|
|
65
|
+
|
|
8
66
|
## [0.2.10] - 2025-10-25
|
|
9
67
|
|
|
10
68
|
### Removed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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/
|
|
@@ -9,7 +9,7 @@ Requires-Python: >=3.10
|
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: aiohttp<4.0.0,>=3.11.12
|
|
12
|
-
Requires-Dist: fastapi<0.
|
|
12
|
+
Requires-Dist: fastapi<1.0.0,>=0.115.6
|
|
13
13
|
Requires-Dist: loguru~=0.7.3
|
|
14
14
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.1
|
|
15
15
|
Requires-Dist: uvicorn<1.0.0,>=0.32.0
|
|
@@ -12,7 +12,7 @@ readme = "README.md"
|
|
|
12
12
|
keywords = ["pipecat", "pipecat-ai", "voice", "ai", "llm"]
|
|
13
13
|
dependencies = [
|
|
14
14
|
"aiohttp>=3.11.12,<4.0.0",
|
|
15
|
-
"fastapi>=0.115.6,<0.
|
|
15
|
+
"fastapi>=0.115.6,<1.0.0",
|
|
16
16
|
"loguru~=0.7.3",
|
|
17
17
|
"python-dotenv>=1.0.1,<2.0.0",
|
|
18
18
|
"uvicorn>=0.32.0,<1.0.0",
|
|
@@ -80,6 +80,7 @@ class DeployConfigParams:
|
|
|
80
80
|
image: Optional[str] = None
|
|
81
81
|
image_credentials: Optional[str] = None
|
|
82
82
|
secret_set: Optional[str] = None
|
|
83
|
+
region: Optional[str] = None
|
|
83
84
|
scaling: ScalingParams = ScalingParams()
|
|
84
85
|
enable_krisp: bool = False
|
|
85
86
|
enable_managed_keys: bool = False
|
|
@@ -97,6 +98,7 @@ class DeployConfigParams:
|
|
|
97
98
|
"image": self.image,
|
|
98
99
|
"image_credentials": self.image_credentials,
|
|
99
100
|
"secret_set": self.secret_set,
|
|
101
|
+
"region": self.region,
|
|
100
102
|
"scaling": self.scaling.to_dict() if self.scaling else None,
|
|
101
103
|
"enable_krisp": self.enable_krisp,
|
|
102
104
|
"enable_managed_keys": self.enable_managed_keys,
|
|
@@ -144,6 +146,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
144
146
|
"image",
|
|
145
147
|
"image_credentials",
|
|
146
148
|
"secret_set",
|
|
149
|
+
"region",
|
|
147
150
|
"scaling",
|
|
148
151
|
"enable_krisp",
|
|
149
152
|
"enable_managed_keys",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Region utilities for Pipecat Cloud CLI.
|
|
9
|
+
|
|
10
|
+
Provides functions to fetch and cache available regions from the API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from pipecatcloud.cli.api import API
|
|
16
|
+
from pipecatcloud.cli.config import config
|
|
17
|
+
|
|
18
|
+
# Module-level cache for regions
|
|
19
|
+
_regions_cache: Optional[List[Dict[str, str]]] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get_regions() -> List[Dict[str, str]]:
|
|
23
|
+
"""
|
|
24
|
+
Fetch available regions from the API with caching.
|
|
25
|
+
|
|
26
|
+
This function is cached so the API is only called once per CLI session.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of region objects with 'code' and 'display_name' fields
|
|
30
|
+
Example: [
|
|
31
|
+
{"code": "us-west", "display_name": "US West"},
|
|
32
|
+
{"code": "eu-central", "display_name": "EU Central"}
|
|
33
|
+
]
|
|
34
|
+
"""
|
|
35
|
+
global _regions_cache
|
|
36
|
+
|
|
37
|
+
if _regions_cache is None:
|
|
38
|
+
org = config.get("org")
|
|
39
|
+
# API returns (data, error) tuple
|
|
40
|
+
data, error = await API.regions(org=org)
|
|
41
|
+
if error or not data:
|
|
42
|
+
return []
|
|
43
|
+
_regions_cache = data
|
|
44
|
+
|
|
45
|
+
return _regions_cache
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def get_region_codes() -> List[str]:
|
|
49
|
+
"""
|
|
50
|
+
Get list of region codes only.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of region codes (e.g., ['us-west', 'eu-central', 'ap-south'])
|
|
54
|
+
"""
|
|
55
|
+
regions = await get_regions()
|
|
56
|
+
return [r["code"] for r in regions]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def validate_region(region: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Validate that a region code is supported.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
region: Region code to validate
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if region is valid, False otherwise
|
|
68
|
+
"""
|
|
69
|
+
valid_codes = await get_region_codes()
|
|
70
|
+
return region in valid_codes
|
|
@@ -86,8 +86,8 @@ class _API:
|
|
|
86
86
|
# Fallback structure matching API format
|
|
87
87
|
self.error = {
|
|
88
88
|
"error": response.reason or "Bad Request",
|
|
89
|
-
"code": str(
|
|
90
|
-
|
|
89
|
+
"code": str(response.status),
|
|
90
|
+
}
|
|
91
91
|
response.raise_for_status()
|
|
92
92
|
|
|
93
93
|
return await response.json()
|
|
@@ -236,13 +236,16 @@ class _API:
|
|
|
236
236
|
|
|
237
237
|
# Secret
|
|
238
238
|
|
|
239
|
-
async def _secrets_list(self, org: str, secret_set: Optional[str] = None) -> dict | None:
|
|
239
|
+
async def _secrets_list(self, org: str, secret_set: Optional[str] = None, region: Optional[str] = None) -> dict | None:
|
|
240
240
|
if secret_set:
|
|
241
241
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}/{secret_set}"
|
|
242
242
|
else:
|
|
243
243
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}"
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
# Build query params if region filter is specified
|
|
246
|
+
params = {"region": region} if region else None
|
|
247
|
+
|
|
248
|
+
result = await self._base_request("GET", url, params=params, not_found_is_empty=True) or {}
|
|
246
249
|
|
|
247
250
|
if "sets" in result:
|
|
248
251
|
return result["sets"]
|
|
@@ -258,12 +261,17 @@ class _API:
|
|
|
258
261
|
Args:
|
|
259
262
|
org: Organization ID,
|
|
260
263
|
secret_set: (optional) name of secret set to lookup
|
|
264
|
+
region: (optional) filter by region
|
|
261
265
|
|
|
262
266
|
"""
|
|
263
267
|
return self.create_api_method(self._secrets_list)
|
|
264
268
|
|
|
265
|
-
async def _secrets_upsert(self, data: dict, set_name: str, org: str) -> dict:
|
|
269
|
+
async def _secrets_upsert(self, data: dict, set_name: str, org: str, region: str) -> dict:
|
|
266
270
|
url = f"{self.construct_api_url('secrets_path').format(org=org)}/{set_name}"
|
|
271
|
+
|
|
272
|
+
# Add region to data payload
|
|
273
|
+
data = {**data, "region": region}
|
|
274
|
+
|
|
267
275
|
return await self._base_request("PUT", url, json=data) or {}
|
|
268
276
|
|
|
269
277
|
@property
|
|
@@ -273,6 +281,7 @@ class _API:
|
|
|
273
281
|
data: key and value of secret to add (or credentials for image pull secrets)
|
|
274
282
|
set_name: name of set to create or update
|
|
275
283
|
org: Organization ID
|
|
284
|
+
region: region for secret set
|
|
276
285
|
"""
|
|
277
286
|
return self.create_api_method(self._secrets_upsert)
|
|
278
287
|
|
|
@@ -316,6 +325,7 @@ class _API:
|
|
|
316
325
|
"image": deploy_config.image,
|
|
317
326
|
"imagePullSecretSet": deploy_config.image_credentials,
|
|
318
327
|
"secretSet": deploy_config.secret_set,
|
|
328
|
+
"region": deploy_config.region,
|
|
319
329
|
"autoScaling": {
|
|
320
330
|
"minAgents": deploy_config.scaling.min_agents,
|
|
321
331
|
"maxAgents": deploy_config.scaling.max_agents,
|
|
@@ -373,9 +383,13 @@ class _API:
|
|
|
373
383
|
"""
|
|
374
384
|
return self.create_api_method(self._agent)
|
|
375
385
|
|
|
376
|
-
async def _agents(self, org: str) -> List[dict] | None:
|
|
386
|
+
async def _agents(self, org: str, region: Optional[str] = None) -> List[dict] | None:
|
|
377
387
|
url = f"{self.construct_api_url('services_path').format(org=org)}"
|
|
378
|
-
|
|
388
|
+
|
|
389
|
+
# Build query params if region filter is specified
|
|
390
|
+
params = {"region": region} if region else None
|
|
391
|
+
|
|
392
|
+
result = await self._base_request("GET", url, params=params) or {}
|
|
379
393
|
|
|
380
394
|
if "services" in result:
|
|
381
395
|
return result["services"]
|
|
@@ -384,6 +398,11 @@ class _API:
|
|
|
384
398
|
|
|
385
399
|
@property
|
|
386
400
|
def agents(self):
|
|
401
|
+
"""List agents/services
|
|
402
|
+
Args:
|
|
403
|
+
org: Organization ID
|
|
404
|
+
region: (optional) filter by region
|
|
405
|
+
"""
|
|
387
406
|
return self.create_api_method(self._agents)
|
|
388
407
|
|
|
389
408
|
async def _start_agent(
|
|
@@ -422,12 +441,20 @@ class _API:
|
|
|
422
441
|
def agent_delete(self):
|
|
423
442
|
return self.create_api_method(self._agent_delete)
|
|
424
443
|
|
|
425
|
-
async def _agent_logs(
|
|
426
|
-
|
|
444
|
+
async def _agent_logs(
|
|
445
|
+
self,
|
|
446
|
+
agent_name: str,
|
|
447
|
+
org: str,
|
|
448
|
+
limit: int = 100,
|
|
449
|
+
deployment_id: Optional[str] = None,
|
|
450
|
+
session_id: Optional[str] = None,
|
|
451
|
+
) -> dict | None:
|
|
427
452
|
url = f"{self.construct_api_url('services_logs_path').format(org=org, service=agent_name)}"
|
|
428
453
|
params: dict[str, Union[int, str]] = {"limit": limit}
|
|
429
454
|
if deployment_id:
|
|
430
455
|
params["deploymentId"] = deployment_id
|
|
456
|
+
if session_id:
|
|
457
|
+
params["sessionId"] = session_id
|
|
431
458
|
return await self._base_request("GET", url, params=params)
|
|
432
459
|
|
|
433
460
|
@property
|
|
@@ -442,7 +469,9 @@ class _API:
|
|
|
442
469
|
def agent_sessions(self):
|
|
443
470
|
return self.create_api_method(self._agent_sessions)
|
|
444
471
|
|
|
445
|
-
async def _agent_session_terminate(
|
|
472
|
+
async def _agent_session_terminate(
|
|
473
|
+
self, agent_name: str, session_id: str, org: str
|
|
474
|
+
) -> dict | None:
|
|
446
475
|
url = f"{self.construct_api_url('services_sessions_path').format(org=org, service=agent_name)}/{session_id}"
|
|
447
476
|
return await self._base_request("DELETE", url, not_found_is_empty=True)
|
|
448
477
|
|
|
@@ -455,3 +484,25 @@ class _API:
|
|
|
455
484
|
org: Organization ID
|
|
456
485
|
"""
|
|
457
486
|
return self.create_api_method(self._agent_session_terminate)
|
|
487
|
+
|
|
488
|
+
# Regions
|
|
489
|
+
|
|
490
|
+
async def _regions(self, org: str) -> list[dict] | None:
|
|
491
|
+
url = self.construct_api_url("regions_path").format(org=org)
|
|
492
|
+
result = await self._base_request("GET", url, not_found_is_empty=True)
|
|
493
|
+
|
|
494
|
+
if result and "regions" in result:
|
|
495
|
+
# Return full region objects with code and display_name
|
|
496
|
+
return result["regions"]
|
|
497
|
+
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def regions(self):
|
|
502
|
+
"""List available regions
|
|
503
|
+
Args:
|
|
504
|
+
org: Organization ID
|
|
505
|
+
Returns:
|
|
506
|
+
List of region objects with 'code' and 'display_name' fields
|
|
507
|
+
"""
|
|
508
|
+
return self.create_api_method(self._regions)
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
9
10
|
|
|
10
11
|
import aiohttp
|
|
11
12
|
import questionary
|
|
@@ -28,9 +29,11 @@ from pipecatcloud._utils.console_utils import (
|
|
|
28
29
|
format_timestamp,
|
|
29
30
|
)
|
|
30
31
|
from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
|
|
32
|
+
from pipecatcloud._utils.regions import get_region_codes, validate_region
|
|
31
33
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
32
34
|
from pipecatcloud.cli.api import API
|
|
33
35
|
from pipecatcloud.cli.config import config
|
|
36
|
+
from pipecatcloud.constants import Region
|
|
34
37
|
|
|
35
38
|
agent_cli = typer.Typer(name="agent", help="Agent management", no_args_is_help=True)
|
|
36
39
|
|
|
@@ -45,13 +48,27 @@ async def list(
|
|
|
45
48
|
organization: str = typer.Option(
|
|
46
49
|
None, "--organization", "-o", help="Organization to list agents for"
|
|
47
50
|
),
|
|
51
|
+
region: Optional[Region] = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--region",
|
|
54
|
+
"-r",
|
|
55
|
+
help="Filter by region",
|
|
56
|
+
),
|
|
48
57
|
):
|
|
49
58
|
org = organization or config.get("org")
|
|
50
59
|
|
|
60
|
+
# Validate region if provided
|
|
61
|
+
if region and not await validate_region(region):
|
|
62
|
+
valid_regions = await get_region_codes()
|
|
63
|
+
console.print(
|
|
64
|
+
f"[red]Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
|
|
65
|
+
)
|
|
66
|
+
return typer.Exit(1)
|
|
67
|
+
|
|
51
68
|
with console.status(
|
|
52
69
|
f"[dim]Fetching agents for organization: [bold]'{org}'[/bold][/dim]", spinner="dots"
|
|
53
70
|
):
|
|
54
|
-
data, error = await API.agents(org=org)
|
|
71
|
+
data, error = await API.agents(org=org, region=region)
|
|
55
72
|
|
|
56
73
|
if error:
|
|
57
74
|
return typer.Exit()
|
|
@@ -66,6 +83,7 @@ async def list(
|
|
|
66
83
|
else:
|
|
67
84
|
table = Table(show_header=True, show_lines=True, border_style="dim", box=box.SIMPLE)
|
|
68
85
|
table.add_column("Name")
|
|
86
|
+
table.add_column("Region")
|
|
69
87
|
table.add_column("Agent ID")
|
|
70
88
|
table.add_column("Active Deployment ID")
|
|
71
89
|
table.add_column("Created At")
|
|
@@ -74,6 +92,7 @@ async def list(
|
|
|
74
92
|
for service in data:
|
|
75
93
|
table.add_row(
|
|
76
94
|
f"[bold]{service['name']}[/bold]",
|
|
95
|
+
service["region"],
|
|
77
96
|
service["id"],
|
|
78
97
|
service["activeDeploymentId"],
|
|
79
98
|
service["createdAt"],
|
|
@@ -440,17 +459,26 @@ async def logs(
|
|
|
440
459
|
deployment_id: str = typer.Option(
|
|
441
460
|
None, "--deployment", "-d", help="Filter logs by deployment ID"
|
|
442
461
|
),
|
|
462
|
+
session_id: str = typer.Option(None, "--session-id", "-s", help="Filter logs by session ID"),
|
|
443
463
|
):
|
|
444
464
|
org = organization or config.get("org")
|
|
445
465
|
|
|
446
|
-
status_text = "agent"
|
|
466
|
+
status_text = "agent"
|
|
467
|
+
if deployment_id:
|
|
468
|
+
status_text = f"deployment ({deployment_id})"
|
|
469
|
+
if session_id:
|
|
470
|
+
status_text = f"session ({session_id})"
|
|
447
471
|
|
|
448
472
|
with console.status(
|
|
449
473
|
f"[dim]Fetching logs for {status_text}: [bold]'{agent_name}'[/bold] with severity: [bold cyan]{level.value if level else 'ALL'}[/bold cyan][/dim]",
|
|
450
474
|
spinner="dots",
|
|
451
475
|
):
|
|
452
476
|
data, error = await API.agent_logs(
|
|
453
|
-
agent_name=agent_name,
|
|
477
|
+
agent_name=agent_name,
|
|
478
|
+
org=org,
|
|
479
|
+
limit=limit,
|
|
480
|
+
deployment_id=deployment_id,
|
|
481
|
+
session_id=session_id,
|
|
454
482
|
)
|
|
455
483
|
|
|
456
484
|
if not data or not data.get("logs"):
|
|
@@ -786,16 +814,13 @@ async def stop(
|
|
|
786
814
|
if not force:
|
|
787
815
|
console.print(
|
|
788
816
|
Panel(
|
|
789
|
-
f"Agent Name: {agent_name}\
|
|
790
|
-
|
|
791
|
-
title=f"[bold]Stop Session[/bold]",
|
|
817
|
+
f"Agent Name: {agent_name}\nSession ID: {session_id}",
|
|
818
|
+
title="[bold]Stop Session[/bold]",
|
|
792
819
|
title_align="left",
|
|
793
820
|
border_style="yellow",
|
|
794
821
|
)
|
|
795
822
|
)
|
|
796
|
-
if not await questionary.confirm(
|
|
797
|
-
"Are you sure you want to stop this session?"
|
|
798
|
-
).ask_async():
|
|
823
|
+
if not await questionary.confirm("Are you sure you want to stop this session?").ask_async():
|
|
799
824
|
console.print("[bold]Aborting stop request[/bold]")
|
|
800
825
|
return typer.Exit(1)
|
|
801
826
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
from typing import Optional
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
from loguru import logger
|
|
@@ -23,7 +24,8 @@ from pipecatcloud._utils.deploy_utils import (
|
|
|
23
24
|
ScalingParams,
|
|
24
25
|
with_deploy_config,
|
|
25
26
|
)
|
|
26
|
-
from pipecatcloud.
|
|
27
|
+
from pipecatcloud._utils.regions import get_region_codes, validate_region
|
|
28
|
+
from pipecatcloud.constants import KRISP_VIVA_MODELS, Region
|
|
27
29
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
28
30
|
from pipecatcloud.cli.api import API
|
|
29
31
|
from pipecatcloud.cli.config import config
|
|
@@ -300,6 +302,13 @@ def create_deploy_command(app: typer.Typer):
|
|
|
300
302
|
help="Agent profile to use for deployment",
|
|
301
303
|
rich_help_panel="Deployment Configuration",
|
|
302
304
|
),
|
|
305
|
+
region: Optional[Region] = typer.Option(
|
|
306
|
+
None,
|
|
307
|
+
"--region",
|
|
308
|
+
"-r",
|
|
309
|
+
help="Region for service deployment",
|
|
310
|
+
rich_help_panel="Deployment Configuration",
|
|
311
|
+
),
|
|
303
312
|
skip_confirm: bool = typer.Option(
|
|
304
313
|
False,
|
|
305
314
|
"--force",
|
|
@@ -372,6 +381,24 @@ def create_deploy_command(app: typer.Typer):
|
|
|
372
381
|
partial_config.enable_managed_keys = managed_keys or partial_config.enable_managed_keys
|
|
373
382
|
partial_config.agent_profile = profile or partial_config.agent_profile
|
|
374
383
|
|
|
384
|
+
# Handle region with warning if not specified
|
|
385
|
+
deploy_region = region or partial_config.region
|
|
386
|
+
if not deploy_region:
|
|
387
|
+
logger.warning(
|
|
388
|
+
"Region not specified, defaulting to 'us-west'. Please use --region to specify explicitly. "
|
|
389
|
+
"This default will be required in a future version."
|
|
390
|
+
)
|
|
391
|
+
deploy_region = "us-west"
|
|
392
|
+
partial_config.region = deploy_region
|
|
393
|
+
|
|
394
|
+
# Validate region against API
|
|
395
|
+
if not await validate_region(deploy_region):
|
|
396
|
+
valid_regions = await get_region_codes()
|
|
397
|
+
console.error(
|
|
398
|
+
f"Invalid region '{deploy_region}'. Valid regions are: {', '.join(valid_regions)}"
|
|
399
|
+
)
|
|
400
|
+
return typer.Exit(1)
|
|
401
|
+
|
|
375
402
|
# Handle Krisp VIVA configuration
|
|
376
403
|
if krisp_viva_audio_filter is not None:
|
|
377
404
|
partial_config.krisp_viva = KrispVivaConfig(audio_filter=krisp_viva_audio_filter)
|
|
@@ -409,6 +436,7 @@ def create_deploy_command(app: typer.Typer):
|
|
|
409
436
|
(f"[bold white]Agent name:[/bold white] [green]{partial_config.agent_name}[/green]"),
|
|
410
437
|
(f"[bold white]Image:[/bold white] [green]{partial_config.image}[/green]"),
|
|
411
438
|
(f"[bold white]Organization:[/bold white] [green]{org}[/green]"),
|
|
439
|
+
(f"[bold white]Region:[/bold white] [green]{partial_config.region}[/green]"),
|
|
412
440
|
(f"[bold white]Secret set:[/bold white] {'[dim]None[/dim]' if not partial_config.secret_set else '[green] '+ partial_config.secret_set + '[/green]'}"),
|
|
413
441
|
(f"[bold white]Image pull secret:[/bold white] {'[dim]None[/dim]' if not partial_config.image_credentials else '[green]' + partial_config.image_credentials + '[/green]'}"),
|
|
414
442
|
(f"[bold white]Agent profile:[/bold white] {'[dim]None[/dim]' if not partial_config.agent_profile else '[green]' + partial_config.agent_profile + '[/green]'}"),
|
|
@@ -14,6 +14,7 @@ from rich.panel import Panel
|
|
|
14
14
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
15
15
|
from pipecatcloud._utils.console_utils import console
|
|
16
16
|
from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
|
|
17
|
+
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
17
18
|
|
|
18
19
|
docker_cli = typer.Typer(
|
|
19
20
|
name="docker", help="Docker build and push utilities", no_args_is_help=True
|
|
@@ -25,9 +26,61 @@ class RegistryType(str, Enum):
|
|
|
25
26
|
CUSTOM = "custom"
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
+
def _suggest_and_install_binfmt():
|
|
30
|
+
"""Suggest and optionally install binfmt_misc support for cross-platform builds."""
|
|
31
|
+
console.print("\n[yellow]⚠️ Cross-platform build support missing[/yellow]")
|
|
32
|
+
console.print(
|
|
33
|
+
"[yellow] Building ARM64 images on x86_64 requires QEMU and binfmt_misc support.[/yellow]"
|
|
34
|
+
)
|
|
35
|
+
console.print(
|
|
36
|
+
"\n[yellow] This is a one-time setup that persists across Docker restarts.[/yellow]"
|
|
37
|
+
)
|
|
38
|
+
console.print(
|
|
39
|
+
"\n[dim] The following command will be run (requires Docker --privileged flag):[/dim]"
|
|
40
|
+
)
|
|
41
|
+
console.print("[dim] docker run --privileged --rm tonistiigi/binfmt --install all[/dim]")
|
|
42
|
+
|
|
43
|
+
if typer.confirm("\nWould you like to install binfmt support now?", default=True):
|
|
44
|
+
console.print("\n[cyan]Installing binfmt support...[/cyan]")
|
|
45
|
+
install_cmd = [
|
|
46
|
+
"docker",
|
|
47
|
+
"run",
|
|
48
|
+
"--privileged",
|
|
49
|
+
"--rm",
|
|
50
|
+
"tonistiigi/binfmt",
|
|
51
|
+
"--install",
|
|
52
|
+
"all",
|
|
53
|
+
]
|
|
54
|
+
if run_docker_command(
|
|
55
|
+
install_cmd, "Setting up cross-platform build support", stream_output=False
|
|
56
|
+
):
|
|
57
|
+
console.success("✓ binfmt support installed! You can now retry your build.")
|
|
58
|
+
else:
|
|
59
|
+
console.error("Failed to install binfmt support.")
|
|
60
|
+
console.print("\n[yellow]You can install it manually by running:[/yellow]")
|
|
61
|
+
console.print(
|
|
62
|
+
"[yellow] docker run --privileged --rm tonistiigi/binfmt --install all[/yellow]"
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
console.print("\n[yellow]Skipping installation. To install manually, run:[/yellow]")
|
|
66
|
+
console.print(
|
|
67
|
+
"[yellow] docker run --privileged --rm tonistiigi/binfmt --install all[/yellow]"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _provide_error_hints(stderr: str, stdout: str, command: list[str], registry_info: dict = None):
|
|
29
72
|
"""Provide helpful error hints only when Docker's message isn't clear enough."""
|
|
30
73
|
stderr_lower = stderr.lower()
|
|
74
|
+
stdout_lower = stdout.lower()
|
|
75
|
+
combined_output = f"{stderr_lower} {stdout_lower}"
|
|
76
|
+
|
|
77
|
+
# Check for exec format error (cross-platform build issue)
|
|
78
|
+
if (
|
|
79
|
+
"exec format error" in combined_output
|
|
80
|
+
or "exec /bin/sh: exec format error" in combined_output
|
|
81
|
+
):
|
|
82
|
+
_suggest_and_install_binfmt()
|
|
83
|
+
return
|
|
31
84
|
|
|
32
85
|
# Only add hints for authentication errors where the solution isn't obvious
|
|
33
86
|
if _is_auth_error(stderr_lower) and "push" in " ".join(command):
|
|
@@ -59,6 +112,8 @@ def run_docker_command(
|
|
|
59
112
|
|
|
60
113
|
if stream_output:
|
|
61
114
|
# Stream output in real-time for build commands
|
|
115
|
+
# Collect output for error analysis
|
|
116
|
+
output_lines = []
|
|
62
117
|
process = subprocess.Popen(
|
|
63
118
|
command,
|
|
64
119
|
stdout=subprocess.PIPE,
|
|
@@ -70,11 +125,17 @@ def run_docker_command(
|
|
|
70
125
|
|
|
71
126
|
# Stream output line by line
|
|
72
127
|
for line in process.stdout:
|
|
128
|
+
output_lines.append(line)
|
|
73
129
|
console.print(f"[dim]{line.rstrip()}[/dim]")
|
|
74
130
|
|
|
75
131
|
process.wait()
|
|
76
132
|
if process.returncode != 0:
|
|
77
|
-
|
|
133
|
+
# Create a pseudo-exception with captured output
|
|
134
|
+
error = subprocess.CalledProcessError(process.returncode, command)
|
|
135
|
+
error.output = "".join(output_lines)
|
|
136
|
+
error.stdout = error.output
|
|
137
|
+
error.stderr = ""
|
|
138
|
+
raise error
|
|
78
139
|
else:
|
|
79
140
|
# Capture output for push commands (less verbose)
|
|
80
141
|
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
|
@@ -84,13 +145,16 @@ def run_docker_command(
|
|
|
84
145
|
return True
|
|
85
146
|
except subprocess.CalledProcessError as e:
|
|
86
147
|
console.error(f"Docker command failed: {' '.join(command)}")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
148
|
+
stdout = getattr(e, "stdout", "") or getattr(e, "output", "") or ""
|
|
149
|
+
stderr = getattr(e, "stderr", "") or ""
|
|
150
|
+
|
|
151
|
+
if stdout:
|
|
152
|
+
console.print(f"[red]stdout: {stdout}[/red]")
|
|
153
|
+
if stderr:
|
|
154
|
+
console.print(f"[red]stderr: {stderr}[/red]")
|
|
91
155
|
|
|
92
|
-
|
|
93
|
-
|
|
156
|
+
# Provide helpful hints for common errors
|
|
157
|
+
_provide_error_hints(stderr, stdout, command, registry_info)
|
|
94
158
|
return False
|
|
95
159
|
except FileNotFoundError:
|
|
96
160
|
console.error("Docker not found. Please ensure Docker is installed and in your PATH.")
|
|
@@ -336,7 +400,7 @@ async def build_push(
|
|
|
336
400
|
console.print("\n[dim]To deploy this image, update your pcc-deploy.toml:[/dim]")
|
|
337
401
|
console.print(f' [bold]image = "{version_tag}"[/bold]')
|
|
338
402
|
console.print("\n[dim]Then run:[/dim]")
|
|
339
|
-
console.print(" [bold]
|
|
403
|
+
console.print(f" [bold]{PIPECAT_CLI_NAME} deploy[/bold]")
|
|
340
404
|
|
|
341
405
|
|
|
342
406
|
def create_docker_command(app: typer.Typer):
|