Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bc512ec
Add chat pannel
CREDO23 Oct 22, 2025
9085cb2
add transition on interaction
CREDO23 Oct 22, 2025
70dd7f1
add chat pannel container
CREDO23 Oct 22, 2025
37bf344
add chat pannel view
CREDO23 Oct 22, 2025
24e366d
add 'generate podcast component'
CREDO23 Oct 22, 2025
34c07d3
add accessibilty
CREDO23 Oct 22, 2025
6d34007
add white mode
CREDO23 Oct 22, 2025
4c22f4a
add the config modal
CREDO23 Oct 22, 2025
ce658b9
Add sserver actions
CREDO23 Oct 22, 2025
05a8bd1
add generate podcast action
CREDO23 Oct 23, 2025
d2f00fd
fix fetchChatDetails return type
CREDO23 Oct 23, 2025
25dfcae
fix fetchChatDetails return type
CREDO23 Oct 23, 2025
4f36b23
get chat details on chat interface mount
CREDO23 Oct 23, 2025
caae2bc
implement generate podcast handler
CREDO23 Oct 23, 2025
f165626
update chat panel view
CREDO23 Oct 23, 2025
58fed46
updae chat config madal
CREDO23 Oct 23, 2025
9007436
fix typo
CREDO23 Oct 23, 2025
e47b1cb
stop propagation in the chat panelcl
CREDO23 Oct 23, 2025
b02b094
show podcast staleness and suggest user to re-generate
CREDO23 Oct 23, 2025
aaa6ee2
implement chat/podcast staleness
CREDO23 Oct 23, 2025
666ea87
reference the chat in the podcast
CREDO23 Oct 23, 2025
13342eb
Add use podcasts
CREDO23 Oct 23, 2025
cc3e040
update the chat state version with messages
CREDO23 Oct 23, 2025
84ac44e
Add ai bot suggestions
CREDO23 Oct 23, 2025
ce4bb7e
clean up
CREDO23 Oct 24, 2025
839453d
fix return type of getPodcastByChat handler
thierryverse Oct 27, 2025
ee139a4
fix hydration issue in podcast config modal
thierryverse Oct 27, 2025
06f541d
add a compoacted podcast player in the chat pannel
thierryverse Oct 29, 2025
678d8fb
fix podcast re-generation
thierryverse Oct 29, 2025
55e5b45
fix podcast generation
thierryverse Nov 11, 2025
ed4ec5c
remove the podcasts menu
thierryverse Nov 11, 2025
cfb62ff
fix typo
thierryverse Nov 11, 2025
9c959ba
clean up comments
thierryverse Nov 11, 2025
2902fd6
inject user instruction in the podcast generation task
thierryverse Nov 11, 2025
87c5d24
fix key down events
thierryverse Nov 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Add podcast staleness detection columns to chats and podcasts tables

This feature allows the system to detect when a podcast is outdated compared to
the current state of the chat it was generated from, enabling users to regenerate
podcasts when needed.

Revision ID: 34
Revises: 33
"""

from collections.abc import Sequence

from alembic import op

# revision identifiers
revision: str = "34"
down_revision: str | None = "33"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add columns only if they don't already exist (safe for re-runs)."""

# Add 'state_version' column to chats table (default 1)
op.execute("""
ALTER TABLE chats
ADD COLUMN IF NOT EXISTS state_version BIGINT DEFAULT 1 NOT NULL
""")

# Add 'chat_state_version' column to podcasts table
op.execute("""
ALTER TABLE podcasts
ADD COLUMN IF NOT EXISTS chat_state_version BIGINT
""")

# Add 'chat_id' column to podcasts table
op.execute("""
ALTER TABLE podcasts
ADD COLUMN IF NOT EXISTS chat_id INTEGER
""")


def downgrade() -> None:
"""Remove columns only if they exist."""

op.execute("""
ALTER TABLE podcasts
DROP COLUMN IF EXISTS chat_state_version
""")

op.execute("""
ALTER TABLE podcasts
DROP COLUMN IF EXISTS chat_id
""")

