pipecatcloud 0.0.9__tar.gz → 0.0.10__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 (50) hide show
  1. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/CHANGELOG.md +15 -0
  2. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/PKG-INFO +21 -11
  3. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/README.md +20 -10
  4. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/pyproject.toml +1 -1
  5. pipecatcloud-0.0.10/src/pipecatcloud/__init__.py +33 -0
  6. pipecatcloud-0.0.10/src/pipecatcloud/_utils/console_utils.py +124 -0
  7. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/api.py +22 -5
  8. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/agent.py +80 -71
  9. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/deploy.py +2 -32
  10. pipecatcloud-0.0.10/src/pipecatcloud/cli/commands/init.py +112 -0
  11. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/secrets.py +0 -7
  12. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/config.py +2 -2
  13. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/exception.py +13 -11
  14. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/session.py +19 -10
  15. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/PKG-INFO +21 -11
  16. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/SOURCES.txt +0 -2
  17. pipecatcloud-0.0.9/src/pipecatcloud/__init__.py +0 -7
  18. pipecatcloud-0.0.9/src/pipecatcloud/_utils/agent_utils.py +0 -9
  19. pipecatcloud-0.0.9/src/pipecatcloud/_utils/console_utils.py +0 -78
  20. pipecatcloud-0.0.9/src/pipecatcloud/cli/commands/init.py +0 -16
  21. pipecatcloud-0.0.9/src/pipecatcloud/errors.py +0 -11
  22. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/.github/workflows/publish-pypi.yml +0 -0
  23. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/.github/workflows/publish-test.yml +0 -0
  24. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/.github/workflows/python-publish.yml +0 -0
  25. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/.gitignore +0 -0
  26. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/LICENSE +0 -0
  27. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/dev-requirements.txt +0 -0
  28. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/pipecat-cloud.png +0 -0
  29. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/publish-pypi.yml +0 -0
  30. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/publish-test.yml +0 -0
  31. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/setup.cfg +0 -0
  32. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/__main__.py +0 -0
  33. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/__version__.py +0 -0
  34. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/_utils/__init__.py +0 -0
  35. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/_utils/async_utils.py +0 -0
  36. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  37. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
  38. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/_utils/local_runner.py +0 -0
  39. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/__init__.py +0 -0
  40. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/api.py +0 -0
  41. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  42. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/auth.py +0 -0
  43. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  44. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/commands/run.py +0 -0
  45. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/config.py +0 -0
  46. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud/cli/entry_point.py +0 -0
  47. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  48. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  49. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/requires.txt +0 -0
  50. {pipecatcloud-0.0.9 → pipecatcloud-0.0.10}/src/pipecatcloud.egg-info/top_level.txt +0 -0
@@ -5,6 +5,21 @@ 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.0.10] - 2025-03-04
9
+
10
+ ### Added
11
+ - `init` convenience command will now populate the working directory with files from the starter project
12
+ - `agent log` allows for optional severity level filtering
13
+
14
+ ### Changed
15
+ - `agent status` and `deploy` no longer show superfluous data
16
+ - `session.py` now correctly handles errors when starting agents
17
+ - `secrets set` no longer prompts twice for confirmation if the secret set does not exist
18
+
19
+ ### Removed
20
+ - `errors.py` removed as redundant (error message and code returned via API)
21
+ - `agent_utils.py` removed as redundant (error message and code returned via API)
22
+
8
23
  ## [0.0.9] - 2025-02-27
9
24
 
10
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pipecatcloud
3
- Version: 0.0.9
3
+ Version: 0.0.10
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/
@@ -70,16 +70,26 @@ pcc auth login
70
70
 
71
71
  ### Usage in Python scripts
72
72
 
73
- If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.agent` module.
73
+ If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.session` module.
74
74
 
75
75
  ```python
76
- from pipecatcloud.agent import Agent
77
-
78
- agent = Agent(
79
- agent_name="your-agent-name",
80
- organization="your-organization",
81
- api_key="your-api-key",
82
- )
83
-
84
- await agent.start()
76
+ from pipecatcloud.session import Session
77
+ from pipecatcloud.exception import AgentStartError
78
+ import asyncio
79
+
80
+ async def main():
81
+ session = Session(
82
+ agent_name="your-agent-name",
83
+ api_key="pk_...",
84
+ )
85
+
86
+ try:
87
+ await session.start()
88
+ except AgentStartError as e:
89
+ print(e)
90
+ except Exception as e:
91
+ raise (e)
92
+
93
+ if __name__ == "__main__":
94
+ asyncio.run(main())
85
95
  ```
@@ -50,16 +50,26 @@ pcc auth login
50
50
 
51
51
  ### Usage in Python scripts
52
52
 
53
- If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.agent` module.
53
+ If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.session` module.
54
54
 
