mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-12 20:45:20 +02:00
refactor(podcasts): gate chat-triggered podcast on brief review
This commit is contained in:
parent
b7604167d8
commit
3eb7cdb2d8
3 changed files with 59 additions and 104 deletions
|
|
@ -1,11 +1,9 @@
|
|||
"""Factory for a podcast-generation tool.
|
||||
|
||||
Dispatches the heavy generation to Celery and then polls the podcast row
|
||||
until it reaches a terminal status (READY/FAILED). The tool always
|
||||
returns a real terminal ``Receipt`` — never a pending one. The wait is
|
||||
bounded by the existing per-invocation safety net
|
||||
(``SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS`` in multi-agent mode,
|
||||
HTTP / process lifetime in single-agent mode).
|
||||
Creates the podcast and proposes its brief (language, voices, length) inline,
|
||||
then returns immediately with the row awaiting review. The user approves the
|
||||
brief and the drafted transcript in the podcast panel before any audio is
|
||||
rendered, so this tool never blocks on generation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
|
@ -18,13 +16,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from app.agents.chat.multi_agent_chat.shared.receipts.command import with_receipt
|
||||
from app.agents.chat.multi_agent_chat.shared.receipts.receipt import make_receipt
|
||||
from app.agents.chat.multi_agent_chat.subagents.builtins.deliverables.deliverable_wait import (
|
||||
wait_for_deliverable,
|
||||
)
|
||||
from app.agents.chat.multi_agent_chat.subagents.builtins.deliverables.tools.thread_resolver import (
|
||||
resolve_root_thread_id,
|
||||
)
|
||||
from app.db import Podcast, PodcastStatus, shielded_async_session
|
||||
from app.db import PodcastStatus, shielded_async_session
|
||||
from app.podcasts.generation.brief import propose_brief
|
||||
from app.podcasts.service import PodcastService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -45,7 +42,7 @@ def create_generate_podcast_tool(
|
|||
user_prompt: str | None = None,
|
||||
) -> Command:
|
||||
"""
|
||||
Generate a podcast from the provided content.
|
||||
Prepare a podcast from the provided content for the user to review.
|
||||
|
||||
Use this tool when the user asks to create, generate, or make a podcast.
|
||||
Common triggers include phrases like:
|
||||
|
|
@ -55,100 +52,55 @@ def create_generate_podcast_tool(
|
|||
- "Make a podcast about..."
|
||||
- "Turn this into a podcast"
|
||||
|
||||
This sets up the podcast and proposes its brief (language, voices,
|
||||
length). The user then reviews and approves the brief and transcript in
|
||||
the podcast panel to produce the audio — generation does not start here.
|
||||
|
||||
Args:
|
||||
source_content: The text content to convert into a podcast.
|
||||
podcast_title: Title for the podcast (default: "SurfSense Podcast")
|
||||
user_prompt: Optional instructions for podcast style, tone, or format.
|
||||
user_prompt: Optional steer for what the episode should focus on.
|
||||
|
||||
Returns:
|
||||
A dictionary containing:
|
||||
- status: PodcastStatus value (pending, generating, or failed)
|
||||
- podcast_id: The podcast ID for polling (when status is pending or generating)
|
||||
- title: The podcast title
|
||||
- message: Status message (or "error" field if status is failed)
|
||||
- status: the podcast lifecycle status (awaiting_brief on success)
|
||||
- podcast_id: the podcast ID to review in the panel
|
||||
- title: the podcast title
|
||||
- message: what the user should do next (or "error" when failed)
|
||||
"""
|
||||
try:
|
||||
# One DB session per tool call so parallel invocations never share an AsyncSession.
|
||||
async with shielded_async_session() as session:
|
||||
podcast = Podcast(
|
||||
service = PodcastService(session)
|
||||
podcast = await service.create(
|
||||
title=podcast_title,
|
||||
status=PodcastStatus.PENDING,
|
||||
search_space_id=search_space_id,
|
||||
thread_id=resolve_root_thread_id(runtime, thread_id),
|
||||
)
|
||||
session.add(podcast)
|
||||
podcast.source_content = source_content
|
||||
spec = await propose_brief(
|
||||
session,
|
||||
search_space_id=search_space_id,
|
||||
focus=user_prompt,
|
||||
)
|
||||
await service.attach_brief(podcast, spec)
|
||||
await session.commit()
|
||||
await session.refresh(podcast)
|
||||
podcast_id = podcast.id
|
||||
|
||||
from app.tasks.celery_tasks.podcast_tasks import (
|
||||
generate_content_podcast_task,
|
||||
)
|
||||
|
||||
task = generate_content_podcast_task.delay(
|
||||
podcast_id=podcast_id,
|
||||
source_content=source_content,
|
||||
search_space_id=search_space_id,
|
||||
user_prompt=user_prompt,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[generate_podcast] Created podcast %s, task: %s",
|
||||
"[generate_podcast] Prepared podcast %s awaiting brief review",
|
||||
podcast_id,
|
||||
task.id,
|
||||
)
|
||||
|
||||
# Wait until the Celery worker flips the row to a terminal
|
||||
# state. The wait is bounded only by the subagent invoke
|
||||
# timeout (multi-agent) or HTTP lifetime (single-agent) —
|
||||
# see app.agents.chat.multi_agent_chat.subagents.builtins.deliverables.deliverable_wait for details.
|
||||
terminal_status, columns, elapsed = await wait_for_deliverable(
|
||||
model=Podcast,
|
||||
row_id=podcast_id,
|
||||
columns=[Podcast.status, Podcast.file_location],
|
||||
terminal_statuses={PodcastStatus.READY, PodcastStatus.FAILED},
|
||||
)
|
||||
|
||||
if terminal_status == PodcastStatus.READY:
|
||||
file_location = columns[1] if columns else None
|
||||
logger.info(
|
||||
"[generate_podcast] Podcast %s READY in %.2fs (file=%s)",
|
||||
podcast_id,
|
||||
elapsed,
|
||||
file_location,
|
||||
)
|
||||
payload: dict[str, Any] = {
|
||||
"status": PodcastStatus.READY.value,
|
||||
"podcast_id": podcast_id,
|
||||
"title": podcast_title,
|
||||
"file_location": file_location,
|
||||
"message": ("Podcast generated and saved to your podcast panel."),
|
||||
}
|
||||
return with_receipt(
|
||||
payload=payload,
|
||||
receipt=make_receipt(
|
||||
route="deliverables",
|
||||
type="podcast",
|
||||
operation="generate",
|
||||
status="success",
|
||||
external_id=str(podcast_id),
|
||||
preview=podcast_title,
|
||||
),
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
)
|
||||
|
||||
# Only other terminal state is FAILED.
|
||||
logger.warning(
|
||||
"[generate_podcast] Podcast %s FAILED in %.2fs",
|
||||
podcast_id,
|
||||
elapsed,
|
||||
)
|
||||
err = "Background worker reported FAILED status for this podcast."
|
||||
payload = {
|
||||
"status": PodcastStatus.FAILED.value,
|
||||
payload: dict[str, Any] = {
|
||||
"status": PodcastStatus.AWAITING_BRIEF.value,
|
||||
"podcast_id": podcast_id,
|
||||
"title": podcast_title,
|
||||
"error": err,
|
||||
"message": (
|
||||
"I've prepared a podcast brief — review the language, "
|
||||
"voices, and length in the podcast panel, then approve it "
|
||||
"to draft and generate the episode."
|
||||
),
|
||||
}
|
||||
return with_receipt(
|
||||
payload=payload,
|
||||
|
|
@ -156,10 +108,9 @@ def create_generate_podcast_tool(
|
|||
route="deliverables",
|
||||
type="podcast",
|
||||
operation="generate",
|
||||
status="failed",
|
||||
status="success",
|
||||
external_id=str(podcast_id),
|
||||
preview=podcast_title,
|
||||
error=err,
|
||||
),
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,22 +15,26 @@ def iter_completion_emission_frames(
|
|||
out = ctx.tool_output
|
||||
payload = out if isinstance(out, dict) else {"result": out}
|
||||
yield ctx.emit_tool_output_card(payload)
|
||||
if isinstance(out, dict) and out.get("status") in (
|
||||
status = out.get("status") if isinstance(out, dict) else None
|
||||
title = out.get("title", "Podcast") if isinstance(out, dict) else "Podcast"
|
||||
if status in (
|
||||
"awaiting_brief",
|
||||
"awaiting_review",
|
||||
"pending",
|
||||
"generating",
|
||||
"processing",
|
||||
"drafting",
|
||||
"rendering",
|
||||
):
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Podcast queued: {out.get('title', 'Podcast')}",
|
||||
f"Podcast brief ready to review: {title}",
|
||||
"success",
|
||||
)
|
||||
elif isinstance(out, dict) and out.get("status") in ("ready", "success"):
|
||||
elif status in ("ready", "success"):
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Podcast generated successfully: {out.get('title', 'Podcast')}",
|
||||
f"Podcast generated successfully: {title}",
|
||||
"success",
|
||||
)
|
||||
elif isinstance(out, dict) and out.get("status") in ("failed", "error"):
|
||||
error_msg = out.get("error", "Unknown error")
|
||||
elif status in ("failed", "error"):
|
||||
error_msg = out.get("error", "Unknown error") if isinstance(out, dict) else "Unknown error"
|
||||
yield ctx.streaming_service.format_terminal_info(
|
||||
f"Podcast generation failed: {error_msg}",
|
||||
"error",
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ def resolve_start_thinking(tool_name: str, tool_input: Any) -> ToolStartThinking
|
|||
d.get("source_content", "") if isinstance(tool_input, dict) else ""
|
||||
)
|
||||
return ToolStartThinking(
|
||||
title="Generating podcast",
|
||||
title="Preparing podcast",
|
||||
items=[
|
||||
f"Title: {podcast_title}",
|
||||
f"Content: {content_len:,} characters",
|
||||
"Preparing audio generation...",
|
||||
"Proposing brief (language, voices, length)...",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -50,17 +50,17 @@ def resolve_completed_thinking(
|
|||
if isinstance(tool_output, dict)
|
||||
else "Podcast"
|
||||
)
|
||||
if podcast_status in ("pending", "generating", "processing"):
|
||||
if podcast_status in (
|
||||
"awaiting_brief",
|
||||
"awaiting_review",
|
||||
"pending",
|
||||
"drafting",
|
||||
"rendering",
|
||||
):
|
||||
completed = [
|
||||
f"Title: {podcast_title}",
|
||||
"Podcast generation started",
|
||||
"Processing in background...",
|
||||
]
|
||||
elif podcast_status == "already_generating":
|
||||
completed = [
|
||||
f"Title: {podcast_title}",
|
||||
"Podcast already in progress",
|
||||
"Please wait for it to complete",
|
||||
"Brief ready for review",
|
||||
"Approve it in the podcast panel to generate",
|
||||
]
|
||||
elif podcast_status in ("failed", "error"):
|
||||
error_msg = (
|
||||
|
|
@ -79,4 +79,4 @@ def resolve_completed_thinking(
|
|||
]
|
||||
else:
|
||||
completed = items
|
||||
return ("Generating podcast", completed)
|
||||
return ("Preparing podcast", completed)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue