pipecatcloud 0.2.5__tar.gz → 0.2.7__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.5 → pipecatcloud-0.2.7}/CHANGELOG.md +30 -0
- {pipecatcloud-0.2.5/src/pipecatcloud.egg-info → pipecatcloud-0.2.7}/PKG-INFO +1 -1
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/deploy_utils.py +3 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/agent.py +9 -7
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/api.py +15 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/agent.py +143 -61
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/deploy.py +9 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/init.py +18 -1
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
- pipecatcloud-0.2.7/tests/test_agent_stop.py +99 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/test_managed_keys.py +99 -116
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.gitignore +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/LICENSE +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/README.md +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pyproject.toml +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pytest.ini +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/setup.cfg +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/console_utils.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/local_runner.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/docker.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/run.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/config.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/__init__.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/test_agent_sessions.py +0 -0
- {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/test_docker_commands.py +0 -0
|
@@ -5,6 +5,36 @@ 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.7] - 2025-10-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added a `stop` command to `pcc agent` that allows stopping an ongoing Pipecat
|
|
13
|
+
Cloud session.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Fallback `RunnerArguments` now include a `body` field in the base class.
|
|
18
|
+
|
|
19
|
+
### Deprecated
|
|
20
|
+
|
|
21
|
+
- The `pcc init` command is now deprecated and will be removed in a future
|
|
22
|
+
version. Use the [Pipecat CLI](https://github.com/pipecat-ai/pipecat-cli)
|
|
23
|
+
instead.
|
|
24
|
+
|
|
25
|
+
## [0.2.6] - 2025-10-09
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added an optional `--profile` option to the `deploy` command which selects between
|
|
30
|
+
`agent-1x`, with 0.5 vCPUs / 1 GB RAM, and `agent-2x`, with 1 vCPU / 2 GB RAM. The
|
|
31
|
+
default profile is `agent-1x`.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Fixed an issue where the `agent status` CLI command would return incorrect
|
|
36
|
+
values for Min and Max Agents.
|
|
37
|
+
|
|
8
38
|
## [0.2.5] - 2025-10-02
|
|
9
39
|
|
|
10
40
|
### Added
|
|
@@ -66,6 +66,7 @@ class DeployConfigParams:
|
|
|
66
66
|
enable_krisp: bool = False
|
|
67
67
|
enable_managed_keys: bool = False
|
|
68
68
|
docker_config: dict = field(factory=dict)
|
|
69
|
+
agent_profile: Optional[str] = None
|
|
69
70
|
|
|
70
71
|
def __attrs_post_init__(self):
|
|
71
72
|
if self.image is not None and ":" not in self.image:
|
|
@@ -81,6 +82,7 @@ class DeployConfigParams:
|
|
|
81
82
|
"enable_krisp": self.enable_krisp,
|
|
82
83
|
"enable_managed_keys": self.enable_managed_keys,
|
|
83
84
|
"docker_config": self.docker_config,
|
|
85
|
+
"agent_profile": self.agent_profile,
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
|
|
@@ -121,6 +123,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
121
123
|
"enable_krisp",
|
|
122
124
|
"enable_managed_keys",
|
|
123
125
|
"docker",
|
|
126
|
+
"agent_profile",
|
|
124
127
|
}
|
|
125
128
|
unexpected_keys = set(config_data.keys()) - expected_keys
|
|
126
129
|
if unexpected_keys:
|
|
@@ -15,8 +15,8 @@ try:
|
|
|
15
15
|
from pipecat.runner.types import (
|
|
16
16
|
DailyRunnerArguments,
|
|
17
17
|
RunnerArguments,
|
|
18
|
-
WebSocketRunnerArguments,
|
|
19
18
|
SmallWebRTCRunnerArguments,
|
|
19
|
+
WebSocketRunnerArguments,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
_PIPECAT_RUNNER_TYPES_AVAILABLE = True
|
|
@@ -33,9 +33,10 @@ except ImportError:
|
|
|
33
33
|
will be removed in a future release.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
handle_sigint: bool = field(init=False)
|
|
37
|
-
handle_sigterm: bool = field(init=False)
|
|
38
|
-
pipeline_idle_timeout_secs: int = field(init=False)
|
|
36
|
+
handle_sigint: bool = field(init=False, kw_only=True)
|
|
37
|
+
handle_sigterm: bool = field(init=False, kw_only=True)
|
|
38
|
+
pipeline_idle_timeout_secs: int = field(init=False, kw_only=True)
|
|
39
|
+
body: Optional[Any] = field(default_factory=dict, kw_only=True)
|
|
39
40
|
|
|
40
41
|
def __post_init__(self):
|
|
41
42
|
self.handle_sigint = False
|
|
@@ -52,7 +53,6 @@ except ImportError:
|
|
|
52
53
|
|
|
53
54
|
room_url: str
|
|
54
55
|
token: str
|
|
55
|
-
body: Any
|
|
56
56
|
|
|
57
57
|
@dataclass
|
|
58
58
|
class WebSocketRunnerArguments(RunnerArguments):
|
|
@@ -63,7 +63,6 @@ except ImportError:
|
|
|
63
63
|
"""
|
|
64
64
|
|
|
65
65
|
websocket: WebSocket
|
|
66
|
-
body: Any
|
|
67
66
|
|
|
68
67
|
@dataclass
|
|
69
68
|
class SmallWebRTCRunnerArguments(RunnerArguments):
|
|
@@ -112,7 +111,9 @@ class PipecatSessionArguments(RunnerArguments, SessionArguments):
|
|
|
112
111
|
For best compatibility, install: pip install pipecatcloud[pipecat]
|
|
113
112
|
"""
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
def __post_init__(self):
|
|
115
|
+
RunnerArguments.__post_init__(self)
|
|
116
|
+
_warn_standalone_usage()
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
@dataclass
|
|
@@ -144,6 +145,7 @@ class WebSocketSessionArguments(WebSocketRunnerArguments, SessionArguments):
|
|
|
144
145
|
WebSocketRunnerArguments.__post_init__(self)
|
|
145
146
|
_warn_standalone_usage()
|
|
146
147
|
|
|
148
|
+
|
|
147
149
|
@dataclass
|
|
148
150
|
class SmallWebRTCSessionArguments(SmallWebRTCRunnerArguments, SessionArguments):
|
|
149
151
|
"""SmallWebRTCTransport based agent session arguments.
|
|
@@ -322,6 +322,7 @@ class _API:
|
|
|
322
322
|
},
|
|
323
323
|
"enableKrisp": deploy_config.enable_krisp,
|
|
324
324
|
"enableIntegratedKeysProxy": deploy_config.enable_managed_keys, # API expects this field name
|
|
325
|
+
"agentProfile": deploy_config.agent_profile,
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
# Remove None values recursively
|
|
@@ -437,3 +438,17 @@ class _API:
|
|
|
437
438
|
@property
|
|
438
439
|
def agent_sessions(self):
|
|
439
440
|
return self.create_api_method(self._agent_sessions)
|
|
441
|
+
|
|
442
|
+
async def _agent_session_terminate(self, agent_name: str, session_id: str, org: str) -> dict | None:
|
|
443
|
+
url = f"{self.construct_api_url('services_sessions_path').format(org=org, service=agent_name)}/{session_id}"
|
|
444
|
+
return await self._base_request("DELETE", url, not_found_is_empty=True)
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def agent_session_terminate(self):
|
|
448
|
+
"""Terminate an active session for an agent.
|
|
449
|
+
Args:
|
|
450
|
+
agent_name: Name of the agent
|
|
451
|
+
session_id: ID of the session to terminate
|
|
452
|
+
org: Organization ID
|
|
453
|
+
"""
|
|
454
|
+
return self.create_api_method(self._agent_session_terminate)
|
|
@@ -21,11 +21,16 @@ from rich.text import Text
|
|
|
21
21
|
|
|
22
22
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
23
23
|
from pipecatcloud._utils.auth_utils import requires_login
|
|
24
|
-
from pipecatcloud._utils.console_utils import
|
|
24
|
+
from pipecatcloud._utils.console_utils import (
|
|
25
|
+
calculate_percentiles,
|
|
26
|
+
console,
|
|
27
|
+
format_duration,
|
|
28
|
+
format_timestamp,
|
|
29
|
+
)
|
|
30
|
+
from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
|
|
25
31
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
26
32
|
from pipecatcloud.cli.api import API
|
|
27
33
|
from pipecatcloud.cli.config import config
|
|
28
|
-
from pipecatcloud._utils.deploy_utils import with_deploy_config, DeployConfigParams
|
|
29
34
|
|
|
30
35
|
agent_cli = typer.Typer(name="agent", help="Agent management", no_args_is_help=True)
|
|
31
36
|
|
|
@@ -54,7 +59,8 @@ async def list(
|
|
|
54
59
|
if not data or len(data) == 0:
|
|
55
60
|
console.error(
|
|
56
61
|
f"[red]No agents found for namespace / organization '{org}'[/red]\n\n"
|
|
57
|
-
f"[dim]Please deploy an agent first using[/dim] [bold cyan]{PIPECAT_CLI_NAME} deploy[/bold cyan]"
|
|
62
|
+
f"[dim]Please deploy an agent first using[/dim] [bold cyan]{PIPECAT_CLI_NAME} deploy[/bold cyan]"
|
|
63
|
+
)
|
|
58
64
|
return typer.Exit(1)
|
|
59
65
|
|
|
60
66
|
else:
|
|
@@ -119,6 +125,15 @@ async def status(
|
|
|
119
125
|
"[bold]Image:[/bold]",
|
|
120
126
|
str(data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("image", "N/A")),
|
|
121
127
|
)
|
|
128
|
+
|
|
129
|
+
# Display agent profile if available
|
|
130
|
+
agent_profile = data.get("agentProfile")
|
|
131
|
+
if agent_profile:
|
|
132
|
+
deployment_table.add_row(
|
|
133
|
+
"[bold]Agent Profile:[/bold]",
|
|
134
|
+
str(agent_profile),
|
|
135
|
+
)
|
|
136
|
+
|
|
122
137
|
deployment_table.add_row(
|
|
123
138
|
"[bold]Active Deployment ID:[/bold]",
|
|
124
139
|
str(data.get("activeDeploymentId", "N/A")),
|
|
@@ -131,10 +146,15 @@ async def status(
|
|
|
131
146
|
"[bold]Updated At:[/bold]",
|
|
132
147
|
str(data.get("updatedAt", "N/A")),
|
|
133
148
|
)
|
|
134
|
-
|
|
149
|
+
|
|
135
150
|
# Check for Managed Keys status
|
|
136
151
|
# API returns integratedKeysProxy but we display as "Managed Keys"
|
|
137
|
-
integrated_keys =
|
|
152
|
+
integrated_keys = (
|
|
153
|
+
data.get("deployment", {})
|
|
154
|
+
.get("manifest", {})
|
|
155
|
+
.get("spec", {})
|
|
156
|
+
.get("integratedKeysProxy", {})
|
|
157
|
+
)
|
|
138
158
|
if isinstance(integrated_keys, dict):
|
|
139
159
|
integrated_keys_enabled = integrated_keys.get("enabled", False)
|
|
140
160
|
else:
|
|
@@ -150,11 +170,11 @@ async def status(
|
|
|
150
170
|
if autoscaling_data:
|
|
151
171
|
scaling_renderables = [
|
|
152
172
|
Panel(
|
|
153
|
-
f"[bold]Minimum Agents[/bold]\n{autoscaling_data.get('
|
|
173
|
+
f"[bold]Minimum Agents[/bold]\n{autoscaling_data.get('minReplicas', 0)}",
|
|
154
174
|
expand=True,
|
|
155
175
|
),
|
|
156
176
|
Panel(
|
|
157
|
-
f"[bold]Maximum Agents[/bold]\n{autoscaling_data.get('
|
|
177
|
+
f"[bold]Maximum Agents[/bold]\n{autoscaling_data.get('maxReplicas', 0)}",
|
|
158
178
|
expand=True,
|
|
159
179
|
),
|
|
160
180
|
]
|
|
@@ -217,9 +237,7 @@ async def status(
|
|
|
217
237
|
async def sessions(
|
|
218
238
|
deploy_config=typer.Option(None, hidden=True),
|
|
219
239
|
agent_name: str = typer.Argument(
|
|
220
|
-
None,
|
|
221
|
-
help="Name of the agent to list sessions for e.g. 'my-agent'",
|
|
222
|
-
show_default=False
|
|
240
|
+
None, help="Name of the agent to list sessions for e.g. 'my-agent'", show_default=False
|
|
223
241
|
),
|
|
224
242
|
session_id: str = typer.Option(
|
|
225
243
|
None,
|
|
@@ -232,7 +250,7 @@ async def sessions(
|
|
|
232
250
|
),
|
|
233
251
|
):
|
|
234
252
|
org = organization or config.get("org")
|
|
235
|
-
|
|
253
|
+
|
|
236
254
|
# Get agent name from argument or deploy config
|
|
237
255
|
if not agent_name:
|
|
238
256
|
if deploy_config and deploy_config.agent_name:
|
|
@@ -264,15 +282,17 @@ async def sessions(
|
|
|
264
282
|
for session in completed_sessions:
|
|
265
283
|
try:
|
|
266
284
|
from datetime import datetime
|
|
267
|
-
|
|
268
|
-
|
|
285
|
+
|
|
286
|
+
created_at = datetime.fromisoformat(session["createdAt"].replace("Z", "+00:00"))
|
|
287
|
+
ended_at = datetime.fromisoformat(session["endedAt"].replace("Z", "+00:00"))
|
|
269
288
|
duration_seconds = (ended_at - created_at).total_seconds()
|
|
270
289
|
durations.append(duration_seconds)
|
|
271
290
|
except BaseException:
|
|
272
291
|
continue
|
|
273
292
|
|
|
274
|
-
bot_start_times = [
|
|
275
|
-
|
|
293
|
+
bot_start_times = [
|
|
294
|
+
s["botStartSeconds"] for s in sessions_list if s.get("botStartSeconds") is not None
|
|
295
|
+
]
|
|
276
296
|
bot_start_metrics = calculate_percentiles(bot_start_times)
|
|
277
297
|
duration_metrics = calculate_percentiles(durations)
|
|
278
298
|
cold_starts_count = sum(1 for s in sessions_list if s.get("coldStart") is True)
|
|
@@ -317,8 +337,9 @@ async def sessions(
|
|
|
317
337
|
if session_id and session["sessionId"] != session_id:
|
|
318
338
|
continue
|
|
319
339
|
|
|
320
|
-
session_duration =
|
|
321
|
-
session["createdAt"], session["endedAt"]) or "[dim]N/A[/dim]"
|
|
340
|
+
session_duration = (
|
|
341
|
+
format_duration(session["createdAt"], session["endedAt"]) or "[dim]N/A[/dim]"
|
|
342
|
+
)
|
|
322
343
|
status = session.get("completionStatus", "")
|
|
323
344
|
if session["endedAt"]:
|
|
324
345
|
if status == "500":
|
|
@@ -333,14 +354,18 @@ async def sessions(
|
|
|
333
354
|
|
|
334
355
|
row_data = [
|
|
335
356
|
session["sessionId"],
|
|
336
|
-
format_timestamp(
|
|
337
|
-
|
|
338
|
-
format_timestamp(
|
|
339
|
-
session["endedAt"]) if session["endedAt"] else "[dim]N/A[/dim]",
|
|
357
|
+
format_timestamp(session["createdAt"]),
|
|
358
|
+
format_timestamp(session["endedAt"]) if session["endedAt"] else "[dim]N/A[/dim]",
|
|
340
359
|
session_duration,
|
|
341
360
|
status_display,
|
|
342
|
-
f"{session['botStartSeconds']}s"
|
|
343
|
-
|
|
361
|
+
f"{session['botStartSeconds']}s"
|
|
362
|
+
if session["botStartSeconds"] is not None
|
|
363
|
+
else "[dim]N/A[/dim]",
|
|
364
|
+
"[red]Yes[/red]"
|
|
365
|
+
if session["coldStart"] is True
|
|
366
|
+
else "No"
|
|
367
|
+
if session["coldStart"] is False
|
|
368
|
+
else "[dim]N/A[/dim]",
|
|
344
369
|
]
|
|
345
370
|
|
|
346
371
|
if is_cold_start:
|
|
@@ -350,10 +375,11 @@ async def sessions(
|
|
|
350
375
|
|
|
351
376
|
console.success(
|
|
352
377
|
Group(
|
|
353
|
-
Columns(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
table
|
|
378
|
+
Columns(metric_renderables, equal=True)
|
|
379
|
+
if metric_renderables and not session_id
|
|
380
|
+
else "",
|
|
381
|
+
table,
|
|
382
|
+
),
|
|
357
383
|
title=f"Session data for agent {agent_name} [dim]({org})[/dim]",
|
|
358
384
|
)
|
|
359
385
|
|
|
@@ -390,32 +416,16 @@ class LogLevelColors(str, Enum):
|
|
|
390
416
|
@synchronizer.create_blocking
|
|
391
417
|
@requires_login
|
|
392
418
|
async def logs(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
help="Level of logs to get"),
|
|
404
|
-
format: LogFormat = typer.Option(
|
|
405
|
-
LogFormat.TEXT,
|
|
406
|
-
"--format",
|
|
407
|
-
"-f",
|
|
408
|
-
help="Logs format"),
|
|
409
|
-
limit: int = typer.Option(
|
|
410
|
-
100,
|
|
411
|
-
"--limit",
|
|
412
|
-
"-n",
|
|
413
|
-
help="Number of logs to get"),
|
|
414
|
-
deployment_id: str = typer.Option(
|
|
415
|
-
None,
|
|
416
|
-
"--deployment",
|
|
417
|
-
"-d",
|
|
418
|
-
help="Filter logs by deployment ID"),
|
|
419
|
+
agent_name: str,
|
|
420
|
+
organization: str = typer.Option(
|
|
421
|
+
None, "--organization", "-o", help="Organization to get status of agent for"
|
|
422
|
+
),
|
|
423
|
+
level: LogLevel = typer.Option(None, "--level", "-l", help="Level of logs to get"),
|
|
424
|
+
format: LogFormat = typer.Option(LogFormat.TEXT, "--format", "-f", help="Logs format"),
|
|
425
|
+
limit: int = typer.Option(100, "--limit", "-n", help="Number of logs to get"),
|
|
426
|
+
deployment_id: str = typer.Option(
|
|
427
|
+
None, "--deployment", "-d", help="Filter logs by deployment ID"
|
|
428
|
+
),
|
|
419
429
|
):
|
|
420
430
|
org = organization or config.get("org")
|
|
421
431
|
|
|
@@ -425,7 +435,9 @@ async def logs(
|
|
|
425
435
|
f"[dim]Fetching logs for {status_text}: [bold]'{agent_name}'[/bold] with severity: [bold cyan]{level.value if level else 'ALL'}[/bold cyan][/dim]",
|
|
426
436
|
spinner="dots",
|
|
427
437
|
):
|
|
428
|
-
data, error = await API.agent_logs(
|
|
438
|
+
data, error = await API.agent_logs(
|
|
439
|
+
agent_name=agent_name, org=org, limit=limit, deployment_id=deployment_id
|
|
440
|
+
)
|
|
429
441
|
|
|
430
442
|
if not data or not data.get("logs"):
|
|
431
443
|
console.print("[dim]No logs found for agent[/dim]")
|
|
@@ -539,12 +551,13 @@ async def deployments(
|
|
|
539
551
|
table.add_column("Updated At")
|
|
540
552
|
|
|
541
553
|
for deployment in data["deployments"]:
|
|
554
|
+
spec = deployment.get("manifest", {}).get("spec", {})
|
|
542
555
|
table.add_row(
|
|
543
|
-
deployment
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
deployment
|
|
547
|
-
deployment
|
|
556
|
+
deployment.get("id", "N/A"),
|
|
557
|
+
spec.get("dailyNodeType", "N/A"),
|
|
558
|
+
spec.get("image", "N/A"),
|
|
559
|
+
deployment.get("createdAt", "N/A"),
|
|
560
|
+
deployment.get("updatedAt", "N/A"),
|
|
548
561
|
)
|
|
549
562
|
|
|
550
563
|
console.print(
|
|
@@ -636,7 +649,8 @@ async def start(
|
|
|
636
649
|
title="Public API Key Required",
|
|
637
650
|
title_align="left",
|
|
638
651
|
border_style="yellow",
|
|
639
|
-
)
|
|
652
|
+
)
|
|
653
|
+
)
|
|
640
654
|
|
|
641
655
|
return typer.Exit(1)
|
|
642
656
|
|
|
@@ -666,7 +680,8 @@ async def start(
|
|
|
666
680
|
title=f"[bold]Start Request for agent: {agent_name}[/bold]",
|
|
667
681
|
title_align="left",
|
|
668
682
|
border_style="yellow",
|
|
669
|
-
)
|
|
683
|
+
)
|
|
684
|
+
)
|
|
670
685
|
if not await questionary.confirm(
|
|
671
686
|
"Are you sure you want to start an active session for this agent?"
|
|
672
687
|
).ask_async():
|
|
@@ -715,3 +730,70 @@ async def start(
|
|
|
715
730
|
url = f"{daily_room}?t={daily_token}"
|
|
716
731
|
console.print("\nJoin your session by visiting the link below:")
|
|
717
732
|
console.print(f"[link={url}]{url}[/link]")
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
@agent_cli.command(name="stop", help="Stop an active agent session")
|
|
736
|
+
@synchronizer.create_blocking
|
|
737
|
+
@requires_login
|
|
738
|
+
@with_deploy_config
|
|
739
|
+
async def stop(
|
|
740
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
741
|
+
agent_name: str = typer.Argument(None, help="Name of the agent e.g. 'my-agent'"),
|
|
742
|
+
session_id: str = typer.Option(
|
|
743
|
+
...,
|
|
744
|
+
"--session-id",
|
|
745
|
+
"-s",
|
|
746
|
+
help="ID of the session to stop",
|
|
747
|
+
),
|
|
748
|
+
organization: str = typer.Option(
|
|
749
|
+
None, "--organization", "-o", help="Organization which the agent belongs to"
|
|
750
|
+
),
|
|
751
|
+
force: bool = typer.Option(
|
|
752
|
+
False,
|
|
753
|
+
"--force",
|
|
754
|
+
"-f",
|
|
755
|
+
help="Bypass prompt for confirmation",
|
|
756
|
+
),
|
|
757
|
+
):
|
|
758
|
+
org = organization or config.get("org")
|
|
759
|
+
|
|
760
|
+
# Load values from deployment config file (if one exists)
|
|
761
|
+
partial_config = deploy_config or DeployConfigParams()
|
|
762
|
+
|
|
763
|
+
# Get agent name from argument or deploy config
|
|
764
|
+
if not agent_name:
|
|
765
|
+
if partial_config and partial_config.agent_name:
|
|
766
|
+
agent_name = partial_config.agent_name
|
|
767
|
+
else:
|
|
768
|
+
console.error("No target agent name provided")
|
|
769
|
+
return typer.Exit(1)
|
|
770
|
+
|
|
771
|
+
# Confirm stop request
|
|
772
|
+
if not force:
|
|
773
|
+
console.print(
|
|
774
|
+
Panel(
|
|
775
|
+
f"Agent Name: {agent_name}\n"
|
|
776
|
+
f"Session ID: {session_id}",
|
|
777
|
+
title=f"[bold]Stop Session[/bold]",
|
|
778
|
+
title_align="left",
|
|
779
|
+
border_style="yellow",
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
if not await questionary.confirm(
|
|
783
|
+
"Are you sure you want to stop this session?"
|
|
784
|
+
).ask_async():
|
|
785
|
+
console.print("[bold]Aborting stop request[/bold]")
|
|
786
|
+
return typer.Exit(1)
|
|
787
|
+
|
|
788
|
+
with console.status(
|
|
789
|
+
f"[dim]Stopping session [bold]'{session_id}'[/bold] for agent [bold]'{agent_name}'[/bold][/dim]",
|
|
790
|
+
spinner="dots",
|
|
791
|
+
):
|
|
792
|
+
data, error = await API.agent_session_terminate(
|
|
793
|
+
agent_name=agent_name, session_id=session_id, org=org
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if error:
|
|
797
|
+
return typer.Exit(1)
|
|
798
|
+
|
|
799
|
+
console.success(f"Session '{session_id}' stopped successfully")
|
|
@@ -285,6 +285,13 @@ def create_deploy_command(app: typer.Typer):
|
|
|
285
285
|
help="Enable Managed Keys for this deployment",
|
|
286
286
|
rich_help_panel="Deployment Configuration",
|
|
287
287
|
),
|
|
288
|
+
profile: str = typer.Option(
|
|
289
|
+
None,
|
|
290
|
+
"--profile",
|
|
291
|
+
"-p",
|
|
292
|
+
help="Agent profile to use for deployment",
|
|
293
|
+
rich_help_panel="Deployment Configuration",
|
|
294
|
+
),
|
|
288
295
|
skip_confirm: bool = typer.Option(
|
|
289
296
|
False,
|
|
290
297
|
"--force",
|
|
@@ -352,6 +359,7 @@ def create_deploy_command(app: typer.Typer):
|
|
|
352
359
|
)
|
|
353
360
|
partial_config.enable_krisp = krisp or partial_config.enable_krisp
|
|
354
361
|
partial_config.enable_managed_keys = managed_keys or partial_config.enable_managed_keys
|
|
362
|
+
partial_config.agent_profile = profile or partial_config.agent_profile
|
|
355
363
|
|
|
356
364
|
# Assert agent name and image are provided
|
|
357
365
|
if not partial_config.agent_name:
|
|
@@ -388,6 +396,7 @@ def create_deploy_command(app: typer.Typer):
|
|
|
388
396
|
(f"[bold white]Organization:[/bold white] [green]{org}[/green]"),
|
|
389
397
|
(f"[bold white]Secret set:[/bold white] {'[dim]None[/dim]' if not partial_config.secret_set else '[green] '+ partial_config.secret_set + '[/green]'}"),
|
|
390
398
|
(f"[bold white]Image pull secret:[/bold white] {'[dim]None[/dim]' if not partial_config.image_credentials else '[green]' + partial_config.image_credentials + '[/green]'}"),
|
|
399
|
+
(f"[bold white]Agent profile:[/bold white] {'[dim]None[/dim]' if not partial_config.agent_profile else '[green]' + partial_config.agent_profile + '[/green]'}"),
|
|
391
400
|
(f"[bold white]Krisp:[/bold white] {'[dim]Disabled[/dim]' if not partial_config.enable_krisp else '[green]Enabled[/green]'}"),
|
|
392
401
|
(f"[bold white]Managed Keys:[/bold white] {'[dim]Disabled[/dim]' if not partial_config.enable_managed_keys else '[green]Enabled[/green]'}"),
|
|
393
402
|
"\n[dim]Scaling configuration:[/dim]",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import warnings
|
|
8
9
|
import zipfile
|
|
9
10
|
from typing import Set
|
|
10
11
|
|
|
@@ -40,9 +41,25 @@ def check_existing_files() -> Set[str]:
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def create_init_command(app: typer.Typer):
|
|
43
|
-
@app.command(
|
|
44
|
+
@app.command(
|
|
45
|
+
name="init",
|
|
46
|
+
help="[DEPRECATED] Initialize project directory with template files",
|
|
47
|
+
deprecated=True,
|
|
48
|
+
)
|
|
44
49
|
@synchronizer.create_blocking
|
|
45
50
|
async def init():
|
|
51
|
+
warnings.warn(
|
|
52
|
+
"The 'pcc init' command is deprecated and will be removed in a future version. "
|
|
53
|
+
"Please use the Pipecat CLI instead: https://github.com/pipecat-ai/pipecat-cli",
|
|
54
|
+
DeprecationWarning,
|
|
55
|
+
stacklevel=2,
|
|
56
|
+
)
|
|
57
|
+
console.print(
|
|
58
|
+
"[yellow]Warning:[/yellow] The 'pcc init' command is deprecated and will be removed in a future version.\n"
|
|
59
|
+
"Please use the Pipecat CLI instead: "
|
|
60
|
+
"[link=https://github.com/pipecat-ai/pipecat-cli]https://github.com/pipecat-ai/pipecat-cli[/link]"
|
|
61
|
+
)
|
|
62
|
+
|
|
46
63
|
if not await questionary.confirm(
|
|
47
64
|
"This will download the latest starter project to the current directory. Continue?"
|
|
48
65
|
).ask_async():
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the 'pcc agent stop' command.
|
|
3
|
+
|
|
4
|
+
Tests focus on core behaviors and edge cases, not implementation details.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import typer
|
|
9
|
+
from unittest.mock import patch, AsyncMock, MagicMock
|
|
10
|
+
|
|
11
|
+
# Import the function under test from local source
|
|
12
|
+
from src.pipecatcloud.cli.commands.agent import stop
|
|
13
|
+
|
|
14
|
+
# Test constants
|
|
15
|
+
TEST_ORG = "test-org"
|
|
16
|
+
TEST_AGENT = "test-agent"
|
|
17
|
+
TEST_SESSION_ID = "session-123"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestAgentStopCommand:
|
|
21
|
+
"""Test the 'pcc agent stop' command behaviors."""
|
|
22
|
+
|
|
23
|
+
def test_stop_respects_force_flag(self):
|
|
24
|
+
"""Verify force flag skips confirmation when set to True."""
|
|
25
|
+
with patch('src.pipecatcloud.cli.commands.agent.console') as mock_console, \
|
|
26
|
+
patch('src.pipecatcloud.cli.commands.agent.config') as mock_config, \
|
|
27
|
+
patch('src.pipecatcloud.cli.commands.agent.questionary') as mock_questionary, \
|
|
28
|
+
patch('src.pipecatcloud.cli.commands.agent.DeployConfigParams') as mock_params:
|
|
29
|
+
|
|
30
|
+
mock_config.get.return_value = TEST_ORG
|
|
31
|
+
mock_params.return_value = MagicMock(agent_name=TEST_AGENT)
|
|
32
|
+
# Mock questionary - should NOT be called when force=True
|
|
33
|
+
mock_questionary.confirm.return_value.ask_async = AsyncMock()
|
|
34
|
+
|
|
35
|
+
# Act with force=True
|
|
36
|
+
stop(
|
|
37
|
+
deploy_config=None,
|
|
38
|
+
agent_name=TEST_AGENT,
|
|
39
|
+
session_id=TEST_SESSION_ID,
|
|
40
|
+
organization=TEST_ORG,
|
|
41
|
+
force=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Assert
|
|
45
|
+
# questionary.confirm should not be called when force=True
|
|
46
|
+
mock_questionary.confirm.assert_not_called()
|
|
47
|
+
|
|
48
|
+
def test_stop_shows_confirmation_without_force(self):
|
|
49
|
+
"""Verify confirmation prompt is shown when force is False."""
|
|
50
|
+
with patch('src.pipecatcloud.cli.commands.agent.console') as mock_console, \
|
|
51
|
+
patch('src.pipecatcloud.cli.commands.agent.config') as mock_config, \
|
|
52
|
+
patch('src.pipecatcloud.cli.commands.agent.questionary') as mock_questionary, \
|
|
53
|
+
patch('src.pipecatcloud.cli.commands.agent.DeployConfigParams') as mock_params:
|
|
54
|
+
|
|
55
|
+
mock_config.get.return_value = TEST_ORG
|
|
56
|
+
mock_params.return_value = MagicMock(agent_name=TEST_AGENT)
|
|
57
|
+
# User agrees to the confirmation
|
|
58
|
+
mock_questionary.confirm.return_value.ask_async = AsyncMock(return_value=True)
|
|
59
|
+
|
|
60
|
+
# Act with force=False
|
|
61
|
+
stop(
|
|
62
|
+
deploy_config=None,
|
|
63
|
+
agent_name=TEST_AGENT,
|
|
64
|
+
session_id=TEST_SESSION_ID,
|
|
65
|
+
organization=TEST_ORG,
|
|
66
|
+
force=False,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Assert
|
|
70
|
+
# questionary.confirm should be called when force=False
|
|
71
|
+
mock_questionary.confirm.assert_called_once()
|
|
72
|
+
|
|
73
|
+
def test_stop_aborts_on_user_rejection(self):
|
|
74
|
+
"""Verify command aborts when user rejects the confirmation."""
|
|
75
|
+
with patch('src.pipecatcloud.cli.commands.agent.console') as mock_console, \
|
|
76
|
+
patch('src.pipecatcloud.cli.commands.agent.config') as mock_config, \
|
|
77
|
+
patch('src.pipecatcloud.cli.commands.agent.questionary') as mock_questionary, \
|
|
78
|
+
patch('src.pipecatcloud.cli.commands.agent.DeployConfigParams') as mock_params:
|
|
79
|
+
|
|
80
|
+
mock_config.get.return_value = TEST_ORG
|
|
81
|
+
mock_params.return_value = MagicMock(agent_name=TEST_AGENT)
|
|
82
|
+
# User rejects the confirmation
|
|
83
|
+
mock_questionary.confirm.return_value.ask_async = AsyncMock(return_value=False)
|
|
84
|
+
|
|
85
|
+
# Act
|
|
86
|
+
result = stop(
|
|
87
|
+
deploy_config=None,
|
|
88
|
+
agent_name=TEST_AGENT,
|
|
89
|
+
session_id=TEST_SESSION_ID,
|
|
90
|
+
organization=TEST_ORG,
|
|
91
|
+
force=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Assert
|
|
95
|
+
# Should exit with code 1 (abort)
|
|
96
|
+
assert isinstance(result, typer.Exit)
|
|
97
|
+
assert result.exit_code == 1
|
|
98
|
+
# Should display abort message
|
|
99
|
+
mock_console.print.assert_any_call("[bold]Aborting stop request[/bold]")
|
|
@@ -5,17 +5,12 @@ Tests follow AAA pattern and focus on behaviors/outcomes rather than implementat
|
|
|
5
5
|
Covers data model, TOML parsing, CLI commands, and API integration.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
import tempfile
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
8
|
+
from unittest.mock import AsyncMock, patch
|
|
12
9
|
|
|
13
10
|
import pytest
|
|
14
|
-
import toml
|
|
15
11
|
|
|
16
12
|
from src.pipecatcloud._utils.deploy_utils import DeployConfigParams, load_deploy_config_file
|
|
17
13
|
from src.pipecatcloud.api import _API
|
|
18
|
-
from src.pipecatcloud.exception import ConfigFileError
|
|
19
14
|
|
|
20
15
|
|
|
21
16
|
class TestDeployConfigDataModel:
|
|
@@ -25,7 +20,7 @@ class TestDeployConfigDataModel:
|
|
|
25
20
|
"""When not specified, managed keys should default to disabled."""
|
|
26
21
|
# Arrange & Act
|
|
27
22
|
config = DeployConfigParams()
|
|
28
|
-
|
|
23
|
+
|
|
29
24
|
# Assert
|
|
30
25
|
assert config.enable_managed_keys is False
|
|
31
26
|
|
|
@@ -33,21 +28,18 @@ class TestDeployConfigDataModel:
|
|
|
33
28
|
"""Integrated keys can be explicitly enabled."""
|
|
34
29
|
# Arrange & Act
|
|
35
30
|
config = DeployConfigParams(enable_managed_keys=True)
|
|
36
|
-
|
|
31
|
+
|
|
37
32
|
# Assert
|
|
38
33
|
assert config.enable_managed_keys is True
|
|
39
34
|
|
|
40
35
|
def test_keys_included_in_dict_representation(self):
|
|
41
36
|
"""Dictionary representation should include managed keys setting."""
|
|
42
37
|
# Arrange
|
|
43
|
-
config = DeployConfigParams(
|
|
44
|
-
|
|
45
|
-
enable_managed_keys=True
|
|
46
|
-
)
|
|
47
|
-
|
|
38
|
+
config = DeployConfigParams(agent_name="test-agent", enable_managed_keys=True)
|
|
39
|
+
|
|
48
40
|
# Act
|
|
49
41
|
result = config.to_dict()
|
|
50
|
-
|
|
42
|
+
|
|
51
43
|
# Assert
|
|
52
44
|
assert "enable_managed_keys" in result
|
|
53
45
|
assert result["enable_managed_keys"] is True
|
|
@@ -59,12 +51,12 @@ class TestDeployConfigDataModel:
|
|
|
59
51
|
agent_name="test-agent",
|
|
60
52
|
image="test:latest",
|
|
61
53
|
enable_krisp=True,
|
|
62
|
-
enable_managed_keys=True
|
|
54
|
+
enable_managed_keys=True,
|
|
63
55
|
)
|
|
64
|
-
|
|
56
|
+
|
|
65
57
|
# Act
|
|
66
58
|
result = config.to_dict()
|
|
67
|
-
|
|
59
|
+
|
|
68
60
|
# Assert
|
|
69
61
|
assert result["agent_name"] == "test-agent"
|
|
70
62
|
assert result["image"] == "test:latest"
|
|
@@ -90,11 +82,11 @@ class TestTOMLConfiguration:
|
|
|
90
82
|
enable_managed_keys = true
|
|
91
83
|
"""
|
|
92
84
|
temp_config_file.write_text(config_content)
|
|
93
|
-
|
|
85
|
+
|
|
94
86
|
# Act
|
|
95
87
|
with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
|
|
96
88
|
config = load_deploy_config_file()
|
|
97
|
-
|
|
89
|
+
|
|
98
90
|
# Assert
|
|
99
91
|
assert config is not None
|
|
100
92
|
assert config.enable_managed_keys is True
|
|
@@ -108,11 +100,11 @@ class TestTOMLConfiguration:
|
|
|
108
100
|
enable_managed_keys = false
|
|
109
101
|
"""
|
|
110
102
|
temp_config_file.write_text(config_content)
|
|
111
|
-
|
|
103
|
+
|
|
112
104
|
# Act
|
|
113
105
|
with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
|
|
114
106
|
config = load_deploy_config_file()
|
|
115
|
-
|
|
107
|
+
|
|
116
108
|
# Assert
|
|
117
109
|
assert config is not None
|
|
118
110
|
assert config.enable_managed_keys is False
|
|
@@ -125,11 +117,11 @@ class TestTOMLConfiguration:
|
|
|
125
117
|
image = "test:latest"
|
|
126
118
|
"""
|
|
127
119
|
temp_config_file.write_text(config_content)
|
|
128
|
-
|
|
120
|
+
|
|
129
121
|
# Act
|
|
130
122
|
with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
|
|
131
123
|
config = load_deploy_config_file()
|
|
132
|
-
|
|
124
|
+
|
|
133
125
|
# Assert
|
|
134
126
|
assert config is not None
|
|
135
127
|
assert config.enable_managed_keys is False
|
|
@@ -149,11 +141,11 @@ class TestTOMLConfiguration:
|
|
|
149
141
|
max_agents = 10
|
|
150
142
|
"""
|
|
151
143
|
temp_config_file.write_text(config_content)
|
|
152
|
-
|
|
144
|
+
|
|
153
145
|
# Act
|
|
154
146
|
with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_config_file)):
|
|
155
147
|
config = load_deploy_config_file()
|
|
156
|
-
|
|
148
|
+
|
|
157
149
|
# Assert
|
|
158
150
|
assert config.agent_name == "test-agent"
|
|
159
151
|
assert config.image == "test:latest"
|
|
@@ -164,7 +156,6 @@ class TestTOMLConfiguration:
|
|
|
164
156
|
assert config.scaling.max_agents == 10
|
|
165
157
|
|
|
166
158
|
|
|
167
|
-
|
|
168
159
|
class TestAPIIntegration:
|
|
169
160
|
"""Test API client integration with managed keys."""
|
|
170
161
|
|
|
@@ -178,17 +169,15 @@ class TestAPIIntegration:
|
|
|
178
169
|
"""Deploy payload should include enableIntegratedKeys field."""
|
|
179
170
|
# Arrange
|
|
180
171
|
config = DeployConfigParams(
|
|
181
|
-
agent_name="test-agent",
|
|
182
|
-
image="test:latest",
|
|
183
|
-
enable_managed_keys=True
|
|
172
|
+
agent_name="test-agent", image="test:latest", enable_managed_keys=True
|
|
184
173
|
)
|
|
185
|
-
|
|
174
|
+
|
|
186
175
|
with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
187
176
|
mock_request.return_value = {"success": True}
|
|
188
|
-
|
|
177
|
+
|
|
189
178
|
# Act
|
|
190
179
|
await api_client._deploy(config, "test-org", update=False)
|
|
191
|
-
|
|
180
|
+
|
|
192
181
|
# Assert
|
|
193
182
|
mock_request.assert_called_once()
|
|
194
183
|
payload = mock_request.call_args[1]["json"]
|
|
@@ -200,17 +189,15 @@ class TestAPIIntegration:
|
|
|
200
189
|
"""Update payload should include enableIntegratedKeys field."""
|
|
201
190
|
# Arrange
|
|
202
191
|
config = DeployConfigParams(
|
|
203
|
-
agent_name="test-agent",
|
|
204
|
-
image="test:latest",
|
|
205
|
-
enable_managed_keys=True
|
|
192
|
+
agent_name="test-agent", image="test:latest", enable_managed_keys=True
|
|
206
193
|
)
|
|
207
|
-
|
|
194
|
+
|
|
208
195
|
with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
209
196
|
mock_request.return_value = {"success": True}
|
|
210
|
-
|
|
197
|
+
|
|
211
198
|
# Act
|
|
212
199
|
await api_client._deploy(config, "test-org", update=True)
|
|
213
|
-
|
|
200
|
+
|
|
214
201
|
# Assert
|
|
215
202
|
mock_request.assert_called_once()
|
|
216
203
|
payload = mock_request.call_args[1]["json"]
|
|
@@ -222,17 +209,15 @@ class TestAPIIntegration:
|
|
|
222
209
|
"""When managed keys is disabled, API should send false value."""
|
|
223
210
|
# Arrange
|
|
224
211
|
config = DeployConfigParams(
|
|
225
|
-
agent_name="test-agent",
|
|
226
|
-
image="test:latest",
|
|
227
|
-
enable_managed_keys=False
|
|
212
|
+
agent_name="test-agent", image="test:latest", enable_managed_keys=False
|
|
228
213
|
)
|
|
229
|
-
|
|
214
|
+
|
|
230
215
|
with patch.object(api_client, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
231
216
|
mock_request.return_value = {"success": True}
|
|
232
|
-
|
|
217
|
+
|
|
233
218
|
# Act
|
|
234
219
|
await api_client._deploy(config, "test-org", update=False)
|
|
235
|
-
|
|
220
|
+
|
|
236
221
|
# Assert
|
|
237
222
|
mock_request.assert_called_once()
|
|
238
223
|
payload = mock_request.call_args[1]["json"]
|
|
@@ -253,24 +238,26 @@ class TestAgentStatusDisplay:
|
|
|
253
238
|
"""Agent status should display when managed keys is enabled."""
|
|
254
239
|
# Arrange
|
|
255
240
|
from src.pipecatcloud.cli.commands.agent import status
|
|
256
|
-
|
|
257
|
-
mock_api.agent = AsyncMock(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
"integratedKeysProxy": {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
241
|
+
|
|
242
|
+
mock_api.agent = AsyncMock(
|
|
243
|
+
return_value=(
|
|
244
|
+
{
|
|
245
|
+
"ready": True,
|
|
246
|
+
"deployment": {
|
|
247
|
+
"manifest": {"spec": {"integratedKeysProxy": {"enabled": True}}}
|
|
248
|
+
},
|
|
249
|
+
"autoScaling": {
|
|
250
|
+
"minReplicas": 0,
|
|
251
|
+
"maxReplicas": 1,
|
|
252
|
+
}, # Avoid undefined variable bug
|
|
253
|
+
},
|
|
254
|
+
None,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
271
258
|
# Act
|
|
272
259
|
status(agent_name="test-agent", organization="test-org")
|
|
273
|
-
|
|
260
|
+
|
|
274
261
|
# Assert
|
|
275
262
|
captured = capsys.readouterr()
|
|
276
263
|
assert "Managed Keys" in captured.out
|
|
@@ -280,20 +267,24 @@ class TestAgentStatusDisplay:
|
|
|
280
267
|
"""Agent status should display when managed keys is disabled."""
|
|
281
268
|
# Arrange
|
|
282
269
|
from src.pipecatcloud.cli.commands.agent import status
|
|
283
|
-
|
|
284
|
-
mock_api.agent = AsyncMock(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"spec": {}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
270
|
+
|
|
271
|
+
mock_api.agent = AsyncMock(
|
|
272
|
+
return_value=(
|
|
273
|
+
{
|
|
274
|
+
"ready": True,
|
|
275
|
+
"deployment": {"manifest": {"spec": {}}},
|
|
276
|
+
"autoScaling": {
|
|
277
|
+
"minReplicas": 0,
|
|
278
|
+
"maxReplicas": 1,
|
|
279
|
+
}, # Avoid undefined variable bug
|
|
280
|
+
},
|
|
281
|
+
None,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
294
285
|
# Act
|
|
295
286
|
status(agent_name="test-agent", organization="test-org")
|
|
296
|
-
|
|
287
|
+
|
|
297
288
|
# Assert
|
|
298
289
|
captured = capsys.readouterr()
|
|
299
290
|
assert "Managed Keys" in captured.out
|
|
@@ -303,22 +294,24 @@ class TestAgentStatusDisplay:
|
|
|
303
294
|
"""Agent status should handle boolean value for backward compatibility."""
|
|
304
295
|
# Arrange
|
|
305
296
|
from src.pipecatcloud.cli.commands.agent import status
|
|
306
|
-
|
|
307
|
-
mock_api.agent = AsyncMock(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
"spec": {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
297
|
+
|
|
298
|
+
mock_api.agent = AsyncMock(
|
|
299
|
+
return_value=(
|
|
300
|
+
{
|
|
301
|
+
"ready": True,
|
|
302
|
+
"deployment": {"manifest": {"spec": {"integratedKeysProxy": True}}},
|
|
303
|
+
"autoScaling": {
|
|
304
|
+
"minReplicas": 0,
|
|
305
|
+
"maxReplicas": 1,
|
|
306
|
+
}, # Avoid undefined variable bug
|
|
307
|
+
},
|
|
308
|
+
None,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
319
312
|
# Act
|
|
320
313
|
status(agent_name="test-agent", organization="test-org")
|
|
321
|
-
|
|
314
|
+
|
|
322
315
|
# Assert
|
|
323
316
|
captured = capsys.readouterr()
|
|
324
317
|
assert "Managed Keys" in captured.out
|
|
@@ -331,15 +324,11 @@ class TestBackwardCompatibility:
|
|
|
331
324
|
def test_existing_config_without_keys_proxy_works(self):
|
|
332
325
|
"""Existing configs without managed keys field should work unchanged."""
|
|
333
326
|
# Arrange
|
|
334
|
-
config = DeployConfigParams(
|
|
335
|
-
|
|
336
|
-
image="test:latest",
|
|
337
|
-
enable_krisp=True
|
|
338
|
-
)
|
|
339
|
-
|
|
327
|
+
config = DeployConfigParams(agent_name="test-agent", image="test:latest", enable_krisp=True)
|
|
328
|
+
|
|
340
329
|
# Act
|
|
341
330
|
result = config.to_dict()
|
|
342
|
-
|
|
331
|
+
|
|
343
332
|
# Assert
|
|
344
333
|
assert result["enable_krisp"] is True
|
|
345
334
|
assert result["enable_managed_keys"] is False
|
|
@@ -347,24 +336,20 @@ class TestBackwardCompatibility:
|
|
|
347
336
|
def test_api_response_without_keys_proxy_doesnt_crash(self):
|
|
348
337
|
"""Agent status should handle API responses without managed keys field."""
|
|
349
338
|
# Arrange
|
|
350
|
-
data = {
|
|
351
|
-
|
|
352
|
-
"deployment": {
|
|
353
|
-
"manifest": {
|
|
354
|
-
"spec": {
|
|
355
|
-
"image": "test:latest"
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
339
|
+
data = {"ready": True, "deployment": {"manifest": {"spec": {"image": "test:latest"}}}}
|
|
340
|
+
|
|
361
341
|
# Act - simulate the logic from agent.py
|
|
362
|
-
integrated_keys =
|
|
342
|
+
integrated_keys = (
|
|
343
|
+
data.get("deployment", {})
|
|
344
|
+
.get("manifest", {})
|
|
345
|
+
.get("spec", {})
|
|
346
|
+
.get("integratedKeysProxy", {})
|
|
347
|
+
)
|
|
363
348
|
if isinstance(integrated_keys, dict):
|
|
364
349
|
keys_proxy_enabled = integrated_keys.get("enabled", False)
|
|
365
350
|
else:
|
|
366
351
|
keys_proxy_enabled = bool(integrated_keys)
|
|
367
|
-
|
|
352
|
+
|
|
368
353
|
# Assert
|
|
369
354
|
assert keys_proxy_enabled is False
|
|
370
355
|
|
|
@@ -389,7 +374,7 @@ class TestBackwardCompatibility:
|
|
|
389
374
|
# Arrange & Act
|
|
390
375
|
with patch("pipecatcloud.cli.config.deploy_config_path", str(temp_legacy_config)):
|
|
391
376
|
config = load_deploy_config_file()
|
|
392
|
-
|
|
377
|
+
|
|
393
378
|
# Assert
|
|
394
379
|
assert config is not None
|
|
395
380
|
assert config.agent_name == "legacy-agent"
|
|
@@ -407,14 +392,12 @@ class TestErrorHandling:
|
|
|
407
392
|
# Arrange
|
|
408
393
|
api = _API(token="test-token", is_cli=True)
|
|
409
394
|
config = DeployConfigParams(
|
|
410
|
-
agent_name="test-agent",
|
|
411
|
-
image="test:latest",
|
|
412
|
-
enable_managed_keys=True
|
|
395
|
+
agent_name="test-agent", image="test:latest", enable_managed_keys=True
|
|
413
396
|
)
|
|
414
|
-
|
|
397
|
+
|
|
415
398
|
with patch.object(api, "_base_request", new_callable=AsyncMock) as mock_request:
|
|
416
399
|
mock_request.side_effect = Exception("Network error")
|
|
417
|
-
|
|
400
|
+
|
|
418
401
|
# Act & Assert
|
|
419
402
|
with pytest.raises(Exception, match="Network error"):
|
|
420
403
|
await api._deploy(config, "test-org", update=False)
|
|
@@ -426,12 +409,12 @@ class TestErrorHandling:
|
|
|
426
409
|
agent_name="test-agent",
|
|
427
410
|
image="test:latest",
|
|
428
411
|
enable_krisp=True,
|
|
429
|
-
enable_managed_keys=True
|
|
412
|
+
enable_managed_keys=True,
|
|
430
413
|
)
|
|
431
|
-
|
|
414
|
+
|
|
432
415
|
# Act
|
|
433
416
|
result = config.to_dict()
|
|
434
|
-
|
|
417
|
+
|
|
435
418
|
# Assert
|
|
436
419
|
assert result["enable_krisp"] is True
|
|
437
|
-
assert result["enable_managed_keys"] is True
|
|
420
|
+
assert result["enable_managed_keys"] is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|