op.execute("""
ALTER TABLE chats
DROP COLUMN IF EXISTS state_version
""")
1 change: 1 addition & 0 deletions surfsense_backend/app/agents/podcaster/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Configuration:
podcast_title: str
user_id: str
search_space_id: int
user_prompt: str | None = None

@classmethod
def from_runnable_config(
Expand Down
3 changes: 2 additions & 1 deletion surfsense_backend/app/agents/podcaster/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def create_podcast_transcript(
configuration = Configuration.from_runnable_config(config)
user_id = configuration.user_id
search_space_id = configuration.search_space_id
user_prompt = configuration.user_prompt

# Get user's long context LLM
llm = await get_user_long_context_llm(state.db_session, user_id, search_space_id)
Expand All @@ -38,7 +39,7 @@ async def create_podcast_transcript(
raise RuntimeError(error_message)

# Get the prompt
prompt = get_podcast_generation_prompt()
prompt = get_podcast_generation_prompt(user_prompt)

# Create the messages
messages = [
Expand Down
13 changes: 12 additions & 1 deletion surfsense_backend/app/agents/podcaster/prompts.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import datetime


def get_podcast_generation_prompt():
def get_podcast_generation_prompt(user_prompt: str | None = None):
return f"""
Today's date: {datetime.datetime.now().strftime("%Y-%m-%d")}
<podcast_generation_system>
You are a master podcast scriptwriter, adept at transforming diverse input content into a lively, engaging, and natural-sounding conversation between two distinct podcast hosts. Your primary objective is to craft authentic, flowing dialogue that captures the spontaneity and chemistry of a real podcast discussion, completely avoiding any hint of robotic scripting or stiff formality. Think dynamic interplay, not just information delivery.

{
f'''
You **MUST** strictly adhere to the following user instruction while generating the podcast script:
<user_instruction>
{user_prompt}
</user_instruction>
'''
if user_prompt
else ""
}

<input>
- '<source_content>': A block of text containing the information to be discussed in the podcast. This could be research findings, an article summary, a detailed outline, user chat history related to the topic, or any other relevant raw information. The content might be unstructured but serves as the factual basis for the podcast dialogue.
</input>
Expand Down
6 changes: 6 additions & 0 deletions surfsense_backend/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ARRAY,
JSON,
TIMESTAMP,
BigInteger,
Boolean,
Column,
Enum as SQLAlchemyEnum,
Expand Down Expand Up @@ -157,6 +158,7 @@ class Chat(BaseModel, TimestampMixin):
title = Column(String, nullable=False, index=True)
initial_connectors = Column(ARRAY(String), nullable=True)
messages = Column(JSON, nullable=False)
state_version = Column(BigInteger, nullable=False, default=1)

search_space_id = Column(
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
Expand Down Expand Up @@ -203,6 +205,10 @@ class Podcast(BaseModel, TimestampMixin):
title = Column(String, nullable=False, index=True)
podcast_transcript = Column(JSON, nullable=False, default={})
file_location = Column(String(500), nullable=False, default="")
chat_id = Column(
Integer, ForeignKey("chats.id", ondelete="CASCADE"), nullable=True
) # If generated from a chat, this will be the chat id, else null ( can be from a document or a chat )
chat_state_version = Column(BigInteger, nullable=True)

search_space_id = Column(
Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False
Expand Down
4 changes: 4 additions & 0 deletions surfsense_backend/app/routes/chats_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ async def read_chats(
Chat.initial_connectors,
Chat.search_space_id,
Chat.created_at,
Chat.state_version,
)
.join(SearchSpace)
.filter(SearchSpace.user_id == user.id)
Expand Down Expand Up @@ -258,7 +259,10 @@ async def update_chat(
db_chat = await read_chat(chat_id, session, user)
update_data = chat_update.model_dump(exclude_unset=True)
for key, value in update_data.items():
if key == "messages":
db_chat.state_version = len(update_data["messages"])
setattr(db_chat, key, value)

await session.commit()
await session.refresh(db_chat)
return db_chat
Expand Down
38 changes: 35 additions & 3 deletions surfsense_backend/app/routes/podcasts_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,19 @@ async def delete_podcast(


async def generate_chat_podcast_with_new_session(
chat_id: int, search_space_id: int, podcast_title: str, user_id: int
chat_id: int,
search_space_id: int,
user_id: int,
podcast_title: str | None = None,
user_prompt: str | None = None,
):
"""Create a new session and process chat podcast generation."""
from app.db import async_session_maker

async with async_session_maker() as session:
try:
await generate_chat_podcast(
session, chat_id, search_space_id, podcast_title, user_id
session, chat_id, search_space_id, user_id, podcast_title, user_prompt
)
except Exception as e:
import logging
Expand Down Expand Up @@ -211,7 +215,11 @@ async def generate_podcast(
# Add Celery tasks for each chat ID
for chat_id in valid_chat_ids:
generate_chat_podcast_task.delay(
chat_id, request.search_space_id, request.podcast_title, user.id
chat_id,
request.search_space_id,
user.id,
request.podcast_title,
request.user_prompt,
)

return {
Expand Down Expand Up @@ -287,3 +295,27 @@ def iterfile():
raise HTTPException(
status_code=500, detail=f"Error streaming podcast: {e!s}"
) from e


@router.get("/podcasts/by-chat/{chat_id}", response_model=PodcastRead | None)
async def get_podcast_by_chat_id(
chat_id: int,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
try:
# Get the podcast and check if user has access
result = await session.execute(
select(Podcast)
.join(SearchSpace)
.filter(Podcast.chat_id == chat_id, SearchSpace.user_id == user.id)
)
podcast = result.scalars().first()

return podcast
except HTTPException as he:
raise he
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Error fetching podcast: {e!s}"
) from e
2 changes: 2 additions & 0 deletions surfsense_backend/app/schemas/chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ class ChatBase(BaseModel):
initial_connectors: list[str] | None = None
messages: list[Any]
search_space_id: int
state_version: int = 1


class ChatBaseWithoutMessages(BaseModel):
type: ChatType
title: str
search_space_id: int
state_version: int = 1


class ClientAttachment(BaseModel):
Expand Down
4 changes: 3 additions & 1 deletion surfsense_backend/app/schemas/podcasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class PodcastBase(BaseModel):
podcast_transcript: list[Any]
file_location: str = ""
search_space_id: int
chat_state_version: int | None = None


class PodcastCreate(PodcastBase):
Expand All @@ -28,4 +29,5 @@ class PodcastGenerateRequest(BaseModel):
type: Literal["DOCUMENT", "CHAT"]
ids: list[int]
search_space_id: int
podcast_title: str = "SurfSense Podcast"
podcast_title: str | None = None
user_prompt: str | None = None
22 changes: 17 additions & 5 deletions surfsense_backend/app/tasks/celery_tasks/podcast_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,31 @@ def get_celery_session_maker():

@celery_app.task(name="generate_chat_podcast", bind=True)
def generate_chat_podcast_task(
self, chat_id: int, search_space_id: int, podcast_title: str, user_id: int
self,
chat_id: int,
search_space_id: int,
user_id: int,
podcast_title: str | None = None,
user_prompt: str | None = None,
):
"""
Celery task to generate podcast from chat.

Args:
chat_id: ID of the chat to generate podcast from
search_space_id: ID of the search space
user_id: ID of the user,
podcast_title: Title for the podcast
user_id: ID of the user
user_prompt: Optional prompt from the user to guide the podcast generation
"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

try:
loop.run_until_complete(
_generate_chat_podcast(chat_id, search_space_id, podcast_title, user_id)
_generate_chat_podcast(
chat_id, search_space_id, user_id, podcast_title, user_prompt
)
)
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
Expand All @@ -63,13 +71,17 @@ def generate_chat_podcast_task(


async def _generate_chat_podcast(
chat_id: int, search_space_id: int, podcast_title: str, user_id: int
chat_id: int,
search_space_id: int,
user_id: int,
podcast_title: str | None = None,
user_prompt: str | None = None,
):
"""Generate chat podcast with new session."""
async with get_celery_session_maker()() as session:
try:
await generate_chat_podcast(
session, chat_id, search_space_id, podcast_title, user_id
session, chat_id, search_space_id, user_id, podcast_title, user_prompt
)
except Exception as e:
logger.error(f"Error generating podcast from chat: {e!s}")
Expand Down
Loading
Loading