Skip to content
Merged
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
63 changes: 58 additions & 5 deletions parallel_web_tools/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,51 @@ def research_processors():
console.print("\n[dim]Use --processor/-p to select a processor[/dim]")


def _extract_executive_summary(content: Any) -> str | None:
"""Extract the executive summary from research content.

For markdown strings, the executive summary is the text before the first
## heading. For dicts, checks for a 'summary' or 'executive_summary' key.
Returns None if no summary can be extracted.
"""
if isinstance(content, str):
# Find text before the first ## heading
text = content.strip()
if not text:
return None

# Split on first ## heading (but not # which is the title)
import re

match = re.search(r"^##\s", text, re.MULTILINE)
if match:
summary = text[: match.start()].strip()
else:
summary = text

# Strip a leading # title line if present
lines = summary.split("\n")
if lines and lines[0].startswith("# "):
lines = lines[1:]
summary = "\n".join(lines).strip()

# Return if we have meaningful content (not just a title)
if summary and len(summary) > 20:
return summary

if isinstance(content, dict):
# Check for {text: "..."} structure
if "text" in content and len(content) == 1:
return _extract_executive_summary(content["text"])

for key in ("summary", "executive_summary"):
if key in content:
val = content[key]
return str(val) if val else None

return None


def _content_to_markdown(content: Any, level: int = 1) -> str:
"""Convert structured content to markdown.

Expand Down Expand Up @@ -1434,12 +1479,20 @@ def _output_research_result(
console.print(f"[dim]Task: {result.get('run_id')}[/dim]")
console.print(f"[dim]URL: {result.get('result_url')}[/dim]\n")

# Show summary of output
# Show executive summary if available
output = result.get("output", {})
if isinstance(output, dict):
console.print(f"[dim]Output contains {len(output)} fields[/dim]")
if not output_file:
console.print("[dim]Use --output to save full JSON to a file, or --json to print to stdout[/dim]")
content = output.get("content") if isinstance(output, dict) else None
summary = _extract_executive_summary(content) if content else None

if summary:
from rich.markdown import Markdown
from rich.panel import Panel

console.print(Panel(Markdown(summary), title="Executive Summary", border_style="cyan"))
console.print()

if not output_file:
console.print("[dim]Use --output to save full results to a file, or --json to print to stdout[/dim]")


if __name__ == "__main__":
Expand Down
138 changes: 137 additions & 1 deletion tests/test_research.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
from click.testing import CliRunner

from parallel_web_tools.cli.commands import main
from parallel_web_tools.cli.commands import _extract_executive_summary, main
from parallel_web_tools.core.research import (
RESEARCH_PROCESSORS,
_serialize_output,
Expand Down Expand Up @@ -692,3 +692,139 @@ def on_status(status, run_id):

assert statuses[0] == "polling"
assert "completed" in statuses


class TestExtractExecutiveSummary:
"""Tests for _extract_executive_summary function."""

def test_markdown_string_with_summary_before_heading(self):
"""Should extract text before the first ## heading."""
content = "# Report Title\n\nThis is the executive summary.\n\n## Section 1\n\nDetails here."
result = _extract_executive_summary(content)
assert result == "This is the executive summary."

def test_markdown_string_no_title(self):
"""Should extract text before the first ## heading when no title."""
content = "This is the summary paragraph.\n\nMore summary text.\n\n## Details\n\nSection content."
result = _extract_executive_summary(content)
assert result == "This is the summary paragraph.\n\nMore summary text."

def test_markdown_string_no_headings(self):
"""Should return all content when no ## headings exist."""
content = "# Title\n\nThis is a short report with no subsections but enough text to be a summary."
result = _extract_executive_summary(content)
assert result is not None
assert "short report" in result

def test_markdown_string_empty(self):
"""Should return None for empty string."""
assert _extract_executive_summary("") is None

