1What this isĐây là gì
This is the Edge layer from the Core & Edge Blueprint: Odoo owns prices, folios, invoices; this repo is everything outside that. Today it holds exactly one working piece — the AI Extractor (packages/extractor/) behind a minimal BFF (apps/casa-bff/), exposed as POST /v1/extract.
Đây là lớp Edge theo Core & Edge Blueprint: Odoo giữ giá, folio, hóa đơn; repo này là mọi thứ nằm ngoài đó. Hiện tại chỉ có đúng một mảnh chạy được — AI Extractor (packages/extractor/) đứng sau một BFF tối giản (apps/casa-bff/), lộ ra qua POST /v1/extract.
The Draft store, the Contract pod's generated Odoo client, and the rest of the BFF endpoints from the Playbook do not exist here yet. See docs/adr/ for the decisions this build is actually based on — the Blueprint and Playbook sites hold the reasoning behind them.
Draft store, Odoo client do Contract pod sinh ra, và các endpoint BFF còn lại trong Playbook chưa tồn tại ở đây. Xem docs/adr/ để biết những quyết định bản này thực sự dựa vào — 2 trang Blueprint và Playbook giữ lý do đằng sau chúng.
2PrerequisitesYêu cầu trước
- Node ≥ 18 (developed against Node 22)
- The Vercel CLI (
npm i -g vercel) — keep it current; an old CLI talking to a newer backend silently breaksvercel inspect/vercel logs(see §8). - Access to the
aidev1-technexts-projectsVercel scope, or your own scope for a separate deployment. - A Gemini API key from Google AI Studio for local runs.
- Node ≥ 18 (đang phát triển trên Node 22)
- Vercel CLI (
npm i -g vercel) — giữ bản mới nhất; CLI cũ nói chuyện với backend mới sẽ âm thầm làm hỏngvercel inspect/vercel logs(xem mục 8). - Quyền truy cập scope Vercel
aidev1-technexts-projects, hoặc scope riêng của bạn nếu deploy độc lập. - Một Gemini API key từ Google AI Studio để chạy local.
3Running it locallyChạy local
git clone https://github.com/TechNextSG/technext-edge.git
cd technext-edge
npm install
npm test # extractor unit tests — no API key needed, no network
To actually call a model:
Để thật sự gọi model:
cp .env.example .env.local # fill in GEMINI_API_KEY
npm run dev:bff # http://localhost:8787
Open http://localhost:8787/ for the test console (see §4), or curl it:
Mở http://localhost:8787/ để vào trang test (xem mục 4), hoặc gọi bằng curl:
curl -s localhost:8787/v1/extract -H 'content-type: application/json' \
-d '{"text":"4 of us, next Saturday, 3 nights"}'
4The test consoleTrang test
A plain page at GET / — not the real estimator UI (that belongs to the Edge UI pod) — for trying extraction without curl or Postman. Live at technext-edge-casa-bff.vercel.app. Visual identity matches apps/estimate-tool in casa-escondida-tools on purpose.
Một trang đơn giản ở GET / — không phải UI estimator thật (cái đó thuộc Edge UI pod) — để thử bóc tách mà không cần curl hay Postman. Chạy tại technext-edge-casa-bff.vercel.app. Nhận diện thị giác cố tình khớp với apps/estimate-tool trong casa-escondida-tools.
It's behind Vercel's Deployment Protection (SSO wall) — anyone hitting the bare URL without a session sees a login page, not the app. To get in, open this once per browser (sets a cookie):
Trang này đứng sau tường SSO của Vercel — ai mở URL trần mà chưa có session sẽ thấy trang đăng nhập, không phải app. Để vào được, mở link này một lần cho mỗi trình duyệt (sẽ gắn cookie):
https://technext-edge-casa-bff.vercel.app/?x-vercel-protection-bypass=<secret>&x-vercel-set-bypass-cookie=true
The provider override panel on the page lets you paste your own key and pick a different provider (Gemini / DeepSeek Flash / DeepSeek Pro) for one request, without touching any Vercel config. The key goes straight from your browser to this server and nowhere else — session storage only, cleared when the tab closes.
Bảng provider override trên trang cho phép bạn dán key riêng và chọn provider khác (Gemini / DeepSeek Flash / DeepSeek Pro) cho một request, không cần đụng cấu hình Vercel. Key đi thẳng từ trình duyệt của bạn tới server này và không đi đâu khác — chỉ lưu trong session storage, mất khi đóng tab.
5DeployingDeploy
Always run Vercel commands from the repo root, never from apps/casa-bff/. The deploy entry point is api/index.ts at the root — it imports packages/extractor by relative path, so a deploy triggered from inside apps/casa-bff/ uploads only that subtree and 404s on the extractor package during install.
Luôn chạy lệnh Vercel từ gốc repo, không bao giờ từ apps/casa-bff/. Entry point deploy là api/index.ts ở gốc — nó import packages/extractor theo đường dẫn tương đối, nên deploy chạy từ trong apps/casa-bff/ chỉ upload đúng nhánh đó và 404 khi cài package extractor.
vercel link # first time only
vercel env add GEMINI_API_KEY production
vercel env add GEMINI_API_KEY preview
rm -rf .vercel/output
vercel build --yes --target production
vercel deploy --prebuilt --prod --yes
Building locally first (vercel build then --prebuilt) is the reliable path — a plain vercel deploy --prod works too but gives you nothing to inspect if something goes wrong mid-build. If a deploy sits at "Building…" for what feels like too long, check vercel logs <deployment-url> first — a 429/quota error, a bad commit-author check, and a genuine hang all look identical from the outside.
Build local trước (vercel build rồi --prebuilt) là cách đáng tin cậy hơn — vercel deploy --prod trơn cũng chạy được nhưng không cho gì để kiểm tra nếu build giữa chừng bị lỗi. Nếu deploy đứng ở "Building…" lâu bất thường, kiểm tra vercel logs <deployment-url> trước — lỗi 429/quota, lỗi quyền tác giả commit, và treo thật đều trông giống hệt nhau từ bên ngoài.
6Environment variablesBiến môi trường
| Variable | Where | Required | Notes |
|---|---|---|---|
GEMINI_API_KEY | Prod + Preview | if provider is gemini (default) | Free tier isn't enough for a real eval run — see ADR-005a. |
GEMINI_MODEL | optional | no | Defaults to gemini-2.5-flash. Verify against ai.google.dev before changing. |
EXTRACTOR_PROVIDER | optional | no | gemini (default) · deepseek-flash · deepseek-pro |
DEEPSEEK_GATEWAY_KEY | optional | if provider is a DeepSeek value | Personal LiteLLM gateway key — ask Anthony. $12 budget, shared across everything you use it for. |
DEBUG_EXTRACT | optional, dev only | no | 1 includes zod issues / stack traces in error responses. Remove after debugging. |
| Biến | Ở đâu | Bắt buộc | Ghi chú |
|---|---|---|---|
GEMINI_API_KEY | Prod + Preview | nếu provider là gemini (mặc định) | Free tier không đủ cho eval thật — xem ADR-005a. |
GEMINI_MODEL | tùy chọn | không | Mặc định gemini-2.5-flash. Kiểm tra lại trên ai.google.dev trước khi đổi. |
EXTRACTOR_PROVIDER | tùy chọn | không | gemini (mặc định) · deepseek-flash · deepseek-pro |
DEEPSEEK_GATEWAY_KEY | tùy chọn | nếu provider là DeepSeek | Key gateway LiteLLM cá nhân — hỏi Anthony. Ngân sách $12, dùng chung cho mọi việc bạn dùng key đó. |
DEBUG_EXTRACT | tùy chọn, chỉ dev | không | 1 đưa thêm lỗi zod/stack trace vào response lỗi. Tắt sau khi debug xong. |
You don't need to touch env vars to try a different provider for one request — see the per-request override in §4 and §7.
Bạn không cần đụng biến môi trường để thử provider khác cho 1 request — xem phần override theo từng request ở mục 4 và 7.
7Eval harnessEval harness
packages/extractor/eval/ scores against the Playbook's five thresholds (fabrication, required-field accuracy, verbatim evidence, latency; question-targeting needs a human pass, not wired up). Two rules that matter most:
packages/extractor/eval/ chấm điểm theo đúng 5 ngưỡng của Playbook (bịa đặt, độ chính xác field bắt buộc, evidence nguyên văn, latency; riêng độ chính xác câu hỏi cần người chấm tay, chưa nối tự động). Hai quy tắc quan trọng nhất:
dataset.synthetic.jsondecides nothing. Researched but made up. Real decisions wait fordataset.real.json— Eloa's 30 real, name-masked messages.- Every result gets read against the actual thresholds, not vibes. A fabricated field is an automatic disqualifier regardless of how good everything else looks.
dataset.synthetic.jsonkhông quyết định gì cả. Có nghiên cứu nhưng vẫn là tự bịa. Quyết định thật chờdataset.real.json— 30 tin nhắn thật, đã che tên, từ Eloa.- Mọi kết quả đọc theo đúng ngưỡng, không theo cảm tính. Một field bịa đặt là loại ngay lập tức, bất kể phần còn lại tốt thế nào.
export EVAL_BYPASS_SECRET=<bypass secret from §4>
node packages/extractor/eval/runner.mjs # server's default provider
# test a specific provider without touching Vercel config:
export EVAL_PROVIDER_API_KEY=<your key for that provider>
node packages/extractor/eval/runner.mjs --provider deepseek-flash
PowerShell: $env:EVAL_BYPASS_SECRET="..." instead of export.
PowerShell: dùng $env:EVAL_BYPASS_SECRET="..." thay vì export.
8Known gotchasCác bẫy đã gặp read before debuggingđọc trước khi debug
vercel.json needs "framework": null. Without it, Vercel's Hono auto-detection expects an entrypoint at the repo root or src/ and fails with "No entrypoint found" — conflicts with the api/ + rewrites layout used here.package.json needs "type": "module". Without it, the deployed function's ESM import syntax gets loaded as CommonJS and crashes — even on GET /healthz, which doesn't touch the extractor at all.@technext-edge/extractor typechecks and builds locally, then 404s in the deployed Lambda with ERR_MODULE_NOT_FOUND. app.ts imports the package by relative path instead.vercel deploy. Vercel checks commit-author permissions against the project owner on Hobby plans; a mismatched local git identity gets silently stuck ("Deployment Blocked"), which looks identical to a hung build from the outside.extract() throws different error types for each; app.ts maps them to different HTTP statuses (429 vs 422). Collapsing them back into one generic error is what made a Gemini quota error read as "the model can't parse this message" during the eval dry run.vercel.json cần "framework": null. Thiếu nó, Vercel tự nhận diện "Hono framework", đòi entrypoint ở gốc repo hoặc src/, báo lỗi "No entrypoint found" — xung đột với cấu trúc api/ + rewrites đang dùng.package.json cần "type": "module". Thiếu nó, function deploy đọc nhầm cú pháp ESM import thành CommonJS rồi crash — kể cả GET /healthz, vốn không đụng gì tới extractor.@technext-edge/extractor build/typecheck local đều ổn, rồi 404 thật trên Lambda với ERR_MODULE_NOT_FOUND. app.ts import package theo đường dẫn tương đối để né hẳn việc này.extract() ném loại lỗi khác nhau cho từng trường hợp; app.ts map sang HTTP status khác nhau (429 vs 422). Gộp chung thành 1 lỗi là lý do lỗi hết quota Gemini từng bị đọc nhầm thành "model không hiểu tin nhắn này" trong lần chạy eval thử.9Before this becomes the real deliverableTrước khi thành sản phẩm thật
- Swap
packages/extractor/src/schema.tsfor the type generated from Phillip's frozencontracts/casa/estimate-api.v1.yaml— it's a placeholder guess right now, flagged inline. - Confirm the real 4 house-norm fields and their default values with Jett/Eloa —
houseNorms.tsis guessed. - Get billing enabled on the Gemini API key — the free tier cannot run a 30-message × multiple-provider bake-off at all.
- Once Eloa's 30 real messages exist, drop them into
packages/extractor/eval/dataset.real.jsonand run the same harness — the result becomes ADR-005b.
- Thay
packages/extractor/src/schema.tsbằng type sinh ra từcontracts/casa/estimate-api.v1.yamlđã đóng băng của Phillip — hiện chỉ là đoán, đã ghi chú ngay trong file. - Xác nhận đúng 4 field house-norm và giá trị mặc định với Jett/Eloa —
houseNorms.tshiện là đoán. - Bật billing cho Gemini API key — free tier không chạy nổi bake-off 30 tin × nhiều provider.
- Khi có 30 tin thật từ Eloa, bỏ vào
packages/extractor/eval/dataset.real.jsonrồi chạy lại đúng harness này — kết quả trở thành ADR-005b.