SurfSense/surfsense_backend/app/agents/shared/tools/video_presentation.py

139 lines
5.3 KiB
Python
Raw Normal View History

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
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
"""
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
from app.agents.shared.deliverable_wait import wait_for_deliverable
from app.db import VideoPresentation, VideoPresentationStatus, shielded_async_session
2026-03-21 22:13:41 -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
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
"""
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:
# 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(
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,
)
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
)
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 {
"status": VideoPresentationStatus.FAILED.value,
"video_presentation_id": video_pres_id,
2026-03-21 22:13:41 -07:00
"title": video_title,
"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