Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions docs/docs/reference/cli/conversations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,45 @@ Examples:
~~~bash
poly conversations list
poly conversations list --limit 20 --offset 10
poly conversations list --cursor <cursor>
poly conversations list --channel voice --channel chat
poly conversations list --in-progress
poly conversations list --json
~~~

The default table view shows conversation ID (rendered as a clickable Agent Studio link), start time, duration, caller number, channel, variant (when present), handoff status, and a short summary heading.
The default table view shows conversation ID (rendered as a clickable Agent Studio link), start time, duration, caller number, channel, variant (when present), and handoff status.

In `us-1`, `uk-1`, and `euw-1`, this command uses the v3 conversations API, which doesn't return a
summary, tags, PolyScore, note, deployment ID, direction, or language for list results — fetch
those per-conversation with [`poly conversations get`](#poly-conversations-get) instead. Other
regions (`dev`, `staging`, `studio`) still use the deprecated v1 endpoint until it's rolled out
there, so their list output currently retains those fields.

| Flag | Description |
|---|---|
| `--limit` | Max number of conversations to return. Defaults to `50`. |
| `--offset` | Number of conversations to skip. Defaults to `0`. |
| `--offset` | Number of conversations to skip. Defaults to `0`. Prefer `--cursor` where available. |
| `--cursor` | Pagination cursor from a previous response's `cursor` field. `us-1`/`uk-1`/`euw-1` only. |
| `--channel` | Filter by channel (e.g. `voice`, `chat`). Repeatable. `us-1`/`uk-1`/`euw-1` only. |
| `--in-progress` / `--no-in-progress` | Filter to only in-progress, or only finished, conversations. `us-1`/`uk-1`/`euw-1` only. |

`--json` output shape:
`--json` passes through the raw API response, so its shape follows the same regional split as the
table above. In `us-1`/`uk-1`/`euw-1` (v3):

~~~json
{
"conversations": [{ "id": "...", "started_at": "...", "...": "..." }],
"next_offset": null,
"cursor": null
}
~~~

In `dev`/`staging`/`studio` (v1, unchanged from before this migration):

~~~json
{
"conversations": [{ "id": "...", "startedAt": "...", "...": "..." }],
"count": 0,
"limit": 50,
"offset": 0
"conversations": [{ "conversationId": "...", "startedAt": "...", "...": "..." }],
"next_offset": null
}
~~~

Expand Down
44 changes: 42 additions & 2 deletions src/poly/cli_commands/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
Copyright PolyAI Limited
"""

from argparse import ArgumentParser, Namespace, RawTextHelpFormatter, _SubParsersAction
from argparse import (
ArgumentParser,
BooleanOptionalAction,
Namespace,
RawTextHelpFormatter,
_SubParsersAction,
)
from typing import Optional

from poly.cli_commands.base import BUILDER_API_GROUP, BaseCommand, Parents
Expand Down Expand Up @@ -64,6 +70,27 @@ def add_arguments(cls, subparsers: _SubParsersAction[ArgumentParser], parents: P
default=0,
help="Number of conversations to skip. Defaults to 0.",
)
conv_list_parser.add_argument(
"--cursor",
type=str,
default=None,
help="Pagination cursor from a previous response's `cursor` field. "
"Preferred over --offset where available.",
)
conv_list_parser.add_argument(
"--channel",
type=str,
action="append",
default=None,
help="Filter by channel (e.g. voice, chat). Repeatable.",
)
conv_list_parser.add_argument(
"--in-progress",
dest="in_progress",
action=BooleanOptionalAction,
default=None,
help="Filter to only in-progress, or only finished (--no-in-progress), conversations.",
)

conv_get_parser = conversations_subparsers.add_parser(
"get",
Expand Down Expand Up @@ -127,6 +154,9 @@ def run(cls, args: Namespace) -> None:
args.path,
args.limit,
args.offset,
cursor=args.cursor,
channel=args.channel,
in_progress=args.in_progress,
output_json=args.json,
)
elif args.conversations_subcommand == "get":
Expand All @@ -151,24 +181,34 @@ def conversations_list(
base_path: str,
limit: int = 50,
offset: int = 0,
cursor: Optional[str] = None,
channel: Optional[list[str]] = None,
in_progress: Optional[bool] = None,
output_json: bool = False,
) -> None:
"""List conversations for the project.

Args:
base_path: Base path for the project.
limit: Max number of conversations to return.
offset: Number of conversations to skip.
offset: Number of conversations to skip. Prefer `cursor` where available.
cursor: Opaque pagination cursor from a previous response. v3 regions only.
channel: Filter by one or more channels. v3 regions only.
in_progress: Filter to only in-progress or only finished conversations. v3 regions only.
output_json: If True, emit machine-readable JSON.
"""
from poly.output.console import info, paged_output, print_conversations

project = load_project(base_path, output_json=output_json)
result = AgentStudioInterface.list_conversations(
region=project.region,
account_id=project.account_id,
project_id=project.project_id,
limit=limit,
offset=offset,
cursor=cursor,
channel=channel,
in_progress=in_progress,
)
conversations = result.get("conversations", [])

Expand Down
17 changes: 14 additions & 3 deletions src/poly/handlers/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,22 +1041,33 @@ def create_pat(region: str, jwt_token: str, name: str) -> str:
@staticmethod
def list_conversations(
region: str,
account_id: str,
project_id: str,
limit: int = 50,
offset: int = 0,
cursor: Optional[str] = None,
channel: Optional[list[str]] = None,
in_progress: Optional[bool] = None,
) -> dict:
"""List conversations for a project.

Args:
region: The region name.
account_id: The account ID. Only used for the v3 endpoint.
project_id: The project ID (agent ID).
limit: Max number of conversations to return.
offset: Number of conversations to skip.
offset: Number of conversations to skip. Prefer `cursor` where available.
cursor: Opaque pagination cursor from a previous v3 response. v3 only.
channel: Filter by one or more channels (e.g. "voice", "chat"). v3 only.
in_progress: Filter to only in-progress (True) or only finished (False)
conversations. v3 only.

Returns:
dict: The API response with conversations, count, limit, offset.
dict: The API response with conversations and pagination info.
"""
return PlatformAPIHandler.list_conversations(region, project_id, limit, offset)
return PlatformAPIHandler.list_conversations(
region, account_id, project_id, limit, offset, cursor, channel, in_progress
)

@staticmethod
def get_conversation(
Expand Down
57 changes: 53 additions & 4 deletions src/poly/handlers/platform_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
# These use public APIs not /adk endpoints
PROMOTE_URL = "/v1/agents/{project_id}/deployments/{deployment_id}/promote"
ROLLBACK_URL = "/v1/agents/{project_id}/deployments/{deployment_id}/rollback"
# v1 conversations list is deprecated (sunset end of Aug 2026). v3 is only live in
# us-1/uk-1/euw-1 today (staging has v3 for us-1 only) — regions without a v3 host
# still use CONVERSATIONS_URL until the backend finishes rolling v3 out. DEVP-664.
CONVERSATIONS_URL = "/v1/agents/{project_id}/conversations"
CONVERSATIONS_V3_URL = "/v3/{account_id}/{project_id}/conversations"
CONVERSATION_URL = "/v1/agents/{project_id}/conversations/{conversation_id}"
CONVERSATION_AUDIO_URL = "/v1/agents/{project_id}/conversations/{conversation_id}/audio"
AUDIO_CACHE_URL = "/v1/agents/{project_id}/audio-cache"
Expand Down Expand Up @@ -82,19 +86,35 @@ class PlatformAPIHandler:
"studio": "https://jupiter-api.plg-us-1-prod.polyai.app",
}

# v3 conversations list host. Only us-1/uk-1/euw-1 (and staging's us-1) are live;
# dev/staging/studio have no v3 host yet, so they intentionally fall back to
# `region_to_base_url` (v1) in `get_base_url`. DEVP-664.
platform_region_to_base_url = {
"euw-1": "https://api.euw-1.platform.polyai.app",
"uk-1": "https://api.uk-1.platform.polyai.app",
"us-1": "https://api.us-1.platform.polyai.app",
}

@staticmethod
def get_base_url(region: str, use_jupiter_api: bool = False) -> str:
def get_base_url(
region: str, use_jupiter_api: bool = False, use_platform_api: bool = False
) -> str:
"""Get the base URL for the Platform API based on the region.

Args:
region (str): The region name
use_jupiter_api (bool): Whether to use the Jupiter API
use_platform_api (bool): Whether to use the v3 platform API. Falls back to
the default (v1) base URL for regions without a v3 host yet.
Returns:
str: The base URL for the Platform API
"""
if use_jupiter_api:
if base_url := PlatformAPIHandler.jupiter_region_to_base_url.get(region):
return base_url
elif use_platform_api:
if base_url := PlatformAPIHandler.platform_region_to_base_url.get(region):
return base_url
else:
if base_url := PlatformAPIHandler.region_to_base_url.get(region):
return base_url
Expand All @@ -109,6 +129,7 @@ def make_request(
params: ty.Optional[dict] = None,
headers: ty.Optional[dict] = None,
use_jupiter_api: bool = False,
use_platform_api: bool = False,
) -> dict:
"""Make a request to the Platform API.

Expand All @@ -118,11 +139,13 @@ def make_request(
method (str): The HTTP method
data (dict | None): The request body for POST/PUT requests
params (dict | None): Query string parameters
use_jupiter_api (bool): Whether to use the Jupiter API
use_platform_api (bool): Whether to use the v3 platform API

Returns:
dict: The response JSON
"""
url = PlatformAPIHandler.get_base_url(region, use_jupiter_api) + endpoint
url = PlatformAPIHandler.get_base_url(region, use_jupiter_api, use_platform_api) + endpoint
correlation_id = f"adk-{uuid.uuid4()}"

if headers is None:
Expand Down Expand Up @@ -895,21 +918,47 @@ def create_pat_internal(region: str, jwt_token: str, name: str) -> str:
@staticmethod
def list_conversations(
region: str,
account_id: str,
project_id: str,
limit: int = 50,
offset: int = 0,
cursor: ty.Optional[str] = None,
channel: ty.Optional[list[str]] = None,
in_progress: ty.Optional[bool] = None,
) -> dict:
"""List conversations for a project.

Uses the v3 conversations API in regions where it's live (us-1, uk-1, euw-1).
Other regions (dev, staging, studio) fall back to the deprecated v1 endpoint
until the backend finishes rolling v3 out to them — see DEVP-664.

Args:
region: The region name.
account_id: The account ID. Only used for the v3 endpoint.
project_id: The project ID (agent ID).
limit: Max number of conversations to return.
offset: Number of conversations to skip.
offset: Number of conversations to skip. Prefer `cursor` where available.
cursor: Opaque pagination cursor from a previous v3 response. v3 only.
channel: Filter by one or more channels (e.g. "voice", "chat"). v3 only.
in_progress: Filter to only in-progress (True) or only finished (False)
conversations. v3 only.

Returns:
dict: The API response with conversations, count, limit, offset.
dict: The API response with conversations and pagination info.
"""
if region in PlatformAPIHandler.platform_region_to_base_url:
endpoint = CONVERSATIONS_V3_URL.format(account_id=account_id, project_id=project_id)
params: dict[str, ty.Any] = {"limit": limit, "offset": offset}
if cursor:
params["cursor"] = cursor
if channel:
params["channel"] = channel
if in_progress is not None:
params["in_progress"] = in_progress
return PlatformAPIHandler.make_request(
region, endpoint, "GET", params=params, use_platform_api=True
)

endpoint = CONVERSATIONS_URL.format(project_id=project_id)
return PlatformAPIHandler.make_request(
region, endpoint, "GET", params={"limit": limit, "offset": offset}
Expand Down
39 changes: 28 additions & 11 deletions src/poly/output/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,17 +994,35 @@ def _format_duration(seconds: int | None) -> str:
return f"{m}m{s:02d}s" if m else f"{s}s"


def _field(c: dict[str, Any], *keys: str) -> Any:
"""Read the first present key from a conversation dict.

The v3 conversations list API (jupiter-independent, snake_case) and the v1
fallback used in regions without v3 yet (camelCase) name fields
differently — see DEVP-664. Callers pass both spellings, snake_case first.
"""
for key in keys:
if key in c:
return c[key]
return None


def print_conversations(
conversations: list[dict[str, Any]],
url_builder: Callable[[str], str] | None = None,
) -> None:
"""Print a table of conversation summaries.

Note: the v3 conversations list API doesn't return a summary, tags,
PolyScore, note, deployment ID, direction, or language — those remain
available per-conversation via `poly conversations get <id>`, which still
targets the unchanged detail endpoint.

Args:
conversations: List of conversation summary dicts.
url_builder: Optional callable(conversation_id) -> str that returns a Studio URL.
"""
show_variant = any(c.get("variantId") for c in conversations)
show_variant = any(_field(c, "variant_id", "variantId") for c in conversations)

table = Table(box=None, show_header=True, header_style="bold", padding=(0, 1))
table.add_column("Conversation ID", style="bold yellow", no_wrap=True)
Expand All @@ -1015,22 +1033,21 @@ def print_conversations(
if show_variant:
table.add_column("Variant", no_wrap=True)
table.add_column("Handoff", no_wrap=True)
table.add_column("Summary", overflow="fold")

for c in conversations:
started = c.get("startedAt") or "—"
started = _field(c, "started_at", "startedAt") or "—"
if started != "—":
started = _format_iso_timestamp(started)
duration = _format_duration(c.get("duration"))
from_number = c.get("fromNumber") or "—"
duration_seconds = _field(c, "total_duration", "duration")
duration = _format_duration(duration_seconds)
from_number = _field(c, "from_number", "fromNumber") or "—"
channel = c.get("channel") or "—"
handoff = ""
if c.get("handoff"):
dest = c.get("handoffDestination") or ""
if _field(c, "handoff"):
dest = _field(c, "handoff_destination", "handoffDestination") or ""
handoff = f"[yellow]{dest}[/yellow]" if dest else "[yellow]yes[/yellow]"
summary = _extract_summary_heading(c.get("shortSummary"))

cid = c.get("conversationId", "—")
cid = _field(c, "id", "conversationId") or "—"
if url_builder and cid != "—":
url = url_builder(cid)
cid_display = f"[link={url}]{cid}[/link]"
Expand All @@ -1039,8 +1056,8 @@ def print_conversations(

row = [cid_display, started, duration, from_number, channel]
if show_variant:
row.append(c.get("variantId") or "—")
row.extend([handoff, summary])
row.append(_field(c, "variant_id", "variantId") or "—")
row.append(handoff)
table.add_row(*row)

console.print(table)
Expand Down
Loading
Loading