bms-chat-engine
v0.3.2
Published
Headless chat engine (orchestrator + tools + LLM layer) for HOSxP/BMS, runnable against any OpenAI-compatible endpoint without a BMS session.
Readme
BMS Session ID — Blank Dashboard Template
Blank starter template for building hospital dashboards with HOSxP data via BMS Session API. Clone this repo, pick a dashboard template from the overview page, and let AI generate a full dashboard automatically.
Quick Start
npm install
npm run devOpen http://localhost:5173/?bms-session-id=YOUR_SESSION_ID
Docker
docker compose up -dOpen http://localhost:3080/?bms-session-id=YOUR_SESSION_ID
How It Works
- User arrives with
?bms-session-id=GUIDin the URL - App retrieves session config from
https://hosxp.net/phapi/PasteJSON - App probes local API gateway at
http://127.0.0.1:45011— uses it if available (faster), falls back to remote tunnel - Auto-detects database type (MySQL/PostgreSQL) via
SELECT VERSION() - Overview page shows 18 dashboard templates grouped by department
- User picks a template, edits the prompt if needed, copies it, and pastes into an AI chat to generate the dashboard
Session Input Methods
| Method | Description |
|--------|-------------|
| URL parameter | ?bms-session-id=GUID — saved to cookie, removed from URL |
| Cookie | Persisted for 7 days, auto-reconnects on next visit |
| Manual input | Login form for pasting a session ID |
Local API Detection
When connecting, the app automatically checks if the HOSxP API gateway is running locally on port 45011. If reachable, all API calls use http://127.0.0.1:45011 instead of the remote *.tunnel.hosxp.net endpoint. This eliminates tunnel latency for users running the gateway on the same machine.
Tech Stack
| Layer | Technology | |-------|------------| | Framework | React 19 + TypeScript 5.x (strict mode) | | Build | Vite 6 | | UI | shadcn/ui + Tailwind CSS v4 | | Tables | TanStack Table v8 | | Charts | Recharts 3.x | | Testing | Vitest + React Testing Library + MSW | | Date | date-fns | | MCP | vite-plugin-mcp (dev tools for AI coding assistants) |
Dashboard Templates (21)
Templates are grouped by hospital department on the overview page:
| Group | Templates | |-------|-----------| | Patient Services | OPD, IPD, Appointments, ER, OPD Screening (Nurse), Doctor Workbench, Refer | | Clinical Support | Lab, Radiology, Pharmacy, Dental, Operating Room | | Community Health (PCU) | Population, NCD Screening, ANC/Labor (Acc.2), MCH (Acc.3), EPI/Vaccine (Acc.4), School Health (Acc.5), Family Planning (Acc.6) | | Administration | Finance/Revenue, Medical Records |
Each template generates a prompt with specific KPIs, chart types, and data points based on HOSxP knowledge base.
Project Structure
src/
services/
bmsSession.ts # Session retrieval, SQL execution, local API probe
apiQueue.ts # Concurrency control, deduplication, retry on 429
queryBuilder.ts # MySQL/PostgreSQL SQL generation
hooks/
useBmsSession.ts # Session state management
useQuery.ts # Async query lifecycle (loading/error/success)
contexts/
BmsSessionContext.tsx # Session provider, auto-connect from URL/cookie
components/
ui/ # shadcn/ui primitives (button, card, dialog, etc.)
layout/ # AppHeader, AppLayout, LoadingSpinner
session/ # LoginForm, SessionExpired, SessionValidator
pages/
Overview.tsx # Main page with grouped dashboard templates
types/
index.ts # TypeScript interfaces
utils/
sessionStorage.ts # Cookie CRUD, URL parameter handling
dateUtils.ts # Date formatting helpers
tests/
unit/ # Service and utility tests
component/ # React component tests
integration/ # Cross-module flow tests
api/ # BMS Session API contract testsAPI Request Queue
All SQL queries go through executeSqlViaApiQueued() which provides:
- Concurrency limiting — max 3 concurrent API calls
- Request deduplication — identical concurrent queries share the same result
- Automatic retry — exponential backoff on HTTP 429 (rate limit)
- Queue cleanup — pending requests cancelled on disconnect
Development
npm run dev # Start dev server (port 5173)
npm test # Run all tests
npm run test:unit # Unit tests only
npm run test:coverage # Coverage report (80% threshold)
npm run lint # ESLint
npm run build # Production buildMCP Dev Tools
This project includes vite-plugin-mcp which exposes an MCP server at http://localhost:5173/__mcp/sse during development. AI coding assistants (Claude Code, Cursor, etc.) can connect to it for Vite config and module graph information. The .mcp.json file is auto-configured when the dev server starts.
BMS Session API Reference
- Session retrieval:
GET https://hosxp.net/phapi/PasteJSON?Action=GET&code={sessionId} - SQL execution:
POST {bms_url}/api/sqlwithAuthorization: Bearer {token} - Allowed SQL: SELECT, DESCRIBE, EXPLAIN, SHOW, WITH (read-only)
- Blocked tables: opduser, opdconfig, sys_var, user_var, user_jwt (max 20 tables per query)
See docs/BMS-SESSION-FOR-DEV.md for the full API specification.
Database Support
The query builder auto-generates SQL for the detected database:
| Function | MySQL | PostgreSQL |
|----------|-------|------------|
| Current date | CURDATE() | CURRENT_DATE |
| Date format | DATE_FORMAT(col, '%Y-%m') | TO_CHAR(col, 'YYYY-MM') |
| Date subtract | DATE_SUB(CURDATE(), INTERVAL 30 DAY) | CURRENT_DATE - INTERVAL '30 days' |
| Age calc | TIMESTAMPDIFF(YEAR, bday, CURDATE()) | EXTRACT(YEAR FROM AGE(bday)) |
| Hour extract | HOUR(col) | EXTRACT(HOUR FROM col)::int |
| Cast to text | CAST(col AS CHAR) | col::text |
License
Private — BMS (Bangkok Medical Software)
Use as a library (bms-chat-engine)
The core chat engine is publishable as a headless npm library and runs without a BMS session — point it at any OpenAI-compatible endpoint.
import { createChatEngine } from 'bms-chat-engine';
const engine = createChatEngine({
llm: { baseURL: 'https://api.openai.com', apiKey: process.env.OPENAI_API_KEY!, model: 'gpt-4o-mini' },
systemPrompt: 'You are a helpful assistant.',
});
const result = await engine.run([{ role: 'user', content: 'Hello' }]);
const reply = [...result.messages].reverse().find((m) => m.role === 'assistant')?.content;Bring your own SDK via the escape hatch:
const engine = createChatEngine({ llm: { client: myLlmClient } }); // implements callLlm/streamLlmWith the BMS/HOSxP preset
import { createBmsChatEngine } from 'bms-chat-engine/bms';
const engine = createBmsChatEngine({
session: { sessionId, config }, // BMS ConnectionConfig
userInfo, // drives the น้องใบเตย persona
});Build
npm run build:lib # tsc + tsc-alias → ESM + .d.ts in dist/Note: the library currently targets bundler/Vite consumers (it uses
import.meta.globto load skills). A bare-Node build and a full core/domain split are planned follow-ups. Seedocs/superpowers/specs/2026-07-01-chat-engine-library-design.md.
Ready-to-use React UI (bms-chat-engine/react)
The bms-chat-engine/react entry ships a batteries-included chat surface. ChatWidget injects its own scoped styles (no CSS import, no Tailwind), drives a ChatEngine turn-by-turn with token streaming, and renders a message list over an input. Wrap it in a sized container — it fills its parent:
import { createChatEngine } from 'bms-chat-engine';
import { ChatWidget } from 'bms-chat-engine/react';
const engine = createChatEngine({
llm: { baseURL: 'https://api.openai.com', apiKey: import.meta.env.VITE_OPENAI_API_KEY, model: 'gpt-4o-mini' },
systemPrompt: 'You are a helpful assistant.',
});
export function App() {
return (
<div style={{ height: 480 }}>
<ChatWidget engine={engine} welcomeMessage="Ask me anything" />
</div>
);
}react and react-dom are optional peer dependencies — only the consumer installs them (the core bms-chat-engine entry stays React-free). Add them to your app:
npm install react react-domTheming
ChatWidget is themeable with the theme prop — a preset ('dark', the default, or 'light') or a partial map of CSS-variable overrides. All styles are scoped under [data-bms-chat], so nothing leaks into the host app:
<ChatWidget engine={engine} theme="light" />
<ChatWidget
engine={engine}
theme={{ '--bmschat-bubble-user': '#0ea5e9', '--bmschat-radius': '20px' }}
/>Headless primitives
Prefer to compose your own layout? Import the primitives and wire them together yourself. useChatEngine owns the message list, the in-flight streaming draft, loading/error state, and abort; MessageList and MessageInput render them:
The primitives are unstyled on their own — you own the CSS. Call ensureChatStyles() once on mount to inject the scoped [data-bms-chat] stylesheet, and spread resolveTheme(...) onto the data-bms-chat root so the CSS variables the primitives read are defined:
import { useEffect } from 'react';
import {
useChatEngine,
MessageList,
MessageInput,
ensureChatStyles,
resolveTheme,
} from 'bms-chat-engine/react';
function MyChat({ engine }) {
const { messages, streamingText, error, isStreaming, send } = useChatEngine(engine);
useEffect(() => ensureChatStyles(), []);
return (
<div data-bms-chat style={{ ...resolveTheme('dark'), height: 480 }}>
<MessageList messages={messages} streamingText={streamingText} error={error} />
<MessageInput onSend={send} disabled={isStreaming} />
</div>
);
}MessageBubble and Markdown are exported too, for full control over how a single message renders.
A2UI tool surfaces (opt-in)
Assistant turns that emit render_ui surfaces can be displayed by passing the A2UISurfaceRenderer from the separate bms-chat-engine/react/a2ui entry to the renderToolSurface prop. It is the only module that pulls in the heavy A2UI renderer, so the base /react bundle stays a2ui-free unless you opt in:
import { ChatWidget } from 'bms-chat-engine/react';
import { A2UISurfaceRenderer } from 'bms-chat-engine/react/a2ui';
<div style={{ height: 480 }}>
<ChatWidget engine={engine} renderToolSurface={A2UISurfaceRenderer} />
</div>