This document describes the system architecture of the Shiny AI Assistant.
The Shiny AI Assistant is a multi-agent system that enables natural language interaction with R Shiny dashboards. It consists of four main layers:
- Widget Layer - React chat interface embedded in the dashboard
- Server Layer - Next.js API that orchestrates AI agents
- Bridge Layer - Platform-agnostic protocol for dashboard interaction
- Adapter Layer - Shiny-specific implementation
┌─────────────────────────────────────────────────────────────────────┐
│ Widget Layer (packages/widget) │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ ChatPanel │ │ MessageList │ │ QuickActionsBar │ │
│ │ FloatingBtn │ │ InputArea │ │ ToolCallIndicator │ │
│ └──────────────┘ └──────────────┘ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ HTTP POST + SSE Streaming
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Server Layer (packages/server) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Orchestrator │ │
│ │ Router Agent → classifyIntent() → Specialist Agent │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Explain │ │Navigate │ │ Action │ │ Locate │ │ Data │ │
│ │ Agent │ │ Agent │ │ Agent │ │ Agent │ │ Agent │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ Tool Execution
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Bridge Layer (packages/bridge) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ DashboardBridge Interface │ │
│ │ introspect() | navigateTo() | executeAction() | queryData() │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ Shiny Messages
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Adapter Layer (Shiny) │
│ ┌──────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ JavaScript (widget) │ │ R (shinyAIAssistant) │ │
│ │ sendToR() / onMessage() │ │ aiAssistantHandler() │ │
│ └──────────────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
The Router Agent classifies user intent and routes to specialist agents.
Intents:
explain- Questions about charts, metrics, datanavigate- Requests to go to pages/tabsaction- Requests to execute actions (set filters, export)locate- Requests to find/highlight UI elementsdata- Analytical queries requiring data accesstour- Requests for guided dashboard toursclarify- Ambiguous requests needing clarification
Implementation: packages/agents/src/router/index.ts
Each agent uses Claude Haiku 4.5 for fast responses via Vercel AI SDK.
| Agent | Purpose | Tools |
|---|---|---|
| ExplainAgent | Answer questions, analyze images | None (streaming) |
| NavigateAgent | Navigate dashboard | navigateTo, searchNavigation |
| ActionAgent | Execute dashboard actions | executeAction, getAvailableActions |
| LocateAgent | Find and highlight elements | searchComponents, highlightElement |
| DataAgent | Query data, provide analytics | queryData, getDataSchema |
| TourAgent | Generate guided tours | getComponents, getCurrentContext |
Implementation: packages/agents/src/specialists/
The Bridge Protocol defines a platform-agnostic interface for AI-dashboard interaction.
type CapabilityLevel = 'level-0' | 'level-1' | 'level-2' | 'level-3';| Level | Features |
|---|---|
| level-0 | Introspection, navigation, highlighting |
| level-1 | Enhanced metadata via manifest |
| level-2 | Action execution |
| level-3 | Data queries |
interface DashboardBridge {
// Level 0 - Always available
introspect(): Promise<DashboardState>;
getComponents(): Promise<ComponentDescriptor[]>;
getNavigationStructure(): Promise<NavigationNode[]>;
getCurrentContext(): Promise<DashboardContext>;
navigateTo(path: string): Promise<NavigationResult>;
highlightElement(selector: string, options?: HighlightOptions): Promise<void>;
scrollToElement(selector: string): Promise<void>;
searchComponents(query: string): Promise<ComponentMatch[]>;
searchNavigation(query: string): Promise<NavigationMatch[]>;
// Level 2 - Actions
supportsActions(): boolean;
getAvailableActions(): Promise<ActionDescriptor[]>;
executeAction(action: DashboardAction): Promise<ActionResult>;
validateAction(action: DashboardAction): Promise<ValidationResult>;
// Level 3 - Data
supportsDataQueries(): boolean;
getDataSchema(): Promise<DataSchema>;
queryData(query: DataQuery): Promise<DataResult>;
// Events
onEvent(event: DashboardEventType, handler: EventHandler): Unsubscribe;
}Implementation: packages/bridge/src/protocol/interface.ts
DashboardContext - Current state for AI awareness:
interface DashboardContext {
currentPage: string;
visibleComponents: ComponentDescriptor[];
activeFilters: Array<{ id: string; value: unknown; label?: string }>;
}DashboardAction - Action execution request:
interface DashboardAction {
type: string; // Action type ID (e.g., "set_filter")
params: Record<string, unknown>;
targetComponent?: string;
}DataQuery - Data query specification:
interface DataQuery {
source: string;
select: string[];
filters?: QueryFilter[];
groupBy?: string[];
aggregations?: QueryAggregation[];
orderBy?: QueryOrderBy[];
limit?: number;
}The Shiny Adapter implements the bridge protocol for R Shiny dashboards.
Shiny Detection:
function isShinyAvailable(): boolean;
function isShinyInitialized(): boolean;Binding Discovery:
function discoverInputBindings(): ShinyBinding[];
function discoverOutputBindings(): ShinyBinding[];
function discoverAllComponents(): ComponentDescriptor[];Action Execution:
// Send action to R
sendToR(action: DashboardAction);
// Listen for results from R
registerMessageHandler('__ai_assistant_result__', handler);Implementation: packages/bridge/src/shiny/
Widget Function:
aiAssistantWidget(
apiUrl, # Required: Chat API endpoint
position = "bottom-right", # Widget position
theme = "auto", # light | dark | auto
manifest = NULL, # Path to manifest YAML
dataSources = NULL, # Level 3 data source config
enableDataQueries = FALSE # Enable Level 3
)Handler Function:
aiAssistantHandler(session, handlers = list(
navigate_to = function(params) { ... },
set_filter = function(params) { ... },
set_date_range = function(params) { ... },
reset_filters = function(params) { ... },
query_data = function(params) { ... }
))Implementation: packages/r-shiny/R/
The widget is built with React and Tailwind CSS.
Widget (main container)
├── FloatingButton (toggle button)
└── ChatPanel (main interface)
├── MessageList
│ └── MessageItem (per message)
│ ├── ToolCallIndicator
│ └── AudioPlayer (TTS)
├── QuickActionsBar
└── InputArea
├── ImagePreview
└── TranscriptionToggle (STT)
| Hook | Purpose |
|---|---|
useChat |
Message state, sending messages |
useSSEStream |
Server-Sent Events parsing |
useSession |
Session management |
useQuickActions |
Quick action buttons |
useToolExecution |
Bridge tool execution |
useTour |
Guided tour state |
useSpeechRecognition |
Voice input |
useTextToSpeech |
Voice output |
<WidgetProvider config={...}>
<BridgeProvider>
<Widget />
</BridgeProvider>
</WidgetProvider>Implementation: packages/widget/src/
The server is a Next.js application with API routes.
| Endpoint | Method | Purpose |
|---|---|---|
/api/chat |
POST | Main chat endpoint |
/api/chat/tool-result |
POST | Tool execution results |
/api/transcribe |
POST | Speech-to-text |
/api/speak |
POST | Text-to-speech |
- Widget sends message via POST to
/api/chat - Server returns SSE stream
- Orchestrator classifies intent via Router Agent
- Specialist agent processes request
- Agent may invoke tools (sent as SSE events)
- Widget executes tools via bridge
- Tool results sent back via
/api/chat/tool-result - Final response streamed to widget
Sessions track:
- Message history
- Active stream handlers
- Rate limiting state
Implementation: packages/server/src/
The manifest provides enhanced metadata for Level 1+ capabilities.
name: "Dashboard Name"
version: "1.0.0"
description: "Dashboard description"
capabilityLevel: "level-3"
aiInstructions: |
Custom instructions for the AI assistant.
glossary:
- term: "ARR"
definition: "Annual Recurring Revenue"
synonyms: ["annual revenue"]
navigation:
- id: "overview"
label: "Overview"
path: "/overview"
description: "Executive summary"
pages:
- id: "overview"
title: "Overview"
components:
- id: "revenue_chart"
label: "Revenue Chart"
type: "chart"
dataBinding:
source: "sales_data"
fields: ["date", "arr"]
dataSources:
- id: "sales_data"
name: "Sales Data"
fields:
- name: "arr"
type: "number"
aggregatable: true
actions:
- id: "set_filter"
name: "Set Filter"
description: "Apply a filter"
category: "update"
parameters:
- name: "filterId"
type: "string"
required: trueImplementation: packages/bridge/src/manifest/
User Input → Widget → Server → Router Agent → Specialist Agent
↓
Tool Call ←── Agent Response
↓
Widget → Bridge → Shiny Adapter → R Handler
↓
Result → Server → Agent → Response → Widget
- Agent requests tool call (e.g.,
executeAction) - Server sends
tool-callSSE event - Widget receives and executes via bridge
- Bridge sends Shiny message to R
- R handler processes and returns result
- Result sent back through chain
- Agent incorporates result in response