Shiny + chat con ellmer

Patrones progresivos para conectar conversación, datos e interfaz

LatinR 2026

2026-11-01

Una idea por vez

  1. Conservar. Partimos siempre desde la misma aplicación.
  2. Agregar. Incorporamos una sola capacidad en cada paso.
  3. Explicar. Revisamos el cambio directamente en R.

El recorrido

  • Shiny · app-00 a app-01. Widgets, outputs y reactividad.
  • Chat + ellmer · app-02 a app-04. Conversación, contexto y SQL.
  • Integración · app-05 a app-08. Datos, interfaz, consultas y mapas.

Shiny

Primero, una interfaz reactiva con inputs, outputs y un estado que conecta ambos.

00 · Widgets y outputs

Espacio reservado para una demo de Shinylive

app-00 · sin API key · integración posterior

00 a 01 · El cambio en el diff

app-00/app.R
sliderInput("n", ...)
- checkboxInput("forecast", ...)

series <- head(pasajeros$pasajeros, input$n)
- plot(forecast::forecast(series), ...)
app-01/app.R
selectInput("continente", ...)
+ textOutput("n_filas")

+ data <- reactive({ ... })
+ output$n_filas <- renderText(nrow(data()))

01 · Dashboard reactivo

Los controles manuales comparten un único estado reactivo.

Todavía no hay chat. Este dashboard establece la línea base.

data <- reactive({
  if (input$continente == "Todos") paises
  else paises[paises$continente == input$continente, ]
})

output$n_filas <- renderText(nrow(data()))

ellmer

Después, una conversación con un modelo: cliente, prompt y respuesta.

ellmer antes de Shiny

Antes de la interfaz

ellmer organiza la conversación con el modelo.

La clave se lee desde el entorno y nunca se escribe en el código.

library(ellmer)

chat <- chat_openai(
  model = "gpt-5-nano",
  system_prompt = paste(
    "Responde brevemente en español sobre los países de Gapminder en su último año.",
    "No inventes resultados numéricos."
  )
)

live_console(chat)

Shiny + ellmer

Conectamos conversación, datos reactivos y acciones de interfaz, una capacidad por vez.

02 · Chat sin acceso a los datos

Agregamos conversación, pero mantenemos una frontera explícita.

El modelo responde sin conocer filtros ni valores visibles.

chat <- ellmer::chat_openai(
  system_prompt = "Ayuda a explorar la app."
)

observeEvent(input$chat_user_input, {
  stream <- chat$stream_async(input$chat_user_input)
  shinychat::chat_append("chat", stream)
})

03 · Una tool aporta contexto real

El modelo consulta un resumen calculado desde los países visibles.

La tool lee el estado. Todavía no modifica el dashboard.

resumir_datos_visibles <- function() {
  df <- isolate(data())
  list(
    registros = nrow(df),
    poblacion_total = sum(df$poblacion),
    esperanza_promedio = round(mean(df$esperanza_de_vida), 1)
  )
}

chat$register_tool(tool(
  resumir_datos_visibles,
  "Resume los países visibles en el dashboard."
))

04 · SQL de solo lectura

La tool acepta consultas acotadas y devuelve resultados verificables.

Solo SELECT, una tabla conocida y un límite de filas.

consultar_paises <- function(consulta) {
  DBI::dbGetQuery(con, validar_select(consulta))
}

chat$register_tool(tool(
  consultar_paises,
  "Ejecuta una consulta SELECT sobre la tabla paises.",
  arguments = list(
    consulta = type_string("Consulta SELECT sin punto y coma.")
  )
))

05 · La tool actualiza Shiny

La conversación modifica el estado reactivo que alimenta el dashboard.

La validación ocurre antes de cambiar lo que ve la persona.

actualizar_dashboard <- function(consulta, titulo) {
  consulta <- validar_select(consulta)
  resultado <- DBI::dbGetQuery(con, consulta)

  consulta_actual(consulta)
  titulo_actual(titulo)

  list(mensaje = "Dashboard actualizado.", registros = nrow(resultado))
}

06 · Tools de interfaz

Una tool abre una tabla o un gráfico sin recargar la aplicación.

El modal también entrega contexto de vuelta al modelo.

mostrar_tabla <- function(n, titulo) {
  n <- max(1L, min(as.integer(n), 100L))
  resultado <- head(isolate(data()), n)
  output$tabla_modal <- renderTable(resultado)

  showModal(modalDialog(
    title = titulo,
    tableOutput("tabla_modal")
  ))

  resultado
}

07 · Inputs tradicionales y chat

Inputs

Predecibles y visibles para preguntas conocidas.

selectInput("continente", "Continente", continentes)
sliderInput("esperanza", "Esperanza de vida", 40, 85, c(60, 85))

Chat + query

Flexible para combinar condiciones difíciles de anticipar.

consulta_actual <- reactiveVal(
  "SELECT * FROM paises"
)

08 · Tool, mapa y cámara

El chat controla el dato y la perspectiva desde la que se observa.

La cámara se actualiza como proxy. El globo no se reconstruye.

ir_a_pais <- function(pais, zoom = 4) {
  ubicacion <- DBI::dbGetQuery(
    con,
    paste(
      "SELECT DISTINCT p.pais, g.longitud, g.latitud",
      "FROM paises p JOIN paises_geo g USING (codigo)",
      "WHERE lower(p.pais) = lower(?)"
    ),
    params = list(pais)
  )
  if (!nrow(ubicacion)) stop("No encontré ese país.")

  mapgl::maplibre_proxy("map", session) |>
    mapgl::fly_to(
      center = c(ubicacion$longitud[1], ubicacion$latitud[1]),
      zoom = zoom
    )

  paste("Mapa centrado en", ubicacion$pais[1])
}

Demos posteriores

Demo de Shiny, video corto o captura interactiva

Reservado para una integración futura con Shinylive

Cierre

La interfaz conserva su función y suma una nueva forma de expresar intención.