55
55
  ```python
56
- from pipecatcloud.agent import Agent
57
-
58
- agent = Agent(
59
- agent_name="your-agent-name",
60
- organization="your-organization",
61
- api_key="your-api-key",
62
- )
63
-
64
- await agent.start()
56
+ from pipecatcloud.session import Session
57
+ from pipecatcloud.exception import AgentStartError
58
+ import asyncio
59
+
60
+ async def main():
61
+ session = Session(
62
+ agent_name="your-agent-name",
63
+ api_key="pk_...",
64
+ )
65
+
66
+ try:
67
+ await session.start()
68
+ except AgentStartError as e:
69
+ print(e)
70
+ except Exception as e:
71
+ raise (e)
72
+
73
+ if __name__ == "__main__":
74
+ asyncio.run(main())
65
75
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pipecatcloud"
7
- version = "0.0.9"
7
+ version = "0.0.10"
8
8
  requires-python = ">=3.10"
9
9
  description = "Cloud hosting for Pipecat AI applications"
10
10
  license = { text = "BSD 2-Clause License" }
@@ -0,0 +1,33 @@
1
+ import os
2
+ import sys
3
+
4
+ from loguru import logger
5
+
6
+ from .exception import (
7
+ AgentNotHealthyError,
8
+ AgentStartError,
9
+ AuthError,
10
+ ConfigError,
11
+ ConfigFileError,
12
+ Error,
13
+ InvalidError,
14
+ )
15
+ from .session import Session, SessionParams
16
+
17
+ logger.remove()
18
+ logger.add(sys.stderr, level=str(os.getenv("PCC_LOG_LEVEL", "INFO")))
19
+
20
+
21
+ __all__ = [
22
+ # Session classes
23
+ "Session",
24
+ "SessionParams",
25
+ # Exception classes
26
+ "Error",
27
+ "ConfigFileError",
28
+ "AuthError",
29
+ "InvalidError",
30
+ "ConfigError",
31
+ "AgentNotHealthyError",
32
+ "AgentStartError",
33
+ ]
@@ -0,0 +1,124 @@
1
+ from typing import Optional, Union
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+
6
+ from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS, PIPECAT_CLI_NAME
7
+
8
+
9
+ class PipecatConsole(Console):
10
+ def success(
11
+ self,
12
+ message,
13
+ title: Optional[str] = None,
14
+ title_extra: Optional[str] = None,
15
+ subtitle: Optional[str] = None):
16
+
17
+ if not title:
18
+ title = f"{PANEL_TITLE_SUCCESS}{f' - {title_extra}' if title_extra is not None else ''}"
19
+
20
+ self.print(
21
+ Panel(
22
+ message,
23
+ title=f"[bold green]{title}[/bold green]",
24
+ subtitle=subtitle,
25
+ title_align="left",
26
+ subtitle_align="left",
27
+ border_style="green"))
28
+
29
+ def error(
30
+ self,
31
+ message,
32
+ title: Optional[str] = None,
33
+ title_extra: Optional[str] = None,
34
+ subtitle: Optional[str] = None):
35
+
36
+ if not title:
37
+ title = f"{PANEL_TITLE_ERROR}{f' - {title_extra}' if title_extra is not None else ''}"
38
+
39
+ self.print(
40
+ Panel(
41
+ message,
42
+ title=f"[bold red]{title}[/bold red]",
43
+ subtitle=subtitle,
44
+ title_align="left",
45
+ subtitle_align="left",
46
+ border_style="red"))
47
+
48
+ def cancel(self):
49
+ self.print("[yellow]Cancelled by user[/yellow]")
50
+
51
+ def unauthorized(self):
52
+ self.print(
53
+ Panel(
54
+ f"Unauthorized request / invalid user token.\n\nPlease log in again using [bold cyan]{PIPECAT_CLI_NAME} auth login[/bold cyan]",
55
+ title=f"[bold red]{PANEL_TITLE_ERROR} - Unauthorized (401)[/bold red]",
56
+ subtitle="",
57
+ title_align="left",
58
+ subtitle_align="left",
59
+ border_style="red"))
60
+
61
+ def api_error(
62
+ self,
63
+ error_code: Optional[Union[str, dict]] = None,
64
+ title: Optional[str] = "API Error",
65
+ hide_subtitle: bool = False):
66
+ DEFAULT_ERROR_MESSAGE = "Unknown error. Please contact support."
67
+
68
+ if isinstance(error_code, dict):
69
+ error_message = error_code.get("error", DEFAULT_ERROR_MESSAGE)
70
+ code = error_code.get("code")
71
+ else:
72
+ error_message = str(error_code) if error_code else DEFAULT_ERROR_MESSAGE
73
+ code = None
74
+
75
+ if not error_message:
76
+ hide_subtitle = True
77
+
78
+ self.print(
79
+ Panel(
80
+ f"[red]{title}[/red]\n\n"
81
+ f"[dim]Error message:[/dim]\n{error_message}",
82
+ title=f"[bold red]{PANEL_TITLE_ERROR}{f' - {code}' if code else ''}[/bold red]",
83
+ subtitle=f"[dim]Docs: https://docs.pipecat.daily.co/agents/error-codes#{code}[/dim]" if not hide_subtitle and code else None,
84
+ title_align="left",
85
+ subtitle_align="left",
86
+ border_style="red"))
87
+
88
+
89
+ console = PipecatConsole()
90
+
91
+
92
+ def format_timestamp(timestamp: str) -> str:
93
+ """
94
+ Format a timestamp string to a more readable format.
95
+ Handles timestamps with variable microsecond precision.
96
+
97
+ Args:
98
+ timestamp (str): The timestamp string in ISO format with microseconds (e.g. "2024-01-01T12:34:56.789Z")
99
+
100
+ Returns:
101
+ str: The formatted timestamp string
102
+ """
103
+ from datetime import datetime
104
+
105
+ # First try parsing the timestamp directly
106
+ try:
107
+ return datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S")
108
+ except ValueError:
109
+ pass
110
+
111
+ # Handle case where microseconds have higher precision
112
+ try:
113
+ parts = timestamp.split(".")
114
+ if len(parts) == 2 and parts[1].endswith("Z"):
115
+ # Truncate microseconds to 6 digits
116
+ microseconds = parts[1][:-1][:6].ljust(6, '0')
117
+ normalized = f"{parts[0]}.{microseconds}Z"
118
+ return datetime.strptime(normalized,
119
+ "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M:%S")
120
+ except (ValueError, IndexError):
121
+ pass
122
+
123
+ # Return original if parsing fails
124
+ return timestamp
@@ -62,7 +62,18 @@ class _API():
62
62
  if not response.ok:
63
63
  if not_found_is_empty and response.status == 404:
64
64
  return None
65
- self.error = response.status
65
+
66
+ # Extract PCC error code, where applicable
67
+ if response.status == 400:
68
+ try:
69
+ error_data = await response.json()
70
+ self.error = error_data
71
+ except Exception as e:
72
+ # Fallback structure matching API format
73
+ self.error = {"error": "Bad Request", "code": str(response.status)}
74
+ else:
75
+ # Match API error format for non-400 errors
76
+ self.error = {"error": response.reason, "code": str(response.status)}
66
77
  response.raise_for_status()
67
78
 
68
79
  return await response.json()
@@ -91,14 +102,12 @@ class _API():
91
102
  def print_error(self):
92
103
  from pipecatcloud._utils.console_utils import console
93
104
 
94
- # @TODO: handle coded errors here
95
-
96
105
  if not self.error:
97
106
  return
98
- if self.error == 401:
107
+ if isinstance(self.error, dict) and self.error.get("code", "400") == "401":
99
108
  console.unauthorized()
100
109
  else:
101
- console.api_error(str(self.error))
110
+ console.api_error(self.error)
102
111
 
103
112
  def bubble_error(self):
104
113
  self.bubble_next = True
@@ -362,3 +371,11 @@ class _API():
362
371
  @property
363
372
  def agent_delete(self):
364
373
  return self.create_api_method(self._agent_delete)
374
+
375
+ async def _agent_logs(self, agent_name: str, org: str, limit: int = 100) -> dict | None:
376
+ url = f"{self.construct_api_url('services_logs_path').format(org=org, service=agent_name)}"
377
+ return await self._base_request("GET", url, params={"limit": limit})
378
+
379
+ @property
380
+ def agent_logs(self):
381
+ return self.create_api_method(self._agent_logs)
@@ -3,15 +3,17 @@ import questionary
3
3
  import typer
4
4
  from loguru import logger
5
5
  from rich import box
6
- from rich.console import Console, Group
6
+ from rich.console import Group
7
+ from rich.columns import Columns
7
8
  from rich.live import Live
8
9
  from rich.panel import Panel
9
10
  from rich.table import Table
11
+ from rich.text import Text
10
12
 
11
- from pipecatcloud._utils.agent_utils import handle_agent_start_error
13
+ from enum import Enum
12
14
  from pipecatcloud._utils.async_utils import synchronizer
13
15
  from pipecatcloud._utils.auth_utils import requires_login
14
- from pipecatcloud._utils.console_utils import console
16
+ from pipecatcloud._utils.console_utils import console, format_timestamp
15
17
  from pipecatcloud.cli import PIPECAT_CLI_NAME
16
18
  from pipecatcloud.cli.api import API
17
19
  from pipecatcloud.cli.config import config
@@ -104,29 +106,6 @@ async def status(
104
106
  console.error(f"No deployment data found for agent with name '{agent_name}'")
105
107
  return typer.Exit()
106
108
 
107
- # Conditions
108
- conditions = data["conditions"]
109
- table = Table(
110
- show_header=True,
111
- show_lines=True,
112
- border_style="dim",
113
- box=box.SIMPLE
114
- )
115
- table.add_column("Status")
116
- table.add_column("Type")
117
- table.add_column("Message")
118
- table.add_column("Reason")
119
- table.add_column("Date")
120
-
121
- for condition in conditions:
122
- table.add_row(
123
- DEPLOY_STATUS_MAP.get(condition["status"], "[dim]Unknown[/dim]"),
124
- condition['type'],
125
- condition.get('message', 'No message'),
126
- condition.get('reason', 'No reason'),
127
- condition['lastTransitionTime']
128
- )
129
-
130
109
  # Deployment info
131
110
 
132
111
  deployment_table = Table(
@@ -136,14 +115,14 @@ async def status(
136
115
  )
137
116
  deployment_table.add_column("Key")
138
117
  deployment_table.add_column("Value")
139
- deployment_table.add_row(
140
- "[bold]Image:[/bold]",
141
- str(data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("image", "N/A")),
142
- )
143
118
  deployment_table.add_row(
144
119
  "[bold]Active Session Count:[/bold]",
145
120
  str(data.get("activeSessionCount", "N/A")),
146
121
  )
122
+ deployment_table.add_row(
123
+ "[bold]Image:[/bold]",
124
+ str(data.get("deployment", {}).get("manifest", {}).get("spec", {}).get("image", "N/A")),
125
+ )
147
126
  deployment_table.add_row(
148
127
  "[bold]Active Deployment ID:[/bold]",
149
128
  str(data.get("activeDeploymentId", "N/A")),
@@ -160,24 +139,17 @@ async def status(
160
139
  # Autoscaling info
161
140
  autoscaling_data = data.get("autoScaling", None)
162
141
  if autoscaling_data:
163
- autoscaling_table = Table(
164
- show_header=False,
165
- show_lines=False,
166
- box=box.SIMPLE
167
- )
168
- autoscaling_table.add_column("Key")
169
- autoscaling_table.add_column("Value")
170
-
171
- autoscaling_table.add_row(
172
- "[bold]Max instances:[/bold]",
173
- str(autoscaling_data.get("maxReplicas", 0)),
174
- )
175
- autoscaling_table.add_row(
176
- "[bold]Min instances:[/bold]",
177
- str(autoscaling_data.get("minReplicas", 0)),
178
- )
179
- autoscaling_panel = Panel(
180
- autoscaling_table,
142
+ scaling_renderables = [
143
+ Panel(
144
+ f"[bold]Minimum Instances[/bold]\n{autoscaling_data.get('minReplicas', 0)}",
145
+ expand=True),
146
+ Panel(
147
+ f"[bold]Maximum Instances[/bold]\n{autoscaling_data.get('maxReplicas', 0)}",
148
+ expand=True)]
149
+ scaling_panel = Panel(
150
+ Columns(
151
+ scaling_renderables
152
+ ),
181
153
  title="[bold]Scaling configuration:[/bold]",
182
154
  title_align="left",
183
155
  border_style="dim",
@@ -190,8 +162,7 @@ async def status(
190
162
  Panel(
191
163
  Group(
192
164
  deployment_table,
193
- autoscaling_panel if autoscaling_panel else "",
194
- table,
165
+ scaling_panel if scaling_panel else "",
195
166
  Panel(
196
167
  f"[{color}]Health: {'Ready' if data['ready'] else 'Stopped'}[/]",
197
168
  border_style="green" if data['ready'] else "yellow",
@@ -252,31 +223,69 @@ async def scale():
252
223
  console.error("Not implemented")
253
224
 
254
225
 
226
+ class LogLevel(str, Enum):
227
+ DEBUG = "DEBUG"
228
+ INFO = "INFO"
229
+ WARNING = "WARNING"
230
+ ERROR = "ERROR"
231
+ CRITICAL = "CRITICAL"
232
+
233
+
234
+ class LogLevelColors(str, Enum):
235
+ DEBUG = "dim white"
236
+ INFO = "white"
237
+ WARNING = "yellow"
238
+ ERROR = "red"
239
+ CRITICAL = "bold red"
240
+
241
+
255
242
  @agent_cli.command(name="logs", help="Get logs for the given agent.")
256
243
  @synchronizer.create_blocking
257
244
  @requires_login
258
- async def logs(ctx: typer.Context, agent_name: str, organization: str = typer.Option(
259
- None,
260
- "--organization",
261
- "-o",
262
- help="Organization to get status of agent for"
263
- ),):
245
+ async def logs(
246
+ agent_name: str,
247
+ organization: str = typer.Option(
248
+ None,
249
+ "--organization",
250
+ "-o",
251
+ help="Organization to get status of agent for"
252
+ ),
253
+ level: LogLevel = typer.Option(
254
+ None,
255
+ "--level", "-l",
256
+ help="Level of logs to get"
257
+ ),
258
+ limit: int = typer.Option(
259
+ 100,
260
+ "--limit", "-n",
261
+ help="Number of logs to get"
262
+ ),
263
+ ):
264
264
  org = organization or config.get("org")
265
- token = config.get("token")
266
265
 
267
- try:
268
- with console.status(f"Fetching logs for agent: [bold]'{agent_name}'[/bold]", spinner="dots"):
269
- async with aiohttp.ClientSession() as session:
270
- response = await session.get(
271
- f"{API.construct_api_url('services_logs_path').format(org=org, service=agent_name)}?limit=100&order=desc",
272
- headers={"Authorization": f"Bearer {token}"},
273
- )
274
- if response.status != 200:
275
- response.raise_for_status()
276
- data = await response.json()
277
- console.print(data)
278
- except Exception:
279
- console.error(f"Unable to get logs for {agent_name}")
266
+ with console.status(f"[dim]Fetching logs for agent: [bold]'{agent_name}'[/bold] with severity: [bold cyan]{level.value if level else 'ALL'}[/bold cyan][/dim]", spinner="dots"):
267
+ data, error = await API.agent_logs(agent_name=agent_name, org=org, limit=limit)
268
+
269
+ if not data or not data.get("logs"):
270
+ console.print("[dim]No logs found for agent[/dim]")
271
+ return typer.Exit(1)
272
+
273
+ for l in data["logs"]:
274
+ log_data = l.get("log", "")
275
+ if log_data:
276
+ timestamp = format_timestamp(l.get("timestamp", ""))
277
+ severity = LogLevel.INFO
278
+ for log_severity in LogLevel:
279
+ if log_severity.value in log_data.upper():
280
+ severity = log_severity
281
+ break
282
+ # filter out any messages that do not match our log level
283
+ if level and severity.value != level.value:
284
+ continue
285
+ color = getattr(LogLevelColors, severity, LogLevelColors.DEBUG).value
286
+ console.print(Text(timestamp, style="bold dim"))
287
+ console.print(Text(l.get("log", ""), style=color))
288
+ console.rule(style="dim")
280
289
 
281
290
 
282
291
  @agent_cli.command(name="delete", help="Delete an agent.")
@@ -486,7 +495,7 @@ async def start(
486
495
  daily_room = data.get("dailyRoom")
487
496
  daily_token = data.get("dailyToken")
488
497
  if daily_room:
489
- message += f"\n\nDaily room: [link={daily_room}?t={daily_token}]{daily_room}?t={daily_token}[/link]"
498
+ message += f"\n\nDaily room: [link={daily_room}?t={daily_token}]{daily_room}?t={daily_token}[/link]\n"
490
499
  return console.success(
491
500
  message,
492
501
  subtitle=f"[white dim]Join the session:[/white dim] [link={daily_room}?t={daily_token}]click here[/link]")
@@ -32,7 +32,7 @@ async def _deploy(params: DeployConfigParams, org, force: bool = False):
32
32
  existing_agent = False
33
33
 
34
34
  # Check for an existing deployment with this agent name
35
- with Live(console.status("[dim]Checking for exist agent deployment...[/dim]", spinner="dots")) as live:
35
+ with Live(console.status("[dim]Checking for existing agent deployment...[/dim]", spinner="dots")) as live:
36
36
  data, error = await API.agent(agent_name=params.agent_name, org=org, live=live)
37
37
 
38
38
  if error:
@@ -106,7 +106,6 @@ async def _deploy(params: DeployConfigParams, org, force: bool = False):
106
106
  # 3. Poll status until healthy
107
107
  """
108
108
  attempts = 0
109
- condition_table = None
110
109
  active_deployment_id = None
111
110
  has_failed = False
112
111
  is_ready = False
@@ -117,8 +116,7 @@ async def _deploy(params: DeployConfigParams, org, force: bool = False):
117
116
  Padding(
118
117
  console.status(
119
118
  f"[dim]Watching for deployment status (attempt: {attempts + 1})[dim]" if not active_deployment_id else f"Waiting for deployment to enter ready state [dim](attempt: {attempts + 1})[/dim]",
120
- spinner="bouncingBar"), (1, 0)),
121
- *([] if condition_table is None else [condition_table])
119
+ spinner="bouncingBar"), (1, 0))
122
120
  ),
123
121
  title=f"[bold]{'Deploying' if not existing_agent else 'Updating'} agent[/bold]",
124
122
  border_style="dim" if not active_deployment_id else "white",
@@ -141,34 +139,6 @@ async def _deploy(params: DeployConfigParams, org, force: bool = False):
141
139
  attempts = 0
142
140
  active_deployment_id = status["activeDeploymentId"]
143
141
 
144
- # Update status display
145
- conditions = status["conditions"]
146
- table = Table(
147
- show_header=False,
148
- show_lines=True,
149
- border_style="dim",
150
- box=box.MINIMAL
151
- )
152
- table.add_column("Status")
153
- table.add_column("Type")
154
- table.add_column("Reason")
155
-
156
- for condition in conditions:
157
- c_status = DEPLOY_STATUS_MAP.get(condition["status"], "[dim]Unknown[/dim]")
158
- if condition.get('reason') == "RevisionFailed":
159
- c_status = "[red]Failded[/red]"
160
- has_failed = condition
161
- if condition["type"] == "Ready":
162
- continue
163
- table.add_row(
164
- c_status,
165
- condition['type'],
166
- condition.get(
167
- 'reason',
168
- 'No reason'),
169
- )
170
- condition_table = table
171
-
172
142
  # Deployment is ready
173
143
  if status["ready"]:
174
144
  live.update("")
@@ -0,0 +1,112 @@
1
+ import typer
2
+ import questionary
3
+ import aiohttp
4
+ import zipfile
5
+ import os
6
+ from typing import Set
7
+
8
+ from pipecatcloud._utils.async_utils import synchronizer
9
+ from pipecatcloud._utils.console_utils import console
10
+ from pipecatcloud.config import config
11
+
12
+ # ----- Init
13
+
14
+ FILES_TO_EXTRACT = {
15
+ "pipecat-cloud-starter-main/bot.py",
16
+ "pipecat-cloud-starter-main/Dockerfile",
17
+ "pipecat-cloud-starter-main/requirements.txt",
18
+ "pipecat-cloud-starter-main/pcc-deploy.toml",
19
+ "pipecat-cloud-starter-main/README.md",
20
+ "pipecat-cloud-starter-main/pipecat-cloud-starter-main/.gitignore",
21
+ "pipecat-cloud-starter-main/local_runner.py",
22
+ }
23
+
24
+
25
+ def check_existing_files() -> Set[str]:
26
+ """Check which target files already exist in the current directory."""
27
+ return {os.path.basename(file)
28
+ for file in FILES_TO_EXTRACT if os.path.exists(os.path.basename(file))}
29
+
30
+
31
+ def create_init_command(app: typer.Typer):
32
+ @app.command(name="init", help="Initialize project directory with template files")
33
+ @synchronizer.create_blocking
34
+ async def init():
35
+ if not await questionary.confirm(
36
+ "This will download the latest starter project to the current directory. Continue?"
37
+ ).ask_async():
38
+ console.print("[bold]Aborting init request[/bold]")
39
+ return typer.Exit(1)
40
+
41
+ # Check existing files more efficiently
42
+ existing_files = check_existing_files()
43
+ if existing_files:
44
+ files_list = "\n".join(f"- {f}" for f in existing_files)
45
+ if not await questionary.confirm(
46
+ f"The following files already exist and will be overwritten:\n{files_list}\nDo you want to continue?"
47
+ ).ask_async():
48
+ console.print("[bold]Aborting init request[/bold]")
49
+ return typer.Exit(1)
50
+
51
+ zip_path = "init.zip"
52
+ try:
53
+ with console.status("[dim]Downloading starter project...[/dim]", spinner="dots"):
54
+ zip_url = config.get("init_zip_url")
55
+ if not zip_url:
56
+ raise ValueError("No starter project URL found in configuration")
57
+
58
+ async with aiohttp.ClientSession() as session:
59
+ async with session.get(zip_url) as response:
60
+ if response.status != 200:
61
+ raise aiohttp.ClientResponseError(
62
+ response.request_info,
63
+ response.history,
64
+ status=response.status,
65
+ message=f"Failed to download starter project: {response.reason}"
66
+ )
67
+
68
+ # Stream the download to a file
69
+ with open(zip_path, "wb") as f:
70
+ async for chunk in response.content.iter_chunked(8192):
71
+ f.write(chunk)
72
+
73
+ # Extract files
74
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
75
+ for file in FILES_TO_EXTRACT:
76
+ try:
77
+ source = zip_ref.read(file)
78
+ target = os.path.basename(file)
79
+ with open(target, "wb") as f:
80
+ f.write(source)
81
+ except KeyError:
82
+ console.print(
83
+ f"[yellow]Warning:[/yellow] File {file} not found in starter project")
84
+ except Exception as e:
85
+ raise Exception(f"Failed to extract {file}: {str(e)}")
86
+
87
+ console.success(
88
+ "You can now start building your agent in [bold]bot.py[/bold]"
89
+ "\n\n"
90
+ "Follow the steps in the [bold]README.md[/bold] file to get started.",
91
+ title="Project files downloaded successfully"
92
+ )
93
+
94
+ except aiohttp.ClientError as e:
95
+ console.error(f"Network error while downloading starter project: {str(e)}")
96
+ return typer.Exit(1)
97
+ except zipfile.BadZipFile:
98
+ console.error("Downloaded file is not a valid zip archive")
99
+ return typer.Exit(1)
100
+ except Exception as e:
101
+ console.error(f"Error during initialization: {str(e)}")
102
+ return typer.Exit(1)
103
+ finally:
104
+ # Cleanup the temporary zip file
105
+ if os.path.exists(zip_path):
106
+ try:
107
+ os.remove(zip_path)
108
+ except Exception as e:
109
+ console.print(
110
+ f"[yellow]Warning:[/yellow] Failed to cleanup temporary file {zip_path}: {str(e)}")
111
+
112
+ return init
@@ -212,13 +212,6 @@ async def set(
212
212
  if not create:
213
213
  console.print("[bold red]Secret set creation cancelled[/bold red]")
214
214
  return typer.Exit(1)
215
- else:
216
- if not skip_confirm:
217
- create = await questionary.confirm(
218
- f"Secret set with name '{name}' does not exist. Would you like to create it?").ask_async()
219
- if not create:
220
- console.print("[bold red]Secret set creation cancelled[/bold red]")
221
- return typer.Exit(1)
222
215
 
223
216
  with console.status(f"[dim]{'Modifying' if existing_set else 'Creating'} secret set [bold]'{name}'[/bold][/dim]", spinner="dots"):
224
217
  for key, value in secrets_dict.items():
@@ -12,6 +12,7 @@ class _Setting(typing.NamedTuple):
12
12
  _SETTINGS = {
13
13
  "api_host": _Setting("https://api.pipecat.daily.co"),
14
14
  "dashboard_host": _Setting("https://pipecat.daily.co"),
15
+ "init_zip_url": _Setting("https://github.com/daily-co/pipecat-cloud-starter/archive/refs/heads/main.zip"),
15
16
  "onboarding_path": _Setting("/v1/onboarding"),
16
17
  "login_path": _Setting("/auth/login"),
17
18
  "login_status_path": _Setting("/auth/status"),
@@ -22,8 +23,7 @@ _SETTINGS = {
22
23
  "services_deployments_path": _Setting("/v1/organizations/{org}/services/{service}/deployments"),
23
24
  "start_path": _Setting("/v1/public/{service}/start"),
24
25
  "api_keys_path": _Setting("/v1/organizations/{org}/apiKeys"),
25
- "secrets_path": _Setting("/v1/organizations/{org}/secrets")
26
- }
26
+ "secrets_path": _Setting("/v1/organizations/{org}/secrets")}
27
27
 
28
28
 
29
29
  class Config:
@@ -1,6 +1,4 @@
1
- from typing import Optional
2
-
3
- from pipecatcloud.errors import ERROR_CODES
1
+ from typing import Optional, Union
4
2
 
5
3
 
6
4
  class Error(Exception):
@@ -48,12 +46,16 @@ class AgentStartError(Error):
48
46
 
49
47
  def __init__(
50
48
  self,
51
- message: str = "Agent start request failed.",
52
- error_code: Optional[str] = None):
53
-
54
- error_message = message if not error_code else ERROR_CODES.get(
55
- error_code, message)
56
-
57
- self.message = f"{error_message} (Error code: {error_code})"
58
- self.error_code = error_code
49
+ error: Optional[Union[str, dict]] = None):
50
+
51
+ if isinstance(error, dict):
52
+ error_message = error.get("error", "Unknown error. Please contact support.")
53
+ code = error.get("code")
54
+ else:
55
+ error_message = str(
56
+ error) if error else "Unknown error. Please contact support."
57
+ code = None
58
+
59
+ self.message = f"{code} - {error_message}"
60
+ self.error_code = code
59
61
  super().__init__(self.message)
@@ -4,8 +4,7 @@ from typing import Optional
4
4
  import aiohttp
5
5
  from loguru import logger
6
6
 
7
- from pipecatcloud._utils.agent_utils import handle_agent_start_error
8
- from pipecatcloud.api import API
7
+ from pipecatcloud.api import _API as API
9
8
  from pipecatcloud.exception import AgentStartError
10
9
 
11
10
 
@@ -32,28 +31,38 @@ class Session:
32
31
 
33
32
  async def start(self) -> bool:
34
33
  if not self.api_key:
35
- raise AgentStartError(error_code="PCC-1002")
34
+ raise AgentStartError({"code": "PCC-1002", "error": "No API key provided"})
36
35
 
37
36
  logger.debug(f"Starting agent {self.agent_name}")
38
37
 
39
- start_error_code = None
38
+ start_error = None
39
+
40
+ payload: dict = {"createDailyRoom": bool(self.params.use_daily)}
41
+ if self.params.data is not None:
42
+ payload["body"] = self.params.data
43
+
40
44
  try:
41
45
  async with aiohttp.ClientSession() as session:
42
46
  response = await session.post(
43
47
  f"{API.construct_api_url('start_path').format(service=self.agent_name)}",
44
48
  headers={"Authorization": f"Bearer {self.api_key}"},
45
- json={
46
- "createDailyRoom": bool(self.params.use_daily),
47
- "body": self.params.data
48
- }
49
+ json=payload
49
50
  )
51
+
50
52
  if response.status != 200:
51
- start_error_code = handle_agent_start_error(response.status)
53
+ # Attempt to parse the error code from the response
54
+ try:
55
+ error_data = await response.json()
56
+ start_error = error_data
57
+ except Exception:
58
+ start_error = {"code": "PCC-1000",
59
+ "error": "Unknown error occurred. Please contact support."}
60
+
52
61
  response.raise_for_status()
53
62
  else:
54
63
  logger.debug(f"Agent {self.agent_name} started successfully")
55
64
  except Exception as e:
56
65
  logger.error(f"Error starting agent {self.agent_name}: {e}")
57
- raise AgentStartError(error_code=start_error_code)
66
+ raise AgentStartError(error=start_error)
58
67
 
59
68
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pipecatcloud
3
- Version: 0.0.9
3
+ Version: 0.0.10
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/
@@ -70,16 +70,26 @@ pcc auth login
70
70
 
71
71
  ### Usage in Python scripts
72
72
 
73
- If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.agent` module.
73
+ If want to programmatically start an agent within a Python script, you can use the `pipecatcloud.session` module.
74
74
 
