open-notebook/pages/stream_app/chat.py
Luis Novo b7e656a319
Version 1 (#160)
New front-end
Launch Chat API
Manage Sources
Enable re-embedding of all contents
Sources can be added without a notebook now
Improved settings
Enable model selector on all chats
Background processing for better experience
Dark mode
Improved Notes

Improved Docs: 
- Remove all Streamlit references from documentation
- Update deployment guides with React frontend setup
- Fix Docker environment variables format (SURREAL_URL, SURREAL_PASSWORD)
- Update docker image tag from :latest to :v1-latest
- Change navigation references (Settings → Models to just Models)
- Update development setup to include frontend npm commands
- Add MIGRATION.md guide for users upgrading from Streamlit
- Update quick-start guide with correct environment variables
- Add port 5055 documentation for API access
- Update project structure to reflect frontend/ directory
- Remove outdated source-chat documentation files
2025-10-18 12:46:22 -03:00

257 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import humanize
import streamlit as st
from loguru import logger
from api.chat_service import chat_service
from api.episode_profiles_service import episode_profiles_service
from api.podcast_service import PodcastService
# from open_notebook.plugins.podcasts import PodcastConfig
from open_notebook.utils import parse_thinking_content, token_count
from pages.stream_app.utils import (
convert_source_references,
create_session_for_notebook,
)
from .note import make_note_from_chat
# todo: build a smarter, more robust context manager function
async def build_context(notebook_id):
# Convert context_config format for API
context_config: dict[str, dict[str, str]] = {"sources": {}, "notes": {}}
for id, status in st.session_state[notebook_id]["context_config"].items():
if not id:
continue
item_type, item_id = id.split(":")
if item_type not in ["note", "source"]:
continue
if item_type == "source":
context_config["sources"][item_id] = status
elif item_type == "note":
context_config["notes"][item_id] = status
# Get context via API
result = await chat_service.build_context(
notebook_id=notebook_id, context_config=context_config
)
# Store in session state for compatibility
st.session_state[notebook_id]["context"] = result["context"]
return st.session_state[notebook_id]["context"]
async def execute_chat(txt_input, context, current_session):
# Execute chat via API
result = await chat_service.execute_chat(
session_id=current_session["id"],
message=txt_input,
context=context
)
# Update session state with API response
st.session_state[current_session["id"]]["messages"] = result["messages"]
return result
def chat_sidebar(current_notebook, current_session):
context = asyncio.run(build_context(notebook_id=current_notebook.id))
tokens = token_count(
str(context) + str(st.session_state[current_session["id"]]["messages"])
)
chat_tab, podcast_tab = st.tabs(["Chat", "Podcast"])
with st.expander(f"Context ({tokens} tokens), {len(str(context))} chars"):
st.json(context)
with podcast_tab:
with st.container(border=True):
# Fetch available episode profiles
try:
episode_profiles = episode_profiles_service.get_all_episode_profiles()
episode_profile_names = [ep.name for ep in episode_profiles]
except Exception as e:
st.error(f"Failed to load episode profiles: {str(e)}")
episode_profiles = []
episode_profile_names = []
if len(episode_profiles) == 0:
st.warning(
"No episode profiles found. Please create profiles in the Podcast Profiles tab first."
)
st.page_link("pages/5_🎙_Podcasts.py", label="🎙️ Go to Podcast Profiles")
else:
# Episode Profile selection
selected_episode_profile = st.selectbox(
"Episode Profile", episode_profile_names
)
# Get the selected episode profile object to access speaker_config
selected_profile_obj = next(
(
ep
for ep in episode_profiles
if ep.name == selected_episode_profile
),
None,
)
# Episode details
episode_name = st.text_input(
"Episode Name", placeholder="e.g., AI and the Future of Work"
)
instructions = st.text_area(
"Additional Instructions (Optional)",
placeholder="Any specific instructions beyond the episode profile's default briefing...",
help="These instructions will be added to the episode profile's default briefing.",
)
# Check for context availability
if len(context.get("note", [])) + len(context.get("source", [])) == 0:
st.warning(
"No notes or sources found in context. You don't want a boring podcast, right? So, add some context first."
)
else:
# Generate button
if st.button("🎙️ Generate Podcast", type="primary"):
if not episode_name.strip():
st.error("Please enter an episode name")
else:
try:
with st.spinner("Starting podcast generation..."):
# Use podcast service to generate podcast
async def generate_podcast():
return await PodcastService.submit_generation_job(
episode_profile_name=selected_episode_profile,
speaker_profile_name=selected_profile_obj.speaker_config
if selected_profile_obj
else "",
episode_name=episode_name.strip(),
content=str(context),
briefing_suffix=instructions.strip()
if instructions.strip()
else None,
notebook_id=str(current_notebook.id),
)
job_id = asyncio.run(generate_podcast())
if job_id:
st.info(
"🎉 Podcast generation started successfully! Check the **Podcasts** page to monitor progress and download results."
)
else:
st.error(
"Failed to start podcast generation: No job ID returned"
)
except Exception as e:
logger.error(f"Error generating podcast: {str(e)}")
st.error(f"Error generating podcast: {str(e)}")
# Navigation link
st.divider()
st.page_link("pages/5_🎙_Podcasts.py", label="🎙️ Go to Podcasts")
with chat_tab:
with st.expander(
f"**Session:** {current_session['title']} - {humanize.naturaltime(current_session['updated'])}"
):
new_session_name = st.text_input(
"Current Session",
key="new_session_name",
value=current_session["title"],
)
c1, c2 = st.columns(2)
if c1.button("Rename", key="rename_session"):
asyncio.run(chat_service.update_session(current_session["id"], new_session_name))
st.rerun()
if c2.button("Delete", key="delete_session_1"):
asyncio.run(chat_service.delete_session(current_session["id"]))
st.session_state[current_notebook.id]["active_session"] = None
st.rerun()
st.divider()
new_session_name = st.text_input(
"New Session Name",
key="new_session_name_f",
placeholder="Enter a name for the new session...",
)
st.caption("If no name provided, we'll use the current date.")
if st.button("Create New Session", key="create_new_session"):
new_session = create_session_for_notebook(
notebook_id=current_notebook.id, session_name=new_session_name
)
st.session_state[current_notebook.id]["active_session"] = new_session["id"]
st.rerun()
st.divider()
sessions = asyncio.run(chat_service.get_sessions(current_notebook.id))
if len(sessions) > 1:
st.markdown("**Other Sessions:**")
for session in sessions:
if session["id"] == current_session["id"]:
continue
st.markdown(
f"{session['title']} - {humanize.naturaltime(session['updated'])}"
)
if st.button(label="Load", key=f"load_session_{session['id']}"):
st.session_state[current_notebook.id]["active_session"] = (
session["id"]
)
st.rerun()
with st.container(border=True):
request = st.chat_input("Enter your question")
# removing for now since it's not multi-model capable right now
if request:
response = asyncio.run(execute_chat(
txt_input=request,
context=context,
current_session=current_session,
))
st.session_state[current_session["id"]]["messages"] = response["messages"]
for msg in st.session_state[current_session["id"]]["messages"][::-1]:
# Handle both domain objects and dict responses from API
msg_type = msg.get("type") if isinstance(msg, dict) else msg.type
msg_content = msg.get("content") if isinstance(msg, dict) else msg.content
msg_id = msg.get("id") if isinstance(msg, dict) else getattr(msg, 'id', 'unknown')
if msg_type not in ["human", "ai"]:
continue
if not msg_content:
continue
with st.chat_message(name=msg_type):
if msg_type == "ai":
# Parse thinking content for AI messages
thinking_content, cleaned_content = parse_thinking_content(
msg_content
)
# Show thinking content in expander if present
if thinking_content:
with st.expander("🤔 AI Reasoning", expanded=False):
st.markdown(thinking_content)
# Show the cleaned regular content
if cleaned_content:
st.markdown(convert_source_references(cleaned_content))
elif (
msg_content
): # Fallback to original if cleaning resulted in empty content
st.markdown(convert_source_references(msg_content))
# New Note button for AI messages
if st.button("💾 New Note", key=f"render_save_{msg_id}"):
make_note_from_chat(
content=msg_content,
notebook_id=current_notebook.id,
)
st.rerun()
else:
# Human messages - display normally
st.markdown(convert_source_references(msg_content))