SurfSense/surfsense_backend/app/agents/new_chat/tools/podcast.py

167 lines
5.7 KiB
Python
Raw Normal View History

"""
Podcast generation tool for the SurfSense agent.
This module provides a factory function for creating the generate_podcast tool
that submits a Celery task for background podcast generation. The frontend
polls for completion and auto-updates when the podcast is ready.
Duplicate request prevention:
- Only one podcast can be generated at a time per search space
- Uses Redis to track active podcast tasks
- Returns a friendly message if a podcast is already being generated
"""
from typing import Any
import redis
from langchain_core.tools import tool
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
2026-01-27 17:51:36 +02:00
from app.db import Podcast, PodcastStatus
# Redis connection for tracking active podcast tasks
# Defaults to the Celery broker when REDIS_APP_URL is not set
REDIS_URL = config.REDIS_APP_URL
_redis_client: redis.Redis | None = None
def get_redis_client() -> redis.Redis:
"""Get or create Redis client for podcast task tracking."""
global _redis_client
if _redis_client is None:
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
return _redis_client
2026-01-27 17:51:36 +02:00
def _redis_key(search_space_id: int) -> str:
return f"podcast:generating:{search_space_id}"
2026-01-27 17:51:36 +02:00
def get_generating_podcast_id(search_space_id: int) -> int | None:
"""Get the podcast ID currently being generated for this search space."""
try:
client = get_redis_client()
2026-01-27 17:51:36 +02:00
value = client.get(_redis_key(search_space_id))
return int(value) if value else None
except Exception:
return None
2026-01-27 17:51:36 +02:00
def set_generating_podcast(search_space_id: int, podcast_id: int) -> None:
"""Mark a podcast as currently generating for this search space."""
try:
client = get_redis_client()
2026-01-27 17:51:36 +02:00
client.setex(_redis_key(search_space_id), 1800, str(podcast_id))
except Exception as e:
print(
f"[generate_podcast] Warning: Could not set generating podcast in Redis: {e}"
)
def create_generate_podcast_tool(
search_space_id: int,
db_session: AsyncSession,
thread_id: int | None = None,
):
"""
Factory function to create the generate_podcast tool with injected dependencies.
2026-01-27 17:51:36 +02:00
Pre-creates podcast record with pending status so podcast_id is available
immediately for frontend polling.
Args:
search_space_id: The user's search space ID
2026-01-27 17:51:36 +02:00
db_session: Database session for creating the podcast record
thread_id: The chat thread ID for associating the podcast
Returns:
A configured tool function for generating podcasts
"""
@tool
async def generate_podcast(
source_content: str,
podcast_title: str = "SurfSense Podcast",
user_prompt: str | None = None,
) -> dict[str, Any]:
"""
Generate a podcast from the provided content.
Use this tool when the user asks to create, generate, or make a podcast.
Common triggers include phrases like:
- "Give me a podcast about this"
- "Create a podcast from this conversation"
- "Generate a podcast summary"
- "Make a podcast about..."
- "Turn this into a podcast"
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.
Returns:
A dictionary containing:
2026-01-27 17:51:36 +02:00
- status: PodcastStatus value (pending, generating, or failed)
- podcast_id: The podcast ID for polling (when status is pending or generating)
- title: The podcast title
2026-01-27 17:51:36 +02:00
- message: Status message (or "error" field if status is failed)
"""
try:
2026-01-27 17:51:36 +02:00
generating_podcast_id = get_generating_podcast_id(search_space_id)
if generating_podcast_id:
print(
2026-01-27 17:51:36 +02:00
f"[generate_podcast] Blocked duplicate request. Generating podcast: {generating_podcast_id}"
)
return {
2026-01-27 17:51:36 +02:00
"status": PodcastStatus.GENERATING.value,
"podcast_id": generating_podcast_id,
"title": podcast_title,
2026-01-27 17:51:36 +02:00
"message": "A podcast is already being generated. Please wait for it to complete.",
}
2026-01-27 17:51:36 +02:00
podcast = Podcast(
title=podcast_title,
status=PodcastStatus.PENDING,
search_space_id=search_space_id,
thread_id=thread_id,
)
db_session.add(podcast)
await db_session.commit()
await db_session.refresh(podcast)
from app.tasks.celery_tasks.podcast_tasks import (
generate_content_podcast_task,
)
task = generate_content_podcast_task.delay(
2026-01-27 17:51:36 +02:00
podcast_id=podcast.id,
source_content=source_content,
search_space_id=search_space_id,
user_prompt=user_prompt,
)
2026-01-27 17:51:36 +02:00
set_generating_podcast(search_space_id, podcast.id)
2026-01-27 17:51:36 +02:00
print(f"[generate_podcast] Created podcast {podcast.id}, task: {task.id}")
return {
2026-01-27 17:51:36 +02:00
"status": PodcastStatus.PENDING.value,
"podcast_id": podcast.id,
"title": podcast_title,
"message": "Podcast generation started. This may take a few minutes.",
}
except Exception as e:
error_message = str(e)
2026-01-27 17:51:36 +02:00
print(f"[generate_podcast] Error: {error_message}")
return {
2026-01-27 17:51:36 +02:00
"status": PodcastStatus.FAILED.value,
"error": error_message,
"title": podcast_title,
2026-01-27 17:51:36 +02:00
"podcast_id": None,
}
return generate_podcast