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.
@@ -0,0 +1,12 @@
1
+ import os
2
+ import sys
3
+
4
+ from loguru import logger
5
+
6
+ PIPECAT_CLI_NAME = "pcc"
7
+ PIPECAT_CREDENTIALS_PATH = "~/.pipecatcloud.toml"
8
+ PIPECAT_DEPLOY_CONFIG_PATH = ".pcc-deploy.toml"
9
+
10
+
11
+ logger.remove()
12
+ logger.add(sys.stderr, level=str(os.getenv("PCC_LOG_LEVEL", "INFO")))
@@ -0,0 +1,9 @@
1
+ from pipecatcloud.cli.entry_point import entrypoint_cli
2
+
3
+
4
+ def main():
5
+ entrypoint_cli()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+
3
+ import toml
4
+
5
+ pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
6
+ version = toml.load(pyproject_path)["project"]["version"]
File without changes
@@ -0,0 +1,9 @@
1
+ def handle_agent_start_error(response_code: int) -> str:
2
+ if response_code == 404:
3
+ return "PCC-1005"
4
+ elif response_code == 400:
5
+ return "PCC-1004"
6
+ elif response_code == 401:
7
+ return "PCC-1006"
8
+ else:
9
+ return "PCC-1000"
@@ -0,0 +1,20 @@
1
+ import inspect
2
+ from typing import (
3
+ TypeVar,
4
+ )
5
+
6
+ import synchronicity
7
+
8
+ synchronizer = synchronicity.Synchronizer()
9
+
10
+
11
+ def synchronize_api(obj, target_module=None):
12
+ if inspect.isclass(obj) or inspect.isfunction(obj):
13
+ blocking_name = obj.__name__.lstrip("_")
14
+ elif isinstance(obj, TypeVar):
15
+ blocking_name = "_BLOCKING_" + obj.__name__
16
+ else:
17
+ blocking_name = None
18
+ if target_module is None:
19
+ target_module = obj.__module__
20
+ return synchronizer.create_blocking(obj, blocking_name, target_module=target_module)
@@ -0,0 +1,26 @@
1
+ import functools
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+
6
+ from pipecatcloud import PIPECAT_CLI_NAME
7
+ from pipecatcloud.cli import PANEL_TITLE_ERROR
8
+ from pipecatcloud.config import config
9
+
10
+
11
+ def requires_login(func):
12
+ @functools.wraps(func)
13
+ def wrapper(*args, **kwargs):
14
+ console = Console()
15
+ org = config.get("org")
16
+ token = config.get("token")
17
+ if org is None or token is None:
18
+ console.print(Panel(
19
+ f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
20
+ title=f"[red]{PANEL_TITLE_ERROR}[/red]",
21
+ title_align="left",
22
+ border_style="red",
23
+ ))
24
+ return
25
+ return func(*args, **kwargs)
26
+ return wrapper
@@ -0,0 +1,25 @@
1
+
2
+ from typing import Optional
3
+
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+
7
+ from pipecatcloud.cli import PANEL_TITLE_ERROR
8
+ from pipecatcloud.errors import ERROR_CODES
9
+
10
+ console = Console()
11
+
12
+
13
+ def print_api_error(error_code: Optional[str], title: str):
14
+ error_message = "Unknown error" if not error_code else ERROR_CODES.get(
15
+ error_code, "Unknown error")
16
+
17
+ console.print(Panel(
18
+ f"[red]{title}[/red]\n\n"
19
+ f"[dim]Error message:[/dim]\n{error_message}",
20
+ title=f"[bold red]{PANEL_TITLE_ERROR} - {error_code}[/bold red]",
21
+ subtitle=f"[dim]Docs: https://docs.pipecat.cloud/troubleshooting/#{error_code}[/dim]",
22
+ title_align="left",
23
+ subtitle_align="left",
24
+ border_style="red"
25
+ ))
@@ -0,0 +1,18 @@
1
+ from loguru import logger
2
+
3
+
4
+ def load_deploy_config_file() -> dict | None:
5
+ import os
6
+
7
+ import toml
8
+
9
+ from pipecatcloud.config import deploy_config_path
10
+
11
+ logger.info(f"Deploy config path: {deploy_config_path}")
12
+ logger.info(f"Deploy config path exists: {os.path.exists(deploy_config_path)}")
13
+
14
+ try:
15
+ with open(deploy_config_path, "r") as f:
16
+ return toml.load(f)
17
+ except Exception:
18
+ return None
@@ -0,0 +1,43 @@
1
+ import contextlib
2
+ import logging
3
+ import socket
4
+
5
+ from aiohttp.web import Application
6
+ from aiohttp.web_runner import AppRunner, SockSite
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @contextlib.asynccontextmanager
12
+ async def run_temporary_http_server(app: Application):
13
+ sock = socket.socket()
14
+ try:
15
+ sock.bind(("", 0))
16
+ port = sock.getsockname()[1]
17
+ host = f"http://127.0.0.1:{port}"
18
+
19
+ runner = AppRunner(app)
20
+ await runner.setup()
21
+ site = SockSite(runner, sock=sock)
22
+ await site.start()
23
+ try:
24
+ yield host
25
+ finally:
26
+ await runner.cleanup()
27
+ except Exception as e:
28
+ logger.error(f"Error setting up temporary HTTP server: {e}")
29
+ raise
30
+ finally:
31
+ sock.close()
32
+
33
+
34
+ def construct_api_url(path: str) -> str:
35
+ from pipecatcloud.config import config
36
+
37
+ if not config.get("server_url", ""):
38
+ raise ValueError("Server URL is not set")
39
+
40
+ if not config.get(path, ""):
41
+ raise ValueError(f"Endpoint {path} is not set")
42
+
43
+ return f"{config.get('server_url', '')}{config.get(path, '')}"
pipecatcloud/agent.py ADDED
@@ -0,0 +1,83 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ import aiohttp
5
+ from loguru import logger
6
+
7
+ from pipecatcloud._utils.agent_utils import handle_agent_start_error
8
+ from pipecatcloud._utils.http_utils import construct_api_url
9
+ from pipecatcloud.exception import AgentStartError
10
+
11
+
12
+ @dataclass
13
+ class AgentParams:
14
+ data: Optional[dict] = None
15
+ use_daily: Optional[bool] = False
16
+
17
+
18
+ class Agent:
19
+ def __init__(
20
+ self,
21
+ agent_name: str,
22
+ organization: str,
23
+ api_key: str,
24
+ params: Optional[AgentParams] = None,
25
+ ):
26
+ self.agent_name = agent_name
27
+ self.api_key = api_key
28
+ self.organization = organization
29
+
30
+ if not self.organization or not self.agent_name:
31
+ raise ValueError("Organization and agent name are required")
32
+
33
+ self.params = params or AgentParams()
34
+
35
+ async def start(self) -> bool:
36
+ if not self.api_key:
37
+ raise AgentStartError(error_code="PCC-1002")
38
+
39
+ # @TODO: Public route not yet implemented
40
+ """
41
+ # Check deployment heath
42
+ error_code = None
43
+ agent_data = None
44
+
45
+ try:
46
+ async with aiohttp.ClientSession() as session:
47
+ response = await session.get(
48
+ f"{construct_api_url('services_path').format(org=self.organization)}/{self.agent_name}",
49
+ headers={"Authorization": f"Bearer {self.api_key}"},
50
+ )
51
+ if response.status != 200:
52
+ error_code = str(response.status)
53
+ response.raise_for_status()
54
+ agent_data = await response.json()
55
+ if agent_data is None or not agent_data["ready"]:
56
+ raise AgentNotHealthyError(error_code=error_code)
57
+ except Exception as e:
58
+ raise Exception(f"Error checking agent health: {e}")
59
+ """
60
+
61
+ logger.debug(f"Starting agent {self.agent_name}")
62
+
63
+ start_error_code = None
64
+ try:
65
+ async with aiohttp.ClientSession() as session:
66
+ response = await session.post(
67
+ f"{construct_api_url('start_path').format(service=self.agent_name)}",
68
+ headers={"Authorization": f"Bearer {self.api_key}"},
69
+ json={
70
+ "createDailyRoom": bool(self.params.use_daily),
71
+ "body": self.params.data
72
+ }
73
+ )
74
+ if response.status != 200:
75
+ start_error_code = handle_agent_start_error(response.status)
76
+ response.raise_for_status()
77
+ else:
78
+ logger.debug(f"Agent {self.agent_name} started successfully")
79
+ except Exception as e:
80
+ logger.error(f"Error starting agent {self.agent_name}: {e}")
81
+ raise AgentStartError(error_code=start_error_code)
82
+
83
+ return True
@@ -0,0 +1,3 @@
1
+ PANEL_TITLE_ERROR = "ᓚᘏᗢ Error"
2
+ PANEL_TITLE_SUCCESS = "ᓚᘏᗢ Success"
3
+ PANEL_TITLE_NOT_IMPLEMENTED = "ᓚᘏᗢ Not yet implemented"
@@ -0,0 +1,405 @@
1
+
2
+ import aiohttp
3
+ import questionary
4
+ import typer
5
+ from loguru import logger
6
+ from rich import box
7
+ from rich.console import Console, Group
8
+ from rich.live import Live
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from pipecatcloud._utils.async_utils import synchronizer
13
+ from pipecatcloud._utils.auth_utils import requires_login
14
+ from pipecatcloud._utils.console_utils import print_api_error
15
+ from pipecatcloud._utils.http_utils import construct_api_url
16
+ from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS
17
+
18
+ agent_cli = typer.Typer(
19
+ name="agent", help="Agent management.", no_args_is_help=True
20
+ )
21
+
22
+
23
+ # ----- Agent Methods -----
24
+
25
+ async def lookup_agent(token: str, org: str, agent_name: str) -> dict | None:
26
+ try:
27
+ error_code = None
28
+ async with aiohttp.ClientSession() as session:
29
+ response = await session.get(
30
+ f"{construct_api_url('services_path').format(org=org)}/{agent_name}",
31
+ headers={"Authorization": f"Bearer {token}"},
32
+ )
33
+ if response.status != 200:
34
+ error_code = str(response.status)
35
+ response.raise_for_status()
36
+ data = await response.json()
37
+ return data["body"]
38
+ except Exception as e:
39
+ logger.debug(e)
40
+ print_api_error(error_code, f"Unable to get deployments for {agent_name}")
41
+ return None
42
+
43
+
44
+ # ----- Agent Commands -----
45
+
46
+ @agent_cli.command(name="status", help="Get status of agent deployment")
47
+ @synchronizer.create_blocking
48
+ @requires_login
49
+ async def status(
50
+ ctx: typer.Context,
51
+ agent_name: str = typer.Argument(
52
+ help="Name of the agent to get status of e.g. 'my-agent'"
53
+ ),
54
+ organization: str = typer.Option(
55
+ None,
56
+ "--organization",
57
+ "--org",
58
+ help="Organization to get status of agent for"
59
+ ),
60
+ ):
61
+ console = Console()
62
+ token = ctx.obj["token"]
63
+ org = organization or ctx.obj["org"]
64
+
65
+ error_code = None
66
+ try:
67
+ with console.status(f"Checking status of agent: [bold]'{agent_name}'[/bold]", spinner="dots"):
68
+ async with aiohttp.ClientSession() as session:
69
+ response = await session.get(
70
+ f"{construct_api_url('services_path').format(org=org)}/{agent_name}",
71
+ headers={"Authorization": f"Bearer {token}"},
72
+ )
73
+ if response.status != 200:
74
+ error_code = str(response.status)
75
+ response.raise_for_status()
76
+
77
+ data = await response.json()
78
+
79
+ conditions = data["body"]["conditions"]
80
+ table = Table(
81
+ show_header=True,
82
+ show_lines=True,
83
+ border_style="dim",
84
+ box=box.SIMPLE
85
+ )
86
+ table.add_column("Date")
87
+ table.add_column("Status")
88
+ table.add_column("Type")
89
+ table.add_column("Message")
90
+ table.add_column("Reason")
91
+
92
+ for condition in conditions:
93
+ table.add_row(
94
+ condition['lastTransitionTime'],
95
+ f"[{'red' if condition['status'] == 'False' else 'green'}]{condition['status']}[/]",
96
+ condition['type'],
97
+ condition.get('message', 'No message'),
98
+ condition.get('reason', 'No reason'),
99
+ )
100
+
101
+ color = "bold green" if data['body']['ready'] else "bold yellow"
102
+ console.print(
103
+ Panel(
104
+ Group(
105
+ Panel(
106
+ f"[{color}]Health: {'Ready' if data['body']['ready'] else 'Stopped'}[/]",
107
+ border_style="green" if data['body']['ready'] else "yellow",
108
+ expand=False,
109
+ ),
110
+ table,
111
+ ),
112
+ padding=1,
113
+ title=f"[bold]Status for agent: {agent_name}[/bold]",
114
+ title_align="left",
115
+ ))
116
+ except Exception:
117
+ print_api_error(error_code, f"Unable to get status for {agent_name}")
118
+ return typer.Exit(1)
119
+
120
+
121
+ @agent_cli.command(name="list", help="List agents in an organization.")
122
+ @synchronizer.create_blocking
123
+ @requires_login
124
+ async def list(
125
+ ctx: typer.Context,
126
+ organization: str = typer.Option(
127
+ None,
128
+ "--organization",
129
+ "--org",
130
+ "-o",
131
+ help="Organization to list agents for"
132
+ ),
133
+ ):
134
+ console = Console()
135
+ token = ctx.obj["token"]
136
+ org = organization or ctx.obj["org"]
137
+
138
+ with console.status(f"Fetching agents for organization: [bold]'{org}'[/bold]", spinner="dots"):
139
+ async with aiohttp.ClientSession() as session:
140
+ response = await session.get(
141
+ f"{construct_api_url('services_path').format(org=org)}",
142
+ headers={"Authorization": f"Bearer {token}"},
143
+ )
144
+ data = await response.json()
145
+
146
+ if "error" in data:
147
+ console.print(Panel(
148
+ f"[red]Unable to get agents for '{org}'[/red]\n\n"
149
+ f"[dim]API response:[/dim] {data['error']}",
150
+ title=f"[red]{PANEL_TITLE_ERROR}[/red]",
151
+ title_align="left",
152
+ border_style="red",
153
+ ))
154
+ else:
155
+ table = Table(show_header=True, show_lines=True, border_style="dim", box=box.SIMPLE)
156
+ table.add_column("Name")
157
+ table.add_column("ID")
158
+ table.add_column("Active Deployment")
159
+ table.add_column("Created At")
160
+ table.add_column("Updated At")
161
+
162
+ for service in data['services']:
163
+ table.add_row(
164
+ f"[bold]{service['name']}[/bold]",
165
+ service['id'],
166
+ service['activeDeploymentId'],
167
+ service['createdAt'],
168
+ service['updatedAt']
169
+ )
170
+
171
+ console.print(Panel(
172
+ table,
173
+ padding=1,
174
+ title=f"[bold]Agents for organization: {org}[/bold]",
175
+ title_align="left",
176
+ border_style="green",
177
+ ))
178
+
179
+
180
+ @agent_cli.command(name="logs", help="Get logs for the given agent.")
181
+ @synchronizer.create_blocking
182
+ @requires_login
183
+ async def logs(ctx: typer.Context, agent_name: str):
184
+ console = Console()
185
+ token = ctx.obj["token"]
186
+ org = ctx.obj["org"]
187
+
188
+ error_code = None
189
+ try:
190
+ with console.status(f"Fetching logs for agent: [bold]'{agent_name}'[/bold]", spinner="dots"):
191
+ async with aiohttp.ClientSession() as session:
192
+ response = await session.get(
193
+ f"{construct_api_url('services_logs_path').format(org=org, service=agent_name)}?limit=100&order=desc",
194
+ headers={"Authorization": f"Bearer {token}"},
195
+ )
196
+ if response.status != 200:
197
+ error_code = str(response.status)
198
+ response.raise_for_status()
199
+ data = await response.json()
200
+ console.print(data)
201
+ except Exception:
202
+ print_api_error(error_code, f"Unable to get logs for {agent_name}")
203
+
204
+
205
+ @agent_cli.command(name="delete", help="Delete an agent.")
206
+ @synchronizer.create_blocking
207
+ @requires_login
208
+ async def delete(
209
+ ctx: typer.Context,
210
+ agent_name: str,
211
+ organization: str = typer.Option(
212
+ None,
213
+ "--organization",
214
+ "--org",
215
+ help="Organization to delete agent from",
216
+ ),
217
+ ):
218
+ console = Console()
219
+ token = ctx.obj["token"]
220
+ org = organization or ctx.obj["org"]
221
+
222
+ with console.status(f"Deleting agent: [bold]'{agent_name}'[/bold]", spinner="dots"):
223
+ async with aiohttp.ClientSession() as session:
224
+ response = await session.delete(
225
+ f"{construct_api_url('services_path').format(org=org)}/{agent_name}",
226
+ headers={"Authorization": f"Bearer {token}"},
227
+ )
228
+ data = await response.json()
229
+ console.print(data)
230
+
231
+
232
+ @agent_cli.command(name="deployments", help="Get deployments for an agent.")
233
+ @synchronizer.create_blocking
234
+ @requires_login
235
+ async def deployments(
236
+ ctx: typer.Context,
237
+ agent_name: str,
238
+ organization: str = typer.Option(
239
+ None,
240
+ "--organization",
241
+ "--org",
242
+ help="Organization to get deployments for",
243
+ ),
244
+ ):
245
+ console = Console()
246
+ token = ctx.obj["token"]
247
+ org = organization or ctx.obj["org"]
248
+
249
+ error_code = None
250
+
251
+ try:
252
+ with console.status(f"Fetching deployments for agent: [bold]'{agent_name}'[/bold]", spinner="dots"):
253
+ async with aiohttp.ClientSession() as session:
254
+ response = await session.get(
255
+ f"{construct_api_url('services_deployments_path').format(org=org, service=agent_name)}",
256
+ headers={"Authorization": f"Bearer {token}"},
257
+ )
258
+ if response.status != 200:
259
+ error_code = str(response.status)
260
+ response.raise_for_status()
261
+ data = await response.json()
262
+
263
+ table = Table(
264
+ show_header=True,
265
+ show_lines=True,
266
+ border_style="dim",
267
+ box=box.SIMPLE,
268
+ )
269
+ table.add_column("ID")
270
+ table.add_column("Node Type")
271
+ table.add_column("Image")
272
+ table.add_column("Created At")
273
+ table.add_column("Updated At")
274
+
275
+ for deployment in data['deployments']:
276
+ table.add_row(
277
+ deployment["id"],
278
+ deployment["manifest"]["spec"]["dailyNodeType"],
279
+ deployment["manifest"]["spec"]["image"],
280
+ deployment["createdAt"],
281
+ deployment["updatedAt"],
282
+ )
283
+
284
+ console.print(Panel(
285
+ table,
286
+ title=f"[bold]Deployments for agent: {agent_name}[/bold]",
287
+ title_align="left",
288
+ ))
289
+ except Exception as e:
290
+ logger.debug(e)
291
+ print_api_error(error_code, f"Unable to get deployments for {agent_name}")
292
+
293
+
294
+ @agent_cli.command(name="start", help="Start an agent instance")
295
+ @synchronizer.create_blocking
296
+ @requires_login
297
+ async def start(
298
+ ctx: typer.Context,
299
+ agent_name: str,
300
+ force: bool = typer.Option(
301
+ False,
302
+ "--force",
303
+ "--f",
304
+ help="Force the start request",
305
+ ),
306
+ organization: str = typer.Option(
307
+ None,
308
+ "--organization",
309
+ "--org",
310
+ help="Organization to start agent for",
311
+ ),
312
+ api_key: str = typer.Option(
313
+ None,
314
+ "--api-key",
315
+ "--key",
316
+ help="Public API key to use for starting agent",
317
+ ),
318
+ data: str = typer.Option(
319
+ None,
320
+ "--data",
321
+ "--d",
322
+ help="Data to pass to the agent (stringified JSON)",
323
+ ),
324
+ use_daily: bool = typer.Option(
325
+ False,
326
+ "--use-daily",
327
+ "--daily",
328
+ help="Create a Daily WebRTC session for the agent",
329
+ ),
330
+ ):
331
+ console = Console()
332
+
333
+ token = ctx.obj["token"]
334
+ org = organization or ctx.obj["org"]
335
+ default_public_key = api_key or ctx.obj["default_public_key"]
336
+ default_public_key_name = "CLI provided" if api_key else ctx.obj["default_public_key_name"]
337
+
338
+ if not default_public_key:
339
+ print_api_error("PCC-1002", f"Unable to start agent '{agent_name}' without public api key")
340
+ return typer.Exit(1)
341
+
342
+ # Confirm start request
343
+ if not force:
344
+ console.print(Panel(
345
+ f"Agent Name: {agent_name}\n"
346
+ f"Organization: {org}\n"
347
+ f"Public API Key: {default_public_key_name} [dim]{default_public_key}[/dim]\n"
348
+ f"Use Daily: {use_daily}\n"
349
+ f"Data: {data}",
350
+ title=f"[bold]Start Request for agent: {agent_name}[/bold]",
351
+ title_align="left",
352
+ border_style="yellow",
353
+ ))
354
+ if not await questionary.confirm("Are you sure you want to start this agent?").ask_async():
355
+ console.print("[bold]Aborting start request[/bold]")
356
+ return typer.Exit(1)
357
+
358
+ # Check if agent exists and is healthy
359
+ with Live(console.status(f"Checking deployment health for agent: [bold]'{agent_name}'[/bold]", spinner="dots"), refresh_per_second=4) as live:
360
+ agent_data = await lookup_agent(token, org, agent_name)
361
+ if agent_data is None or not agent_data["ready"]:
362
+ print_api_error("PCC-1001", f"Unable to start agent '{agent_name}'")
363
+ return typer.Exit(1)
364
+
365
+ live.update(
366
+ console.status(
367
+ f"Sending start request with key: {default_public_key_name}",
368
+ spinner="dots"))
369
+
370
+ error_code = None
371
+ try:
372
+ async with aiohttp.ClientSession() as session:
373
+ response = await session.post(
374
+ f"{construct_api_url('start_path').format(service=agent_name)}",
375
+ headers={"Authorization": f"Bearer {default_public_key}"},
376
+ json={
377
+ "createDailyRoom": bool(use_daily),
378
+ "body": {}
379
+ }
380
+ )
381
+ if response.status != 200:
382
+ if response.status == 400:
383
+ error_code = "PCC-1004"
384
+ else:
385
+ error_code = "PCC-1003"
386
+ response.raise_for_status()
387
+
388
+ except Exception as e:
389
+ live.update(console.status(f"Agent '{agent_name}' failed to start", spinner="dots"))
390
+ live.stop()
391
+ logger.debug(e)
392
+ print_api_error(
393
+ error_code,
394
+ f"Unable to start agent '{agent_name}'. Please check logs for more information.")
395
+ return typer.Exit(1)
396
+
397
+ live.update(console.status(f"Agent '{agent_name}' started successfully", spinner="dots"))
398
+ live.stop()
399
+
400
+ console.print(Panel(
401
+ f"Agent '{agent_name}' started successfully",
402
+ title=f"{PANEL_TITLE_SUCCESS}",
403
+ title_align="left",
404
+ border_style="green",
405
+ ))