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.
Files changed (52) hide show
  1. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/CHANGELOG.md +30 -0
  2. {pipecatcloud-0.2.5/src/pipecatcloud.egg-info → pipecatcloud-0.2.7}/PKG-INFO +1 -1
  3. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/deploy_utils.py +3 -0
  4. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/agent.py +9 -7
  5. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/api.py +15 -0
  6. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/agent.py +143 -61
  7. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/deploy.py +9 -0
  8. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/init.py +18 -1
  9. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
  10. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
  11. pipecatcloud-0.2.7/tests/test_agent_stop.py +99 -0
  12. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/test_managed_keys.py +99 -116
  13. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/publish-pypi.yml +0 -0
  14. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/publish-test.yml +0 -0
  15. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.github/workflows/tests.yml +0 -0
  16. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/.gitignore +0 -0
  17. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/LICENSE +0 -0
  18. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/README.md +0 -0
  19. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pipecat-cloud.png +0 -0
  20. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pyproject.toml +0 -0
  21. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/pytest.ini +0 -0
  22. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/setup.cfg +0 -0
  23. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__init__.py +0 -0
  24. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__main__.py +0 -0
  25. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/__version__.py +0 -0
  26. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/__init__.py +0 -0
  27. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/async_utils.py +0 -0
  28. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  29. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/console_utils.py +0 -0
  30. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/_utils/local_runner.py +0 -0
  31. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/__init__.py +0 -0
  32. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/api.py +0 -0
  33. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  34. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/auth.py +0 -0
  35. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/docker.py +0 -0
  36. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  37. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/run.py +0 -0
  38. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/commands/secrets.py +0 -0
  39. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/config.py +0 -0
  40. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/cli/entry_point.py +0 -0
  41. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/config.py +0 -0
  42. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/exception.py +0 -0
  43. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/py.typed +0 -0
  44. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/session.py +0 -0
  45. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  46. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  47. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  48. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/requires.txt +0 -0
  49. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  50. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/__init__.py +0 -0
  51. {pipecatcloud-0.2.5 → pipecatcloud-0.2.7}/tests/test_agent_sessions.py +0 -0
  52. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.5
3
+ Version: 0.2.7
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/
@@ -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
- body: Any
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 console, format_duration, format_timestamp, calculate_percentiles
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 = data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("integratedKeysProxy", {})
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('minAgents', 0)}",
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('maxAgents', 0)}",
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
- created_at = datetime.fromisoformat(session["createdAt"].replace('Z', '+00:00'))
268
- ended_at = datetime.fromisoformat(session["endedAt"].replace('Z', '+00:00'))
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 = [s["botStartSeconds"]
275
- for s in sessions_list if s.get("botStartSeconds") is not None]
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 = format_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
- session["createdAt"]),
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" if session["botStartSeconds"] is not None else "[dim]N/A[/dim]",
343
- "[red]Yes[/red]" if session["coldStart"] is True else "No" if session["coldStart"] is False else "[dim]N/A[/dim]",
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
- metric_renderables,
355
- equal=True) if metric_renderables and not session_id else "",
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
- agent_name: str,
394
- organization: str = typer.Option(
395
- None,
396
- "--organization",
397
- "-o",
398
- help="Organization to get status of agent for"),
399
- level: LogLevel = typer.Option(
400
- None,
401
- "--level",
402
- "-l",
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(agent_name=agent_name, org=org, limit=limit, deployment_id=deployment_id)
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["id"],
544
- deployment["manifest"]["spec"]["dailyNodeType"],
545
- deployment["manifest"]["spec"]["image"],
546
- deployment["createdAt"],
547
- deployment["updatedAt"],
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(name="init", help="Initialize project directory with template files")
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.5
3
+ Version: 0.2.7
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/
@@ -45,5 +45,6 @@ src/pipecatcloud/cli/commands/secrets.py
45
45
  src/pipecatcloud/smallwebrtc/session_manager.py
46
46
  tests/__init__.py
47
47
  tests/test_agent_sessions.py
48
+ tests/test_agent_stop.py
48
49
  tests/test_docker_commands.py
49
50
  tests/test_managed_keys.py
@@ -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 json
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
- agent_name="test-agent",
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(return_value=({
258
- "ready": True,
259
- "deployment": {
260
- "manifest": {
261
- "spec": {
262
- "integratedKeysProxy": {
263
- "enabled": True
264
- }
265
- }
266
- }
267
- },
268
- "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
269
- }, None))
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(return_value=({
285
- "ready": True,
286
- "deployment": {
287
- "manifest": {
288
- "spec": {}
289
- }
290
- },
291
- "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
292
- }, None))
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(return_value=({
308
- "ready": True,
309
- "deployment": {
310
- "manifest": {
311
- "spec": {
312
- "integratedKeysProxy": True
313
- }
314
- }
315
- },
316
- "autoScaling": {"minAgents": 0, "maxAgents": 1} # Avoid undefined variable bug
317
- }, None))
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
- agent_name="test-agent",
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
- "ready": True,
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 = data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("integratedKeysProxy", {})
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