75
75
  ```python
76
- from pipecatcloud.agent import Agent
77
-
78
- agent = Agent(
79
- agent_name="your-agent-name",
80
- organization="your-organization",
81
- api_key="your-api-key",
82
- )
83
-
84
- await agent.start()
76
+ from pipecatcloud.session import Session
77
+ from pipecatcloud.exception import AgentStartError
78
+ import asyncio
79
+
80
+ async def main():
81
+ session = Session(
82
+ agent_name="your-agent-name",
83
+ api_key="pk_...",
84
+ )
85
+
86
+ try:
87
+ await session.start()
88
+ except AgentStartError as e:
89
+ print(e)
90
+ except Exception as e:
91
+ raise (e)
92
+
93
+ if __name__ == "__main__":
94
+ asyncio.run(main())
85
95
  ```
@@ -15,7 +15,6 @@ src/pipecatcloud/__main__.py
15
15
  src/pipecatcloud/__version__.py
16
16
  src/pipecatcloud/api.py
17
17
  src/pipecatcloud/config.py
18
- src/pipecatcloud/errors.py
19
18
  src/pipecatcloud/exception.py
20
19
  src/pipecatcloud/session.py
21
20
  src/pipecatcloud.egg-info/PKG-INFO
@@ -25,7 +24,6 @@ src/pipecatcloud.egg-info/entry_points.txt
25
24
  src/pipecatcloud.egg-info/requires.txt
