qodev-apollo-cli 1.0.0__tar.gz → 1.1.0__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.
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/CHANGELOG.md +8 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/PKG-INFO +3 -1
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/README.md +2 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/pyproject.toml +2 -2
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/app.py +2 -0
- qodev_apollo_cli-1.1.0/src/apollo_cli/commands/conversations.py +48 -0
- qodev_apollo_cli-1.1.0/src/apollo_cli/formatters/conversations.py +109 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/SKILL.md +9 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/conftest.py +15 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_commands.py +59 -0
- qodev_apollo_cli-1.1.0/tests/test_conversations_formatter.py +70 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/uv.lock +4 -4
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.github/workflows/ci.yml +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.github/workflows/publish.yml +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.gitignore +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/LICENSE +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/__init__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/__main__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/__init__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/accounts.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/calls.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/contacts.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/deals.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/emails.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/enrich.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/install.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/jobs.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/news.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/notes.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/people.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/pipelines.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/tasks.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/usage.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/context.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/__init__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/accounts.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/contacts.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/deals.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/generic.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/help_reference.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/linkedin.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/output.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/__init__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/__init__.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/account-workflows.md +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/contact-workflows.md +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/deal-workflows.md +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/util.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_context.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_install.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_linkedin.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_output.py +0 -0
- {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_util.py +0 -0
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.1.0] - 2026-07-08
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **conversations**: new command group exposing the recorded-conversation endpoints (Zoom/Teams/Meet). `conversations search [--query]` lists conversations; `conversations get ID` returns the full detail including transcript and AI call summary (outcome, pain points, objections, next steps). The underlying `qodev-apollo-api` client already supported `search_conversations`/`get_conversation` — only the CLI surface was missing. Note this is distinct from `calls`, which covers dialer/phone-call activity, not recorded meetings.
|
|
14
|
+
|
|
7
15
|
## [1.0.0] - 2026-07-01
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qodev-apollo-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Agent-friendly CLI for the Apollo API
|
|
5
5
|
Project-URL: Homepage, https://github.com/qodevai/apollo-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/qodevai/apollo-cli
|
|
@@ -111,6 +111,8 @@ $ qodev-apollo-cli usage
|
|
|
111
111
|
| **tasks** | `search` | Search tasks (`--type`, `--status`) |
|
|
112
112
|
| | `create` | Create a task (`--contact-ids`, `--note`, `--due-at`) |
|
|
113
113
|
| **calls** | `search` | Search call activities |
|
|
114
|
+
| **conversations** | `search` | Search recorded conversations (`--query`) |
|
|
115
|
+
| | `get` | Get a conversation with transcript and AI summary |
|
|
114
116
|
| **emails** | `search` | Search email activities |
|
|
115
117
|
| **news** | `search` | Search news (`--categories`) |
|
|
116
118
|
| **jobs** | `search` | Search job postings (`--job-titles`, `--company-domains`) |
|
|
@@ -80,6 +80,8 @@ $ qodev-apollo-cli usage
|
|
|
80
80
|
| **tasks** | `search` | Search tasks (`--type`, `--status`) |
|
|
81
81
|
| | `create` | Create a task (`--contact-ids`, `--note`, `--due-at`) |
|
|
82
82
|
| **calls** | `search` | Search call activities |
|
|
83
|
+
| **conversations** | `search` | Search recorded conversations (`--query`) |
|
|
84
|
+
| | `get` | Get a conversation with transcript and AI summary |
|
|
83
85
|
| **emails** | `search` | Search email activities |
|
|
84
86
|
| **news** | `search` | Search news (`--categories`) |
|
|
85
87
|
| **jobs** | `search` | Search job postings (`--job-titles`, `--company-domains`) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qodev-apollo-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.0"
|
|
4
4
|
description = "Agent-friendly CLI for the Apollo API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -48,7 +48,7 @@ build-backend = "hatchling.build"
|
|
|
48
48
|
packages = ["src/apollo_cli"]
|
|
49
49
|
|
|
50
50
|
[tool.uv.sources]
|
|
51
|
-
qodev-apollo-api = { path = "
|
|
51
|
+
qodev-apollo-api = { path = "../apollo-api" }
|
|
52
52
|
|
|
53
53
|
[tool.ruff]
|
|
54
54
|
target-version = "py311"
|
|
@@ -26,6 +26,7 @@ app.meta.group_parameters = Group("Global Options", sort_key=0)
|
|
|
26
26
|
from apollo_cli.commands.accounts import accounts_app # noqa: E402
|
|
27
27
|
from apollo_cli.commands.calls import calls_app # noqa: E402
|
|
28
28
|
from apollo_cli.commands.contacts import contacts_app # noqa: E402
|
|
29
|
+
from apollo_cli.commands.conversations import conversations_app # noqa: E402
|
|
29
30
|
from apollo_cli.commands.deals import deals_app # noqa: E402
|
|
30
31
|
from apollo_cli.commands.emails import emails_app # noqa: E402
|
|
31
32
|
from apollo_cli.commands.enrich import enrich_app # noqa: E402
|
|
@@ -50,6 +51,7 @@ _sub_apps = [
|
|
|
50
51
|
notes_app,
|
|
51
52
|
tasks_app,
|
|
52
53
|
calls_app,
|
|
54
|
+
conversations_app,
|
|
53
55
|
emails_app,
|
|
54
56
|
news_app,
|
|
55
57
|
jobs_app,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Conversations command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from apollo_cli.context import ctx
|
|
10
|
+
from apollo_cli.formatters.conversations import format_conversation_detail, format_conversation_list
|
|
11
|
+
from apollo_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
conversations_app = App(name="conversations", help="Recorded conversations (Zoom/Teams/Meet).")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@conversations_app.command
|
|
17
|
+
async def search(
|
|
18
|
+
*,
|
|
19
|
+
query: Annotated[str, Parameter(name=["--query", "-q"], help="Search keyword (topic/title)")] = "",
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Search recorded conversations."""
|
|
22
|
+
filters: dict = {}
|
|
23
|
+
if query:
|
|
24
|
+
filters["q_keywords"] = query
|
|
25
|
+
|
|
26
|
+
async with ctx.client() as client:
|
|
27
|
+
result = await client.search_conversations(page=ctx.page, limit=ctx.limit, **filters)
|
|
28
|
+
|
|
29
|
+
output_list(
|
|
30
|
+
items=result.items,
|
|
31
|
+
total=result.total,
|
|
32
|
+
page=result.page,
|
|
33
|
+
limit=ctx.limit,
|
|
34
|
+
ctx=ctx,
|
|
35
|
+
format_fn=format_conversation_list,
|
|
36
|
+
resource_name="Conversations",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@conversations_app.command
|
|
41
|
+
async def get(
|
|
42
|
+
id: Annotated[str, Parameter(help="Conversation ID")],
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Get conversation details by ID (includes transcript and AI summary)."""
|
|
45
|
+
async with ctx.client() as client:
|
|
46
|
+
conversation = await client.get_conversation(id)
|
|
47
|
+
|
|
48
|
+
output(conversation, ctx=ctx, format_fn=format_conversation_detail)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Conversation-specific formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from apollo_cli.formatters.generic import detail_table, list_table
|
|
8
|
+
|
|
9
|
+
CONVERSATION_LIST_COLUMNS = [
|
|
10
|
+
("ID", "id"),
|
|
11
|
+
("Topic", "topic"),
|
|
12
|
+
("Type", "conversation_type"),
|
|
13
|
+
("Start", "start_time"),
|
|
14
|
+
("Duration", "duration"),
|
|
15
|
+
("Host", "host"),
|
|
16
|
+
("State", "state"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
CONVERSATION_DETAIL_FIELDS = [
|
|
20
|
+
("ID", "id"),
|
|
21
|
+
("Topic", "topic"),
|
|
22
|
+
("Type", "conversation_type"),
|
|
23
|
+
("Start", "start_time"),
|
|
24
|
+
("Duration (s)", "duration"),
|
|
25
|
+
("Host", "host"),
|
|
26
|
+
("Host ID", "host_id"),
|
|
27
|
+
("State", "state"),
|
|
28
|
+
("Internal", "is_internal"),
|
|
29
|
+
("Private", "is_private"),
|
|
30
|
+
("Comments", "comment_count"),
|
|
31
|
+
("Participants", "participant_names"),
|
|
32
|
+
("Accounts", "account_names"),
|
|
33
|
+
("Recording", "video_recording.url"),
|
|
34
|
+
("Pushed to CRM", "pushed_to_crm"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_conversation_list(items: list[Any], *, total: int = 0, page: int = 1) -> str:
|
|
39
|
+
"""Format a list of conversations as a markdown table."""
|
|
40
|
+
return list_table(items, CONVERSATION_LIST_COLUMNS, title="Conversations", total=total, page=page)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def format_conversation_detail(data: Any) -> str:
|
|
44
|
+
"""Format a single conversation (with transcript & summary) as a markdown detail view."""
|
|
45
|
+
topic = getattr(data, "topic", None) or "Conversation"
|
|
46
|
+
md = detail_table(data, CONVERSATION_DETAIL_FIELDS, title=f"Conversation: {topic}")
|
|
47
|
+
|
|
48
|
+
# Participants (richer than the participant_names list in the metadata table)
|
|
49
|
+
participants = getattr(data, "participants_info", None) or []
|
|
50
|
+
if participants:
|
|
51
|
+
md += "\n\n## Participants\n"
|
|
52
|
+
for p in participants:
|
|
53
|
+
name = getattr(p, "name", None) or "Unknown"
|
|
54
|
+
title = getattr(p, "title", None)
|
|
55
|
+
account = getattr(p, "account_name", None)
|
|
56
|
+
internal = getattr(p, "is_internal_participant", None)
|
|
57
|
+
suffix = ", ".join(x for x in [title, account] if x)
|
|
58
|
+
tag = " (internal)" if internal else ""
|
|
59
|
+
md += f"\n- **{name}**{tag}" + (f" — {suffix}" if suffix else "")
|
|
60
|
+
|
|
61
|
+
# Associated deals
|
|
62
|
+
deals = getattr(data, "deals", None) or []
|
|
63
|
+
if deals:
|
|
64
|
+
md += "\n\n## Deals\n"
|
|
65
|
+
for d in deals:
|
|
66
|
+
name = getattr(d, "name", None) or getattr(d, "id", "Unknown")
|
|
67
|
+
account = getattr(d, "account_name", None)
|
|
68
|
+
md += f"\n- {name}" + (f" ({account})" if account else "")
|
|
69
|
+
|
|
70
|
+
# AI-generated call summary (detail endpoint only)
|
|
71
|
+
summary = getattr(data, "call_summary", None)
|
|
72
|
+
if summary:
|
|
73
|
+
md += _format_summary(summary)
|
|
74
|
+
|
|
75
|
+
# Transcript (detail endpoint only)
|
|
76
|
+
transcript = getattr(data, "transcript", None) or []
|
|
77
|
+
if transcript:
|
|
78
|
+
md += "\n\n## Transcript\n"
|
|
79
|
+
for seg in transcript:
|
|
80
|
+
speaker = getattr(seg, "participant_name", None) or "Unknown"
|
|
81
|
+
sentence = getattr(seg, "spoken_sentence", None) or ""
|
|
82
|
+
md += f"\n- **{speaker}:** {sentence}"
|
|
83
|
+
|
|
84
|
+
return md
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _format_summary(summary: Any) -> str:
|
|
88
|
+
"""Render the AI call summary (outcome, pain points, objections, next steps)."""
|
|
89
|
+
md = "\n\n## Call Summary\n"
|
|
90
|
+
outcome = getattr(summary, "outcome", None)
|
|
91
|
+
if outcome:
|
|
92
|
+
md += f"\n**Outcome:** {outcome}\n"
|
|
93
|
+
pricing = getattr(summary, "pricing_discussion", None)
|
|
94
|
+
if pricing:
|
|
95
|
+
md += f"\n**Pricing discussion:** {pricing}\n"
|
|
96
|
+
|
|
97
|
+
for label, attr, field in (
|
|
98
|
+
("Pain Points", "pain_points", "text"),
|
|
99
|
+
("Objections", "objections", "text"),
|
|
100
|
+
("Next Steps", "next_steps", "step"),
|
|
101
|
+
):
|
|
102
|
+
items = getattr(summary, attr, None) or []
|
|
103
|
+
if items:
|
|
104
|
+
md += f"\n### {label}\n"
|
|
105
|
+
for item in items:
|
|
106
|
+
text = getattr(item, field, None) or ""
|
|
107
|
+
who = getattr(item, "participant_name", None)
|
|
108
|
+
md += f"\n- {text}" + (f" — _{who}_" if who else "")
|
|
109
|
+
return md
|
|
@@ -98,6 +98,15 @@ Get your API key from [Apollo.io Settings → API](https://app.apollo.io/#/setti
|
|
|
98
98
|
|---------|-------------|
|
|
99
99
|
| `calls search` | Search call activities |
|
|
100
100
|
|
|
101
|
+
### conversations
|
|
102
|
+
|
|
103
|
+
Recorded meetings (Zoom/Teams/Meet) with transcript and AI summary — distinct from `calls` (dialer activity).
|
|
104
|
+
|
|
105
|
+
| Command | Description |
|
|
106
|
+
|---------|-------------|
|
|
107
|
+
| `conversations search [--query TEXT]` | Search recorded conversations |
|
|
108
|
+
| `conversations get ID` | Get a conversation with transcript and AI summary |
|
|
109
|
+
|
|
101
110
|
### emails
|
|
102
111
|
|
|
103
112
|
| Command | Description |
|
|
@@ -74,6 +74,21 @@ def sample_deal():
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
@pytest.fixture
|
|
78
|
+
def sample_conversation():
|
|
79
|
+
"""Sample conversation dict for testing."""
|
|
80
|
+
return {
|
|
81
|
+
"id": "test-conversation-321",
|
|
82
|
+
"topic": "Acme <> QoDev discovery call",
|
|
83
|
+
"conversation_type": "zoom",
|
|
84
|
+
"start_time": "2026-06-01T14:00:00Z",
|
|
85
|
+
"duration": 1800,
|
|
86
|
+
"host": "Jane Smith",
|
|
87
|
+
"state": "processed",
|
|
88
|
+
"participant_names": ["Jane Smith", "John Doe"],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
77
92
|
@pytest.fixture
|
|
78
93
|
def sample_pipeline():
|
|
79
94
|
"""Sample pipeline dict for testing."""
|
|
@@ -291,6 +291,65 @@ class TestDealsCommands:
|
|
|
291
291
|
assert data["stage_name"] == "Negotiation"
|
|
292
292
|
|
|
293
293
|
|
|
294
|
+
class TestConversationsCommands:
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_conversations_search_json(self, sample_conversation: dict, capsys) -> None:
|
|
297
|
+
"""Test conversations search in JSON mode."""
|
|
298
|
+
mock_client = MagicMock()
|
|
299
|
+
mock_client.search_conversations = AsyncMock(
|
|
300
|
+
return_value=MockSearchResult(items=[sample_conversation], total=1, page=1)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
_ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
|
|
304
|
+
|
|
305
|
+
with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
|
|
306
|
+
from apollo_cli.commands.conversations import search
|
|
307
|
+
|
|
308
|
+
await search()
|
|
309
|
+
|
|
310
|
+
captured = capsys.readouterr()
|
|
311
|
+
data = json.loads(captured.out)
|
|
312
|
+
assert data["items"][0]["id"] == "test-conversation-321"
|
|
313
|
+
assert data["items"][0]["conversation_type"] == "zoom"
|
|
314
|
+
assert data["total"] == 1
|
|
315
|
+
|
|
316
|
+
@pytest.mark.asyncio
|
|
317
|
+
async def test_conversations_search_with_query(self, sample_conversation: dict, capsys) -> None:
|
|
318
|
+
"""Test conversations search forwards the keyword as q_keywords."""
|
|
319
|
+
mock_client = MagicMock()
|
|
320
|
+
mock_client.search_conversations = AsyncMock(
|
|
321
|
+
return_value=MockSearchResult(items=[sample_conversation], total=1, page=1)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
_ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
|
|
325
|
+
|
|
326
|
+
with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
|
|
327
|
+
from apollo_cli.commands.conversations import search
|
|
328
|
+
|
|
329
|
+
await search(query="discovery")
|
|
330
|
+
|
|
331
|
+
mock_client.search_conversations.assert_called_once()
|
|
332
|
+
assert mock_client.search_conversations.call_args.kwargs["q_keywords"] == "discovery"
|
|
333
|
+
|
|
334
|
+
@pytest.mark.asyncio
|
|
335
|
+
async def test_conversations_get(self, sample_conversation: dict, capsys) -> None:
|
|
336
|
+
"""Test conversations get command."""
|
|
337
|
+
mock_client = MagicMock()
|
|
338
|
+
mock_client.get_conversation = AsyncMock(return_value=sample_conversation)
|
|
339
|
+
|
|
340
|
+
_ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
|
|
341
|
+
|
|
342
|
+
with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
|
|
343
|
+
from apollo_cli.commands.conversations import get
|
|
344
|
+
|
|
345
|
+
await get(id="test-conversation-321")
|
|
346
|
+
|
|
347
|
+
mock_client.get_conversation.assert_called_once_with("test-conversation-321")
|
|
348
|
+
captured = capsys.readouterr()
|
|
349
|
+
data = json.loads(captured.out)
|
|
350
|
+
assert data["topic"] == "Acme <> QoDev discovery call"
|
|
351
|
+
|
|
352
|
+
|
|
294
353
|
class TestUsageCommand:
|
|
295
354
|
@pytest.mark.asyncio
|
|
296
355
|
async def test_usage_json(self, sample_usage: dict, capsys) -> None:
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Tests for the conversation detail/list formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from qodev_apollo_api.models import Conversation, ConversationDetail
|
|
6
|
+
|
|
7
|
+
from apollo_cli.formatters.conversations import format_conversation_detail, format_conversation_list
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_list_formatter_renders_columns() -> None:
|
|
11
|
+
conv = Conversation.model_validate(
|
|
12
|
+
{"id": "c1", "topic": "Discovery", "conversation_type": "zoom", "duration": 1800}
|
|
13
|
+
)
|
|
14
|
+
md = format_conversation_list([conv], total=1, page=1)
|
|
15
|
+
assert "Conversations" in md
|
|
16
|
+
assert "Discovery" in md
|
|
17
|
+
assert "c1" in md
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_detail_formatter_renders_transcript_and_summary() -> None:
|
|
21
|
+
detail = ConversationDetail.model_validate(
|
|
22
|
+
{
|
|
23
|
+
"id": "c1",
|
|
24
|
+
"topic": "Acme discovery",
|
|
25
|
+
"conversation_type": "zoom",
|
|
26
|
+
"duration": 1800,
|
|
27
|
+
"participants_info": [
|
|
28
|
+
{"id": "p1", "name": "Jane Smith", "title": "VP", "is_internal_participant": True},
|
|
29
|
+
{"id": "p2", "name": "John Doe", "account_name": "Acme"},
|
|
30
|
+
],
|
|
31
|
+
"deals": [{"id": "d1", "name": "Enterprise Deal", "account_name": "Acme"}],
|
|
32
|
+
"call_summary": {
|
|
33
|
+
"outcome": "Positive — moving to POC",
|
|
34
|
+
"pain_points": [{"id": "pp1", "text": "Manual data entry", "participant_name": "John Doe"}],
|
|
35
|
+
"next_steps": [{"id": "ns1", "step": "Send proposal"}],
|
|
36
|
+
},
|
|
37
|
+
"transcript": [
|
|
38
|
+
{"id": "t1", "participant_name": "Jane Smith", "spoken_sentence": "Thanks for joining."},
|
|
39
|
+
{"id": "t2", "participant_name": "John Doe", "spoken_sentence": "Happy to be here."},
|
|
40
|
+
],
|
|
41
|
+
"video_recording": {"url": "https://example.com/rec"},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
md = format_conversation_detail(detail)
|
|
45
|
+
|
|
46
|
+
# Metadata
|
|
47
|
+
assert "Conversation: Acme discovery" in md
|
|
48
|
+
assert "https://example.com/rec" in md
|
|
49
|
+
# Participants section
|
|
50
|
+
assert "## Participants" in md
|
|
51
|
+
assert "Jane Smith" in md and "(internal)" in md
|
|
52
|
+
# Deals section
|
|
53
|
+
assert "## Deals" in md and "Enterprise Deal" in md
|
|
54
|
+
# Summary section
|
|
55
|
+
assert "## Call Summary" in md
|
|
56
|
+
assert "Positive — moving to POC" in md
|
|
57
|
+
assert "Manual data entry" in md
|
|
58
|
+
assert "Send proposal" in md
|
|
59
|
+
# Transcript section
|
|
60
|
+
assert "## Transcript" in md
|
|
61
|
+
assert "**Jane Smith:** Thanks for joining." in md
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_detail_formatter_minimal_conversation() -> None:
|
|
65
|
+
"""A search-level Conversation (no transcript/summary) still renders cleanly."""
|
|
66
|
+
conv = Conversation.model_validate({"id": "c1", "topic": "Quick sync"})
|
|
67
|
+
md = format_conversation_detail(conv)
|
|
68
|
+
assert "Conversation: Quick sync" in md
|
|
69
|
+
assert "## Transcript" not in md
|
|
70
|
+
assert "## Call Summary" not in md
|
|
@@ -472,8 +472,8 @@ wheels = [
|
|
|
472
472
|
|
|
473
473
|
[[package]]
|
|
474
474
|
name = "qodev-apollo-api"
|
|
475
|
-
version = "0.
|
|
476
|
-
source = { directory = "
|
|
475
|
+
version = "0.2.2"
|
|
476
|
+
source = { directory = "../apollo-api" }
|
|
477
477
|
dependencies = [
|
|
478
478
|
{ name = "httpx" },
|
|
479
479
|
{ name = "pydantic" },
|
|
@@ -494,7 +494,7 @@ provides-extras = ["dev"]
|
|
|
494
494
|
|
|
495
495
|
[[package]]
|
|
496
496
|
name = "qodev-apollo-cli"
|
|
497
|
-
version = "
|
|
497
|
+
version = "1.1.0"
|
|
498
498
|
source = { editable = "." }
|
|
499
499
|
dependencies = [
|
|
500
500
|
{ name = "cyclopts" },
|
|
@@ -518,7 +518,7 @@ requires-dist = [
|
|
|
518
518
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0" },
|
|
519
519
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.25.0" },
|
|
520
520
|
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.15" },
|
|
521
|
-
{ name = "qodev-apollo-api", directory = "
|
|
521
|
+
{ name = "qodev-apollo-api", directory = "../apollo-api" },
|
|
522
522
|
{ name = "rich", specifier = ">=13.0" },
|
|
523
523
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15" },
|
|
524
524
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/deal-workflows.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|