---
title: "Streaming LLM responses to Shiny apps"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Streaming LLM responses to Shiny apps}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

This vignette shows a minimal example of how to stream a LLM response
gathered with 'tidyprompt' to a Shiny app in real-time. 

Shiny apps run on a single R process, meaning that when you call an LLM synchronously,
the UI will be blocked until the response is complete. Therefore, when integrating LLM calls
into Shiny apps, you typically want to use `send_prompt()` asynchronously 
(e.g., with the 'future' and 'promises' packages).

If you just want to show the final LLM response after it is complete, this is 
fairly straightforward. But if you want to show the LLM response as it streams in 
token-by-token, this is a bit more complex. The asynchronous process will have
to somehow communicate back to the main Shiny R process to update the UI in real-time
as new tokens arrive.

Therefore, for LLM providers created with 'tidyprompt' that support streaming responses, 
you can set the `stream_callback` field of your `llm_provider` object. This is a
function that is called for each token (or text chunk) as it arrives from the LLM.
You can then leverage this to push the tokens into the Shiny app in real-time. 
The 'ipc' package can facilitate herein, allowing inter-process communication between
the background R process (where the LLM call runs) and the main Shiny R process
(where the UI runs). Each time a new output token arrives, the `stream_callback` can push
then push the token into a reactive value in the main Shiny process.

## Example

Below is a minimal example which shows how to achieve this. We will:

- create an `llm_provider` with streaming enabled;
- define a `stream_callback` that writes tokens into an `ipc::shinyQueue`;
- start a `future` in a separate R process (`future::plan(multisession)`) where we call `send_prompt()`;
- consume the queue from the Shiny main process to update the UI, showing a live stream of LLM output.

```{r setup, eval = FALSE}
# Install & load required packages
packages <- c("shiny", "ipc", "future", "promises", "tidyprompt")
for (pkg in packages) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    install.packages(pkg)
  }
  library(pkg, character.only = TRUE)
}

# Enable asynchronous processing
future::plan(future::multisession)

# Base provider (OpenAI; has streaming enabled by default)
base_provider <- llm_provider_openai()

ui <- fluidPage(
  titlePanel("tidyprompt streaming demo"),

  sidebarLayout(
    sidebarPanel(
      textInput(
        "prompt",
        "Prompt",
        value = "Tell me a short story about a cat and a robot."
      ),
      actionButton("run", "Ask model"),
      helpText("Tokens will appear below as they stream in.")
    ),

    mainPanel(
      verbatimTextOutput("partial_response")
    )
  )
)

server <- function(input, output, session) {
  # Queue to bridge async future back into Shiny
  queue <- shinyQueue()
  queue$consumer$start(100) # process queue every 100 ms

  # Reactive that holds the accumulated streamed text
  partial_response <- reactiveVal("")

  # Streaming callback run inside the provider
  stream_cb <- function(token, meta) {
    # meta$partial_response is the accumulated text so far
    queue$producer$fireAssignReactive(
      "partial_response",
      meta$partial_response
    )
    invisible(TRUE)
  }

  # Clone provider for this session and attach callback + streaming
  provider <- base_provider$clone()
  provider$parameters$stream <- TRUE
  provider$stream_callback <- stream_cb

  # Expose the reactive value to the UI
  output$partial_response <- renderText({
    req(partial_response())
    partial_response()
  })

  observeEvent(input$run, {
    # Reset current streamed text on each run
    partial_response("")

    user_prompt <- input$prompt

    future_promise(
      {
        tidyprompt::send_prompt(
          prompt = user_prompt,
          llm_provider = provider,
          return_mode = "only_response"
        )
      },
      globals = list(
        user_prompt = user_prompt,
        provider = provider
      )
    ) |>
      then(
        onFulfilled = function(value) {
          # Final response once streaming finishes
          partial_response(value)
        },
        onRejected = function(error) {
          showNotification(
            paste("Error:", error$message),
            type = "error"
          )
          print(error)
        }
      )

    NULL
  })
}

shinyApp(ui = ui, server = server)
```
