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
237 lines
8.2 KiB
Python
237 lines
8.2 KiB
Python
import asyncio
|
|
import re
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
import nest_asyncio
|
|
import streamlit as st
|
|
from loguru import logger
|
|
|
|
nest_asyncio.apply()
|
|
from api.chat_service import chat_service
|
|
from api.models_service import models_service
|
|
from open_notebook.utils import (
|
|
compare_versions,
|
|
get_installed_version,
|
|
get_version_from_github,
|
|
)
|
|
|
|
|
|
def version_sidebar():
|
|
with st.sidebar:
|
|
# Get current version
|
|
try:
|
|
current_version = get_installed_version("open-notebook")
|
|
except Exception:
|
|
# Fallback to reading directly from pyproject.toml
|
|
import tomli
|
|
|
|
with open("pyproject.toml", "rb") as f:
|
|
pyproject = tomli.load(f)
|
|
current_version = pyproject["project"]["version"]
|
|
|
|
st.write(f"Open Notebook: {current_version}")
|
|
|
|
# Try to get latest version, but don't fail if unavailable
|
|
try:
|
|
# Use session state cache to avoid repeated checks
|
|
if 'latest_version' not in st.session_state or 'version_check_failed' not in st.session_state:
|
|
latest_version = get_version_from_github(
|
|
"https://www.github.com/lfnovo/open-notebook", "main"
|
|
)
|
|
st.session_state.latest_version = latest_version
|
|
st.session_state.version_check_failed = False
|
|
else:
|
|
latest_version = st.session_state.latest_version
|
|
|
|
if not st.session_state.version_check_failed and compare_versions(current_version, latest_version) < 0:
|
|
st.warning(
|
|
f"New version {latest_version} available. [Click here for upgrade instructions](https://github.com/lfnovo/open-notebook/blob/main/docs/SETUP.md#upgrading-open-notebook)"
|
|
)
|
|
except Exception:
|
|
# Cache the fact that version check failed to avoid repeated attempts
|
|
st.session_state.version_check_failed = True
|
|
# Optionally show a subtle message about failed update check
|
|
st.caption("⚠️ Could not check for updates (offline or GitHub unavailable)")
|
|
|
|
|
|
def create_session_for_notebook(notebook_id: str, session_name: Optional[str] = None):
|
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
title = f"Chat Session {current_time}" if not session_name else session_name
|
|
session_data = asyncio.run(chat_service.create_session(notebook_id, title))
|
|
return session_data
|
|
|
|
|
|
def setup_stream_state(current_notebook) -> dict:
|
|
"""
|
|
Sets the value of the current session_id for API-based chat functionality.
|
|
Creates or retrieves a chat session and sets up session state.
|
|
"""
|
|
assert current_notebook is not None and current_notebook.id, (
|
|
"Current Notebook not selected properly"
|
|
)
|
|
|
|
if "context_config" not in st.session_state[current_notebook.id]:
|
|
st.session_state[current_notebook.id]["context_config"] = {}
|
|
|
|
current_session_id = st.session_state[current_notebook.id].get("active_session")
|
|
|
|
# gets the chat session if provided
|
|
chat_session = None
|
|
if current_session_id:
|
|
try:
|
|
chat_session = asyncio.run(chat_service.get_session(current_session_id))
|
|
except Exception as e:
|
|
logger.warning(f"Could not retrieve session {current_session_id}: {e}")
|
|
|
|
# if there is no chat session, create one or get the first one
|
|
if not chat_session:
|
|
sessions = asyncio.run(chat_service.get_sessions(current_notebook.id))
|
|
if not sessions or len(sessions) == 0:
|
|
logger.debug("Creating new chat session")
|
|
chat_session = create_session_for_notebook(current_notebook.id)
|
|
else:
|
|
logger.debug("Getting last updated session")
|
|
chat_session = sessions[0]
|
|
|
|
if not chat_session or not chat_session.get("id"):
|
|
raise ValueError("Problem acquiring chat session")
|
|
|
|
# sets the active session for the notebook
|
|
session_id = chat_session["id"]
|
|
st.session_state[current_notebook.id]["active_session"] = session_id
|
|
|
|
# Initialize session state for messages if not exists
|
|
if session_id not in st.session_state:
|
|
# Load the full session with messages from API
|
|
try:
|
|
full_session = asyncio.run(chat_service.get_session(session_id))
|
|
messages = full_session.get("messages", [])
|
|
except Exception as e:
|
|
logger.warning(f"Could not load messages for session {session_id}: {e}")
|
|
messages = []
|
|
|
|
st.session_state[session_id] = {
|
|
"messages": messages,
|
|
"context": None,
|
|
"notebook": None,
|
|
"context_config": {}
|
|
}
|
|
|
|
return chat_session
|
|
|
|
|
|
def check_migration():
|
|
"""
|
|
DEPRECATED: This function is no longer used.
|
|
Database migrations now run automatically when the API starts up.
|
|
See api/main.py lifespan handler for the new migration logic.
|
|
|
|
This function is kept for backward compatibility but does nothing.
|
|
"""
|
|
# Migrations are now handled automatically by the API on startup
|
|
# No user interaction needed
|
|
if "migration_required" not in st.session_state:
|
|
st.session_state["migration_required"] = False
|
|
pass
|
|
|
|
|
|
def check_models(only_mandatory=True, stop_on_error=True):
|
|
default_models = models_service.get_default_models()
|
|
mandatory_models = [
|
|
default_models.default_chat_model,
|
|
default_models.default_transformation_model,
|
|
default_models.default_embedding_model,
|
|
]
|
|
all_models = mandatory_models + [
|
|
default_models.default_speech_to_text_model,
|
|
default_models.large_context_model,
|
|
]
|
|
|
|
if not all(mandatory_models):
|
|
st.error(
|
|
"You are missing some default models and the app will not work as expected. Please, select them on the Models page."
|
|
)
|
|
if stop_on_error:
|
|
st.stop()
|
|
|
|
if not only_mandatory:
|
|
if not all(all_models):
|
|
st.warning(
|
|
"You are missing some important optional models. The app might not work as expected. Please, select them on the Models page."
|
|
)
|
|
|
|
|
|
def handle_error(func):
|
|
"""Decorator for consistent error handling"""
|
|
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
logger.error(f"Error in {func.__name__}: {str(e)}")
|
|
logger.exception(e)
|
|
st.error(f"An error occurred: {str(e)}")
|
|
|
|
return wrapper
|
|
|
|
|
|
def setup_page(
|
|
title: str,
|
|
layout="wide",
|
|
sidebar_state="expanded",
|
|
only_check_mandatory_models=True,
|
|
stop_on_model_error=True,
|
|
skip_model_check=False,
|
|
):
|
|
"""Common page setup for all pages"""
|
|
st.set_page_config(
|
|
page_title=title, layout=layout, initial_sidebar_state=sidebar_state
|
|
)
|
|
|
|
# Check authentication first
|
|
from pages.stream_app.auth import check_password
|
|
check_password()
|
|
|
|
check_migration()
|
|
|
|
# Skip model check if requested (e.g., on Models page)
|
|
if not skip_model_check:
|
|
check_models(
|
|
only_mandatory=only_check_mandatory_models, stop_on_error=stop_on_model_error
|
|
)
|
|
|
|
version_sidebar()
|
|
|
|
|
|
def convert_source_references(text):
|
|
"""
|
|
Converts source references in brackets to markdown-style links.
|
|
|
|
Matches patterns like [source_insight:id], [note:id], [source:id], or [source_embedding:id]
|
|
and converts them to markdown links.
|
|
|
|
Args:
|
|
text (str): The input text containing source references
|
|
|
|
Returns:
|
|
str: Text with source references converted to markdown links
|
|
|
|
Example:
|
|
>>> text = "Here is a reference [source_insight:abc123]"
|
|
>>> convert_source_references(text)
|
|
'Here is a reference [source_insight:abc123](/?object_id=source_insight:abc123)'
|
|
"""
|
|
|
|
# Pattern matches [type:id] where type can be source_insight, note, source, or source_embedding
|
|
pattern = r"\[((?:source_insight|note|source|source_embedding):[\w\d]+)\]"
|
|
|
|
def replace_match(match):
|
|
"""Helper function to create the markdown link"""
|
|
source_ref = match.group(1) # Gets the content inside brackets
|
|
return f"[[{source_ref}]](/?object_id={source_ref})"
|
|
|
|
# Replace all matches in the text
|
|
converted_text = re.sub(pattern, replace_match, text)
|
|
|
|
return converted_text
|