A production-shaped prototype for HCI research combining a multi-bot conversation engine built on LangChain primitives, React PWA frontend, passkey-first authentication (via h4ckath0n), SSE-based real-time chat, and vendor-neutral Web Push notifications.
| Pillar | Stack | Description |
|---|---|---|
| Backend | FastAPI + h4ckath0n + SQLAlchemy 2.x | Async API with passkey auth, multi-tenant project model, and SSE fan-out |
| Conversation Engine | LangChain agents + Router | Multi-bot architecture: Router (single writer) + Intake/Feedback/Coach specialists with proposal/commit flow |
| Frontend | React 19 + Vite + Tailwind CSS | PWA with service worker, SSE streaming chat, and Web Push notification support |
The engine uses a Router + specialist architecture:
- Router — Routes each turn to the correct specialist, validates all patch proposals, and owns the only commit path to UserProfile (Store A) and Memory (Store B).
- Intake Bot — Onboarding and profile setup. Can propose updates to onboarding fields.
- Feedback Bot — Habit tracking and barrier analysis. Can propose rolling coaching field updates and memory items.
- Coach Bot — Normal conversation and encouragement. Can propose candidates only (higher confidence threshold).
All bots use LangChain agents and tools. Proposals are validated against a permission matrix with confidence thresholds and evidence span requirements. See docs/current-architecture.md for full details.
cd api
uv sync
# Copy .env.example to .env and configure
cp ../.env.example ../.env
uv run uvicorn app.main:app --reloadcd web
npm install
npm run devThe frontend dev server runs at http://localhost:5173 and proxies API requests to the backend.
Flow/
├── api/ # FastAPI backend
│ ├── app/
│ │ ├── main.py # App entry point (h4ckath0n create_app)
│ │ ├── routes.py # API route handlers
│ │ ├── models.py # SQLAlchemy 2.x models (incl. UserProfile, Memory, AuditLog)
│ │ ├── db.py # Database session management
│ │ ├── middleware.py # CSP and other middleware
│ │ ├── id_utils.py # Custom ID generation (p... / u...)
│ │ ├── agents/ # NEW: Multi-bot LangChain agents
│ │ │ ├── engine.py # Turn engine: Router + specialist pipeline
│ │ │ ├── router.py # Routing coordinator (structured output)
│ │ │ ├── intake.py # Intake specialist agent
│ │ │ ├── feedback.py # Feedback specialist agent
│ │ │ ├── coach.py # Coach specialist agent
│ │ │ └── orchestrator.py # Legacy orchestrator (backward compat)
│ │ ├── schemas/ # Pydantic models
│ │ │ ├── router.py # RouteDecision (INTAKE/FEEDBACK/COACH)
│ │ │ ├── patches.py # Proposals, evidence, permissions, profile/memory schemas
│ │ │ └── tool_schemas.py # Legacy tool argument schemas
│ │ ├── services/ # NEW: Business logic layer
│ │ │ └── profile_service.py # Profile/memory persistence, validation, audit
│ │ ├── tools/ # LangChain tools
│ │ │ ├── proposal_tools.py # NEW: propose_profile_patch, propose_memory_patch
│ │ │ └── langchain_tools.py # Legacy tool wrappers
│ │ └── engine/ # Legacy conversation engine (deprecated for new work)
│ │ ├── flow.py # Legacy orchestrator
│ │ ├── modules.py # IntakeModule, FeedbackModule, tool loop
│ │ ├── state.py # State enums, Pydantic models, DataKeys
│ │ ├── tools.py # Tool implementations
│ │ ├── scheduler.py # Daily prompts, reminders, auto-feedback
│ │ └── tone.py # Tone adaptation (EMA, hysteresis, whitelist)
│ ├── prompts/ # System prompt templates
│ ├── tests/ # Backend test suite
│ └── pyproject.toml
├── web/ # React PWA frontend
│ ├── public/
│ │ ├── manifest.json # PWA web app manifest
│ │ └── sw.js # Service worker (push + notificationclick)
│ ├── src/
│ │ ├── App.tsx # Route definitions
│ │ ├── pages/ # Page components
│ │ │ ├── Dashboard.tsx # Project thread list
│ │ │ ├── Activation.tsx # Join project via invite link
│ │ │ ├── ChatThread.tsx # Real-time chat with SSE
│ │ │ ├── Notifications.tsx # Push notification management
│ │ │ ├── Landing.tsx # Public landing page
│ │ │ ├── Login.tsx # Passkey login
│ │ │ ├── Register.tsx # Passkey registration
│ │ │ └── Settings.tsx # User settings
│ │ ├── auth/ # Passkey auth (from h4ckath0n scaffold)
│ │ ├── api/ # API client and types
│ │ ├── components/ # Shared UI components
│ │ └── gen/ # Generated OpenAPI TypeScript client
│ └── package.json
├── docs/
│ ├── current-architecture.md # NEW: Current architecture (authoritative)
│ ├── legacy-conversation-flow-contract.md # DEPRECATED: Legacy behavior reference
│ └── parity-matrix.md # Legacy → new code mapping
├── .env.example
└── AGENTS.md # Agent behavior rules
All project-scoped endpoints require passkey authentication.
| Method | Path | Tag | Description |
|---|---|---|---|
GET |
/healthz |
infra | Readiness probe |
GET |
/dashboard |
dashboard | List user's project memberships |
POST |
/p/{project_id}/activate/claim |
activation | Claim invite code, create membership + conversation |
GET |
/p/{project_id}/me |
activation | Get membership status, conversation ID, stored email |
POST |
/p/{project_id}/messages |
messaging | Send message, get assistant reply |
GET |
/p/{project_id}/events |
streaming | SSE event stream for real-time updates |
GET |
/p/{project_id}/push/vapid-public-key |
push | Get VAPID public key for push subscription |
POST |
/p/{project_id}/push/subscribe |
push | Store a push subscription |
POST |
/p/{project_id}/push/unsubscribe |
push | Revoke a push subscription |
GET |
/demo/ping |
demo | Liveness ping |
POST |
/demo/echo |
demo | Echo with reverse |
GET |
/demo/sse |
demo | Authenticated SSE demo stream |
WS |
/demo/ws |
demo | Authenticated WebSocket demo |
| Table | ID Type | Description |
|---|---|---|
projects |
p... (custom, 32 chars) |
User-visible research projects |
project_invites |
auto-increment int | Hashed invite codes with expiry |
project_memberships |
auto-increment int | Links (project, user) with status; unique constraint |
participant_contacts |
auto-increment int | Optional email contact metadata (not used for identity) |
conversations |
auto-increment int | 1:1 with membership |
messages |
auto-increment int | Chat history with server_msg_id (UUID) |
conversation_runtime_state |
FK to conversation | JSON blob for engine state |
user_profiles |
auto-increment int | Store A — structured profile JSON (1:1 with membership) |
memory_items |
auto-increment int | Store B — semi-structured memory items per membership |
patch_audit_log |
auto-increment int | Audit trail: proposals, decisions, commits |
push_subscriptions |
auto-increment int | Web Push endpoints + crypto keys per device |
outbox_events |
auto-increment int | Durable scheduled events with dedupe keys |
The engine implements the architecture defined in docs/current-architecture.md.
- Persist user message
- Load UserProfile (Store A) + Memory (Store B) + recent chat history
- Router decides which specialist to run (INTAKE, FEEDBACK, or COACH)
- Invoke specialist agent (LangChain tool-calling agent)
- Collect patch proposals made during the agent run
- Router validates proposals (permissions, confidence, evidence) and commits approved ones
- Persist assistant message
- Emit SSE events for UI update
| Condition | Route |
|---|---|
| Required onboarding fields missing | INTAKE |
| Currently in feedback protocol | FEEDBACK |
| Profile complete, normal conversation | COACH |
Specialist bots propose changes via propose_profile_patch and propose_memory_patch tools. The Router validates each proposal against:
- Permission matrix: Intake → onboarding fields, Feedback → coaching fields, Coach → candidates only
- Confidence thresholds: INTAKE/FEEDBACK ≥ 0.5, COACH ≥ 0.8
- Evidence spans: Must reference recent message IDs
- Memory rules: Items ≤ 500 chars with source pointers
All proposals and decisions are logged in the patch_audit_log table.
The legacy conversation flow engine (api/app/engine/) is retained for backward compatibility. The legacy behavioral contract (docs/legacy-conversation-flow-contract.md) is deprecated.
| Route | Page | Description |
|---|---|---|
/ |
Landing | Public landing page |
/register |
Register | Passkey registration |
/login |
Login | Passkey login |
/dashboard |
Dashboard | List project threads (active/ended) |
/p/:projectId/activate |
Activation | Join project via invite link, collect optional email |
/p/:projectId/chat |
ChatThread | Send messages via POST, receive via SSE |
/p/:projectId/notifications |
Notifications | PWA install guidance, enable push notifications |
/settings |
Settings | User settings |
- Web App Manifest —
web/public/manifest.jsonenables "Add to Home Screen" - Service Worker (
web/public/sw.js):- Handles
pushevents → shows system notifications - Handles
notificationclick→ deep-links to the relevant project chat (/p/{project_id}/chat)
- Handles
- Subscription flow: explicit user action → request permission → subscribe with VAPID public key from backend → store subscription server-side
- iOS: "Add to Home Screen" guidance is shown before enabling notifications
cd api && uv run python -m pytest tests/ -vTest modules:
test_api.py— API endpoint integration tests (messaging wired to new engine)test_new_architecture.py— NEW: Router permissions, confidence thresholds, evidence spans, profile/memory validation, proposal tools, deterministic routingtest_engine_integration.py— NEW: Full turn pipeline with async DB, profile persistence, memory persistence, audit logtest_langchain_router.py— Router structured output and deterministic routingtest_langchain_agents.py— Agent tool permissions and orchestrator pipelinetest_langchain_tools.py— LangChain tool schemas and invocationtest_flow.py— Legacy conversation engine routing and historytest_scheduler.py— Daily prompts, reminders, auto-feedback, intensitytest_tone.py— Tone adaptation, EMA, whitelist validationtest_tools.py— Tool execution and state management
The audit trail can be inspected by querying the patch_audit_log table after processing messages. The test_engine_integration.py::TestAuditLog tests verify that proposals and commit decisions are properly recorded.
cd web && npm test # Unit tests (Vitest)
cd web && npm run test:e2e # E2E tests (Playwright)This is a prototype. The following are mocked or incomplete:
- LLM calls — In stub mode, specialist agents return fixed responses. Connect a real LLM (OpenAI, Anthropic, etc.) by passing
llmandrouter_llmparameters to the engine. - Web Push delivery — Push subscriptions are stored in the database but no actual push messages are sent. Wire up
pywebpushwith VAPID keys to enable delivery. - Outbox event processing — Outbox events (reminders, auto-feedback timers) are created with dedupe keys but no background worker processes them. Add a polling worker or task queue to fire events at
available_at. - Message endpoint —
POST /p/{project_id}/messagesruns the Router + specialist pipeline in stub mode (deterministic routing, fixed responses). With a real LLM, it produces contextual responses.
Configure in .env at the repository root (see .env.example):
| Variable | Description |
|---|---|
H4CKATH0N_ENV |
Environment mode (development / production) |
H4CKATH0N_DATABASE_URL |
SQLAlchemy async database URL |
H4CKATH0N_AUTH_SIGNING_KEY |
Hex secret for JWT signing |
H4CKATH0N_RP_ID |
WebAuthn relying party ID (e.g., localhost) |
H4CKATH0N_ORIGIN |
Allowed origin for CORS and WebAuthn |
VITE_API_BASE_URL |
API base URL for the frontend (e.g., /api) |
VAPID_PUBLIC_KEY |
VAPID public key for Web Push |
VAPID_PRIVATE_KEY |
VAPID private key for Web Push (never log this) |
See LICENSE.