Skip to content

Save and restore state, incl dataframes #1980

@vnijs

Description

@vnijs

Hi @schloerke: @cpsievert suggested connecting with you about the new state save and restore features. Hopefully this path is appropriate.

I cobbled together a working solution to save and restore data, incl dataframes, but it seems a bit clunky (i.e., state is being updated many times to avoid losing information on browser refresh). See example below. Would love to hear your suggestions.

A few additional questions:

  • The shiny bookmarks folder can fill up quickly with this approach. My ideal would be to store state for a specific user in one file (or folder with a data and a json file) only on (1) browser refresh and (2) a user button click. I see a setting for ".dir". Is there a setting to control the file name for state?
  • If you "Add data" in the app and then refresh, the "Select data" dropdown visibly flips from C to A to C. Not an issue in this app but if an analysis would run based on the value of "Select data" ...
  • Any suggestions on how to best approach save state to a specific file/folder and then restoring state from a specific file / folder in shiny-for-python would be very interesting.
import os

from chatlas import ChatOpenAI
from shiny import App, ui, reactive, render
from shiny.bookmark import BookmarkState, RestoreState
from starlette.requests import Request
from dotenv import load_dotenv
import pandas as pd
import pickle
from pathlib import Path

load_dotenv()

chat_client = ChatOpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    model="gpt-4o",
    system_prompt="You are a helpful assistant.",
)


DATASETS_PATH = Path("datasets.pkl")


def app_ui(request: Request):
    return ui.page_sidebar(
        ui.sidebar(
            ui.h4("Test"),
            ui.input_select("select_letter", "Select letter", choices=["X", "Y", "Z"]),
            ui.input_select("select_data", "Select data", choices=["A", "B"]),
            # ui.output_ui("ui_select_data"),  # Use different ID for output
            ui.input_action_button("add_data", "Add Data"),
            ui.input_action_button("remove_data", "Remove Data"),
            ui.output_ui("data"),
            width=400,
        ),
        ui.chat_ui(
            id="chat",
            messages=["Hello! How can I help you today?"],
        ),
        width=400,
    )


def server(input, output, session):
    if DATASETS_PATH.exists() and DATASETS_PATH.stat().st_size > 0:
        with open(DATASETS_PATH, "rb") as f:
            _datasets = pickle.load(f)
    else:
        _datasets = {
            "A": pd.DataFrame({"a": [1, 2], "b": [3, 4], "c": [5, 6]}),
            "B": pd.DataFrame({"b": [3, 4], "c": [7, 8], "d": [9, 10]}),
        }

    datasets = reactive.value(_datasets)
    available_datasets = reactive.value(list(_datasets.keys()))

    chat = ui.Chat(id="chat")

    @reactive.Effect
    @reactive.event(input.add_data)
    def _():
        tmp = datasets.get()
        tmp["C"] = pd.DataFrame({"c": [5, 6], "d": [7, 8], "e": [9, 10]})
        datasets.set(tmp)
        tmp = list(tmp.keys())
        available_datasets.set(tmp)
        ui.update_select("select_data", choices=tmp, selected="C")

    @reactive.Effect
    @reactive.event(input.remove_data)
    def _():
        tmp = datasets.get()
        del tmp[input.select_data()]
        datasets.set(tmp)

        tmp = list(tmp.keys())
        available_datasets.set(tmp)
        ui.update_select("select_data", choices=tmp, selected=tmp[0])

    @render.ui
    def ui_select_data():  # Match the output ID
        choices = available_datasets.get()
        return ui.input_select("select_data", "Select data", choices=choices)

    @render.ui
    @reactive.event(input.select_data)
    def data():
        selected = input.select_data()
        data_dict = datasets.get()
        if selected in data_dict:
            return ui.output_data_frame("selected_data")
        return ui.p("No data selected")

    @render.data_frame
    def selected_data():
        selected = input.select_data()
        data_dict = datasets.get()
        return data_dict.get(selected)

    chat.enable_bookmarking(chat_client)

    @chat.on_user_submit
    async def handle_user_input(user_input: str):
        response = await chat_client.stream_async(user_input)
        await chat.append_message_stream(response)

    @session.bookmark.on_restore
    def _(state: RestoreState) -> None:
        if "available_datasets" in state.values and "select_data" in state.values:
            tmp = available_datasets.get()
            ui.update_select(
                "select_data", choices=tmp, selected=state.values["select_data"]
            )

    @session.bookmark.on_bookmark
    def _(state: BookmarkState) -> None:
        state.values["available_datasets"] = available_datasets.get()
        state.values["select_data"] = input.select_data()
        state.values["select_letter"] = input.select_letter()

    @reactive.Effect
    @reactive.event(
        input.select_data, input.select_letter, input.add_data, input.remove_data
    )
    async def _():
        await session.bookmark()

    @reactive.Effect
    @reactive.event(input.add_data, input.remove_data)
    def _():
        with open(DATASETS_PATH, "wb") as f:
            pickle.dump(datasets.get(), f)


app = App(app_ui, server, bookmark_store="server")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions