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.
Files changed (53) hide show
  1. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/CHANGELOG.md +8 -0
  2. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/PKG-INFO +3 -1
  3. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/README.md +2 -0
  4. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/pyproject.toml +2 -2
  5. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/app.py +2 -0
  6. qodev_apollo_cli-1.1.0/src/apollo_cli/commands/conversations.py +48 -0
  7. qodev_apollo_cli-1.1.0/src/apollo_cli/formatters/conversations.py +109 -0
  8. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/SKILL.md +9 -0
  9. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/conftest.py +15 -0
  10. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_commands.py +59 -0
  11. qodev_apollo_cli-1.1.0/tests/test_conversations_formatter.py +70 -0
  12. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/uv.lock +4 -4
  13. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.github/workflows/ci.yml +0 -0
  14. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.github/workflows/publish.yml +0 -0
  15. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/.gitignore +0 -0
  16. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/LICENSE +0 -0
  17. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/__init__.py +0 -0
  18. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/__main__.py +0 -0
  19. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/__init__.py +0 -0
  20. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/accounts.py +0 -0
  21. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/calls.py +0 -0
  22. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/contacts.py +0 -0
  23. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/deals.py +0 -0
  24. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/emails.py +0 -0
  25. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/enrich.py +0 -0
  26. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/install.py +0 -0
  27. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/jobs.py +0 -0
  28. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/news.py +0 -0
  29. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/notes.py +0 -0
  30. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/people.py +0 -0
  31. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/pipelines.py +0 -0
  32. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/tasks.py +0 -0
  33. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/commands/usage.py +0 -0
  34. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/context.py +0 -0
  35. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/__init__.py +0 -0
  36. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/accounts.py +0 -0
  37. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/contacts.py +0 -0
  38. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/deals.py +0 -0
  39. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/formatters/generic.py +0 -0
  40. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/help_reference.py +0 -0
  41. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/linkedin.py +0 -0
  42. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/output.py +0 -0
  43. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/__init__.py +0 -0
  44. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/__init__.py +0 -0
  45. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/account-workflows.md +0 -0
  46. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/contact-workflows.md +0 -0
  47. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/skills/references/deal-workflows.md +0 -0
  48. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/src/apollo_cli/util.py +0 -0
  49. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_context.py +0 -0
  50. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_install.py +0 -0
  51. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_linkedin.py +0 -0
  52. {qodev_apollo_cli-1.0.0 → qodev_apollo_cli-1.1.0}/tests/test_output.py +0 -0
  53. {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.0.0
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.0.0"
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 = "../../libs/apollo" }
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.1.3"
476
- source = { directory = "../../libs/apollo" }
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 = "0.1.0"
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 = "../../libs/apollo" },
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
  ]