Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
A Pune interior-design studio gets ~200 enquiries a month and replies to fewer than half in time. This system answers every call, qualifies it
against the founder's rubric, books a consultation, and puts the lead in front of the people who need it. The agent never quotes a price
(enforced in code, not just in the prompt).
Live dashboard (auto-deployed from this repo by Vercel): https://aangan-phone-agent-wine.vercel.app/dashboard (public view: no names, phone numbers or transcripts)檢視完整 README ↗A Pune interior-design studio gets ~200 enquiries a month and replies to fewer than half in time. This system answers every call, qualifies it against the founder's rubric, books a consultation, and puts the lead in front of the people who need it. The agent never quotes a price (enforced in code, not just in the prompt).
Live dashboard (auto-deployed from this repo by Vercel): https://aangan-phone-agent-wine.vercel.app/dashboard (public view: no names, phone numbers or transcripts)
Same format as the case's Components Map: Trigger → Input → Context → Processing → AI → Output, with the human Review gate dashed. (open full size)
Customer calls ──> Vaani voice agent answers (AI disclosed, English/Hinglish), asks one question at a time
│ qualifies against the rubric in context/*.md (editable without code)
│ if it qualifies: Find slots -> Book consultation ──> Cal.com blocks the calendar
▼
call ends: Vaani webhook ──> /api/voice/webhook (this app)
├─ classify the call (10 outcomes, flags, extracted fields) ── Gemini
├─ Supabase call log + cost per call
├─ Telegram: complete lead note to the designers' group (claim by replying "taking it")
├─ HubSpot: contact + note for the founder
└─ Dashboard: results, funnel, designer response, cost per qualified lead
Booking details: the assistant calls two tools (our endpoints) during the call; they ask Cal.com for real free slots and create the booking there, so the slot is blocked on the studio calendar. Every booking also posts "CONSULTATION BOOKED" in the designers' Telegram group immediately.
Read first: CLAUDE.md, docs/PRD_build_spec.md, and CHANGELOG.md (every decision, assumption and open question).
| Path | What |
|---|---|
context/*.md | Business rules Nikhil can edit without code: services.md, qualified.md, pricing.md, outcomes.md, agent.md (greeting + fixed wording). Read at runtime on every call. |
src/lib/classify.ts | classifyCall(transcript, callMeta) returns schema-validated JSON (outcome, reason, fields, flags, pricing_asked). |
src/lib/agent.ts | agentTurn(state, callerUtterance): the conversational agent. |
src/lib/priceFilter.ts, promiseFilter.ts | The price-leak and no-promised-slot filters applied to every spoken line. |
src/lib/pipeline.ts | processCall: classify, merge repeat callers by caller ID, write the log and costs, send handoffs. |
src/lib/handoff.ts | Telegram note, claim-by-reply, designer follow-up replies, unclaimed-lead alert, escalation alert. |
src/lib/voice/ | Provider-agnostic voice layer: adapter interface, stub adapter, registry, webhook core. |
src/app/api/voice/webhook | End-of-call reports and telephony call-log rows (FR19). |
src/app/dashboard | The nine-panel dashboard and call detail pages. src/app/simulator is a text chat to rehearse calls. |
supabase/migrations/ | 0001_init.sql, 0002_dashboard.sql. |
scripts/ | replay.ts, simulate.ts, seed.ts. |
Requires Node 20+ (this repo was built with Node 24). If you have no Node, a project-local copy may exist in .tools/node:
export PATH="$PWD/.tools/node/bin:$PATH".
npm install
cp .env.example .env.local # then fill in; never commit .env.local
.env.example; real values only in .env.local / Vercel)| Variable | Needed for | Notes |
|---|---|---|
LLM_PROVIDER, GEMINI_API_KEY / ANTHROPIC_API_KEY | agent + classifier | gemini or anthropic; inferred from the model name if unset. |
MODEL_CLASSIFIER, MODEL_AGENT | agent + classifier | Model ids, so cost can be tuned. EFFORT_CLASSIFIER, EFFORT_AGENT set reasoning depth (empty omits the parameter). |
PRICE_*, USD_TO_INR, PRICE_VOICE_PER_MIN_INR | cost panel | Unit prices. Left at 0/blank on purpose; set them from your provider invoices. Without USD_TO_INR the dashboard shows USD and INR side by side. |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY | call log, dashboard | Server side only. The service role key must never reach the browser. |
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_NIKHIL_CHAT_ID, TELEGRAM_WEBHOOK_SECRET | handoffs | Designers' group, Nikhil's chat (falls back to the group), webhook secret token. |
CRON_SECRET | unclaimed-lead alert | Bearer secret for /api/cron/unclaimed. |
VOICE_PROVIDER, VOICE_WEBHOOK_SECRET | voice webhook | Adapter name (default stub) and its shared secret. |
DASHBOARD_PASSWORD / DASHBOARD_PUBLIC | dashboard, simulator | DASHBOARD_PASSWORD: HTTP Basic auth. Without it the pages are closed (503) in production unless DASHBOARD_PUBLIC=true, which opens the dashboard to anyone with the URL (call numbers and transcripts become public) and keeps the simulator closed. |
DECLINE_RULE, LEAD_TIME_*, COMMERCIAL_MIN_SQFT, OFFICE_*, FIRST_CONTACT_*, ESCALATED_REPEAT_ALSO_NOTES ... | open-question switches | Defaults and meaning are in .env.example and CHANGELOG.md. |
Apply supabase/migrations/0001_init.sql, 0002_dashboard.sql and 0003_harden.sql (Supabase SQL editor, or supabase db push).
The migrations create calls, leads, handoffs, costs and block deleting declined/escalated calls. Optional demo data:
npm run seed -- --reset # ~200 synthetic calls (provider = "demo"); demo rows are the only deletable protected rows
npm test # unit + acceptance tests (no network, no keys needed)
npm run typecheck
npm run replay # the 20 phone transcripts through classifyCall; expected vs actual (labels are PROVISIONAL)
npm run simulate -- --mode persona # live conversations against the agent; add --mode literal for the scripted caller lines
npm run dev # http://localhost:3000/dashboard (demo data if Supabase is not configured) and /simulator
replay and simulate call the real model, so they need the LLM key and cost a few cents. simulate never sends to
Telegram (in-memory store, capturing notifier) and prints the spec's acceptance checks. Transcripts and the notes that would
have been sent are written to out/.
.env.example under Project Settings, Environment Variables. Set DASHBOARD_PASSWORD.curl "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
-d "url=https://<your-app>.vercel.app/api/telegram/webhook" -d "secret_token=<TELEGRAM_WEBHOOK_SECRET>"
Add the bot to the designers' group. Telegram delivers replies to the bot's own messages even with privacy mode on, so claim-by-reply works without changing it./api/cron/unclaimed called every few minutes with Authorization: Bearer <CRON_SECRET>.
Vercel Cron sends that header automatically when CRON_SECRET is set, but sub-daily schedules need a Pro plan
(vercel.json: {"crons":[{"path":"/api/cron/unclaimed","schedule":"*/5 * * * *"}]}; I did not commit it because a Hobby deploy would reject it).
On Hobby, call the URL from GitHub Actions or any uptime pinger instead.https://<your-app>.vercel.app/api/voice/webhook (see below).The webhook function allows up to 60 s (maxDuration), because classification and the Telegram send happen inside the request.
Production: https://aangan-phone-agent-wine.vercel.app (Vercel project aangan-phone-agent). Redeploy with npx vercel deploy --prod --yes
after npx vercel login; environment variables live in Vercel (Settings, Environment Variables) and mirror .env.local.
Finding Telegram chat ids: add the bot to a chat and send /chatid; the bot replies with the id. Put the designers' group id in
TELEGRAM_CHAT_ID and Nikhil's in TELEGRAM_NIKHIL_CHAT_ID (Vercel and .env.local), then redeploy.
Scheduling the unclaimed-lead alert every 5 minutes without a paid Vercel plan (run once in the Supabase SQL editor; replace the placeholder with your CRON_SECRET):
create extension if not exists pg_cron;
create extension if not exists pg_net;
select cron.schedule('unclaimed-lead-alerts', '*/5 * * * *', $$
select net.http_get(
url := 'https://aangan-phone-agent-wine.vercel.app/api/cron/unclaimed',
headers := jsonb_build_object('authorization', 'Bearer <YOUR CRON_SECRET>')) $$);
-- to stop it: select cron.unschedule('unclaimed-lead-alerts');
Designers reply to the note in Telegram, in plain words:
| Say | Effect |
|---|---|
| "taking it", "mine", "on it" | Claims the lead (first claim wins; later ones are told who has it). |
| "called", "contacted", "spoke to them" | Records first contact (also claims an unclaimed lead). On an escalation message, "called back" records the callback. |
| "consultation booked" / "scheduled" / "done" | Marks the consultation (funnel). |
| "good fit" / "poor fit" | Fit verdict (designer-response panel). |
| "signed", "won", "project started" | Marks the project (funnel). |
POST /api/voice/webhook accepts, through the configured adapter, either an end-of-call report (transcript, caller number,
start/end time, provider cost) or a call-log row from the telephony provider (FR19, e.g. a call the front desk answered).
It authenticates (fails closed), parses, then for reports runs classifyCall, merges repeat callers, writes the log and cost rows,
and sends the handoff. It is idempotent on (provider, providerCallId), so provider retries and a call that shows up both as a
report and as a call-log row are recorded once. It answers 500 when an event fails so the provider retries.
With the stub adapter (header x-webhook-secret: <VOICE_WEBHOOK_SECRET>):
curl -X POST https://<your-app>/api/voice/webhook -H "content-type: application/json" -H "x-webhook-secret: $VOICE_WEBHOOK_SECRET" -d '{
"type": "end_of_call_report", "providerCallId": "demo-1", "callerPhone": "+919999900001",
"startedAt": "2026-10-09T09:11:00Z", "endedAt": "2026-10-09T09:16:00Z",
"transcript": "AI Agent: Hello. Caller: I want to redo my 3BHK in Kothrud.",
"providerCost": {"amount": 4.2, "currency": "INR"} }'
# front-desk call from the telephony log (logged, not classified, no note):
# {"type":"call_log","providerCallId":"fd-1","callerPhone":"+919999900002","startedAt":"...","answeredBy":"front_desk","durationSec":200}
# several at once: {"events":[ ... ]}
Built from Vaani's public docs (https://docs.vaanivoice.ai) on 2026-10-10. What the adapter (src/lib/voice/vaani.ts) does:
call_postprocessing event (the finished call with its transcript) and ignores every other event.GET https://api.vaanivoice.ai/api/call-history, header X-API-Key) using VAANI_API_KEY. If that lookup fails it carries on without them.https://<app>/api/voice/webhook?key=<VOICE_WEBHOOK_SECRET>.npm run vaani:prompt writes
vaani/agent-prompt.md (greeting, approved price sentence word for word, never-promise-times, the studio's rules, no rupee figures) to paste into the Vaani agent;
(2) every finished call is scanned for price-like agent sentences, flagged agent_price_leak, and Nikhil is alerted immediately.The voice layer is deliberately provider-agnostic; no vendor is hard-coded and none of the real providers' APIs were coded (you are choosing after your validation spike, and vendor payloads change, so check the provider's current docs first).
src/lib/voice/<provider>.ts exporting a VoiceAdapter (src/lib/voice/types.ts):
verify(headers, rawBody): check the vendor's signature or secret. Must return false when anything is missing.parse(body): map the vendor payload to VoiceEvent[]: end_of_call_report (transcript text or speaker turns, caller number,
startedAt, endedAt, durationSec, providerCost, answeredBy), call_log for telephony-log rows, or ignored for
event types you do not need. Throw on malformed input (the route answers 400).src/lib/voice/registry.ts (one line) and set VOICE_PROVIDER=<name>.tests/voice.test.ts using a real sample payload from the vendor's docs.Nothing else changes: classification, handoff, log and dashboard consume the normalised events. For FR19, point the provider's call
log (webhook or a scheduled export) at the same endpoint as call_log events. If the vendor runs the live conversation itself with
a custom-LLM hook, agentTurn (pure function of state + utterance) is the function to call; /api/simulator/turn shows the shape.
CAL_API_KEY, CAL_EVENT_TYPE_ID (and CAL_ATTENDEE_EMAIL, where Cal.com sends confirmations because callers rarely give an email).
With these set, /api/booking/slots and /api/booking/book talk to Cal.com (slots: API 2024-09-04; bookings: API 2026-02-25). Without them the built-in
scheduler (a table in Supabase, Mon-Sat 10am-7pm) is used. If Cal.com is down the assistant is told not to say anything is booked and a designer follows up.HUBSPOT_ACCESS_TOKEN (a private-app token with contacts read/write and notes scopes). Each qualified lead creates or updates a contact
(found by phone) and adds a note with every detail and the booked slot. Failures never affect the call log or Telegram; they are retried by the cron route..env* is git-ignored; .env.example lists names only). Real keys live in Vercel and a local .env.local.context/pricing.md is the public version (the approved sentence only). The private guide comes from the
PRICING_GUIDE_MD environment variable or a git-ignored context/pricing.private.md, and is used only by the classifier.DASHBOARD_PUBLIC=true and no DASHBOARD_PASSWORD the dashboard is a public view: no names, phone numbers, reasons or transcripts. Set a password to see everything.findPriceLeaks; a leak is replaced by the approved line.context/agent.md and config.CHANGELOG.md (decline rule, lead time, commercial minimum, assignment, answering mode, consent/DPDP, Marathi, cost ceiling) are
switches or notes, not decisions.