def test_markdown_string_too_short(self):
"""Should return None when summary is too short."""
content = "# Title\n\nShort.\n\n## Section\n\nDetails."
assert _extract_executive_summary(content) is None

def test_dict_with_text_key(self):
"""Should extract summary from dict with text key."""
content = {
"text": "# Title\n\nThe executive summary here is long enough to be meaningful.\n\n## Section\n\nBody."
}
result = _extract_executive_summary(content)
assert result is not None
assert "executive summary" in result

def test_dict_with_summary_key(self):
"""Should extract summary from dict with summary key."""
content = {"summary": "This is the summary.", "key_findings": ["a", "b"]}
result = _extract_executive_summary(content)
assert result == "This is the summary."

def test_dict_with_executive_summary_key(self):
"""Should extract from executive_summary key."""
content = {"executive_summary": "Executive overview here.", "details": "..."}
result = _extract_executive_summary(content)
assert result == "Executive overview here."

def test_none_content(self):
"""Should return None for None content."""
assert _extract_executive_summary(None) is None

def test_non_string_non_dict(self):
"""Should return None for unsupported types."""
assert _extract_executive_summary(42) is None
assert _extract_executive_summary([1, 2, 3]) is None


class TestResearchOutputExecutiveSummary:
"""Tests that the executive summary is printed to console."""

def test_research_run_prints_executive_summary(self, runner):
"""Should print executive summary when research completes."""
with mock.patch("parallel_web_tools.cli.commands.run_research") as mock_run:
mock_run.return_value = {
"run_id": "trun_123",
"result_url": "https://platform.parallel.ai/play/deep-research/trun_123",
"status": "completed",
"output": {
"content": "# Deep Research Report\n\nThis is the executive summary of the research findings.\n\n## Section 1\n\nDetailed analysis here."
},
}

result = runner.invoke(main, ["research", "run", "What is AI?", "--poll-interval", "1"])

assert result.exit_code == 0
assert "Research Complete" in result.output
assert "Executive Summary" in result.output
assert "executive summary of the research" in result.output

def test_research_poll_prints_executive_summary(self, runner):
"""Should print executive summary when polling completes."""
with mock.patch("parallel_web_tools.cli.commands.poll_research") as mock_poll:
mock_poll.return_value = {
"run_id": "trun_456",
"result_url": "https://platform.parallel.ai/play/deep-research/trun_456",
"status": "completed",
"output": {
"content": "# Report\n\nHere is a substantial executive summary with enough content.\n\n## Analysis\n\nBody text."
},
}

result = runner.invoke(main, ["research", "poll", "trun_456", "--poll-interval", "1"])

assert result.exit_code == 0
assert "Executive Summary" in result.output
assert "substantial executive summary" in result.output

def test_no_summary_when_content_missing(self, runner):
"""Should not crash when content is missing."""
with mock.patch("parallel_web_tools.cli.commands.run_research") as mock_run:
mock_run.return_value = {
"run_id": "trun_789",
"result_url": "https://platform.parallel.ai/play/deep-research/trun_789",
"status": "completed",
"output": {"other_field": "value"},
}

result = runner.invoke(main, ["research", "run", "What is AI?", "--poll-interval", "1"])

assert result.exit_code == 0
assert "Research Complete" in result.output
assert "Executive Summary" not in result.output

def test_no_summary_for_json_output(self, runner):
"""Should not print summary panel when --json is used."""
with mock.patch("parallel_web_tools.cli.commands.run_research") as mock_run:
mock_run.return_value = {
"run_id": "trun_json",
"result_url": "https://platform.parallel.ai/play/deep-research/trun_json",
"status": "completed",
"output": {
"content": "# Report\n\nThis is a long executive summary for testing.\n\n## Section\n\nBody."
},
}

result = runner.invoke(main, ["research", "run", "What is AI?", "--poll-interval", "1", "--json"])

assert result.exit_code == 0
assert "Executive Summary" not in result.output