pipecatcloud 0.1.5__tar.gz → 0.1.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.1.5 → pipecatcloud-0.1.7}/CHANGELOG.md +16 -0
- {pipecatcloud-0.1.5/src/pipecatcloud.egg-info → pipecatcloud-0.1.7}/PKG-INFO +2 -2
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/pyproject.toml +2 -2
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/console_utils.py +63 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/deploy_utils.py +45 -12
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/api.py +10 -2
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/agent.py +119 -29
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/deploy.py +51 -27
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/config.py +1 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7/src/pipecatcloud.egg-info}/PKG-INFO +2 -2
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/requires.txt +1 -1
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/.github/workflows/python-publish.yml +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/.gitignore +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/LICENSE +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/README.md +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/dev-requirements.txt +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/pipecat-cloud.png +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/setup.cfg +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/__main__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/auth_utils.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/_utils/local_runner.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/auth.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/init.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/organizations.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/run.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/commands/secrets.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/config.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/cli/entry_point.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/SOURCES.txt +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-0.1.5 → pipecatcloud-0.1.7}/src/pipecatcloud.egg-info/top_level.txt +0 -0
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] - 2025-06-12
|
|
11
|
+
|
|
12
|
+
# Added
|
|
13
|
+
|
|
14
|
+
- `agent sessions` command lists session history and various statistics (avg. start times, cold starts etc.)
|
|
15
|
+
|
|
16
|
+
# Fixed
|
|
17
|
+
|
|
18
|
+
- `typer` bumped to `0.15` which was causing errors on some setups
|
|
19
|
+
|
|
20
|
+
## [0.1.6] - 2025-04-25
|
|
21
|
+
|
|
22
|
+
# Changed
|
|
23
|
+
|
|
24
|
+
- `min-instances` and `max-instances` has been changed to reflect API terminology changes.
|
|
25
|
+
|
|
10
26
|
## [0.1.5] - 2025-04-09
|
|
11
27
|
|
|
12
28
|
# Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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/
|
|
@@ -13,7 +13,7 @@ Requires-Dist: fastapi~=0.115.6
|
|
|
13
13
|
Requires-Dist: loguru~=0.7.3
|
|
14
14
|
Requires-Dist: python-dotenv~=1.0.1
|
|
15
15
|
Requires-Dist: uvicorn~=0.32.0
|
|
16
|
-
Requires-Dist: typer~=0.
|
|
16
|
+
Requires-Dist: typer~=0.15.3
|
|
17
17
|
Requires-Dist: synchronicity~=0.7.6
|
|
18
18
|
Requires-Dist: toml~=0.10.2
|
|
19
19
|
Requires-Dist: questionary~=2.1.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipecatcloud"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Cloud hosting for Pipecat AI applications"
|
|
10
10
|
license = { text = "BSD 2-Clause License" }
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
"loguru~=0.7.3",
|
|
17
17
|
"python-dotenv~=1.0.1",
|
|
18
18
|
"uvicorn~=0.32.0",
|
|
19
|
-
"typer~=0.
|
|
19
|
+
"typer~=0.15.3",
|
|
20
20
|
"synchronicity~=0.7.6",
|
|
21
21
|
"toml~=0.10.2",
|
|
22
22
|
"questionary~=2.1.0"
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
from typing import Optional, Union
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
import statistics
|
|
8
10
|
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.panel import Panel
|
|
@@ -140,3 +142,64 @@ def format_timestamp(timestamp: str) -> str:
|
|
|
140
142
|
|
|
141
143
|
# Return original if parsing fails
|
|
142
144
|
return timestamp
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def format_duration(created_at_str: str, ended_at_str: str) -> str | None:
|
|
148
|
+
"""Calculate and format session duration as HH:MM:SS"""
|
|
149
|
+
if not ended_at_str:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
|
154
|
+
ended_at = datetime.fromisoformat(ended_at_str.replace('Z', '+00:00'))
|
|
155
|
+
duration = ended_at - created_at
|
|
156
|
+
|
|
157
|
+
# Convert to total seconds
|
|
158
|
+
total_seconds = int(duration.total_seconds())
|
|
159
|
+
|
|
160
|
+
# Calculate hours, minutes, seconds
|
|
161
|
+
hours = total_seconds // 3600
|
|
162
|
+
minutes = (total_seconds % 3600) // 60
|
|
163
|
+
seconds = total_seconds % 60
|
|
164
|
+
|
|
165
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def calculate_percentiles(data: list[float]) -> tuple[float, float, float] | None:
|
|
171
|
+
"""
|
|
172
|
+
Calculate average, 5th percentile, and 95th percentile for a list of numeric data.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
data: List of float values to analyze
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (average, p5, p95) if data is provided, None if empty list.
|
|
179
|
+
For single values, p5 and p95 will equal the single value.
|
|
180
|
+
"""
|
|
181
|
+
if not data:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
avg = statistics.mean(data)
|
|
185
|
+
|
|
186
|
+
if len(data) == 1:
|
|
187
|
+
return avg, data[0], data[0]
|
|
188
|
+
|
|
189
|
+
sorted_data = sorted(data)
|
|
190
|
+
|
|
191
|
+
def percentile(data_list, p):
|
|
192
|
+
if not data_list:
|
|
193
|
+
return 0
|
|
194
|
+
k = (len(data_list) - 1) * p / 100
|
|
195
|
+
f = int(k)
|
|
196
|
+
c = k - f
|
|
197
|
+
if f + 1 < len(data_list):
|
|
198
|
+
return data_list[f] * (1 - c) + data_list[f + 1] * c
|
|
199
|
+
else:
|
|
200
|
+
return data_list[f]
|
|
201
|
+
|
|
202
|
+
p5 = percentile(sorted_data, 5)
|
|
203
|
+
p95 = percentile(sorted_data, 95)
|
|
204
|
+
|
|
205
|
+
return avg, p5, p95
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
-
|
|
8
|
+
import functools
|
|
9
|
+
from typing import Optional, Callable, Any
|
|
9
10
|
|
|
10
11
|
import toml
|
|
11
|
-
from attr import dataclass
|
|
12
|
+
from attr import dataclass, field
|
|
12
13
|
from loguru import logger
|
|
13
14
|
|
|
14
15
|
from pipecatcloud.exception import ConfigFileError
|
|
@@ -22,23 +23,37 @@ DEPLOY_STATUS_MAP = {
|
|
|
22
23
|
|
|
23
24
|
@dataclass
|
|
24
25
|
class ScalingParams:
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
min_agents: Optional[int] = 0
|
|
27
|
+
max_agents: Optional[int] = None
|
|
28
|
+
# @deprecated
|
|
29
|
+
min_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
|
|
30
|
+
# @deprecated
|
|
31
|
+
max_instances: Optional[int] = field(default=None, metadata={"deprecated": True})
|
|
27
32
|
|
|
28
33
|
def __attrs_post_init__(self):
|
|
34
|
+
# Handle deprecated fields
|
|
29
35
|
if self.min_instances is not None:
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
logger.warning("min_instances is deprecated, use min_agents instead")
|
|
37
|
+
self.min_agents = self.min_instances
|
|
32
38
|
|
|
33
39
|
if self.max_instances is not None:
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
logger.warning("max_instances is deprecated, use max_agents instead")
|
|
41
|
+
self.max_agents = self.max_instances
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
# Validation
|
|
44
|
+
if self.min_agents is not None:
|
|
45
|
+
if self.min_agents < 0:
|
|
46
|
+
raise ValueError("min_agents must be greater than or equal to 0")
|
|
47
|
+
|
|
48
|
+
if self.max_agents is not None:
|
|
49
|
+
if self.max_agents < 1:
|
|
50
|
+
raise ValueError("max_agents must be greater than 0")
|
|
51
|
+
|
|
52
|
+
if self.min_agents is not None and self.max_agents < self.min_agents:
|
|
53
|
+
raise ValueError("max_agents must be greater than or equal to min_agents")
|
|
39
54
|
|
|
40
55
|
def to_dict(self):
|
|
41
|
-
return {"
|
|
56
|
+
return {"min_agents": self.min_agents, "max_agents": self.max_agents}
|
|
42
57
|
|
|
43
58
|
|
|
44
59
|
@dataclass
|
|
@@ -95,7 +110,8 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
95
110
|
"image_credentials",
|
|
96
111
|
"secret_set",
|
|
97
112
|
"scaling",
|
|
98
|
-
"enable_krisp"
|
|
113
|
+
"enable_krisp",
|
|
114
|
+
}
|
|
99
115
|
unexpected_keys = set(config_data.keys()) - expected_keys
|
|
100
116
|
if unexpected_keys:
|
|
101
117
|
raise ConfigFileError(f"Unexpected keys in config file: {unexpected_keys}")
|
|
@@ -105,3 +121,20 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
|
|
|
105
121
|
except Exception as e:
|
|
106
122
|
logger.debug(e)
|
|
107
123
|
raise ConfigFileError(str(e))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def with_deploy_config(func: Callable) -> Callable:
|
|
127
|
+
"""
|
|
128
|
+
Decorator that loads the deploy config file and injects it into the function.
|
|
129
|
+
If the config file exists, it will be loaded and passed to the function as `deploy_config`.
|
|
130
|
+
"""
|
|
131
|
+
@functools.wraps(func)
|
|
132
|
+
def wrapper(*args, **kwargs):
|
|
133
|
+
try:
|
|
134
|
+
deploy_config = load_deploy_config_file()
|
|
135
|
+
kwargs['deploy_config'] = deploy_config
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Error loading deploy config: {e}")
|
|
138
|
+
raise ConfigFileError(str(e))
|
|
139
|
+
return func(*args, **kwargs)
|
|
140
|
+
return wrapper
|
|
@@ -317,8 +317,8 @@ class _API:
|
|
|
317
317
|
"imagePullSecretSet": deploy_config.image_credentials,
|
|
318
318
|
"secretSet": deploy_config.secret_set,
|
|
319
319
|
"autoScaling": {
|
|
320
|
-
"
|
|
321
|
-
"
|
|
320
|
+
"minAgents": deploy_config.scaling.min_agents,
|
|
321
|
+
"maxAgents": deploy_config.scaling.max_agents,
|
|
322
322
|
},
|
|
323
323
|
"enableKrisp": deploy_config.enable_krisp,
|
|
324
324
|
}
|
|
@@ -428,3 +428,11 @@ class _API:
|
|
|
428
428
|
@property
|
|
429
429
|
def agent_logs(self):
|
|
430
430
|
return self.create_api_method(self._agent_logs)
|
|
431
|
+
|
|
432
|
+
async def _agent_sessions(self, agent_name: str, org: str) -> dict | None:
|
|
433
|
+
url = f"{self.construct_api_url('services_sessions_path').format(org=org, service=agent_name)}"
|
|
434
|
+
return await self._base_request("GET", url) or {}
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def agent_sessions(self):
|
|
438
|
+
return self.create_api_method(self._agent_sessions)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import statistics
|
|
8
9
|
from enum import Enum
|
|
9
10
|
|
|
10
11
|
import aiohttp
|
|
@@ -21,11 +22,11 @@ from rich.text import Text
|
|
|
21
22
|
|
|
22
23
|
from pipecatcloud._utils.async_utils import synchronizer
|
|
23
24
|
from pipecatcloud._utils.auth_utils import requires_login
|
|
24
|
-
from pipecatcloud._utils.console_utils import console, format_timestamp
|
|
25
|
+
from pipecatcloud._utils.console_utils import console, format_duration, format_timestamp, calculate_percentiles
|
|
25
26
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
26
27
|
from pipecatcloud.cli.api import API
|
|
27
28
|
from pipecatcloud.cli.config import config
|
|
28
|
-
from pipecatcloud._utils.deploy_utils import
|
|
29
|
+
from pipecatcloud._utils.deploy_utils import with_deploy_config, DeployConfigParams
|
|
29
30
|
|
|
30
31
|
agent_cli = typer.Typer(name="agent", help="Agent management", no_args_is_help=True)
|
|
31
32
|
|
|
@@ -137,11 +138,11 @@ async def status(
|
|
|
137
138
|
if autoscaling_data:
|
|
138
139
|
scaling_renderables = [
|
|
139
140
|
Panel(
|
|
140
|
-
f"[bold]Minimum
|
|
141
|
+
f"[bold]Minimum Agents[/bold]\n{autoscaling_data.get('minAgents', 0)}",
|
|
141
142
|
expand=True,
|
|
142
143
|
),
|
|
143
144
|
Panel(
|
|
144
|
-
f"[bold]Maximum
|
|
145
|
+
f"[bold]Maximum Agents[/bold]\n{autoscaling_data.get('maxAgents', 0)}",
|
|
145
146
|
expand=True,
|
|
146
147
|
),
|
|
147
148
|
]
|
|
@@ -200,18 +201,31 @@ async def status(
|
|
|
200
201
|
@agent_cli.command(name="sessions", help="List active sessions for an agent")
|
|
201
202
|
@synchronizer.create_blocking
|
|
202
203
|
@requires_login
|
|
204
|
+
@with_deploy_config
|
|
203
205
|
async def sessions(
|
|
204
|
-
|
|
206
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
207
|
+
agent_name: str = typer.Argument(
|
|
208
|
+
None,
|
|
209
|
+
help="Name of the agent to list sessions for e.g. 'my-agent'",
|
|
210
|
+
show_default=False
|
|
211
|
+
),
|
|
212
|
+
session_id: str = typer.Option(
|
|
213
|
+
None,
|
|
214
|
+
"--id",
|
|
215
|
+
"-i",
|
|
216
|
+
help="Session ID to filter by",
|
|
217
|
+
),
|
|
205
218
|
organization: str = typer.Option(
|
|
206
219
|
None, "--organization", "-o", help="Organization to list sessions for"
|
|
207
220
|
),
|
|
208
221
|
):
|
|
209
222
|
org = organization or config.get("org")
|
|
223
|
+
agent_name = agent_name or deploy_config.agent_name
|
|
210
224
|
|
|
211
225
|
with Live(
|
|
212
226
|
console.status(f"[dim]Looking up agent with name '{agent_name}'[/dim]", spinner="dots")
|
|
213
227
|
) as live:
|
|
214
|
-
data, error = await API.
|
|
228
|
+
data, error = await API.agent_sessions(agent_name=agent_name, org=org, live=live)
|
|
215
229
|
|
|
216
230
|
live.stop()
|
|
217
231
|
|
|
@@ -219,24 +233,104 @@ async def sessions(
|
|
|
219
233
|
return typer.Exit()
|
|
220
234
|
|
|
221
235
|
if not data:
|
|
222
|
-
console.error(f"No
|
|
236
|
+
console.error(f"No session data found for agent with name '{agent_name}'")
|
|
223
237
|
return typer.Exit()
|
|
224
238
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
239
|
+
sessions_list = data.get("sessions", [])
|
|
240
|
+
total_sessions = len(sessions_list)
|
|
228
241
|
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
completed_sessions = [s for s in sessions_list if s.get("endedAt")]
|
|
243
|
+
|
|
244
|
+
durations = []
|
|
245
|
+
for session in completed_sessions:
|
|
246
|
+
try:
|
|
247
|
+
from datetime import datetime
|
|
248
|
+
created_at = datetime.fromisoformat(session["createdAt"].replace('Z', '+00:00'))
|
|
249
|
+
ended_at = datetime.fromisoformat(session["endedAt"].replace('Z', '+00:00'))
|
|
250
|
+
duration_seconds = (ended_at - created_at).total_seconds()
|
|
251
|
+
durations.append(duration_seconds)
|
|
252
|
+
except BaseException:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
bot_start_times = [s["botStartSeconds"]
|
|
256
|
+
for s in sessions_list if s.get("botStartSeconds") is not None]
|
|
257
|
+
bot_start_metrics = calculate_percentiles(bot_start_times)
|
|
258
|
+
duration_metrics = calculate_percentiles(durations)
|
|
259
|
+
cold_starts_count = sum(1 for s in sessions_list if s.get("coldStart") is True)
|
|
260
|
+
cold_start_percent = cold_starts_count / total_sessions * 100
|
|
261
|
+
|
|
262
|
+
if duration_metrics and bot_start_metrics and total_sessions > 0:
|
|
263
|
+
metric_renderables = [
|
|
264
|
+
Panel(
|
|
265
|
+
f"[bold]Total Sessions:[/bold]\n{total_sessions}\n ",
|
|
266
|
+
expand=True,
|
|
267
|
+
),
|
|
268
|
+
Panel(
|
|
269
|
+
f"[bold]Average Duration:[/bold]\n{duration_metrics[0]:.1f}s\n[dim](p5: {duration_metrics[1]:.1f}s, p95: {duration_metrics[2]:.1f}s)[/dim]",
|
|
270
|
+
expand=True,
|
|
271
|
+
),
|
|
272
|
+
Panel(
|
|
273
|
+
f"[bold]Bot Start Time:[/bold]\n{bot_start_metrics[0]:.1f}s\n[dim](p5: {bot_start_metrics[1]:.1f}s, p95: {bot_start_metrics[2]:.1f}s)[/dim]",
|
|
274
|
+
expand=True,
|
|
275
|
+
),
|
|
276
|
+
Panel(
|
|
277
|
+
f"[bold]Cold Starts:[/bold]\n{cold_starts_count}/{total_sessions}\n[dim]({cold_start_percent:.1f}%)[/dim]",
|
|
278
|
+
expand=True,
|
|
279
|
+
),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
table = Table(show_header=True, show_lines=True, border_style="dim", box=box.SIMPLE)
|
|
283
|
+
table.add_column("Session ID")
|
|
284
|
+
table.add_column("Created At")
|
|
285
|
+
table.add_column("Ended At")
|
|
286
|
+
table.add_column("Duration")
|
|
287
|
+
table.add_column("Status")
|
|
288
|
+
table.add_column("Bot Start Time")
|
|
289
|
+
table.add_column("Cold Start")
|
|
290
|
+
|
|
291
|
+
for session in data.get("sessions", []):
|
|
292
|
+
if session_id and session["sessionId"] != session_id:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
session_duration = format_duration(
|
|
296
|
+
session["createdAt"], session["endedAt"]) or "[dim]N/A[/dim]"
|
|
297
|
+
status = session.get("completionStatus", "")
|
|
298
|
+
if session["endedAt"]:
|
|
299
|
+
if status == "500":
|
|
300
|
+
status_display = "[red]Error (500)[/red]"
|
|
301
|
+
else:
|
|
302
|
+
status_display = "Complete"
|
|
303
|
+
else:
|
|
304
|
+
status_display = "[yellow]Active[/yellow]"
|
|
305
|
+
|
|
306
|
+
is_cold_start = session["coldStart"] is True
|
|
307
|
+
row_style = "on red" if is_cold_start else ""
|
|
308
|
+
|
|
309
|
+
row_data = [
|
|
310
|
+
session["sessionId"],
|
|
311
|
+
format_timestamp(
|
|
312
|
+
session["createdAt"]),
|
|
313
|
+
format_timestamp(
|
|
314
|
+
session["endedAt"]) if session["endedAt"] else "[dim]N/A[/dim]",
|
|
315
|
+
session_duration,
|
|
316
|
+
status_display,
|
|
317
|
+
f"{session['botStartSeconds']}s" if session["botStartSeconds"] is not None else "[dim]N/A[/dim]",
|
|
318
|
+
"[red]Yes[/red]" if session["coldStart"] is True else "No" if session["coldStart"] is False else "[dim]N/A[/dim]",
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
if is_cold_start:
|
|
322
|
+
row_data = [f"[{row_style}]{cell}[/]" for cell in row_data]
|
|
323
|
+
|
|
324
|
+
table.add_row(*row_data)
|
|
325
|
+
|
|
326
|
+
console.success(
|
|
327
|
+
Group(
|
|
328
|
+
Columns(
|
|
329
|
+
metric_renderables,
|
|
330
|
+
equal=True) if metric_renderables and not session_id else "",
|
|
331
|
+
table),
|
|
332
|
+
title=f"Session data for agent {agent_name} [dim]({org})[/dim]",
|
|
333
|
+
)
|
|
240
334
|
|
|
241
335
|
|
|
242
336
|
@agent_cli.command(name="scale", help="Modify agent runtime configuration")
|
|
@@ -443,7 +537,9 @@ async def deployments(
|
|
|
443
537
|
@agent_cli.command(name="start", help="Start an agent instance")
|
|
444
538
|
@synchronizer.create_blocking
|
|
445
539
|
@requires_login
|
|
540
|
+
@with_deploy_config
|
|
446
541
|
async def start(
|
|
542
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
447
543
|
agent_name: str = typer.Argument(None, help="Name of the agent to start e.g. 'my-agent'"),
|
|
448
544
|
force: bool = typer.Option(
|
|
449
545
|
False,
|
|
@@ -495,20 +591,14 @@ async def start(
|
|
|
495
591
|
)
|
|
496
592
|
|
|
497
593
|
# Load values from deployment config file (if one exists)
|
|
498
|
-
partial_config = DeployConfigParams()
|
|
499
|
-
try:
|
|
500
|
-
if deploy_config := load_deploy_config_file():
|
|
501
|
-
partial_config = deploy_config
|
|
502
|
-
except Exception as e:
|
|
503
|
-
console.error(str(e))
|
|
504
|
-
return typer.Exit()
|
|
594
|
+
partial_config = deploy_config or DeployConfigParams()
|
|
505
595
|
|
|
506
596
|
# Get agent name from pcc-deploy.toml if not provided
|
|
507
597
|
if not agent_name:
|
|
508
598
|
default_agent_name = partial_config.agent_name
|
|
509
599
|
|
|
510
600
|
if not default_agent_name:
|
|
511
|
-
console.error("No agent name provided
|
|
601
|
+
console.error("No target agent name provided")
|
|
512
602
|
return typer.Exit(1)
|
|
513
603
|
|
|
514
604
|
agent_name = default_agent_name
|
|
@@ -20,7 +20,7 @@ from pipecatcloud._utils.console_utils import console
|
|
|
20
20
|
from pipecatcloud._utils.deploy_utils import (
|
|
21
21
|
DeployConfigParams,
|
|
22
22
|
ScalingParams,
|
|
23
|
-
|
|
23
|
+
with_deploy_config,
|
|
24
24
|
)
|
|
25
25
|
from pipecatcloud.cli import PIPECAT_CLI_NAME
|
|
26
26
|
from pipecatcloud.cli.api import API
|
|
@@ -223,26 +223,28 @@ def create_deploy_command(app: typer.Typer):
|
|
|
223
223
|
@app.command(name="deploy", help="Deploy agent to Pipecat Cloud")
|
|
224
224
|
@synchronizer.create_blocking
|
|
225
225
|
@requires_login
|
|
226
|
+
@with_deploy_config
|
|
226
227
|
async def deploy(
|
|
228
|
+
deploy_config=typer.Option(None, hidden=True),
|
|
227
229
|
agent_name: str = typer.Argument(
|
|
228
230
|
None, help="Name of the agent to deploy e.g. 'my-agent'", show_default=False
|
|
229
231
|
),
|
|
230
232
|
image: str = typer.Argument(
|
|
231
233
|
None, help="Docker image location e.g. 'my-image:latest'", show_default=False
|
|
232
234
|
),
|
|
233
|
-
|
|
235
|
+
min_agents: int = typer.Option(
|
|
234
236
|
None,
|
|
235
|
-
"--min-
|
|
237
|
+
"--min-agents",
|
|
236
238
|
"-min",
|
|
237
|
-
help="Minimum number of
|
|
239
|
+
help="Minimum number of agents to keep warm",
|
|
238
240
|
rich_help_panel="Deployment Configuration",
|
|
239
241
|
min=0,
|
|
240
242
|
),
|
|
241
|
-
|
|
243
|
+
max_agents: int = typer.Option(
|
|
242
244
|
None,
|
|
243
|
-
"--max-
|
|
245
|
+
"--max-agents",
|
|
244
246
|
"-max",
|
|
245
|
-
help="Maximum number of allowed
|
|
247
|
+
help="Maximum number of allowed agents",
|
|
246
248
|
rich_help_panel="Deployment Configuration",
|
|
247
249
|
min=1,
|
|
248
250
|
max=50,
|
|
@@ -281,7 +283,34 @@ def create_deploy_command(app: typer.Typer):
|
|
|
281
283
|
"-f",
|
|
282
284
|
help="Force deployment / skip confirmation",
|
|
283
285
|
),
|
|
286
|
+
# @deprecated
|
|
287
|
+
min_instances: int = typer.Option(
|
|
288
|
+
None,
|
|
289
|
+
"--min-instances",
|
|
290
|
+
help="[Deprecated] Use --min-agents instead",
|
|
291
|
+
hidden=True,
|
|
292
|
+
min=0,
|
|
293
|
+
),
|
|
294
|
+
# @deprecated
|
|
295
|
+
max_instances: int = typer.Option(
|
|
296
|
+
None,
|
|
297
|
+
"--max-instances",
|
|
298
|
+
help="[Deprecated] Use --max-agents instead",
|
|
299
|
+
hidden=True,
|
|
300
|
+
min=1,
|
|
301
|
+
max=50,
|
|
302
|
+
),
|
|
284
303
|
):
|
|
304
|
+
|
|
305
|
+
# Handle @deprecated options
|
|
306
|
+
if min_instances is not None:
|
|
307
|
+
logger.warning("min_instances is deprecated, use min_agents instead")
|
|
308
|
+
min_agents = min_instances
|
|
309
|
+
|
|
310
|
+
if max_instances is not None:
|
|
311
|
+
logger.warning("max_instances is deprecated, use max_agents instead")
|
|
312
|
+
max_agents = max_instances
|
|
313
|
+
|
|
285
314
|
org = organization or config.get("org")
|
|
286
315
|
|
|
287
316
|
# Compose deployment config from CLI options and config file (if provided)
|
|
@@ -290,15 +319,7 @@ def create_deploy_command(app: typer.Typer):
|
|
|
290
319
|
# 2. Values from the config toml file
|
|
291
320
|
# 3. CLI command defaults
|
|
292
321
|
|
|
293
|
-
partial_config = DeployConfigParams()
|
|
294
|
-
|
|
295
|
-
# Load values from deployment config file (if one exists)
|
|
296
|
-
try:
|
|
297
|
-
if deploy_config := load_deploy_config_file():
|
|
298
|
-
partial_config = deploy_config
|
|
299
|
-
except Exception as e:
|
|
300
|
-
console.error(str(e))
|
|
301
|
-
return typer.Exit()
|
|
322
|
+
partial_config = deploy_config or DeployConfigParams()
|
|
302
323
|
|
|
303
324
|
# Override any local config values from passed CLI arguments
|
|
304
325
|
partial_config.agent_name = agent_name or partial_config.agent_name
|
|
@@ -306,12 +327,12 @@ def create_deploy_command(app: typer.Typer):
|
|
|
306
327
|
partial_config.image_credentials = credentials or partial_config.image_credentials
|
|
307
328
|
partial_config.secret_set = secret_set or partial_config.secret_set
|
|
308
329
|
partial_config.scaling = ScalingParams(
|
|
309
|
-
|
|
310
|
-
if
|
|
311
|
-
else partial_config.scaling.
|
|
312
|
-
|
|
313
|
-
if
|
|
314
|
-
else partial_config.scaling.
|
|
330
|
+
min_agents=min_agents
|
|
331
|
+
if min_agents is not None
|
|
332
|
+
else partial_config.scaling.min_agents,
|
|
333
|
+
max_agents=max_agents
|
|
334
|
+
if max_agents is not None
|
|
335
|
+
else partial_config.scaling.max_agents,
|
|
315
336
|
)
|
|
316
337
|
partial_config.enable_krisp = krisp or partial_config.enable_krisp
|
|
317
338
|
|
|
@@ -328,8 +349,11 @@ def create_deploy_command(app: typer.Typer):
|
|
|
328
349
|
table = Table(show_header=False, border_style="dim", show_edge=True, show_lines=True)
|
|
329
350
|
table.add_column("Property", style="cyan")
|
|
330
351
|
table.add_column("Value", style="green")
|
|
331
|
-
table.add_row("Min
|
|
332
|
-
|
|
352
|
+
table.add_row("Min agents", str(partial_config.scaling.min_agents))
|
|
353
|
+
if partial_config.scaling.max_agents:
|
|
354
|
+
table.add_row("Max agents", str(partial_config.scaling.max_agents))
|
|
355
|
+
else:
|
|
356
|
+
table.add_row("Max agents", "[dim]Use existing or default[/dim]")
|
|
333
357
|
|
|
334
358
|
content = Group(
|
|
335
359
|
(f"[bold white]Agent name:[/bold white] [green]{partial_config.agent_name}[/green]"),
|
|
@@ -344,11 +368,11 @@ def create_deploy_command(app: typer.Typer):
|
|
|
344
368
|
(
|
|
345
369
|
[
|
|
346
370
|
Text(
|
|
347
|
-
f"Note: Usage costs will apply for {partial_config.scaling.
|
|
371
|
+
f"Note: Usage costs will apply for {partial_config.scaling.min_agents} reserved agent(s). Please see: https://www.daily.co/pricing/pipecat-cloud/",
|
|
348
372
|
style="red",
|
|
349
|
-
)] if partial_config.scaling.
|
|
373
|
+
)] if partial_config.scaling.min_agents else [
|
|
350
374
|
Text(
|
|
351
|
-
"Note: Deploying with 0 minimum
|
|
375
|
+
"Note: Deploying with 0 minimum agents may result in cold starts",
|
|
352
376
|
style="red",
|
|
353
377
|
)]),
|
|
354
378
|
)
|
|
@@ -30,6 +30,7 @@ _SETTINGS = {
|
|
|
30
30
|
"services_path": _Setting("/v1/organizations/{org}/services"),
|
|
31
31
|
"services_logs_path": _Setting("/v1/organizations/{org}/services/{service}/logs"),
|
|
32
32
|
"services_deployments_path": _Setting("/v1/organizations/{org}/services/{service}/deployments"),
|
|
33
|
+
"services_sessions_path": _Setting("/v1/organizations/{org}/services/{service}/sessions"),
|
|
33
34
|
"start_path": _Setting("/v1/public/{service}/start"),
|
|
34
35
|
"api_keys_path": _Setting("/v1/organizations/{org}/apiKeys"),
|
|
35
36
|
"secrets_path": _Setting("/v1/organizations/{org}/secrets"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pipecatcloud
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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/
|
|
@@ -13,7 +13,7 @@ Requires-Dist: fastapi~=0.115.6
|
|
|
13
13
|
Requires-Dist: loguru~=0.7.3
|
|
14
14
|
Requires-Dist: python-dotenv~=1.0.1
|
|
15
15
|
Requires-Dist: uvicorn~=0.32.0
|
|
16
|
-
Requires-Dist: typer~=0.
|
|
16
|
+
Requires-Dist: typer~=0.15.3
|
|
17
17
|
Requires-Dist: synchronicity~=0.7.6
|
|
18
18
|
Requires-Dist: toml~=0.10.2
|
|
19
19
|
Requires-Dist: questionary~=2.1.0
|
|
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
|