shinychat

Chatbots made easy

Turn a chatlas client into a full chatbot UI — in 5 lines.

The 5-line chatbot

app.py
from chatlas import ChatAnthropic
from shinychat.express import Chat

client = ChatAnthropic(system_prompt="You are a helpful assistant.")
chat = Chat(id="chat", client=client)
chat.ui()
shiny run app.py

What client= gives you for free

  • Streaming responses
  • Multi-turn conversation
  • Tool calling with display
  • Cancel mid-generation
  • File attachments (images, PDFs)
  • Bookmarkable state
  • ✅ Automatic error handling
  • Async by default

You could wire all this up manually… but why would you?

Bring your own backend

Not using chatlas? Wire up any async generator:

app.py
from shinychat.express import Chat

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

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

Feature tour

Slash commands

@chat.slash_command(
    "help",
    "Show help"
)
async def _():
    await chat.append_message(
        "Here's how to use me..."
    )

Input suggestions

await chat.append_message(
    ui.HTML("""
    Try asking:
    <ul>
      <li><span class='suggestion'>
        Summarize the data
      </span></li>
      <li><span class='suggestion'>
        Show a bar chart
      </span></li>
    </ul>
    """)
)

Attachments

# Enabled automatically
# when client= is set.
#
# Or restrict to specific
# file types:
chat.ui(
    allow_attachments=[
        "image/png",
        "application/pdf",
    ],
)

Custom tool displays

By default, tool results show as collapsible text. But you can make them beautiful.

SCREENSHOT: side-by-side comparison —
(left) default tool result display (plain collapsible text)
(right) custom ToolResultDisplay with icon, title, and formatted markdown

Building a tool display

from chatlas import ContentToolResult
from shinychat.types import ToolResultDisplay
import faicons

def get_weather(lat: float, lng: float, location: str) -> ContentToolResult:
    """Get the current weather for a location.
    ...
    """
    data = call_weather_api(lat, lng)
    return ContentToolResult(
        value=data,                                  # what the LLM sees
        extra={
            "display": ToolResultDisplay(
                title=f"Weather: {location}",
                icon=faicons.icon_svg("cloud-sun"),
                markdown=f"**{data['temp']}°C** — {data['condition']}",
            )
        },
    )

Tool display in action

SCREENSHOT: a shinychat conversation showing a custom tool display —
the weather card with icon, title, and formatted content
embedded naturally in the chat flow

Observability with OpenTelemetry

chatlas emits OTel spans for every LLM call — latency, tokens, errors.

# In production, configure your OTel exporter before the app starts
# e.g., with Logfire, Datadog, Honeycomb, etc.
import logfire
logfire.configure()

# Then just run your shinychat app as normal — tracing is automatic

See what your users are actually asking, how long it takes, and what fails.

🛠️ Hands-on: build a chatbot

~15 minutes — pick your adventure:

🧪 Option A: Add a tool display

Start from the exercise file:

  1. Run the app — it has a working tool
  2. Upgrade the tool to return a ToolResultDisplay
  3. Bonus: add a slash command

🚀 Option B: Deploy it

Take your working chatbot and:

  1. Deploy to Connect Cloud
  2. Share the URL with a neighbor
  3. Watch the OTel traces roll in

Exercise file: exercises/02-shinychat-starter.py

☕ Break — 5 min