Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
Aangan Studio용 AI 전화 문의 에이전트, Vaani 음성, Cal.com 예약, 텔레그램·HubSpot 연동, 실시간 대시보드 제공
AI phone-enquiry agent for Aangan Studio: Vaani voice agent, Cal.com booking, Telegram + HubSpot handoff, live dashboard
비즈니스TypeScript · PLpgSQLGitHub @bhartidivya273-web온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.