№ 49
Plugin hỗ trợ lễ tân khách sạn budget trong việc quản lý thông tin ca trực, bàn giao task, phản hồi review của khách
Lễ tân nhận bàn giao ca chỉ bằng một nút bấm — Opstream kéo dữ liệu PMS thực tế và tạo tóm tắt ca trực bằng AI.
🔗 Live demo: https://www.opstream.id.vn/ — đăng nhập tại /login (deploy trên Railway từ Dockerfile).
Lễ tân khách sạn boutique tại phố cổ Hà Nội bàn giao ca thủ công: tra từng màn hình PMS, chép tay note, rồi truyền miệng. Mỗi ca mất 15–20 phút chỉ để tổng hợp thông tin, dễ bỏ sót ETA khách, transfer chưa đặt, hoặc yêu cầu đặc biệt. Khi khách sạn quản lý 5 property cùng lúc, rủi ro nhân lên.
Opstream kéo toàn bộ dữ liệu PMS (check-in, check-out, dịch vụ, thanh toán) qua API, làm sạch boilerplate OTA, rồi sinh tóm tắt có cấu trúc bằng LLM — trong dưới 30 giây.
mid gộp một dòng đúng), cơ hội bán tour. Bấm một dòng tóm tắt để tạo việc (gắn luôn mã đặt phòng). Danh sách đến/đi đầy đủ để ở dashboard, không lặp lại trong tóm tắt.| Layer | Technology |
|---|
| AI Agent | LangGraph (fetch → clean → summarize) |
| Backend | FastAPI + Python 3.12 |
| Frontend | Server-rendered HTML (FastAPI) + DayPilot (vanilla JS) |
| Frontend (Dashboard) | Server-rendered HTML + DayPilot Scheduler (vanilla JS) |
| Auth | Cookie session (itsdangerous) + bcrypt; role/hotel access control |
| Persistence | SQLAlchemy 2.0 ORM + Alembic (SQLite POC → Postgres); append-only audit log (FR-08) |
| LLM | Claude (Anthropic) / Llama 3.3 70B (NVIDIA NIM) / Gemini 2.0 Flash |
| DevOps | Docker + GitHub Actions |
# 1. Clone repo (code is on main)
git clone https://github.com/AI20K-Build-Cohort-2/C2-App-071.git
cd C2-App-071
# 2. Setup environment
cp .env.example .env
# Điền ANTHROPIC_API_KEY, NVIDIA_API_KEY hoặc GEMINI_API_KEY vào .env
# 3. Seed tài khoản đăng nhập (dev) — users.json bị gitignore
cp users.example.json users.json
# users.example.json chỉ chứa bcrypt password_hash, không chứa plaintext password.
# Nếu đổi mật khẩu demo, sinh hash trước rồi thay vào users.json:
# python -c "import bcrypt; print(bcrypt.hashpw(b'opstream', bcrypt.gensalt()).decode())"
# 4. Cài dependencies
pip install -r requirements.txt
# 5. Chạy server — FastAPI phục vụ cả hai màn hình
uvicorn src.main:app --reload --port 8000
Mở http://localhost:8000 — vì các màn hình giờ yêu cầu đăng nhập, bạn sẽ được chuyển tới /login. Đăng nhập bằng tài khoản demo (hiện ngay trên màn hình, bấm để điền): an / binh / gm, mật khẩu opstream. Sau khi đăng nhập, tự chuyển tới Lịch phòng. Dùng menu trên cùng để chuyển giữa Lịch phòng (/dashboard) và Tóm tắt ca (/summary); khách sạn đang chọn được giữ nguyên khi chuyển màn hình.
Phân quyền (FR-10): lễ tân chỉ thấy khách sạn của mình; GM thấy tất cả. Yếu tố ca trực đã được gỡ bỏ hoàn toàn (2026-06-23) khỏi việc và phân quyền — phân quyền chỉ theo khách sạn, và việc tồn tự động chuyển sang ngày sau (bàn giao theo ngày, một danh sách mọi việc chưa xong). Tài khoản đọc từ
users.json(Phase 1); bảngusersđã có trong database (Phase 2) làm gương FK/định danh, nhưng auth vẫn đọcusers.json.
Windows (no
make): theMakefiletargets are convenience wrappers — run the commands directly ifmakeisn't installed:
Make target Direct command make runuvicorn src.main:app --reload --port 8000make testpytest tests/ -vmake lintruff check src/ tests/make formatruff format src/ tests/
├── src/
│ ├── agents/
│ │ ├── graph.py # LangGraph: fetch → clean → summarize
│ │ ├── state.py # AgentState schema
│ │ ├── prompts.py # SYSTEM_PROMPT + boilerplate/service keywords
│ │ ├── nodes/
│ │ │ ├── fetch_node.py # Async PMS fetch + check-out balances + room statuses + anomalies
│ │ │ ├── clean_node.py # Strip OTA boilerplate, lead with unresolved tasks, inject filtered tables + anomalies, build LLM prompt
│ │ │ └── summarize_node.py # Dispatch to Claude / Nvidia / Gemini
│ │ └── tools/
│ │ └── reviews_tool.py # OTA review stub (FR-01, pending)
│ ├── auth/ # Auth foundation (Phase 1)
│ │ ├── users.py # Seeded user store (JSON→memory, bcrypt)
│ │ ├── session.py # Signed cookie session (itsdangerous)
│ │ └── dependencies.py # get_current_user, require_role, can_access_hotel (FR-10)
│ ├── api/
│ │ ├── auth.py # POST /auth/login, /auth/logout, GET /auth/me
│ │ ├── summary.py # POST /api/v1/summary
│ │ ├── tasks.py # Task lifecycle API (Phase 3): CRUD + transition/undo/reopen/audit
│ │ ├── assistant.py # POST /api/v1/assistant/message (conversational ops assistant)
│ │ └── dashboard.py # Dashboard JSON feeds: rooms/events/unpaid/reservation
│ ├── web/
│ │ └── dashboard.py # Dashboard screens (gated): GET /login, /dashboard, /summary, /tasks, /dashboard/view
│ ├── dashboard/ # Static screen assets
│ │ ├── login.html # Login screen (demo accounts, click-to-fill)
│ │ ├── index.html # Scheduler overview screen
│ │ ├── summary.html # Shift-summary screen (form → /api/v1/summary)
│ │ ├── tasks.html # Task board (Phase 3): create, advance lifecycle, undo, audit
│ │ ├── view.html # Read-only reservation detail screen
│ │ └── static/ # Vendored DayPilot JS + theme CSS + assistant.{js,css} (chat widget)
│ ├── models/
│ │ ├── schemas.py # Pydantic contracts: Summary, Login/UserOut, Task/Audit/Verify
│ │ └── orm.py # SQLAlchemy ORM (Phase 2): User, Task, append-only AuditLog
│ ├── services/
│ │ ├── pms.py # Async PMS API client (httpx + asyncio.gather)
│ │ ├── dashboard_pms.py # PMS client + transforms for the dashboard
│ │ ├── tasks.py # Task lifecycle service (Phase 3): FR-05/08/09/10 rules
│ │ ├── verifier.py # Verify-vs-PMS + note extraction (Phase 4): deterministic-wins
│ │ ├── assistant.py # Conversational ops assistant: live context + propose-only actions
│ │ ├── result_cache.py # In-memory LLM result cache (skip re-runs when PMS state unchanged)
│ │ └── llm.py # Multi-provider LLM dispatcher
│ ├── db.py # Lazy engine + session factory (reads database_url)
│ ├── config.py # Pydantic Settings (all env vars)
│ └── main.py # FastAPI app entry point
├── alembic/ # DB migrations (Phase 2); env reads settings.database_url
│ └── versions/ # 0001_create_users_tasks_audit
├── alembic.ini
├── tests/
│ ├── test_agents/
│ │ ├── test_nodes.py # Unit tests for each LangGraph node
│ │ └── test_routing.py # Conditional edge routing tests
│ ├── test_auth/
│ │ └── test_auth.py # Login/logout/me, session signing, FR-10 hotel scope
│ ├── test_db/
│ │ └── test_orm.py # Schema + append-only audit guard (in-memory SQLite)
│ ├── test_services/
│ │ ├── test_tasks.py # Lifecycle/RBAC/undo/reopen rules (Phase 3)
│ │ ├── test_verifier.py # Deterministic-wins, no_evidence routing, defensive extract (Phase 4)
│ │ ├── test_assistant.py # Ops assistant: context shape, JSON parse, action safety rails, briefing cache
│ │ └── test_result_cache.py # Content-addressed cache: hit/miss on fingerprint match
│ └── test_api/
│ ├── test_summary.py # Integration tests for /api/v1/summary
│ ├── test_assistant.py # /assistant/message: auth gate, RBAC, response contract
│ └── test_tasks.py # Task API: create→list, skip-to-Done rejected, audit
├── users.example.json # Template for dev login accounts (copy → users.json)
├── docs/
│ └── architecture_diagram.md # Mermaid diagrams
├── ARCHITECTURE.md # Full architecture document
├── Dockerfile
├── docker-compose.yml
└── .github/workflows/ # CI/CD (lint + test)
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check |
| POST | /api/v1/auth/login | Verify credentials, set session cookie |
| POST | /api/v1/auth/logout | Clear session cookie |
| GET | /api/v1/auth/me | Current signed-in user (401 if none) |
| POST | /api/v1/summary | Generate shift summary via LangGraph agent |
| GET | /api/v1/tasks?hotel={code}&date= | List the hotel's handoff board (unresolved + the date's work, FR-10) |
| POST | /api/v1/tasks | Create a manual task |
| POST | /api/v1/tasks/{id}/transition | Advance one lifecycle step (FR-05; never skips to Done) |
| POST | /api/v1/tasks/{id}/undo | Undo the last move — actor-only, ≤5 min (FR-09) |
| POST | /api/v1/tasks/{id}/reopen | Reopen — GM only, after the undo window (FR-09) |
| GET | /api/v1/tasks/{id}/audit | Append-only audit trail for a task (FR-08) |
| POST | /api/v1/tasks/{id}/verify | Verify a ticked task vs PMS evidence (advisory; deterministic-wins) |
| POST | /api/v1/reservations/{id}/extract-tasks?hotel=&date= | Split a booking note into tasks (on-demand) |
| POST | /api/v1/assistant/message | Conversational ops assistant — "what to do now" + record-action helper. Returns a reply plus proposed actions the receptionist confirms; the assistant never writes (executes through the audited /tasks routes). RBAC own-hotel (FR-10) |
| GET | /api/v1/dev/prompts | Dev only — the system prompt behind each agent input (summary/extract/verify/assistant); 404 outside app_env=development |
| GET/POST/PATCH/DELETE | /api/v1/dev/db/* | Dev + GM only — DB inspector: browse tasks/audit_log/users, edit/delete tasks/users. audit_log read-only (FR-08); task-delete keeps its audit rows; password_hash redacted. Screen at /dev/db |
| GET | /docs | Swagger UI |
Screens (web layer — user-facing, no /api prefix). All dashboard screens require a session — unauthenticated requests redirect to /login?next=…:
| Method | Path | Description |
|---|---|---|
| GET | / | Redirects to /dashboard |
| GET | /login | Login screen (public) |
| GET | /dashboard?hotel={code} | Receptionist dashboard — 7-day scheduler overview |
| GET | /summary?hotel={code} | Shift-summary screen — form that calls POST /api/v1/summary |
| GET | /tasks?hotel={code} | Task board — create, advance lifecycle, verify, undo, audit (who-did-what + verify verdict & evidence), inline reservation detail |
| GET | /dashboard/view?hotel={code}&id={id} | Read-only reservation detail (modal) |
| GET | /dev/db | Dev + GM only — DB inspector (browse tables; edit/delete tasks & users; audit_log read-only); 404 outside app_env=development |
A floating ops-assistant chat widget (src/dashboard/static/assistant.{js,css}) is embedded on the dashboard, summary, and task screens. Opening it greets the shift with a prioritized "what to do now" briefing built from live PMS + task state, answers follow-up questions, and turns "I just did X" into confirm-to-create/advance task buttons that run through the audited /tasks routes — so the AI proposes but the human (and the existing FR-05/FR-08/FR-09/FR-10 guards) still commit.
Both the summary and the assistant briefing are backed by an in-memory result cache (src/services/result_cache.py): the LLM input is a deterministic fingerprint of the PMS data + tasks, so an unchanged state reuses the last result and skips the LLM call (a subtle "data unchanged" hint shows); creating/advancing a task changes the fingerprint and re-runs automatically.
Data feeds (API layer — JSON consumed by the screens):
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/dashboard/rooms?hotel={code} | Room list grouped by floor (DayPilot resources) |
| GET | /api/v1/dashboard/events?hotel={code}&start=&end= | Bookings as scheduler events (status from inout) |
| GET | /api/v1/dashboard/unpaid?hotel={code} | Checked-out reservations with outstanding balance |
| GET | /api/v1/dashboard/reservation?hotel={code}&id={id} | Full reservation detail (group, services, payments) |
# At least one LLM key is required
ANTHROPIC_API_KEY=sk-ant-...
NVIDIA_API_KEY=...
GEMINI_API_KEY=...
# PMS API (default already set)
PMS_BASE_URL=https://dongxuanboutiquehotel.com/api/
# Auth / session (defaults work for dev; set a real secret in production)
SESSION_SECRET=change-me-in-production
# SESSION_COOKIE=opstream_session
# SESSION_MAX_AGE=43200 # seconds (12h)
# USERS_FILE=users.json # dev login accounts (gitignored)
# Database (Phase 2). Default is SQLite for the POC; gitignored data/app.db.
DATABASE_URL=sqlite:///./data/app.db
Database (Phase 2): persistence is back as schema only — SQLAlchemy ORM (
src/models/orm.py) + Alembic. Create the local POC DB with:DATABASE_URL='sqlite:///./data/app.db' alembic upgrade headTables:
users,tasks(FR-05 lifecycle),audit_log(append-only, FR-08). Thedata/app.dbfile is gitignored.
pytest tests/ --cov=src --cov-report=term-missing
# 98 tests passing
main)docs/architecture_diagram.md, ARCHITECTURE.md)presentation/)JOURNAL.md)WORKLOG.md)eval/) — 7 agent eval cases, output thực tế (python -m eval.run_eval)| Member | Role | Student ID |
|---|---|---|
| Huy Dang | Project Owner | 2A202600641 |
| Tien Dat | Insfrastucture Lead | 2A202600xxx |
MIT