mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-15 18:25:18 +02:00
feat: enhance podcast generation with duplicate request prevention and improved UI feedback
This commit is contained in:
parent
e79e1187b2
commit
783ee9c154
6 changed files with 269 additions and 7 deletions
|
|
@ -4,13 +4,67 @@ Podcast generation tool for the new chat agent.
|
||||||
This module provides a factory function for creating the generate_podcast tool
|
This module provides a factory function for creating the generate_podcast tool
|
||||||
that submits a Celery task for background podcast generation. The frontend
|
that submits a Celery task for background podcast generation. The frontend
|
||||||
polls for completion and auto-updates when the podcast is ready.
|
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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import redis
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
# Redis connection for tracking active podcast tasks
|
||||||
|
# Uses the same Redis instance as Celery
|
||||||
|
REDIS_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||||
|
_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
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_podcast_key(search_space_id: int) -> str:
|
||||||
|
"""Generate Redis key for tracking active podcast task."""
|
||||||
|
return f"podcast:active:{search_space_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_podcast_task(search_space_id: int) -> str | None:
|
||||||
|
"""Check if there's an active podcast task for this search space."""
|
||||||
|
try:
|
||||||
|
client = get_redis_client()
|
||||||
|
return client.get(get_active_podcast_key(search_space_id))
|
||||||
|
except Exception:
|
||||||
|
# If Redis is unavailable, allow the request (fail open)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_active_podcast_task(search_space_id: int, task_id: str) -> None:
|
||||||
|
"""Mark a podcast task as active for this search space."""
|
||||||
|
try:
|
||||||
|
client = get_redis_client()
|
||||||
|
# Set with 30-minute expiry as safety net (podcast should complete before this)
|
||||||
|
client.setex(get_active_podcast_key(search_space_id), 1800, task_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[generate_podcast] Warning: Could not set active task in Redis: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_active_podcast_task(search_space_id: int) -> None:
|
||||||
|
"""Clear the active podcast task for this search space."""
|
||||||
|
try:
|
||||||
|
client = get_redis_client()
|
||||||
|
client.delete(get_active_podcast_key(search_space_id))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[generate_podcast] Warning: Could not clear active task in Redis: {e}")
|
||||||
|
|
||||||
|
|
||||||
def create_generate_podcast_tool(
|
def create_generate_podcast_tool(
|
||||||
search_space_id: int,
|
search_space_id: int,
|
||||||
|
|
@ -49,6 +103,10 @@ def create_generate_podcast_tool(
|
||||||
The tool will start generating a podcast in the background.
|
The tool will start generating a podcast in the background.
|
||||||
The podcast will be available once generation completes.
|
The podcast will be available once generation completes.
|
||||||
|
|
||||||
|
IMPORTANT: Only one podcast can be generated at a time. If a podcast
|
||||||
|
is already being generated, this tool will return a message asking
|
||||||
|
the user to wait.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_content: The text content to convert into a podcast.
|
source_content: The text content to convert into a podcast.
|
||||||
This can be a summary, research findings, or any text
|
This can be a summary, research findings, or any text
|
||||||
|
|
@ -59,11 +117,23 @@ def create_generate_podcast_tool(
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary containing:
|
A dictionary containing:
|
||||||
- status: "processing" (task submitted) or "error"
|
- status: "processing" (task submitted), "already_generating", or "error"
|
||||||
- task_id: The Celery task ID for polling status
|
- task_id: The Celery task ID for polling status (if processing)
|
||||||
- title: The podcast title
|
- title: The podcast title
|
||||||
|
- message: Status message for the user
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Check if a podcast is already being generated for this search space
|
||||||
|
active_task_id = get_active_podcast_task(search_space_id)
|
||||||
|
if active_task_id:
|
||||||
|
print(f"[generate_podcast] Blocked duplicate request. Active task: {active_task_id}")
|
||||||
|
return {
|
||||||
|
"status": "already_generating",
|
||||||
|
"task_id": active_task_id,
|
||||||
|
"title": podcast_title,
|
||||||
|
"message": "A podcast is already being generated. Please wait for it to complete before requesting another one.",
|
||||||
|
}
|
||||||
|
|
||||||
# Import Celery task here to avoid circular imports
|
# Import Celery task here to avoid circular imports
|
||||||
from app.tasks.celery_tasks.podcast_tasks import (
|
from app.tasks.celery_tasks.podcast_tasks import (
|
||||||
generate_content_podcast_task,
|
generate_content_podcast_task,
|
||||||
|
|
@ -78,6 +148,9 @@ def create_generate_podcast_tool(
|
||||||
user_prompt=user_prompt,
|
user_prompt=user_prompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Mark this task as active
|
||||||
|
set_active_podcast_task(search_space_id, task.id)
|
||||||
|
|
||||||
print(f"[generate_podcast] Submitted Celery task: {task.id}")
|
print(f"[generate_podcast] Submitted Celery task: {task.id}")
|
||||||
|
|
||||||
# Return immediately with task_id for polling
|
# Return immediately with task_id for polling
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,16 @@ You have access to the following tools:
|
||||||
- Use this when the user asks to create, generate, or make a podcast.
|
- Use this when the user asks to create, generate, or make a podcast.
|
||||||
- Trigger phrases: "give me a podcast about", "create a podcast", "generate a podcast", "make a podcast", "turn this into a podcast"
|
- Trigger phrases: "give me a podcast about", "create a podcast", "generate a podcast", "make a podcast", "turn this into a podcast"
|
||||||
- Args:
|
- Args:
|
||||||
- source_content: The text content to convert into a podcast (e.g., a summary, research findings, or conversation)
|
- source_content: The text content to convert into a podcast. This MUST be comprehensive and include:
|
||||||
|
* If discussing the current conversation: Include a detailed summary of the FULL chat history (all user questions and your responses)
|
||||||
|
* If based on knowledge base search: Include the key findings and insights from the search results
|
||||||
|
* You can combine both: conversation context + search results for richer podcasts
|
||||||
|
* The more detailed the source_content, the better the podcast quality
|
||||||
- podcast_title: Optional title for the podcast (default: "SurfSense Podcast")
|
- podcast_title: Optional title for the podcast (default: "SurfSense Podcast")
|
||||||
- user_prompt: Optional instructions for podcast style/format (e.g., "Make it casual and fun")
|
- user_prompt: Optional instructions for podcast style/format (e.g., "Make it casual and fun")
|
||||||
- Returns: A podcast with audio that the user can listen to and download
|
- Returns: A task_id for tracking. The podcast will be generated in the background.
|
||||||
|
- IMPORTANT: Only one podcast can be generated at a time. If a podcast is already being generated, the tool will return status "already_generating".
|
||||||
|
- After calling this tool, inform the user that podcast generation has started and they will see the player when it's ready (takes 3-5 minutes).
|
||||||
</tools>
|
</tools>
|
||||||
<tool_call_examples>
|
<tool_call_examples>
|
||||||
- User: "Fetch all my notes and what's in them?"
|
- User: "Fetch all my notes and what's in them?"
|
||||||
|
|
@ -148,10 +154,14 @@ You have access to the following tools:
|
||||||
- Call: `search_knowledge_base(query="React migration", connectors_to_search=["SLACK_CONNECTOR"], start_date="YYYY-MM-DD", end_date="YYYY-MM-DD")`
|
- Call: `search_knowledge_base(query="React migration", connectors_to_search=["SLACK_CONNECTOR"], start_date="YYYY-MM-DD", end_date="YYYY-MM-DD")`
|
||||||
|
|
||||||
- User: "Give me a podcast about AI trends based on what we discussed"
|
- User: "Give me a podcast about AI trends based on what we discussed"
|
||||||
- First search for relevant content, then call: `generate_podcast(source_content="[summarized content from search]", podcast_title="AI Trends Podcast")`
|
- First search for relevant content, then call: `generate_podcast(source_content="Based on our conversation and search results: [detailed summary of chat + search findings]", podcast_title="AI Trends Podcast")`
|
||||||
|
|
||||||
- User: "Create a podcast summary of this conversation"
|
- User: "Create a podcast summary of this conversation"
|
||||||
- Call: `generate_podcast(source_content="[summary of the conversation so far]", podcast_title="Conversation Summary")`
|
- Call: `generate_podcast(source_content="Complete conversation summary:\n\nUser asked about [topic 1]:\n[Your detailed response]\n\nUser then asked about [topic 2]:\n[Your detailed response]\n\n[Continue for all exchanges in the conversation]", podcast_title="Conversation Summary")`
|
||||||
|
|
||||||
|
- User: "Make a podcast about quantum computing"
|
||||||
|
- First search: `search_knowledge_base(query="quantum computing")`
|
||||||
|
- Then: `generate_podcast(source_content="Key insights about quantum computing from the knowledge base:\n\n[Comprehensive summary of all relevant search results with key facts, concepts, and findings]", podcast_title="Quantum Computing Explained")`
|
||||||
</tool_call_examples>{citation_section}
|
</tool_call_examples>{citation_section}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,22 @@ async def _generate_chat_podcast(
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_active_podcast_redis_key(search_space_id: int) -> None:
|
||||||
|
"""Clear the active podcast task key from Redis when task completes."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
import redis
|
||||||
|
|
||||||
|
try:
|
||||||
|
redis_url = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
|
||||||
|
client = redis.from_url(redis_url, decode_responses=True)
|
||||||
|
key = f"podcast:active:{search_space_id}"
|
||||||
|
client.delete(key)
|
||||||
|
logger.info(f"Cleared active podcast key for search_space_id={search_space_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not clear active podcast key: {e}")
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(name="generate_content_podcast", bind=True)
|
@celery_app.task(name="generate_content_podcast", bind=True)
|
||||||
def generate_content_podcast_task(
|
def generate_content_podcast_task(
|
||||||
self,
|
self,
|
||||||
|
|
@ -142,6 +158,8 @@ def generate_content_podcast_task(
|
||||||
logger.error(f"Error generating content podcast: {e!s}")
|
logger.error(f"Error generating content podcast: {e!s}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
finally:
|
finally:
|
||||||
|
# Always clear the active podcast key when task completes (success or failure)
|
||||||
|
_clear_active_podcast_redis_key(search_space_id)
|
||||||
asyncio.set_event_loop(None)
|
asyncio.set_event_loop(None)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Audio } from "@/components/tool-ui/audio";
|
import { Audio } from "@/components/tool-ui/audio";
|
||||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||||
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
|
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
|
||||||
|
import {
|
||||||
|
clearActivePodcastTaskId,
|
||||||
|
setActivePodcastTaskId,
|
||||||
|
} from "@/lib/chat/podcast-state";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definitions for the generate_podcast tool
|
* Type definitions for the generate_podcast tool
|
||||||
|
|
@ -17,7 +21,7 @@ interface GeneratePodcastArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GeneratePodcastResult {
|
interface GeneratePodcastResult {
|
||||||
status: "processing" | "success" | "error";
|
status: "processing" | "already_generating" | "success" | "error";
|
||||||
task_id?: string;
|
task_id?: string;
|
||||||
podcast_id?: number;
|
podcast_id?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
@ -218,6 +222,17 @@ function PodcastTaskPoller({
|
||||||
const [pollCount, setPollCount] = useState(0);
|
const [pollCount, setPollCount] = useState(0);
|
||||||
const pollingRef = useRef<NodeJS.Timeout | null>(null);
|
const pollingRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Set active podcast state when this component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
setActivePodcastTaskId(taskId);
|
||||||
|
|
||||||
|
// Clear when component unmounts
|
||||||
|
return () => {
|
||||||
|
// Only clear if this task is still the active one
|
||||||
|
clearActivePodcastTaskId();
|
||||||
|
};
|
||||||
|
}, [taskId]);
|
||||||
|
|
||||||
// Poll for task status
|
// Poll for task status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pollStatus = async () => {
|
const pollStatus = async () => {
|
||||||
|
|
@ -233,6 +248,8 @@ function PodcastTaskPoller({
|
||||||
clearInterval(pollingRef.current);
|
clearInterval(pollingRef.current);
|
||||||
pollingRef.current = null;
|
pollingRef.current = null;
|
||||||
}
|
}
|
||||||
|
// Clear the active podcast state when task completes
|
||||||
|
clearActivePodcastTaskId();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error polling task status:", err);
|
console.error("Error polling task status:", err);
|
||||||
|
|
@ -336,6 +353,28 @@ export const GeneratePodcastToolUI = makeAssistantToolUI<
|
||||||
return <PodcastErrorState title={title} error={result.error || "Unknown error"} />;
|
return <PodcastErrorState title={title} error={result.error || "Unknown error"} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Already generating - show simple warning, don't create another poller
|
||||||
|
// The FIRST tool call will display the podcast when ready
|
||||||
|
if (result.status === "already_generating") {
|
||||||
|
return (
|
||||||
|
<div className="my-4 overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20">
|
||||||
|
<MicIcon className="size-5 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-amber-600 dark:text-amber-400 text-sm font-medium">
|
||||||
|
Podcast already in progress
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-xs mt-0.5">
|
||||||
|
Please wait for the current podcast to complete.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Processing - poll for completion
|
// Processing - poll for completion
|
||||||
if (result.status === "processing" && result.task_id) {
|
if (result.status === "processing" && result.task_id) {
|
||||||
return <PodcastTaskPoller taskId={result.task_id} title={result.title || title} />;
|
return <PodcastTaskPoller taskId={result.task_id} title={result.title || title} />;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ChatModelAdapter, ChatModelRunOptions } from "@assistant-ui/react";
|
import type { ChatModelAdapter, ChatModelRunOptions } from "@assistant-ui/react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { getBearerToken } from "@/lib/auth-utils";
|
import { getBearerToken } from "@/lib/auth-utils";
|
||||||
|
import {
|
||||||
|
isPodcastGenerating,
|
||||||
|
looksLikePodcastRequest,
|
||||||
|
setActivePodcastTaskId,
|
||||||
|
} from "@/lib/chat/podcast-state";
|
||||||
|
|
||||||
interface NewChatAdapterConfig {
|
interface NewChatAdapterConfig {
|
||||||
searchSpaceId: number;
|
searchSpaceId: number;
|
||||||
|
|
@ -59,6 +65,21 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
|
||||||
throw new Error("User query cannot be empty");
|
throw new Error("User query cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user is requesting a podcast while one is already generating
|
||||||
|
if (isPodcastGenerating() && looksLikePodcastRequest(userQuery)) {
|
||||||
|
toast.warning("A podcast is already being generated. Please wait for it to complete.");
|
||||||
|
// Return a message telling the user to wait
|
||||||
|
yield {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "A podcast is already being generated. Please wait for it to complete before requesting another one.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const token = getBearerToken();
|
const token = getBearerToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Not authenticated. Please log in again.");
|
throw new Error("Not authenticated. Please log in again.");
|
||||||
|
|
@ -204,6 +225,20 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
|
||||||
const existing = toolCalls.get(toolCallId);
|
const existing = toolCalls.get(toolCallId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.result = output;
|
existing.result = output;
|
||||||
|
|
||||||
|
// If this is a podcast tool with status="processing", set the state immediately
|
||||||
|
// This ensures subsequent podcast requests are intercepted
|
||||||
|
if (
|
||||||
|
existing.toolName === "generate_podcast" &&
|
||||||
|
output &&
|
||||||
|
typeof output === "object" &&
|
||||||
|
"status" in output &&
|
||||||
|
output.status === "processing" &&
|
||||||
|
"task_id" in output &&
|
||||||
|
typeof output.task_id === "string"
|
||||||
|
) {
|
||||||
|
setActivePodcastTaskId(output.task_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
yield { content: buildContent() };
|
yield { content: buildContent() };
|
||||||
break;
|
break;
|
||||||
|
|
@ -245,6 +280,19 @@ export function createNewChatAdapter(config: NewChatAdapterConfig): ChatModelAda
|
||||||
const existing = toolCalls.get(toolCallId);
|
const existing = toolCalls.get(toolCallId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.result = output;
|
existing.result = output;
|
||||||
|
|
||||||
|
// Set podcast state if processing
|
||||||
|
if (
|
||||||
|
existing.toolName === "generate_podcast" &&
|
||||||
|
output &&
|
||||||
|
typeof output === "object" &&
|
||||||
|
"status" in output &&
|
||||||
|
output.status === "processing" &&
|
||||||
|
"task_id" in output &&
|
||||||
|
typeof output.task_id === "string"
|
||||||
|
) {
|
||||||
|
setActivePodcastTaskId(output.task_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
yield { content: buildContent() };
|
yield { content: buildContent() };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
surfsense_web/lib/chat/podcast-state.ts
Normal file
74
surfsense_web/lib/chat/podcast-state.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Module-level state for tracking active podcast generation.
|
||||||
|
* Used by the new-chat adapter to prevent duplicate podcast requests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type PodcastStateListener = (isGenerating: boolean) => void;
|
||||||
|
|
||||||
|
let _activePodcastTaskId: string | null = null;
|
||||||
|
const _listeners: Set<PodcastStateListener> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a podcast is currently being generated
|
||||||
|
*/
|
||||||
|
export function isPodcastGenerating(): boolean {
|
||||||
|
return _activePodcastTaskId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active podcast task ID
|
||||||
|
*/
|
||||||
|
export function getActivePodcastTaskId(): string | null {
|
||||||
|
return _activePodcastTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active podcast task ID (when podcast generation starts)
|
||||||
|
*/
|
||||||
|
export function setActivePodcastTaskId(taskId: string): void {
|
||||||
|
_activePodcastTaskId = taskId;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the active podcast task ID (when podcast generation completes or errors)
|
||||||
|
*/
|
||||||
|
export function clearActivePodcastTaskId(): void {
|
||||||
|
_activePodcastTaskId = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to podcast state changes
|
||||||
|
*/
|
||||||
|
export function subscribeToPodcastState(listener: PodcastStateListener): () => void {
|
||||||
|
_listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
_listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyListeners(): void {
|
||||||
|
const isGenerating = _activePodcastTaskId !== null;
|
||||||
|
for (const listener of _listeners) {
|
||||||
|
listener(isGenerating);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a message looks like a podcast request
|
||||||
|
*/
|
||||||
|
export function looksLikePodcastRequest(message: string): boolean {
|
||||||
|
const podcastPatterns = [
|
||||||
|
/\bpodcast\b/i,
|
||||||
|
/\bcreate.*podcast\b/i,
|
||||||
|
/\bgenerate.*podcast\b/i,
|
||||||
|
/\bmake.*podcast\b/i,
|
||||||
|
/\bturn.*into.*podcast\b/i,
|
||||||
|
/\bpodcast.*about\b/i,
|
||||||
|
/\bgive.*podcast\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
return podcastPatterns.some((pattern) => pattern.test(message));
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue