Hệ thống nhận một câu lệnh tiếng Việt và biến nó thành intent điều khiển thiết bị, qua 3 bước:
action, filter, routing).floor_id / room_id / device_type, rồi chấm điểm mềm theo tên và cắt top-K.0 = mơ hồ), guard kiểm tra rồi gateway thực thi.Kèm theo là một Web Playground để nhìn thấy cả 3 bước, tự tạo dataset thiết bị, và kéo trọng số của công thức tính điểm.
npm install
npm run web:install # cài dependency của frontend (web/)
npm run web:build # build frontend ra web/dist
npm run build # build backend ra dist/
npm start # server ở http://localhost:3000
Mở http://localhost:3000 — backend phục vụ luôn web/dist, nên frontend và API cùng một origin, không cần CORS, không cần chạy hai process.
Nếu chưa chạy npm run web:build, server vẫn khởi động và API vẫn hoạt động bình thường; vào / sẽ nhận HTTP 503 kèm đúng câu lệnh cần chạy để có bản build.
npm run dev # backend, http://localhost:3000
npm run web # frontend Vite, http://localhost:5173 (proxy /v1 và /healthz sang :3000)
Mở http://localhost:5173. Vite lo hot-reload; các request /v1/... được proxy sang backend (xem web/vite.config.ts).
| # | Yêu cầu | Ở đâu | Code |
|---|---|---|---|
| 1 | Ô nhập text, đầu ra là intent điều khiển thiết bị | Ô "Câu lệnh" ở tab Playground (kèm dropdown tầng/phòng lấy từ chính dataset đang dùng). Bấm Chạy → step ① hiện intent JSON parser trả về, step ③ hiện lệnh thực thi + deviceIds | web/src/components/CommandBar.tsx, POST /v1/parse |
| 2 | Upload CSV/XLSX floor_id, room_id, device_type, hoặc UI tự tạo dữ liệu | Tab Dataset: nút chọn file (.csv, .xlsx, .xls) và bảng tự thêm/sửa/xoá từng dòng. Cả hai đường đều đi qua nút Áp dụng → POST /v1/dataset → server trả datasetId + danh sách warning | web/src/components/DatasetTab.tsx, web/src/lib/parseSpreadsheet.ts, src/data/datasetStore.ts |
| 3 | Nhìn thấy cả 3 bước của thuật toán | Ba card ①②③ trên tab Playground: intent của parser; bảng ứng viên với score và thanh phân rã 4 thành phần điểm, số thiết bị vào/ra mỗi tầng lọc, thời gian chạy; kết quả final extract kèm alias được chọn, reason, và các guard (topScore, scoreGap) | web/src/components/StepParser.tsx, StepFilter.tsx, StepFinal.tsx |
| 4 | Chỉnh được trọng số trong công thức tính điểm | Thanh trọng số dưới cùng: 4 slider trọng số + 4 ngưỡng (semanticThreshold, topK, finalMinScore, ambiguityGap). Kéo → chỉ step ② chạy lại (local, miễn phí); step ③ bị đánh dấu "cũ" và chỉ chạy lại LLM khi bạn tự bấm | web/src/components/WeightsBar.tsx, src/scoring/tuning.ts |
Thanh trọng số hiển thị đúng con số server báo là đã dùng (effectiveTuning trong response của /v1/filter), không phải vị trí slider — hai thứ này có thể lệch nhau trong lúc request đang bay.
Mỗi thiết bị còn lại sau lọc cứng được chấm 4 điểm thành phần trong [0, 1] (src/filter/semantic.ts):
| Thành phần | Nghĩa | Trọng số mặc định |
|---|---|---|
semantic | cosine similarity giữa name_query và "<device_name> <tags>" (char n-gram trên text đã chuẩn hoá tiếng Việt) | 0.58 |
numeric | khớp gợi ý số ("đèn 2", "điều hoà số 1") | 0.18 |
type | name_query có nhắc đúng loại thiết bị không | 0.14 |
room | name_query có nhắc đúng tên phòng không | 0.10 |
score = Σ (component × weight_normalized) với weight_normalized = weight / Σ weights
Trọng số được server chuẩn hoá về tổng = 1 (src/scoring/weights.ts). Nghĩa là chỉ tỉ lệ giữa 4 trọng số có ý nghĩa, không phải độ lớn tuyệt đối: {4, 1, 1, 1} và {0.4, 0.1, 0.1, 0.1} cho ra kết quả y hệt. Nhờ vậy score luôn nằm trong [0, 1] và các ngưỡng semanticThreshold / finalMinScore giữ nguyên ý nghĩa khi bạn kéo slider.
Lưu ý trung thực: nếu parser không trả name_query (ví dụ "bật đèn phòng khách" đã đủ rõ bằng lọc cứng), soft filter bị bỏ qua — mọi thiết bị nhận score = 1 và trọng số không ảnh hưởng gì. Lệnh batch (batchAll, ví dụ "bật tất cả đèn") cũng không đọc một con số tuning nào: nó thực thi thẳng kết quả lọc cứng và bỏ qua final extract.
Kéo một slider không bao giờ gọi LLM: playground chỉ gọi lại POST /v1/filter (chạy local). Step ③ chuyển sang trạng thái "cũ — trọng số đã đổi" và chỉ chạy lại khi bạn bấm Chạy lại step ③.
Cột bắt buộc: floor_id, room_id, device_type.
Cột tuỳ chọn: device_id, device_name, room_name, tags (chuỗi ngăn cách bằng dấu phẩy, ví dụ ceiling,left).
device_type hợp lệ: light, air_conditioner, fan, sensor, appliance, tv, curtain, door, other.
Server (src/data/datasetStore.ts) không im lặng — mọi can thiệp đều trả về trong danh sách warnings và playground in ra ngay tại tab Dataset:
| Dữ liệu vào | Server làm gì |
|---|---|
Thiếu floor_id, room_id hoặc device_type | Bỏ dòng đó, warning ghi rõ số dòng |
device_type lạ (đèn, lamp, ...) | Ép về other + warning. Thiết bị vẫn nằm trong dataset, nhưng lọc cứng theo loại sẽ không còn tìm ra nó như một cái đèn |
Thiếu device_id | Tự sinh "<type>_<room_id>_<n>", đảm bảo không trùng |
device_id do bạn khai bị trùng nhau | Bỏ dòng trùng (giữ dòng đầu) + warning |
Thiếu device_name | Tự sinh "<Nhãn loại> <n> <room_id>" + warning: soft filter sẽ kém chính xác — nó chấm điểm trên chính tên thiết bị, nên tên máy sinh làm thành phần semantic gần như vô nghĩa |
Thiếu room_name | Dùng room_id thay thế + warning (thành phần room vì thế cũng yếu đi) |
Một room_id xuất hiện ở nhiều floor_id | Giữ floor_id gặp đầu tiên làm chính thức + warning |
| Không còn dòng hợp lệ nào | HTTP 400, không tạo dataset |
| Quá 5000 dòng | HTTP 400 — cả file bị từ chối, không cắt bớt |
Dataset upload sống trong bộ nhớ của process (ds_1, ds_2, ...) và mất khi restart server. Dataset mẫu (sample, hơn 1000 thiết bị) luôn có sẵn.
Server phục vụ cả API và frontend trên cùng cổng PORT (mặc định 3000).
| Route | Mô tả |
|---|---|
GET /healthz | {"ok":true} |
GET /v1/config/defaults | trọng số + ngưỡng mặc định, danh sách device_type, provider đang dùng, có đang mock không |
GET /v1/dataset/:id | thiết bị + staticContext của một dataset |
POST /v1/dataset | { rows: [...] } → { datasetId, deviceCount, warnings, staticContext } |
POST /v1/parse | step ① — LLM |
POST /v1/filter | step ② — local, nhận tuning để chạy lại với trọng số khác |
POST /v1/select | step ③ — LLM + execute |
POST /v1/command | chạy thẳng cả 3 bước (API gốc, không qua playground) |
| mọi path khác | file trong web/dist, hoặc index.html cho route SPA |
Hai luật của phần phục vụ file tĩnh (src/api/staticFiles.ts), đều để tránh nói dối client:
/v1/..., /healthz) luôn trả JSON 404 — không bao giờ trả HTML. Client gọi API mà nhận về HTML sẽ chết bằng một lỗi parse JSON vô nghĩa./assets/index-cu.js) là 404 thật, không fallback về index.html. Trả index.html với mã 200 cho một file .js thiếu sẽ khiến trình duyệt chạy HTML như JavaScript và báo Unexpected token '<' — trong khi nguyên nhân thật (build cũ / chưa build) bị giấu đi. Chỉ path không có đuôi file mới là route SPA.Ví dụ POST /v1/command:
{ "text": "Bật đèn trần góc trái", "context": { "room_id": "living_room_01", "floor_id": "floor_1" } }
{
"correlationId": "...",
"status": "executed",
"action": { "command": "turn_on", "params": {} },
"deviceIds": ["light_lv_ceiling_left"],
"trace": { "parser": { ... }, "filter": { ... }, "finalExtract": { ... } }
}
Copy .env.example sang .env rồi sửa. Không commit .env.
Runtime hỗ trợ các provider tương thích OpenAI: Cerebras, OpenRouter và OpenAI. Khuyến nghị dùng Cerebras với gpt-oss-120b để có chi phí thấp và tốc độ suy luận cao. Nếu provider được chọn không có API key, hoặc GEMINI_USE_MOCK=true, hệ thống fallback sang mock để chạy local.
LLM runtime
MODEL_PROVIDER: cerebras, openrouter hoặc openai. Nếu không đặt, hệ thống tự chọn Cerebras/OpenRouter khi có đúng API key tương ứng; còn lại là OpenAI.CEREBRAS_API_KEY: API key Cerebras. Đặt MODEL_PROVIDER=cerebras; model parser/final mặc định là gpt-oss-120b. Có thể ghi đè chung bằng CEREBRAS_MODEL, hoặc riêng bằng CEREBRAS_PARSER_MODEL và CEREBRAS_FINAL_MODEL. CEREBRAS_BASE_URL mặc định là https://api.cerebras.ai/v1.OPENROUTER_API_KEY: API key OpenRouter. Model được cấu hình bởi OPENROUTER_MODEL hoặc hai biến OPENROUTER_PARSER_MODEL / OPENROUTER_FINAL_MODEL.OPENAI_API_KEY: API key OpenAI khi đặt MODEL_PROVIDER=openai.OPENAI_PARSER_MODEL (mặc định gpt-4o-mini), OPENAI_FINAL_MODEL (gpt-4o-mini), OPENAI_USE_STRUCTURED_OUTPUTS (mặc định false), OPENAI_PARSER_TIMEOUT_MS (20000), OPENAI_FINAL_TIMEOUT_MS (15000).CAG_ENABLED (mặc định true), CAG_TTL_MS (mặc định 300000) và CAG_MAX_ENTRIES (mặc định 1000): cache kết quả parse/chọn thiết bị cho cùng câu lệnh, prompt, dataset và danh sách ứng viên. Cache được fingerprint theo toàn bộ metadata nhà nên dataset upload không thể dùng nhầm kết quả của nhà khác.GEMINI_USE_MOCK: cờ bật/tắt mock. Mặc định true; muốn gọi provider thật thì đặt GEMINI_USE_MOCK=false và cung cấp API key tương ứng.GEMINI_API_KEY, GEMINI_PARSER_MODEL, GEMINI_FINAL_MODEL, GEMINI_CONTEXT_CACHE_NAME: legacy/test compatibility.Badge trên góc phải playground hiển thị mock hay tên provider — nó đọc từ GET /v1/config/defaults, tức là từ chính config server đang chạy.
Trọng số và ngưỡng (giá trị khởi tạo; slider trong playground ghi đè cho từng request, không sửa .env)
SMART_HOME_WEIGHT_SEMANTIC (0.58), SMART_HOME_WEIGHT_NUMERIC (0.18), SMART_HOME_WEIGHT_TYPE (0.14), SMART_HOME_WEIGHT_ROOM (0.10) — cũng được chuẩn hoá về tổng 1.SMART_HOME_SEMANTIC_THRESHOLD (0.35): score dưới ngưỡng bị loại ở soft filter.SMART_HOME_TOP_K (5): số ứng viên giữ lại sau soft filter.SMART_HOME_FINAL_EXTRACT_MAX_CANDIDATES (20): trần số ứng viên đưa vào prompt final extract.SMART_HOME_FINAL_MIN_SCORE (0.45): score tối thiểu để guard cho phép thực thi.SMART_HOME_AMBIGUITY_GAP (0.08): khoảng cách tối thiểu giữa top-1 và top-2; hẹp hơn mức này là mơ hồ → yêu cầu làm rõ.PORT (3000).npm test # backend (vitest)
npm --prefix web test # frontend (vitest + testing-library)
npm run build # tsc backend
npm run web:build # build frontend
Chạy không cần API key: mọi test dùng mock adapter.
npm run demo # 3 kịch bản end-to-end
npm run trace # nhập lệnh, in từng bước input → parser → filter → final_extract → execute
src/)src/api/routes.ts — router HTTP: các endpoint theo từng step + POST /v1/command.src/api/staticFiles.ts — phục vụ web/dist (guard traversal bằng path.relative, không fallback HTML cho asset thiếu).src/api/errors.ts — mã lỗi chuẩn (invalid_request, dataset_not_found, upstream_error, internal_error; router trả thêm not_found).src/pipeline/smartHomePipeline.ts — orchestrator 3 bước, validate, trace, metrics.src/parser/ — openaiParser.ts, geminiParser.ts, mockParser.ts, nameQueryRepair.ts (chung interface parserInterface.ts).src/filter/ — deterministic.ts (lọc cứng), semantic.ts (4 điểm thành phần + blend), index.ts (ghép + top-K + batch bypass).src/finalExtract/ — openaiFinalExtractor.ts, geminiFinalExtractor.ts, mockFinalExtractor.ts.src/scoring/weights.ts — trọng số mặc định + chuẩn hoá tổng = 1; src/scoring/tuning.ts — áp tuning cho từng request (không mutate config chung).src/data/datasetStore.ts — dựng dataset từ các dòng CSV/XLSX, sinh warning, store in-memory; src/data/sampleHome.ts — dataset mẫu.src/clients/ — client OpenAI / Gemini / Gemini context cache.src/gateway/mockGateway.ts — gateway giả để chạy local không cần phần cứng.src/config.ts, src/env.ts, src/schemas.ts, src/types.ts, src/logger.ts, src/utils/.web/src/)App.tsx — hai tab (Playground / Dataset), cả hai luôn mount để đổi tab không xoá dữ liệu đang gõ.state/playgroundStore.ts + state/usePlayground.ts — reducer 3 step, epoch chống race, debounce slider.components/ — CommandBar, StepParser, StepFilter, StepFinal, StepCard, WeightsBar, DatasetTab, DeviceTable, ScoreBreakdownBar.lib/parseSpreadsheet.ts — đọc CSV/XLSX ở client; lib/deviceRows.ts — validate dòng trước khi gửi.api/client.ts — gọi các endpoint của backend, dịch mã lỗi server thành thông báo tiếng Việt.CAG cache hit hoặc số prompt token OpenAI đã cache.55 commits
TypeScript
97.9%
CSS
2.0%
Hệ thống nhận một câu lệnh tiếng Việt và biến nó thành intent điều khiển thiết bị, qua 3 bước:
action, filter, routing).floor_id / room_id / device_type, rồi chấm điểm mềm theo tên và cắt top-K.0 = mơ hồ), guard kiểm tra rồi gateway thực thi.Kèm theo là một Web Playground để nhìn thấy cả 3 bước, tự tạo dataset thiết bị, và kéo trọng số của công thức tính điểm.
npm install
npm run web:install # cài dependency của frontend (web/)
npm run web:build # build frontend ra web/dist
npm run build # build backend ra dist/
npm start # server ở http://localhost:3000
Mở http://localhost:3000 — backend phục vụ luôn web/dist, nên frontend và API cùng một origin, không cần CORS, không cần chạy hai process.
Nếu chưa chạy npm run web:build, server vẫn khởi động và API vẫn hoạt động bình thường; vào / sẽ nhận HTTP 503 kèm đúng câu lệnh cần chạy để có bản build.
npm run dev # backend, http://localhost:3000
npm run web # frontend Vite, http://localhost:5173 (proxy /v1 và /healthz sang :3000)
Mở http://localhost:5173. Vite lo hot-reload; các request /v1/... được proxy sang backend (xem web/vite.config.ts).
| # | Yêu cầu | Ở đâu | Code |
|---|---|---|---|
| 1 | Ô nhập text, đầu ra là intent điều khiển thiết bị | Ô "Câu lệnh" ở tab Playground (kèm dropdown tầng/phòng lấy từ chính dataset đang dùng). Bấm Chạy → step ① hiện intent JSON parser trả về, step ③ hiện lệnh thực thi + deviceIds | web/src/components/CommandBar.tsx, POST /v1/parse |
| 2 | Upload CSV/XLSX floor_id, room_id, device_type, hoặc UI tự tạo dữ liệu | Tab Dataset: nút chọn file (.csv, .xlsx, .xls) và bảng tự thêm/sửa/xoá từng dòng. Cả hai đường đều đi qua nút Áp dụng → POST /v1/dataset → server trả datasetId + danh sách warning | web/src/components/DatasetTab.tsx, web/src/lib/parseSpreadsheet.ts, src/data/datasetStore.ts |
| 3 | Nhìn thấy cả 3 bước của thuật toán | Ba card ①②③ trên tab Playground: intent của parser; bảng ứng viên với score và thanh phân rã 4 thành phần điểm, số thiết bị vào/ra mỗi tầng lọc, thời gian chạy; kết quả final extract kèm alias được chọn, reason, và các guard (topScore, scoreGap) | web/src/components/StepParser.tsx, StepFilter.tsx, StepFinal.tsx |
| 4 | Chỉnh được trọng số trong công thức tính điểm | Thanh trọng số dưới cùng: 4 slider trọng số + 4 ngưỡng (semanticThreshold, topK, finalMinScore, ambiguityGap). Kéo → chỉ step ② chạy lại (local, miễn phí); step ③ bị đánh dấu "cũ" và chỉ chạy lại LLM khi bạn tự bấm | web/src/components/WeightsBar.tsx, src/scoring/tuning.ts |
Thanh trọng số hiển thị đúng con số server báo là đã dùng (effectiveTuning trong response của /v1/filter), không phải vị trí slider — hai thứ này có thể lệch nhau trong lúc request đang bay.
Mỗi thiết bị còn lại sau lọc cứng được chấm 4 điểm thành phần trong [0, 1] (src/filter/semantic.ts):
| Thành phần | Nghĩa | Trọng số mặc định |
|---|---|---|
semantic | cosine similarity giữa name_query và "<device_name> <tags>" (char n-gram trên text đã chuẩn hoá tiếng Việt) | 0.58 |
numeric | khớp gợi ý số ("đèn 2", "điều hoà số 1") | 0.18 |
type | name_query có nhắc đúng loại thiết bị không | 0.14 |
room | name_query có nhắc đúng tên phòng không | 0.10 |
score = Σ (component × weight_normalized) với weight_normalized = weight / Σ weights
Trọng số được server chuẩn hoá về tổng = 1 (src/scoring/weights.ts). Nghĩa là chỉ tỉ lệ giữa 4 trọng số có ý nghĩa, không phải độ lớn tuyệt đối: {4, 1, 1, 1} và {0.4, 0.1, 0.1, 0.1} cho ra kết quả y hệt. Nhờ vậy score luôn nằm trong [0, 1] và các ngưỡng semanticThreshold / finalMinScore giữ nguyên ý nghĩa khi bạn kéo slider.
Lưu ý trung thực: nếu parser không trả name_query (ví dụ "bật đèn phòng khách" đã đủ rõ bằng lọc cứng), soft filter bị bỏ qua — mọi thiết bị nhận score = 1 và trọng số không ảnh hưởng gì. Lệnh batch (batchAll, ví dụ "bật tất cả đèn") cũng không đọc một con số tuning nào: nó thực thi thẳng kết quả lọc cứng và bỏ qua final extract.
Kéo một slider không bao giờ gọi LLM: playground chỉ gọi lại POST /v1/filter (chạy local). Step ③ chuyển sang trạng thái "cũ — trọng số đã đổi" và chỉ chạy lại khi bạn bấm Chạy lại step ③.
Cột bắt buộc: floor_id, room_id, device_type.
Cột tuỳ chọn: device_id, device_name, room_name, tags (chuỗi ngăn cách bằng dấu phẩy, ví dụ ceiling,left).
device_type hợp lệ: light, air_conditioner, fan, sensor, appliance, tv, curtain, door, other.
Server (src/data/datasetStore.ts) không im lặng — mọi can thiệp đều trả về trong danh sách warnings và playground in ra ngay tại tab Dataset:
| Dữ liệu vào | Server làm gì |
|---|---|
Thiếu floor_id, room_id hoặc device_type | Bỏ dòng đó, warning ghi rõ số dòng |
device_type lạ (đèn, lamp, ...) | Ép về other + warning. Thiết bị vẫn nằm trong dataset, nhưng lọc cứng theo loại sẽ không còn tìm ra nó như một cái đèn |
Thiếu device_id | Tự sinh "<type>_<room_id>_<n>", đảm bảo không trùng |
device_id do bạn khai bị trùng nhau | Bỏ dòng trùng (giữ dòng đầu) + warning |
Thiếu device_name | Tự sinh "<Nhãn loại> <n> <room_id>" + warning: soft filter sẽ kém chính xác — nó chấm điểm trên chính tên thiết bị, nên tên máy sinh làm thành phần semantic gần như vô nghĩa |
Thiếu room_name | Dùng room_id thay thế + warning (thành phần room vì thế cũng yếu đi) |
Một room_id xuất hiện ở nhiều floor_id | Giữ floor_id gặp đầu tiên làm chính thức + warning |
| Không còn dòng hợp lệ nào | HTTP 400, không tạo dataset |
| Quá 5000 dòng | HTTP 400 — cả file bị từ chối, không cắt bớt |
Dataset upload sống trong bộ nhớ của process (ds_1, ds_2, ...) và mất khi restart server. Dataset mẫu (sample, hơn 1000 thiết bị) luôn có sẵn.
Server phục vụ cả API và frontend trên cùng cổng PORT (mặc định 3000).
| Route | Mô tả |
|---|---|
GET /healthz | {"ok":true} |
GET /v1/config/defaults | trọng số + ngưỡng mặc định, danh sách device_type, provider đang dùng, có đang mock không |
GET /v1/dataset/:id | thiết bị + staticContext của một dataset |
POST /v1/dataset | { rows: [...] } → { datasetId, deviceCount, warnings, staticContext } |
POST /v1/parse | step ① — LLM |
POST /v1/filter | step ② — local, nhận tuning để chạy lại với trọng số khác |
POST /v1/select | step ③ — LLM + execute |
POST /v1/command | chạy thẳng cả 3 bước (API gốc, không qua playground) |
| mọi path khác | file trong web/dist, hoặc index.html cho route SPA |
Hai luật của phần phục vụ file tĩnh (src/api/staticFiles.ts), đều để tránh nói dối client:
/v1/..., /healthz) luôn trả JSON 404 — không bao giờ trả HTML. Client gọi API mà nhận về HTML sẽ chết bằng một lỗi parse JSON vô nghĩa./assets/index-cu.js) là 404 thật, không fallback về index.html. Trả index.html với mã 200 cho một file .js thiếu sẽ khiến trình duyệt chạy HTML như JavaScript và báo Unexpected token '<' — trong khi nguyên nhân thật (build cũ / chưa build) bị giấu đi. Chỉ path không có đuôi file mới là route SPA.Ví dụ POST /v1/command:
{ "text": "Bật đèn trần góc trái", "context": { "room_id": "living_room_01", "floor_id": "floor_1" } }
{
"correlationId": "...",
"status": "executed",
"action": { "command": "turn_on", "params": {} },
"deviceIds": ["light_lv_ceiling_left"],
"trace": { "parser": { ... }, "filter": { ... }, "finalExtract": { ... } }
}
Copy .env.example sang .env rồi sửa. Không commit .env.
Runtime hỗ trợ các provider tương thích OpenAI: Cerebras, OpenRouter và OpenAI. Khuyến nghị dùng Cerebras với gpt-oss-120b để có chi phí thấp và tốc độ suy luận cao. Nếu provider được chọn không có API key, hoặc GEMINI_USE_MOCK=true, hệ thống fallback sang mock để chạy local.
LLM runtime
MODEL_PROVIDER: cerebras, openrouter hoặc openai. Nếu không đặt, hệ thống tự chọn Cerebras/OpenRouter khi có đúng API key tương ứng; còn lại là OpenAI.CEREBRAS_API_KEY: API key Cerebras. Đặt MODEL_PROVIDER=cerebras; model parser/final mặc định là gpt-oss-120b. Có thể ghi đè chung bằng CEREBRAS_MODEL, hoặc riêng bằng CEREBRAS_PARSER_MODEL và CEREBRAS_FINAL_MODEL. CEREBRAS_BASE_URL mặc định là https://api.cerebras.ai/v1.OPENROUTER_API_KEY: API key OpenRouter. Model được cấu hình bởi OPENROUTER_MODEL hoặc hai biến OPENROUTER_PARSER_MODEL / OPENROUTER_FINAL_MODEL.OPENAI_API_KEY: API key OpenAI khi đặt MODEL_PROVIDER=openai.OPENAI_PARSER_MODEL (mặc định gpt-4o-mini), OPENAI_FINAL_MODEL (gpt-4o-mini), OPENAI_USE_STRUCTURED_OUTPUTS (mặc định false), OPENAI_PARSER_TIMEOUT_MS (20000), OPENAI_FINAL_TIMEOUT_MS (15000).CAG_ENABLED (mặc định true), CAG_TTL_MS (mặc định 300000) và CAG_MAX_ENTRIES (mặc định 1000): cache kết quả parse/chọn thiết bị cho cùng câu lệnh, prompt, dataset và danh sách ứng viên. Cache được fingerprint theo toàn bộ metadata nhà nên dataset upload không thể dùng nhầm kết quả của nhà khác.GEMINI_USE_MOCK: cờ bật/tắt mock. Mặc định true; muốn gọi provider thật thì đặt GEMINI_USE_MOCK=false và cung cấp API key tương ứng.GEMINI_API_KEY, GEMINI_PARSER_MODEL, GEMINI_FINAL_MODEL, GEMINI_CONTEXT_CACHE_NAME: legacy/test compatibility.Badge trên góc phải playground hiển thị mock hay tên provider — nó đọc từ GET /v1/config/defaults, tức là từ chính config server đang chạy.
Trọng số và ngưỡng (giá trị khởi tạo; slider trong playground ghi đè cho từng request, không sửa .env)
SMART_HOME_WEIGHT_SEMANTIC (0.58), SMART_HOME_WEIGHT_NUMERIC (0.18), SMART_HOME_WEIGHT_TYPE (0.14), SMART_HOME_WEIGHT_ROOM (0.10) — cũng được chuẩn hoá về tổng 1.SMART_HOME_SEMANTIC_THRESHOLD (0.35): score dưới ngưỡng bị loại ở soft filter.SMART_HOME_TOP_K (5): số ứng viên giữ lại sau soft filter.SMART_HOME_FINAL_EXTRACT_MAX_CANDIDATES (20): trần số ứng viên đưa vào prompt final extract.SMART_HOME_FINAL_MIN_SCORE (0.45): score tối thiểu để guard cho phép thực thi.SMART_HOME_AMBIGUITY_GAP (0.08): khoảng cách tối thiểu giữa top-1 và top-2; hẹp hơn mức này là mơ hồ → yêu cầu làm rõ.PORT (3000).npm test # backend (vitest)
npm --prefix web test # frontend (vitest + testing-library)
npm run build # tsc backend
npm run web:build # build frontend
Chạy không cần API key: mọi test dùng mock adapter.
npm run demo # 3 kịch bản end-to-end
npm run trace # nhập lệnh, in từng bước input → parser → filter → final_extract → execute
src/)src/api/routes.ts — router HTTP: các endpoint theo từng step + POST /v1/command.src/api/staticFiles.ts — phục vụ web/dist (guard traversal bằng path.relative, không fallback HTML cho asset thiếu).src/api/errors.ts — mã lỗi chuẩn (invalid_request, dataset_not_found, upstream_error, internal_error; router trả thêm not_found).src/pipeline/smartHomePipeline.ts — orchestrator 3 bước, validate, trace, metrics.src/parser/ — openaiParser.ts, geminiParser.ts, mockParser.ts, nameQueryRepair.ts (chung interface parserInterface.ts).src/filter/ — deterministic.ts (lọc cứng), semantic.ts (4 điểm thành phần + blend), index.ts (ghép + top-K + batch bypass).src/finalExtract/ — openaiFinalExtractor.ts, geminiFinalExtractor.ts, mockFinalExtractor.ts.src/scoring/weights.ts — trọng số mặc định + chuẩn hoá tổng = 1; src/scoring/tuning.ts — áp tuning cho từng request (không mutate config chung).src/data/datasetStore.ts — dựng dataset từ các dòng CSV/XLSX, sinh warning, store in-memory; src/data/sampleHome.ts — dataset mẫu.src/clients/ — client OpenAI / Gemini / Gemini context cache.src/gateway/mockGateway.ts — gateway giả để chạy local không cần phần cứng.src/config.ts, src/env.ts, src/schemas.ts, src/types.ts, src/logger.ts, src/utils/.web/src/)App.tsx — hai tab (Playground / Dataset), cả hai luôn mount để đổi tab không xoá dữ liệu đang gõ.state/playgroundStore.ts + state/usePlayground.ts — reducer 3 step, epoch chống race, debounce slider.components/ — CommandBar, StepParser, StepFilter, StepFinal, StepCard, WeightsBar, DatasetTab, DeviceTable, ScoreBreakdownBar.lib/parseSpreadsheet.ts — đọc CSV/XLSX ở client; lib/deviceRows.ts — validate dòng trước khi gửi.api/client.ts — gọi các endpoint của backend, dịch mã lỗi server thành thông báo tiếng Việt.CAG cache hit hoặc số prompt token OpenAI đã cache.55 commits
TypeScript
97.9%
CSS
2.0%