pipecatcloud 0.0.6__py3-none-any.whl
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/__init__.py +12 -0
- pipecatcloud/__main__.py +9 -0
- pipecatcloud/__version__.py +6 -0
- pipecatcloud/_utils/__init__.py +0 -0
- pipecatcloud/_utils/agent_utils.py +9 -0
- pipecatcloud/_utils/async_utils.py +20 -0
- pipecatcloud/_utils/auth_utils.py +26 -0
- pipecatcloud/_utils/console_utils.py +25 -0
- pipecatcloud/_utils/deploy_utils.py +18 -0
- pipecatcloud/_utils/http_utils.py +43 -0
- pipecatcloud/agent.py +83 -0
- pipecatcloud/cli/__init__.py +3 -0
- pipecatcloud/cli/agent.py +405 -0
- pipecatcloud/cli/auth.py +274 -0
- pipecatcloud/cli/deploy.py +230 -0
- pipecatcloud/cli/entry_point.py +83 -0
- pipecatcloud/cli/init.py +0 -0
- pipecatcloud/cli/organizations.py +351 -0
- pipecatcloud/cli/run.py +28 -0
- pipecatcloud/cli/secrets.py +426 -0
- pipecatcloud/config.py +158 -0
- pipecatcloud/errors.py +11 -0
- pipecatcloud/exception.py +55 -0
- pipecatcloud-0.0.6.dist-info/METADATA +13 -0
- pipecatcloud-0.0.6.dist-info/RECORD +28 -0
- pipecatcloud-0.0.6.dist-info/WHEEL +5 -0
- pipecatcloud-0.0.6.dist-info/entry_points.txt +3 -0
- pipecatcloud-0.0.6.dist-info/top_level.txt +1 -0
pipecatcloud/cli/auth.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import itertools
|
|
3
|
+
import webbrowser
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import AsyncGenerator, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
import typer
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from pipecatcloud._utils.async_utils import synchronize_api, synchronizer
|
|
14
|
+
from pipecatcloud._utils.auth_utils import requires_login
|
|
15
|
+
from pipecatcloud._utils.http_utils import construct_api_url
|
|
16
|
+
from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS
|
|
17
|
+
from pipecatcloud.config import _remove_user_config, _store_user_config, config, user_config_path
|
|
18
|
+
|
|
19
|
+
auth_cli = typer.Typer(name="auth", help="Manage Pipecat Cloud credentials.", no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _AuthFlow:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@asynccontextmanager
|
|
27
|
+
async def start(self) -> AsyncGenerator[Tuple[Optional[str], Optional[str]], None]:
|
|
28
|
+
async with aiohttp.ClientSession() as session:
|
|
29
|
+
try:
|
|
30
|
+
async with session.post(f"{construct_api_url('login_path')}") as resp:
|
|
31
|
+
if resp.status != 200:
|
|
32
|
+
raise Exception(f"Failed to start auth flow: {resp.status}")
|
|
33
|
+
data = await resp.json()
|
|
34
|
+
self.token_flow_id = data["token_flow_id"]
|
|
35
|
+
self.wait_secret = data["wait_secret"]
|
|
36
|
+
web_url = data["web_url"]
|
|
37
|
+
|
|
38
|
+
yield (self.token_flow_id, web_url)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
yield (None, None)
|
|
42
|
+
|
|
43
|
+
async def finish(self, timeout: float = 40.0, network_timeout: float = 5.0) -> Optional[str]:
|
|
44
|
+
start_time = asyncio.get_event_loop().time()
|
|
45
|
+
async with aiohttp.ClientSession() as session:
|
|
46
|
+
while (asyncio.get_event_loop().time() - start_time) < timeout:
|
|
47
|
+
try:
|
|
48
|
+
async with session.get(
|
|
49
|
+
f"{config.get('server_url')}{config.get('login_status_path')}",
|
|
50
|
+
params={
|
|
51
|
+
"token_flow_id": self.token_flow_id,
|
|
52
|
+
"wait_secret": self.wait_secret
|
|
53
|
+
},
|
|
54
|
+
timeout=aiohttp.ClientTimeout(total=timeout + network_timeout),
|
|
55
|
+
) as resp:
|
|
56
|
+
if resp.status == 200:
|
|
57
|
+
data = await resp.json()
|
|
58
|
+
if data["status"] == "complete":
|
|
59
|
+
return data["token"]
|
|
60
|
+
if data["status"] == "failure":
|
|
61
|
+
return "failure"
|
|
62
|
+
await asyncio.sleep(2)
|
|
63
|
+
continue
|
|
64
|
+
except (asyncio.TimeoutError, aiohttp.ClientError):
|
|
65
|
+
continue
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
AuthFlow = synchronize_api(_AuthFlow)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _open_url(url: str) -> bool:
|
|
73
|
+
try:
|
|
74
|
+
browser = webbrowser.get()
|
|
75
|
+
if isinstance(browser, webbrowser.GenericBrowser) and browser.name != "open":
|
|
76
|
+
return False
|
|
77
|
+
else:
|
|
78
|
+
return browser.open_new_tab(url)
|
|
79
|
+
except webbrowser.Error:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _set_credentials(
|
|
84
|
+
token: str,
|
|
85
|
+
account_org: str,
|
|
86
|
+
):
|
|
87
|
+
console = Console()
|
|
88
|
+
with console.status("Storing user credentials", spinner="dots"):
|
|
89
|
+
_store_user_config(token, account_org)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _get_account_org(
|
|
93
|
+
token: str, active_org: Optional[str] = None) -> Optional[Tuple[str, str]]:
|
|
94
|
+
console = Console()
|
|
95
|
+
# Retrieve account organization
|
|
96
|
+
with console.status("[dim]Obtaining account organization data[/dim]", spinner="dots"):
|
|
97
|
+
async with aiohttp.ClientSession() as session:
|
|
98
|
+
async with session.get(
|
|
99
|
+
f"{construct_api_url('organization_path')}",
|
|
100
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
101
|
+
) as resp:
|
|
102
|
+
if resp.status == 200:
|
|
103
|
+
data = await resp.json()
|
|
104
|
+
organizations = data["organizations"]
|
|
105
|
+
|
|
106
|
+
# If active_org is specified, try to find it in the list
|
|
107
|
+
if active_org:
|
|
108
|
+
for org in organizations:
|
|
109
|
+
if org["name"] == active_org:
|
|
110
|
+
return org["name"], org["verboseName"]
|
|
111
|
+
|
|
112
|
+
# Default to first organization if active_org not found or not specified
|
|
113
|
+
if organizations:
|
|
114
|
+
return organizations[0]["name"], organizations[0]["verboseName"]
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
else:
|
|
118
|
+
raise Exception(f"Failed to retrieve account organization: {resp.status}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ----- Login
|
|
122
|
+
|
|
123
|
+
async def _login(active_org: Optional[str] = None):
|
|
124
|
+
console = Console()
|
|
125
|
+
auth_flow = _AuthFlow()
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
async with auth_flow.start() as (token_flow_id, web_url):
|
|
129
|
+
if web_url is None:
|
|
130
|
+
console.print(
|
|
131
|
+
Panel(
|
|
132
|
+
"Unable to connect to Pipecat Cloud API. Please check your network connection and try again.",
|
|
133
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
134
|
+
title_align="left",
|
|
135
|
+
border_style="red",
|
|
136
|
+
))
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
with console.status("Waiting for authentication in the web browser", spinner="dots"):
|
|
140
|
+
# Open the web url in the browser
|
|
141
|
+
if _open_url(web_url):
|
|
142
|
+
console.print(
|
|
143
|
+
Panel(
|
|
144
|
+
"The web browser should have opened for you to authenticate with Pipecat Cloud.\n"
|
|
145
|
+
"If it didn't, please copy this URL into your web browser manually:\n\n"
|
|
146
|
+
f"[blue][link={web_url}]{web_url}[/link][/blue]\n", ))
|
|
147
|
+
else:
|
|
148
|
+
console.print(Panel(
|
|
149
|
+
"[red]Was not able to launch web browser[/red]",
|
|
150
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
151
|
+
title_align="left",
|
|
152
|
+
border_style="red",
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
with console.status("Waiting for token flow to complete...", spinner="dots") as status:
|
|
156
|
+
for attempt in itertools.count():
|
|
157
|
+
result = await auth_flow.finish()
|
|
158
|
+
if result is not None:
|
|
159
|
+
break
|
|
160
|
+
status.update(f"Waiting for token flow to complete... (attempt {attempt + 1})")
|
|
161
|
+
if result is None:
|
|
162
|
+
console.print(Panel(
|
|
163
|
+
"[red]Authentication failed[/red]",
|
|
164
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
165
|
+
title_align="left",
|
|
166
|
+
border_style="red",
|
|
167
|
+
))
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Retrieve user namespace
|
|
171
|
+
if not active_org:
|
|
172
|
+
try:
|
|
173
|
+
account_name, account_name_verbose = await _get_account_org(result)
|
|
174
|
+
logger.debug(f"Setting namespace to {account_name_verbose}")
|
|
175
|
+
if account_name is None:
|
|
176
|
+
raise
|
|
177
|
+
except Exception:
|
|
178
|
+
console.print(
|
|
179
|
+
Panel(
|
|
180
|
+
"[red]Account has no associated namespace. Have you completed the onboarding process? Please first sign in via the web dashboard (https://dashboard.pipecat.cloud).[/red]",
|
|
181
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
182
|
+
title_align="left",
|
|
183
|
+
border_style="red",
|
|
184
|
+
))
|
|
185
|
+
return
|
|
186
|
+
else:
|
|
187
|
+
account_name = active_org
|
|
188
|
+
|
|
189
|
+
console.print(Panel(
|
|
190
|
+
"[green]Web authentication finished successfully![/green]\n"
|
|
191
|
+
f"Account details stored to [magenta]{user_config_path}[/magenta]",
|
|
192
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
193
|
+
title_align="left",
|
|
194
|
+
border_style="green",
|
|
195
|
+
))
|
|
196
|
+
await _set_credentials(result, account_name)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@auth_cli.command(name="login", help="Login to Pipecat Cloud and get a new token")
|
|
202
|
+
@synchronizer.create_blocking
|
|
203
|
+
async def login(ctx: typer.Context):
|
|
204
|
+
active_org = ctx.obj["org"]
|
|
205
|
+
await _login(active_org)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ----- Logut
|
|
209
|
+
|
|
210
|
+
@auth_cli.command(name="logout", help="Logout from Pipecat Cloud")
|
|
211
|
+
@synchronizer.create_blocking
|
|
212
|
+
@requires_login
|
|
213
|
+
async def logout():
|
|
214
|
+
"""
|
|
215
|
+
# This command should delete the session token
|
|
216
|
+
# due to networkless token verification, the token will not be invalidated
|
|
217
|
+
# it will just be removed from the local config file
|
|
218
|
+
# if we want token invalidation, we'll need to either use sign-in tokens with clerk
|
|
219
|
+
# or use a networked token verification system of our own design
|
|
220
|
+
"""
|
|
221
|
+
console = Console()
|
|
222
|
+
|
|
223
|
+
with console.status("Removing user ID", spinner="dots"):
|
|
224
|
+
_remove_user_config()
|
|
225
|
+
|
|
226
|
+
console.print(Panel(
|
|
227
|
+
"You are now logged out of Pipecat Cloud.",
|
|
228
|
+
title=f"[yellow]{PANEL_TITLE_SUCCESS}[/yellow]",
|
|
229
|
+
title_align="left",
|
|
230
|
+
border_style="yellow",
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@auth_cli.command(name="whoami", help="Display data about the current user.")
|
|
235
|
+
@synchronizer.create_blocking
|
|
236
|
+
@requires_login
|
|
237
|
+
async def whomai():
|
|
238
|
+
console = Console()
|
|
239
|
+
token = config.get("token")
|
|
240
|
+
active_org = config.get("org")
|
|
241
|
+
|
|
242
|
+
async with aiohttp.ClientSession() as session:
|
|
243
|
+
try:
|
|
244
|
+
# Retrieve user data from whoami endpoint
|
|
245
|
+
with console.status("[dim]Obtaining user data[/dim]", spinner="dots"):
|
|
246
|
+
async with session.get(
|
|
247
|
+
f"{construct_api_url('whoami_path')}",
|
|
248
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
249
|
+
) as resp:
|
|
250
|
+
if resp.status == 200:
|
|
251
|
+
data = await resp.json()
|
|
252
|
+
else:
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
# Retrieve default user organization
|
|
256
|
+
account_name, account_name_verbose = await _get_account_org(token, active_org)
|
|
257
|
+
if account_name is None:
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
console.print(Panel(
|
|
261
|
+
f"[bold]User ID:[/bold] {data['user']['userId']}\n"
|
|
262
|
+
f"[bold]Active Organization:[/bold] {account_name_verbose} [dim]({account_name})[/dim]",
|
|
263
|
+
title="whoami",
|
|
264
|
+
title_align="left",
|
|
265
|
+
border_style="dim",
|
|
266
|
+
))
|
|
267
|
+
except Exception:
|
|
268
|
+
console.print(Panel(
|
|
269
|
+
"[red]Failed to get user data. Please contact support.[/red]",
|
|
270
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
271
|
+
title_align="left",
|
|
272
|
+
border_style="red",
|
|
273
|
+
))
|
|
274
|
+
return
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console, Group
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from pipecatcloud._utils.async_utils import synchronizer
|
|
11
|
+
from pipecatcloud._utils.auth_utils import requires_login
|
|
12
|
+
from pipecatcloud._utils.deploy_utils import load_deploy_config_file
|
|
13
|
+
from pipecatcloud._utils.http_utils import construct_api_url
|
|
14
|
+
from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
TEST_CONFIG_FILE = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _poll_deployment_status(
|
|
22
|
+
token: str,
|
|
23
|
+
organization: str,
|
|
24
|
+
agent_id: str,
|
|
25
|
+
max_attempts: int = 10):
|
|
26
|
+
|
|
27
|
+
attempts = 0
|
|
28
|
+
while attempts < max_attempts:
|
|
29
|
+
with console.status(f"Monitoring status for deployment id: [bold]'{agent_id}'[/bold] (attempt {attempts + 1} of {max_attempts})", spinner="dots"):
|
|
30
|
+
async with aiohttp.ClientSession() as session:
|
|
31
|
+
response = await session.get(
|
|
32
|
+
f"{construct_api_url('services_path').format(org=organization)}/{agent_id}",
|
|
33
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
34
|
+
)
|
|
35
|
+
data = await response.json()
|
|
36
|
+
if data["body"]["ready"]:
|
|
37
|
+
console.print(Panel(
|
|
38
|
+
f"[green]Agent '{agent_id}' is ready[/green]",
|
|
39
|
+
title=f"[green]{PANEL_TITLE_SUCCESS}[/green]",
|
|
40
|
+
title_align="left",
|
|
41
|
+
border_style="green",
|
|
42
|
+
))
|
|
43
|
+
break
|
|
44
|
+
"""
|
|
45
|
+
if not data["body"]["ready"]:
|
|
46
|
+
console.print(Panel(
|
|
47
|
+
f"[red]Unable to deploy '{agent_id}'[/red]\n\n"
|
|
48
|
+
f"[dim]Message from API:[/dim]\n{data['body']['conditions'][-1]['message']}",
|
|
49
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
50
|
+
title_align="left",
|
|
51
|
+
border_style="red",
|
|
52
|
+
))
|
|
53
|
+
break
|
|
54
|
+
"""
|
|
55
|
+
await asyncio.sleep(2)
|
|
56
|
+
attempts += 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _deploy(
|
|
60
|
+
token: str,
|
|
61
|
+
agent_name: str,
|
|
62
|
+
image: str,
|
|
63
|
+
deployment_config: dict,
|
|
64
|
+
credentials: Optional[str]):
|
|
65
|
+
organization = deployment_config.get("organization")
|
|
66
|
+
if not organization or not token:
|
|
67
|
+
console.print("[red]Not logged in[/red]")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with console.status(f"Deploying [bold]'{agent_name}'[/bold]", spinner="dots"):
|
|
72
|
+
async with aiohttp.ClientSession() as session:
|
|
73
|
+
request_url = f"{construct_api_url('services_path').format(org=organization)}"
|
|
74
|
+
# Build request payload
|
|
75
|
+
payload = {
|
|
76
|
+
"serviceName": agent_name,
|
|
77
|
+
"image": image,
|
|
78
|
+
}
|
|
79
|
+
if deployment_config.get("secrets"):
|
|
80
|
+
payload["secretSet"] = deployment_config["secrets"]
|
|
81
|
+
|
|
82
|
+
if credentials:
|
|
83
|
+
payload["imagePullSecretSet"] = credentials
|
|
84
|
+
|
|
85
|
+
async with session.put(
|
|
86
|
+
request_url,
|
|
87
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
88
|
+
json=payload,
|
|
89
|
+
) as resp:
|
|
90
|
+
resp.raise_for_status()
|
|
91
|
+
except Exception as e:
|
|
92
|
+
console.print(Panel(
|
|
93
|
+
f"[red]Unable to deploy '{agent_name}'. {e}[/red]",
|
|
94
|
+
title=f"[red]{PANEL_TITLE_ERROR}[/red]",
|
|
95
|
+
title_align="left",
|
|
96
|
+
border_style="red",
|
|
97
|
+
))
|
|
98
|
+
return typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
# Poll the deployment status until it's ready
|
|
101
|
+
await _poll_deployment_status(token, organization, agent_name)
|
|
102
|
+
|
|
103
|
+
# ----- Deploy
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_deploy_command(app: typer.Typer):
|
|
107
|
+
# Note we wrap the deploy command to avoid circular imports
|
|
108
|
+
@app.command(name="deploy", help="Deploy to Pipecat Cloud")
|
|
109
|
+
@synchronizer.create_blocking
|
|
110
|
+
@requires_login
|
|
111
|
+
async def deploy(
|
|
112
|
+
ctx: typer.Context,
|
|
113
|
+
agent_name: str = typer.Argument(
|
|
114
|
+
None,
|
|
115
|
+
help="Name of the agent to deploy e.g. 'my-agent'",
|
|
116
|
+
show_default=False),
|
|
117
|
+
image: str = typer.Argument(
|
|
118
|
+
None,
|
|
119
|
+
help="Docker image location e.g. 'my-image:latest'",
|
|
120
|
+
show_default=False),
|
|
121
|
+
min_instances: int = typer.Option(
|
|
122
|
+
1,
|
|
123
|
+
"--min-instances",
|
|
124
|
+
help="Minimum number of instances to keep warm",
|
|
125
|
+
rich_help_panel="Deployment Configuration",
|
|
126
|
+
min=1),
|
|
127
|
+
max_instances: int = typer.Option(
|
|
128
|
+
20,
|
|
129
|
+
"--max-instances",
|
|
130
|
+
help="Maximum number of allowed instances",
|
|
131
|
+
rich_help_panel="Deployment Configuration",
|
|
132
|
+
min=0,
|
|
133
|
+
max=50),
|
|
134
|
+
secrets: str = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--secrets",
|
|
137
|
+
"--s",
|
|
138
|
+
help="Secret set to use for deployment",
|
|
139
|
+
rich_help_panel="Deployment Configuration",
|
|
140
|
+
),
|
|
141
|
+
organization: str = typer.Option(
|
|
142
|
+
None,
|
|
143
|
+
"--organization",
|
|
144
|
+
"--org",
|
|
145
|
+
help="Organization to deploy to",
|
|
146
|
+
rich_help_panel="Deployment Configuration",
|
|
147
|
+
),
|
|
148
|
+
credentials: str = typer.Option(
|
|
149
|
+
None,
|
|
150
|
+
"--credentials",
|
|
151
|
+
"--c",
|
|
152
|
+
help="Image pull secret to use for deployment",
|
|
153
|
+
rich_help_panel="Deployment Configuration",
|
|
154
|
+
),
|
|
155
|
+
):
|
|
156
|
+
token = ctx.obj["token"]
|
|
157
|
+
|
|
158
|
+
# Compose deployment config from CLI options and config file (if provided)
|
|
159
|
+
# Order of precedence:
|
|
160
|
+
# 1. Arguments provided to the CLI deploy command
|
|
161
|
+
# 2. Values from the config toml file
|
|
162
|
+
# 3. CLI command defaults
|
|
163
|
+
deployment_config = {
|
|
164
|
+
"min_instances": min_instances or 1,
|
|
165
|
+
"max_instances": max_instances or 20,
|
|
166
|
+
"organization": organization or ctx.obj.get("org"),
|
|
167
|
+
"secrets": secrets,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Collect passed values from CLI arguments (ignoring defaults)
|
|
171
|
+
passed_values = {}
|
|
172
|
+
for param in ctx.command.params:
|
|
173
|
+
if param.name == "agent_name" or param.name == "image":
|
|
174
|
+
continue
|
|
175
|
+
value = ctx.params.get(str(param.name))
|
|
176
|
+
# Only include if the value is different from the parameter's default
|
|
177
|
+
if value != param.default:
|
|
178
|
+
passed_values[param.name] = value
|
|
179
|
+
deployment_config.update(passed_values)
|
|
180
|
+
|
|
181
|
+
# Merge with values from deployment config file
|
|
182
|
+
if deploy_config := load_deploy_config_file():
|
|
183
|
+
for key, value in deploy_config.items():
|
|
184
|
+
deployment_config.setdefault(key, value)
|
|
185
|
+
|
|
186
|
+
final_agent_name = agent_name or deployment_config.get("agent_name")
|
|
187
|
+
final_image = image or deployment_config.get("image")
|
|
188
|
+
|
|
189
|
+
# Assert agent name and image are provided
|
|
190
|
+
if not final_agent_name:
|
|
191
|
+
raise typer.BadParameter("Agent name is required")
|
|
192
|
+
if not final_image:
|
|
193
|
+
console.print("[red]Error:[/red] Image location is required", style="bold red")
|
|
194
|
+
raise typer.BadParameter("Image location is required")
|
|
195
|
+
|
|
196
|
+
# Create and display table
|
|
197
|
+
table = Table(
|
|
198
|
+
show_header=False,
|
|
199
|
+
border_style="dim",
|
|
200
|
+
show_edge=True,
|
|
201
|
+
show_lines=True)
|
|
202
|
+
table.add_column("Property", style="cyan")
|
|
203
|
+
table.add_column("Value", style="green")
|
|
204
|
+
table.add_row("Min instances", str(deployment_config['min_instances']))
|
|
205
|
+
table.add_row("Max instances", str(deployment_config['max_instances']))
|
|
206
|
+
|
|
207
|
+
content = Group(
|
|
208
|
+
(f"[bold white]Agent name:[/bold white] [green]{final_agent_name}[/green]"),
|
|
209
|
+
(f"[bold white]Image:[/bold white] [green]{final_image}[/green]"),
|
|
210
|
+
(f"[bold white]Organization:[/bold white] [green]{deployment_config['organization']}[/green]"),
|
|
211
|
+
(f"[bold white]Secret set:[/bold white] [green]{secrets}[/green]"),
|
|
212
|
+
(f"[bold white]Image pull secret:[/bold white] [green]{credentials}[/green]"),
|
|
213
|
+
"\n[dim]Deployment configuration:[/dim]",
|
|
214
|
+
table)
|
|
215
|
+
|
|
216
|
+
console.print(
|
|
217
|
+
Panel(
|
|
218
|
+
content,
|
|
219
|
+
title="Review deployment",
|
|
220
|
+
title_align="left",
|
|
221
|
+
padding=1,
|
|
222
|
+
style="yellow",
|
|
223
|
+
border_style="yellow"))
|
|
224
|
+
|
|
225
|
+
if not typer.confirm("\nDo you want to proceed with deployment?", default=True):
|
|
226
|
+
raise typer.Abort()
|
|
227
|
+
|
|
228
|
+
# Deploy method posts the deployment config to the API
|
|
229
|
+
# and polls the deployment status until it's ready
|
|
230
|
+
await _deploy(token, final_agent_name, final_image, deployment_config, credentials)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from pipecatcloud.cli.agent import agent_cli
|
|
7
|
+
from pipecatcloud.cli.auth import auth_cli
|
|
8
|
+
from pipecatcloud.cli.deploy import create_deploy_command
|
|
9
|
+
from pipecatcloud.cli.organizations import organization_cli
|
|
10
|
+
from pipecatcloud.cli.run import create_run_command
|
|
11
|
+
from pipecatcloud.cli.secrets import secrets_cli
|
|
12
|
+
from pipecatcloud.config import config
|
|
13
|
+
|
|
14
|
+
logger.remove()
|
|
15
|
+
logger.add(sys.stderr, level=str(config.get("cli_log_level", "INFO")))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def version_callback(value: bool):
|
|
19
|
+
if value:
|
|
20
|
+
from pipecatcloud.__version__ import version
|
|
21
|
+
|
|
22
|
+
typer.echo(
|
|
23
|
+
f"ᓚᘏᗢ Pipecat Cloud Client Version: {typer.style(version, fg=typer.colors.GREEN)}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def config_callback(value: bool):
|
|
28
|
+
if value:
|
|
29
|
+
from rich import print_json
|
|
30
|
+
from rich.pretty import pprint
|
|
31
|
+
|
|
32
|
+
from pipecatcloud._utils.deploy_utils import load_deploy_config_file
|
|
33
|
+
from pipecatcloud.config import config
|
|
34
|
+
|
|
35
|
+
# Check for deploy config
|
|
36
|
+
deploy_config = load_deploy_config_file()
|
|
37
|
+
if deploy_config:
|
|
38
|
+
print("Deploy config:")
|
|
39
|
+
print_json(data=deploy_config)
|
|
40
|
+
|
|
41
|
+
print("Config:")
|
|
42
|
+
print_json(data=config.to_dict())
|
|
43
|
+
|
|
44
|
+
pprint(config.to_dict())
|
|
45
|
+
raise typer.Exit()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
entrypoint_cli_typer = typer.Typer(
|
|
49
|
+
no_args_is_help=True,
|
|
50
|
+
add_completion=False,
|
|
51
|
+
rich_markup_mode="markdown",
|
|
52
|
+
help="""
|
|
53
|
+
ᓚᘏᗢ Pipecat Cloud CLI
|
|
54
|
+
See website at https://pipecat.cloud
|
|
55
|
+
""",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@entrypoint_cli_typer.callback()
|
|
60
|
+
def pipecat(
|
|
61
|
+
ctx: typer.Context,
|
|
62
|
+
_version: bool = typer.Option(None, "--version", callback=version_callback, help="CLI version"),
|
|
63
|
+
_config: bool = typer.Option(None, "--config", callback=config_callback, help="CLI config"),
|
|
64
|
+
):
|
|
65
|
+
if not ctx.obj:
|
|
66
|
+
ctx.obj = {}
|
|
67
|
+
|
|
68
|
+
# All commands require an active namespace (organization)
|
|
69
|
+
# The CLI reads config data and sets the users currently active org in context
|
|
70
|
+
# which is used as a default when an `--org` flag is not provided
|
|
71
|
+
ctx.obj["org"] = config.get("org")
|
|
72
|
+
ctx.obj["token"] = config.get("token")
|
|
73
|
+
ctx.obj["default_public_key"] = config.get("default_public_key", None)
|
|
74
|
+
ctx.obj["default_public_key_name"] = config.get("default_public_key_name", None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
create_deploy_command(entrypoint_cli_typer)
|
|
78
|
+
create_run_command(entrypoint_cli_typer)
|
|
79
|
+
entrypoint_cli_typer.add_typer(auth_cli, rich_help_panel="Commands")
|
|
80
|
+
entrypoint_cli_typer.add_typer(organization_cli, rich_help_panel="Commands")
|
|
81
|
+
entrypoint_cli_typer.add_typer(agent_cli, rich_help_panel="Commands")
|
|
82
|
+
entrypoint_cli_typer.add_typer(secrets_cli, rich_help_panel="Commands")
|
|
83
|
+
entrypoint_cli = typer.main.get_command(entrypoint_cli_typer)
|
pipecatcloud/cli/init.py
ADDED
|
File without changes
|