2026-03-21 22:13:41 -07:00
|
|
|
"""
|
|
|
|
|
Video presentation generation tool for the SurfSense agent.
|
|
|
|
|
|
|
|
|
|
This module provides a factory function for creating the generate_video_presentation
|
2026-05-27 14:58:10 -07:00
|
|
|
tool that submits a Celery task for background video presentation generation. The
|
|
|
|
|
tool then polls the row until it reaches a terminal status (READY/FAILED) and
|
|
|
|
|
returns that status. The wait is bounded by the chat's HTTP / process lifetime;
|
|
|
|
|
see app.agents.shared.deliverable_wait for details.
|
2026-03-21 22:13:41 -07:00
|
|
|
"""
|
|
|
|
|
|
2026-05-27 14:58:10 -07:00
|
|
|
import logging
|
2026-03-21 22:13:41 -07:00
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from langchain_core.tools import tool
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
2026-05-27 14:58:10 -07:00
|
|
|
from app.agents.shared.deliverable_wait import wait_for_deliverable
|
2026-04-28 04:32:52 -07:00
|
|
|
from app.db import VideoPresentation, VideoPresentationStatus, shielded_async_session
|
2026-03-21 22:13:41 -07:00
|
|
|
|
2026-05-27 14:58:10 -07:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-03-21 22:13:41 -07:00
|
|
|
|
|
|
|
|
def create_generate_video_presentation_tool(
|
|
|
|
|
search_space_id: int,
|
|
|
|
|
db_session: AsyncSession,
|
|
|
|
|
thread_id: int | None = None,
|
|
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Factory function to create the generate_video_presentation tool with injected dependencies.
|
|
|
|
|
|
|
|
|
|
Pre-creates video presentation record with pending status so the ID is available
|
2026-04-28 04:32:52 -07:00
|
|
|
immediately for frontend polling. The row is written via a fresh, tool-local
|
|
|
|
|
session so parallel tool calls (e.g. video + podcast in the same agent step)
|
|
|
|
|
don't share an ``AsyncSession`` (which is not concurrency-safe).
|
2026-03-21 22:13:41 -07:00
|
|
|
"""
|
2026-04-28 04:32:52 -07:00
|
|
|
del db_session # writes use a fresh tool-local session, see below
|
2026-03-21 22:13:41 -07:00
|
|
|
|
|
|
|
|
@tool
|
|
|
|
|
async def generate_video_presentation(
|
|
|
|
|
source_content: str,
|
|
|
|
|
video_title: str = "SurfSense Presentation",
|
|
|
|
|
user_prompt: str | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Generate a video presentation from the provided content.
|
|
|
|
|
|
|
|
|
|
Use this tool when the user asks to create a video, presentation, slides, or slide deck.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
source_content: The text content to turn into a presentation.
|
|
|
|
|
video_title: Title for the presentation (default: "SurfSense Presentation")
|
|
|
|
|
user_prompt: Optional style/tone instructions.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2026-04-28 04:32:52 -07:00
|
|
|
# See podcast.py for the rationale: parallel tool calls share the
|
|
|
|
|
# streaming session, and AsyncSession is not concurrency-safe —
|
|
|
|
|
# interleaved flushes produce "Session.add() during flush" and
|
|
|
|
|
# poison the transaction for every concurrent tool.
|
|
|
|
|
async with shielded_async_session() as session:
|
|
|
|
|
video_pres = VideoPresentation(
|
|
|
|
|
title=video_title,
|
|
|
|
|
status=VideoPresentationStatus.PENDING,
|
|
|
|
|
search_space_id=search_space_id,
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
)
|
|
|
|
|
session.add(video_pres)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(video_pres)
|
|
|
|
|
video_pres_id = video_pres.id
|
2026-03-21 22:13:41 -07:00
|
|
|
|
|
|
|
|
from app.tasks.celery_tasks.video_presentation_tasks import (
|
|
|
|
|
generate_video_presentation_task,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
task = generate_video_presentation_task.delay(
|
2026-04-28 04:32:52 -07:00
|
|
|
video_presentation_id=video_pres_id,
|
2026-03-21 22:13:41 -07:00
|
|
|
source_content=source_content,
|
|
|
|
|
search_space_id=search_space_id,
|
|
|
|
|
user_prompt=user_prompt,
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-27 14:58:10 -07:00
|
|
|
logger.info(
|
|
|
|
|
"[generate_video_presentation] Created video presentation %s, task: %s",
|
|
|
|
|
video_pres_id,
|
|
|
|
|
task.id,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Wait until the Celery worker flips the row to a terminal
|
|
|
|
|
# state. No internal budget — see deliverable_wait module.
|
|
|
|
|
terminal_status, _columns, elapsed = await wait_for_deliverable(
|
|
|
|
|
model=VideoPresentation,
|
|
|
|
|
row_id=video_pres_id,
|
|
|
|
|
columns=[VideoPresentation.status],
|
|
|
|
|
terminal_statuses={
|
|
|
|
|
VideoPresentationStatus.READY,
|
|
|
|
|
VideoPresentationStatus.FAILED,
|
|
|
|
|
},
|
2026-03-21 22:13:41 -07:00
|
|
|
)
|
|
|
|
|
|
2026-05-27 14:58:10 -07:00
|
|
|
if terminal_status == VideoPresentationStatus.READY:
|
|
|
|
|
logger.info(
|
|
|
|
|
"[generate_video_presentation] %s READY in %.2fs",
|
|
|
|
|
video_pres_id,
|
|
|
|
|
elapsed,
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"status": VideoPresentationStatus.READY.value,
|
|
|
|
|
"video_presentation_id": video_pres_id,
|
|
|
|
|
"title": video_title,
|
|
|
|
|
"message": "Video presentation generated and saved.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Only other terminal state is FAILED.
|
|
|
|
|
logger.warning(
|
|
|
|
|
"[generate_video_presentation] %s FAILED in %.2fs",
|
|
|
|
|
video_pres_id,
|
|
|
|
|
elapsed,
|
|
|
|
|
)
|
2026-03-21 22:13:41 -07:00
|
|
|
return {
|
2026-05-27 14:58:10 -07:00
|
|
|
"status": VideoPresentationStatus.FAILED.value,
|
2026-04-28 04:32:52 -07:00
|
|
|
"video_presentation_id": video_pres_id,
|
2026-03-21 22:13:41 -07:00
|
|
|
"title": video_title,
|
2026-05-27 14:58:10 -07:00
|
|
|
"error": (
|
|
|
|
|
"Background worker reported FAILED status for this "
|
|
|
|
|
"video presentation."
|
|
|
|
|
),
|
2026-03-21 22:13:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error_message = str(e)
|
2026-05-28 19:21:29 -07:00
|
|
|
logger.exception("[generate_video_presentation] Error: %s", error_message)
|
2026-03-21 22:13:41 -07:00
|
|
|
return {
|
|
|
|
|
"status": VideoPresentationStatus.FAILED.value,
|
|
|
|
|
"error": error_message,
|
|
|
|
|
"title": video_title,
|
|
|
|
|
"video_presentation_id": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return generate_video_presentation
|