Chatbots (i.e., web apps)

Building chatbots with chatlas and shinychat

Recap: the .app() method

  • We’ve already seen chat.app() – a working chatbot in 1 line of code.
  • chatlas + shinychat provides a delightful UX, but you could also:
    1. Use chatlas with your own frontend (e.g., Streamlit, FastAPI, etc.)
    2. Use shinychat with your own backend (e.g., LangChain, Pydantic AI, etc.)

shinychat: high-level API

.app() is a thin wrapper around shinychat. It’s basically just:


import chatlas as ctl
from shinychat.express import Chat

chat = Chat(
  "chat", 
  client=ctl.ChatBedrockAnthropic()
)

chat.ui()
  • client= is what wires up all the chat features (automatically).
  • This lets you easily drop a chat UI in a larger Shiny app.

shinychat: low-level API

shinychat has a low-level API that gives you full control over the chat.


import chatlas as ctl
from shinychat.express import Chat

chat = Chat("chat")
chat.ui()
client = ctl.ChatBedrockAnthropic()

@chat.on_user_submit
async def handle_user_input(user_input: str):
    # This next line could also be a LangChain chain, 
    # Pydantic AI call, or any generator.
    response = await client.stream_async(user_input)
    await chat.append_message_stream(response)

What client= gives you for free

  • History (multiple conversations)
  • Tool calling with display
  • Async streaming responses
  • Cancel mid-generation
  • File attachments (images, PDFs)
  • Multi-turn conversation
  • ✅ Automatic error handling

shinychat: tool displays

shinychat: custom tool displays

shinychat: custom tool displays

Main idea: Return ContentToolResult with a ToolResultDisplay.

import chatlas as ctl
from shinychat.types import ToolResultDisplay

def my_tool():
    return ctl.ContentToolResult(
        value="value for the LLM",
        extra={
            "display": ToolResultDisplay(
                title="Custom title for the user",
                icon="maybe an icon here",
                html="<b>HTML content for the user</b>",
            )
        }
    )

shinychat uses this to display rich tables and visualizations in the chat. The LLM sees the raw data, user sees a polished display.

Slash commands

@chat.slash_command("clear", "Clear the conversation")
async def _(user_input: str):
    await chat.clear_messages()

Another slash command

@chat.slash_command("summarise", "Summarise the conversation")
async def _(user_input: str):
    prompt = f"""
      The user has requested a summary of the 
      conversation with additional context:\n\n{user_input}
      """
    stream = await chat.stream_async(prompt)
    chat.append_message_stream(stream)

Exercise

  1. Open exercises/shinychat-app.py and run it.
    • shiny run exercises/shinychat-app.py (or press run in Positron).
  2. Add a slash command (e.g. "clear") that clears the conversation.
  3. Ask about the weather somewhere — notice the default tool display.
  4. Clear the conversation.
  5. Update get_weather() to return a ToolResultDisplay with a custom display.