26
25
  src/pipecatcloud.egg-info/top_level.txt
27
26
  src/pipecatcloud/_utils/__init__.py
28
- src/pipecatcloud/_utils/agent_utils.py
29
27
  src/pipecatcloud/_utils/async_utils.py
30
28
  src/pipecatcloud/_utils/auth_utils.py
31
29
  src/pipecatcloud/_utils/console_utils.py
@@ -1,7 +0,0 @@
1
- import os
2
- import sys
3
-
4
- from loguru import logger
5
-
6
- logger.remove()
7
- logger.add(sys.stderr, level=str(os.getenv("PCC_LOG_LEVEL", "INFO")))
@@ -1,9 +0,0 @@
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"
@@ -1,78 +0,0 @@
1
- from typing import Optional
2
-
3
- from rich.console import Console
4
- from rich.panel import Panel
5
-
6
- from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS
7
- from pipecatcloud.errors import ERROR_CODES
8
-
9
-
10
- class PipecatConsole(Console):
11
- def success(
12
- self,
13
- message,
14
- title: Optional[str] = None,
15
- title_extra: Optional[str] = None,
16
- subtitle: Optional[str] = None):
17
-
18
- if not title:
19
- title = f"{PANEL_TITLE_SUCCESS}{f' - {title_extra}' if title_extra is not None else ''}"
20
-
21
- self.print(
22
- Panel(
23
- message,
24
- title=f"[bold green]{title}[/bold green]",
25
- subtitle=subtitle,
26
- title_align="left",
27
- subtitle_align="left",
28
- border_style="green"))
29
-
30
- def error(
31
- self,
32
- message,
33
- title: Optional[str] = None,
34
- title_extra: Optional[str] = None,
35
- subtitle: Optional[str] = None):
36
-
37
- if not title:
38
- title = f"{PANEL_TITLE_ERROR}{f' - {title_extra}' if title_extra is not None else ''}"
39
-
40
- self.print(
41
- Panel(
42
- message,
43
- title=f"[bold red]{title}[/bold red]",
44
- subtitle=subtitle,
45
- title_align="left",
46
- subtitle_align="left",
47
- border_style="red"))
48
-
49
- def cancel(self):
50
- self.print("[yellow]Cancelled by user[/yellow]")
51
-
52
- def unauthorized(self):
53
- self.api_error("401", "Unauthorized", hide_subtitle=True)
54
-
55
- def api_error(
56
- self,
57
- error_code: Optional[str],
58
- title: Optional[str] = "API Error",
59
- hide_subtitle: bool = False):
60
- DEFAULT_ERROR_MESSAGE = "Unknown error. Please contact support."
61
- ERROR_MESSAGE = ERROR_CODES.get(error_code, None) if error_code else None
62
-
63
- if not ERROR_MESSAGE:
64
- hide_subtitle = True
65
-
66
- error_message = ERROR_MESSAGE if ERROR_MESSAGE else DEFAULT_ERROR_MESSAGE
67
- self.print(
68
- Panel(
69
- f"[red]{title}[/red]\n\n"
70
- f"[dim]Error message:[/dim]\n{error_message}",
71
- title=f"[bold red]{PANEL_TITLE_ERROR} - {error_code}[/bold red]",
72
- subtitle=f"[dim]Docs: https://docs.pipecat.daily.co/agents/error-codes#{error_code}[/dim]" if not hide_subtitle else None,
73
- title_align="left",
74
- subtitle_align="left",
75
- border_style="red"))
76
-
77
-
78
- console = PipecatConsole()
@@ -1,16 +0,0 @@
1
- import typer
2
-
3
- from pipecatcloud._utils.async_utils import synchronizer
4
- from pipecatcloud._utils.console_utils import console
5
-
6
- # ----- Run
7
-
8
-
9
- def create_init_command(app: typer.Typer):
10
- @app.command(name="init", help="Initialize project directory with template files")
11
- @synchronizer.create_blocking
12
- async def init(
13
-
14
- ):
15
- console.error("Not yet implemented")
16
- return init
@@ -1,11 +0,0 @@
1
- ERROR_CODES = {
2
- "401": "Unauthorized request to API. Please run `pcc auth login` to login again.\nIf you are attempting to start an agent, please ensure the public API key used as part of the request is valid.",
3
- "404": "API endpoint not found / namespace or organization not found.",
4
- "PCC-1000": "Unable to start agent.",
5
- "PCC-1001": "Attempt to start agent when deployment is not in ready state",
6
- "PCC-1002": "Attempt to start agent without public api key. Try running `pcc organizations keys use`.",
7
- "PCC-1003": "Unknown error occurred. Please check logs for more information.",
8
- "PCC-1004": "Billing credentials not set. Please set billing credentials via the Pipecat Cloud dashboard.",
9
- "PCC-1005": "Agent deployment with name not found.",
10
- "PCC-1006": "Not authorized / invalid API key.",
11
- }
File without changes
File without changes
File without changes