- FastAPI skeleton, SQLAlchemy models (§13), Alembic initial migration - SchedulingProvider interface with google_calendar (free/busy read-only), partner_api (Appendix B client) and mock implementations - Proposal engine: create → provider-routed delivery → owner actions (resolve/confirm+SMS/reject) → expiry + reminders (§9) - Signed single-use action links, .ics METHOD:REQUEST attachment - Partner outcome webhook with HMAC verification + polling fallback - SmsProvider (console) with Bosnian templates (§5.5), EmailProvider (console/SMTP) - Fake partner API server in tests/ — Appendix B reference implementation - 43 tests: slot math, proposal lifecycle, action links, partner contract Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45 KiB
Gogo Telefon — Product Specification (v1.0)
Product name: Gogo Telefon Market: Bosnia and Herzegovina (architecture must support later expansion to Serbia and Kosovo) Spec status: Draft v1.4 — living document agreed in planning session; open questions in §17 Intended reader: Claude Code (implementation agent) and the founder
1. Product Overview
Gogo Telefon is a voice-first AI receptionist for small service businesses in Bosnia and Herzegovina (beauty salons, hairdressers, pedicure/manicure studios, and similar businesses with up to ~15 employees). These businesses lose bookings because nobody can answer the phone while working, and especially outside working hours.
The product:
- Voice agent answers phone calls forwarded from the salon's existing number, speaks Bosnian/Serbian/Croatian, knows the salon's services and prices, checks real availability in the salon's scheduling system (Google Calendar or the salon's own booking software via Partner API), and collects a booking request from the caller.
- Chat agent — an embeddable web widget on the salon's website that does the same over text, using the same backend logic.
- Crucially, the agent never books appointments itself. It sends a proposal (booking request) to the owner by email. The primary flow: the owner contacts the client directly, arranges the appointment themselves, and marks the request as resolved with one click (no SMS is sent by us in that case). A secondary one-click option lets the owner confirm a proposed slot and have Gogo send the confirmation SMS to the client. Gogo never writes to the owner's calendar.
- Humans first: before the agent answers, the call rings the salon staff's softphones (SIP clients on their mobile phones). The agent picks up only if nobody answers, or immediately outside working hours.
Business model: flat rate ≈ 30 KM/month per salon (target; annual prepay option ~300 KM/yr), with a fair-use limit on agent voice minutes (see §12). Payment is handled offline via B2B bank transfer — no online payments / card processing in this product.
Distribution: word of mouth. There is no public sign-up. A super-admin creates and configures every salon.
2. Key Design Principles
- Propose, don't book. The agent gathers intent + availability and produces a structured proposal. The owner is always the decision-maker.
- The owner keeps their tools. Salon keeps its phone number (conditional call forwarding) and keeps its scheduling tool — Google Calendar (often multiple color-coded calendars per service type) or its own custom booking software (Gogo integrates via the Partner API, Appendix B). Proposals arrive where the owner already works: plain email, or directly inside their booking software. The dashboard is for setup and occasional review, not daily work.
- Voice is the primary product. Chat is a secondary channel sharing the same backend.
- Own the infrastructure, minimize per-minute costs. Self-hosted pipeline in the founder's own data center from day one. Only LLM and TTS remain paid APIs (both behind swappable interfaces).
- Short calls by design. Target call length 60–120 seconds. The agent is efficient and polite, not chatty.
- Multi-country-ready foundations. Currency, language, phone formats, and locale are per-tenant configuration, not hardcoded (BiH now; Serbia/Kosovo later).
3. System Architecture
Caller (PSTN, salon's customers)
│
salon's own number (m:tel / BH Telecom / HT Eronet)
│ conditional call forwarding
│ (no answer / busy / out of hours)
▼
Our SIM number (per salon or pooled)
│
GSM Gateway (multi-SIM device,
e.g. GoIP/Yeastar class) — also used
for outbound SMS to clients
│ SIP
▼
SIP Server (Asterisk or FreeSWITCH) — founder's DC
│ dial-plan: ring group first (staff softphones,
│ N seconds, within working hours only)
│ no answer → route to agent
▼
Voice Pipeline (Pipecat, Python) — founder's DC
┌──────────────────────────────────────────────┐
│ STT: faster-whisper (large-v3) on local GPU │
│ LLM: API (Claude Haiku class) — swappable │
│ TTS: API (Azure Neural or ElevenLabs Flash) │
│ — swappable, decided by Phase-0 PoC │
│ Turn-taking / barge-in: Pipecat built-ins │
└──────────────────────────────────────────────┘
│ tool calls (HTTP)
▼
Backend API (FastAPI, Python) — founder's DC
┌──────────────┬───────────────┬──────────────┬───────────────┐
│ Scheduling │ Proposal │ SMS sender │ Tenant config │
│ providers: │ engine + │ (via GSM │ services, │
│ GCal free/ │ email (.ics) │ gateway API) │ hours, limits │
│ busy or │ or partner │ │ │
│ Partner API │ push │ │ │
└──────────────┴───────────────┴──────────────┴───────────────┘
▲
│ same tools API
Chat Agent (web widget, WebSocket)
▲
Dashboard (owner) + Super-admin panel
Deployment: everything runs in the founder's own data center. GPU node for Whisper (one RTX 4090 / L4-class GPU handles 10–15 concurrent calls). Docker Compose for MVP (no Kubernetes). PostgreSQL as the primary database.
4. Tech Stack
| Layer | Choice | Notes |
|---|---|---|
| Language | Python 3.12+ | one language across the whole repo |
| Voice orchestration | Pipecat | open source; handles turn-taking, barge-in, SIP integration |
| SIP server | Asterisk (or FreeSWITCH if Pipecat integration proves simpler) | dial-plan: ring group → agent fallback; call recording |
| STT | faster-whisper, large-v3, on local GPU | language hint: sr (covers spoken bs/sr/hr); Phase-0 PoC validates |
| LLM | Anthropic API, Haiku-class model | behind LLMProvider interface; model per-tenant configurable |
| TTS | Azure Neural TTS or ElevenLabs Flash API | behind TTSProvider interface; Phase-0 PoC decides default voice |
| Backend | FastAPI + SQLAlchemy + Alembic | REST + WebSocket (chat) |
| DB | PostgreSQL 16 | |
| Queue/scheduling | Built-in (arq or APScheduler) | proposal expiry, SMS dispatch retries |
SMTP (configurable provider) + generated .ics attachments |
one-click action links | |
| Dashboard | Server-rendered (FastAPI + Jinja2 + htmx) or lightweight React — implementer's choice, favor simplicity | Bosnian UI, Latin script for MVP |
| Chat widget | Vanilla JS embeddable snippet + WebSocket | no framework requirements on salon's site |
| Auth | Session-based; single account per salon + super-admin role | no public registration |
5. Telephony Layer
5.1 Inbound call path
- Salon activates conditional call forwarding on its own number: forward on no-answer (~15s), on busy, and (if operator supports schedules — usually not, so handled by us) out of hours. Dashboard shows per-operator activation codes (m:tel, BH Telecom, HT Eronet) generated for the salon's assigned Gogo number.
- Calls arrive at a SIM in the GSM gateway mapped to exactly one salon (MVP: 1 SIM = 1 salon; pooling with DID-style mapping is Phase 2).
- GSM gateway converts to SIP → Asterisk.
5.2 Dial-plan logic (per salon, driven by tenant config)
IF within salon working hours AND ring_group not empty AND ring_first_enabled:
ring staff softphones (strategy: ring-all or sequential; default ring-all)
timeout: configurable, default 12s
IF answered by human → bridge call, record, tag outcome = "human_answered"
ELSE → route to AI agent
ELSE (out of hours, or ring group empty/disabled):
route to AI agent immediately
5.3 Staff softphones
- Staff use off-the-shelf SIP clients (Linphone / Zoiper / Groundwire). We build nothing mobile for MVP.
- Dashboard: owner adds a worker (name) → system generates SIP credentials + QR code / config link for easy setup.
- Workers have no accounts in the product — they exist only as SIP endpoints in the ring group.
5.4 Call recording & disclosure
- All agent calls are recorded (audio) and transcribed.
- The agent's greeting must include a recording disclosure, e.g.:
"Dobar dan, dobili ste salon {naziv}. Ja sam Gogo, virtuelni asistent. Razgovor se snima. Kako vam mogu pomoći?"
- Recordings retention: default 90 days (configurable), then delete audio, keep transcript.
5.5 SMS (outbound, via GSM gateway)
- The same GSM gateway sends SMS through the salon's assigned SIM (effectively free within SIM plan).
- MVP SMS use cases:
- Missed-call SMS: caller hung up before human or agent answered → "Poštovani, dobili ste {salon}. Možete zakazati i putem poruke ili chata: {link}. Nazvaćemo vas ili nas pozovite ponovo."
- Request received (optional, config): "Primili smo vaš zahtjev za termin. Javićemo vam potvrdu u najkraćem roku."
- Confirmation: "Potvrđen termin: {usluga}, {dan} {datum} u {vrijeme}h — {salon}."
- Rejection / new proposal: "{salon}: nažalost traženi termin nije moguć. {alternativa ili molba da pozovu}."
- SMS templates are per-tenant editable (with safe defaults), always in Bosnian.
6. Voice Agent
6.1 Language & voice
- Understands and speaks Bosnian/Serbian/Croatian (treated as one spoken language; STT language hint
sr). - TTS voice: 2–3 curated voices to choose from in dashboard (final list decided by Phase-0 PoC).
- Agent persona name: "Gogo". Tone: warm, brief, professional. No small talk beyond politeness.
6.2 Conversation goals (in order)
- Greet + recording disclosure (template with salon name).
- Identify caller's need: (a) book an appointment, (b) ask a question (prices, hours, location, services), (c) cancel/reschedule → MVP: agent takes a message for the owner (full flow is Phase 2), (d) other → take a message.
- For bookings, collect: service (match against tenant service list), client name, callback phone number (confirm digit by digit if unclear; default to caller ID with verbal confirmation), home visit? (only if the service is flagged as available at home — then also collect address/area), and time preferences.
- Call the availability tool (§8.2) and offer up to 3 concrete free slots consistent with the caller's preference. The caller may also state their own preferred time — the agent checks it, and if busy, offers nearest alternatives.
- Close explicitly: "Vaš zahtjev prosljeđujem salonu — kontaktiraće vas u najkraćem roku radi potvrde termina. Hvala i prijatno!"
6.3 Prompt composition (owners are never prompt engineers)
- The agent's system prompt is built from a global prompt template owned and maintained by the super-admin (versioned; improving the template upgrades all salons at once).
- The template contains placeholders filled from structured tenant data only:
{salon_profile},{working_hours},{services_table},{notes},{price_mode},{greeting}. - The greeting is auto-generated from the template + salon name (includes the recording disclosure). Owners do not edit prompt text anywhere.
- Per-tenant prompt overrides exist but are super-admin-only (advanced escape hatch for unusual salons).
- Owner-entered "notes" are plain informational fields injected as data, not free-form instructions to the model; the template instructs the model to treat them as facts about the salon.
6.4 Hard rules (system prompt requirements)
- Never state a booking as confirmed. Always "zahtjev" / "prijedlog" until the owner approves.
- Never invent services, prices, or free slots — only from tenant data and availability tool results.
- Price answering mode per tenant config: exact prices / ranges only / "cijene na upit".
- Include per-tenant custom notes in context (e.g., "ne primamo djecu ispod 7 godina", "parking iza zgrade").
- Target call duration 60–120s; the agent steers back to the goal politely if the caller drifts.
- If the caller is abusive or the agent cannot understand after 2 clarification attempts → apologize, promise a callback, end call, log a message for the owner.
- Escalation to human mid-call: Phase 2 (see §14).
6.5 Tools exposed to the agent (LLM function-calling)
Identical for voice and chat:
| Tool | Purpose |
|---|---|
get_salon_info() |
working hours, address, services with duration/price/notes, custom notes |
check_availability(service_id, date_range, preference) |
returns free slots via the tenant's SchedulingProvider (§8): Google Calendar free/busy computation, or the partner's GET /availability; buffers applied for GCal tenants, partner tenants return ready slots |
submit_booking_request(...) |
creates the proposal: service, client name, phone, 1–3 slots (ordered by client preference), home-visit flag + address, free-text summary of the request/problem |
take_message(text, client_name, phone) |
for non-booking intents; emailed to owner |
7. Chat Agent (web widget)
- Embeddable JS snippet (one
<script>tag + tenant public key) rendering a chat bubble. - Same system prompt, same tools, same proposal flow — text instead of voice.
- Collects client's phone number in chat (required for confirmation SMS).
- Rate-limited per IP; simple anti-spam (honeypot + optional Turnstile).
- Chat transcripts appear in the dashboard alongside call history.
- No client accounts; ephemeral session, resumable via localStorage token for 24h.
8. Scheduling Providers (pluggable — core MVP abstraction)
Availability lookup and proposal delivery are abstracted behind a SchedulingProvider interface, selected and configured per tenant by the super-admin. MVP ships two implementations. Adding a third provider must require no changes to the agent, tools, or proposal engine.
class SchedulingProvider(Protocol):
def get_services(self) -> list[Service] | None # optional catalog sync
def get_availability(self, service_id, date_from, date_to) -> list[Slot]
def deliver_request(self, booking_request) -> DeliveryResult
# provider signals request outcome back via callback/webhook or is
# closed manually by the owner (email actions / dashboard)
8.1 Provider A — Google Calendar (google_calendar)
- OAuth 2.0 (Google), initiated from the dashboard by the owner (or super-admin during onboarding, on the owner's device/account).
- Read (free/busy) scope only. Gogo never requests calendar write access and never creates/modifies events.
- A salon maps each service (or service group) to one calendar — this mirrors the common real-world pattern of separate color-coded calendars per service type. Default: single primary calendar for everything.
- Availability logic: free/busy computed against the mapped calendar; slot generation = working hours minus busy blocks, quantized to service duration (+ optional per-service buffer minutes); tenant buffers
min_notice_hours(default 2) andmax_days_ahead(default 14). - Proposal delivery: email to owner with action links +
.ics(§9). - Sync without write access: the owner enters appointments into their own calendar (directly or via the
.ics); free/busy reads then automatically reflect them in the slots the agent offers. If the owner uses the "confirm + SMS" action, the backend re-checks free/busy first and warns if the slot has since been taken.
8.2 Provider B — Partner API (partner_api)
For salons that run their own custom booking software (the first two customers are in this category). Their software is the source of truth for both availability and requests:
- Availability: Gogo calls the partner's HTTP endpoint for free slots — no slot computation on our side, the partner returns ready-to-offer slots.
- Proposal delivery: Gogo pushes the booking request into the partner's software via HTTP; salon staff handle it in the tool they already use. Email to the owner is optional per tenant (default off for partner-API tenants) — the partner system is the inbox.
- Request outcome: the partner system calls our webhook when staff resolve/confirm/reject the request, which drives the client SMS and closes the request in Gogo. If no webhook is feasible, super-admin can enable polling mode as fallback.
- Service catalog: optionally synced from the partner (
GET /services) so the owner maintains services in one place; otherwise entered in the Gogo dashboard as usual. - The exact HTTP contract the partner's IT team must implement is specified in Appendix B — it is deliberately minimal (2 required endpoints + 1 webhook).
8.3 Super-admin provider configuration (per tenant)
- Provider type:
google_calendar|partner_api. - For
partner_api: base URL, API key (ours → theirs), webhook shared secret (theirs → ours), catalog sync on/off, email-to-owner on/off, polling fallback on/off. - Connectivity test button: runs a live
get_availabilityprobe and a no-op request delivery (dry-run flag, see Appendix B) and shows the raw response — essential for debugging partner integrations during onboarding.
9. Proposal Engine (the core flow)
9.1 Creation
submit_booking_request creates a BookingRequest with status pending and delivers it through the tenant's scheduling provider (§8):
google_calendartenants → email to owner (below).partner_apitenants → HTTP push into the partner's software (Appendix B); email optional per tenant config. Outcome arrives via the partner's webhook (or polling fallback) and triggers the same state transitions and client SMS as the email actions do.
Email to owner (one or more configured addresses):
- Subject:
Novi zahtjev za termin — {Usluga} — {Ime} ({dan/i}) - Body: client name, phone, service, requested slots (ordered), home-visit info if any, short conversation summary, link to full transcript in dashboard.
- Action buttons (signed, single-use links to backend, no login required):
[Riješeno — kontaktirao sam klijenta](primary)[Potvrdi {slot 1} + pošalji SMS] [Potvrdi {slot 2} + pošalji SMS] ...(secondary convenience)[Odbij] .icsattachment for the first-choice slot (METHOD:REQUEST) so Gmail offers "Add to calendar"; the owner can open, move, and rearrange the event freely before saving to their own calendar. Buttons remain the mechanism that closes the request in Gogo;.icsis convenience for calendar entry.
Regardless of provider: optional "request received" SMS to the client (per tenant config).
9.2 Owner actions
- Riješeno (primary): owner has contacted the client directly and arranged everything themselves → status
resolved_by_owner; Gogo sends no SMS; request is closed. The owner adds the appointment to their own calendar (manually or via the.ics), which free/busy reads then reflect automatically. - Confirm slot N + SMS (secondary): for owners who prefer one click over calling back → backend re-checks free/busy (warns if slot taken), status
confirmed, confirmation SMS to client. No calendar write — the owner still enters the event themselves. - Reject → status
rejected; SMS to client (template, editable before send via a minimal web form on the action link). - Expiry: if no owner action within
proposal_ttl_hours(default 24, configurable), statusexpired, client gets an apology SMS asking to call again. Reminder email to owner at 50% of TTL.
9.3 States
pending → resolved_by_owner | confirmed | rejected | expired
All transitions timestamped and visible in dashboard.
10. Dashboard (owner) — MVP scope
Single account per salon (username/email + password, created by super-admin). Bosnian UI, Latin script.
Settings
- Salon profile: name, address, city, phone, working hours per weekday incl. breaks.
- Services: name, duration (min), price or price range, "home visit available" flag, note for the agent. CRUD table plus "Zalijepi cjenovnik" import: owner pastes their price list as free text (from website, Word, Facebook, anywhere) → LLM parses it into a structured services table → owner reviews/edits durations and confirms. Same paste-to-parse approach for working hours.
- Scheduling (contents depend on tenant's provider): for
google_calendar— connect Google account (free/busy read only), map services → calendars, buffers (min_notice_hours,max_days_ahead); forpartner_api— read-only view of connection status and last sync (all configuration is super-admin-only, §8.3). - Telephony: assigned Gogo number; per-operator forwarding activation/deactivation codes; ring group management (add worker → name + generated SIP credentials + QR); ring timeout seconds; ring-first on/off.
- Agent: voice selection (2–3 curated options), price-answering mode (exact / ranges / on request), plain informational notes fields (e.g. "parking iza zgrade"); chat widget embed snippet. No prompt or greeting editing — the greeting and all agent behavior come from the super-admin-owned template (§6.3).
- Notifications: proposal email recipients; SMS template tweaks; "request received" SMS on/off.
Review
- Booking requests list with statuses + same action buttons as email.
- Call history: timestamp, caller number, duration, outcome (
human_answered / request_created / info_only / message_taken / abandoned), transcript, audio player. - Chat history (same format, text only).
- Usage: agent minutes this month vs. plan limit (simple progress bar).
Explicitly out of MVP dashboard: statistics/analytics, client CRM, multi-user roles, reminders configuration.
11. Super-admin panel (internal)
- Create/edit/disable salons (tenant config incl. plan and minute limit).
- Prompt template management: edit the global agent prompt template (versioned, with rollback); per-tenant prompt overrides; test playground (run a text conversation against a tenant's composed prompt before publishing).
- Assign SIM/number to salon; see GSM gateway port mapping.
- Impersonate ("open dashboard as salon") for support and hands-on onboarding — expected primary onboarding path: super-admin sets everything up for the owner (word-of-mouth sales, "mi vam sve podesimo za 15 minuta").
- Global usage overview: minutes, SMS count, per-salon costs (LLM+TTS estimated), proposals funnel.
- System health: GPU/STT status, SIP registrations, gateway status, email/SMS delivery errors.
12. Plans, metering, limits
- Metering unit: agent voice minutes (human-answered ring-group time does NOT count; chat does not count toward voice minutes).
- MVP plan (single plan): "Gogo Start" — 30 KM/mj (annual option ~300 KM), includes fair-use voice minutes:
included_minutesper tenant, default 300 min/month (tunable per tenant by super-admin). - Soft-limit behavior: at 80% → email to owner + banner in dashboard; at 100% → agent still answers (never silently drop customer calls) but super-admin is alerted for an upsell/limit conversation. Hard cutoff is a config flag, default off.
- Billing itself is offline (bank transfer); the product only tracks plan, period, and usage. No invoicing module in MVP (super-admin marks "paid until" date; dashboard shows it).
13. Data model (core tables, indicative)
tenants(salon): profile, locale (bs-Latn-BAdefault), currency (BAM), plan, included_minutes, paid_until, status, scheduling_provider (google_calendar|partner_api)provider_configs: tenant_id, provider type, config jsonb (partner base URL, encrypted API key, webhook secret, sync/polling/email flags)services: tenant_id, name, duration_min, price_min/price_max, home_visit bool, agent_note, calendar_id (FK to calendar mapping)calendar_connections: tenant_id, google account, scopes, token data (encrypted)calendar_mappings: service_id → google_calendar_idworkers: tenant_id, name, sip_username, sip_secret (hashed/managed), active boolphone_numbers: tenant_id, msisdn, gateway_port, sim_statuscalls: tenant_id, started_at, caller_msisdn, duration_s, outcome, recording_path, transcript, agent_minutes_chargedchat_sessions/chat_messagesbooking_requests: tenant_id, source (voice/chat), client_name, client_phone, service_id, slots (jsonb, ordered), home_visit + address, summary, status, action tokens, timestampsmessages_for_owner(non-booking messages)sms_log,email_logusage_counters(per tenant per month)users(owner accounts) +admins
14. Phasing
Phase 0 — Proof of Concept (before any product code)
Small scripts, ~1–2 days, kill the biggest risk first:
- STT test: transcribe recorded local speech samples via faster-whisper large-v3 (language
sr): service names ("trajna", "pramenovi", "gel nokti", "depilacija"), personal names, and phone numbers spoken aloud. Measure practical accuracy. - TTS bake-off: generate 5–6 typical agent sentences with Azure Neural (sr/hr/bs voices) and ElevenLabs (Flash v2.5 hr voice; v3-conversational sr/bs if latency acceptable). Founder listens, picks default voice(s); record latency and per-character cost.
- End-to-end latency smoke test: one Pipecat demo call through SIP (can be a softphone→Asterisk→Pipecat loop, no GSM needed yet) measuring mouth-to-ear response time. Target: < 1.5 s response latency.
Gate: if STT on phone numbers/names is unusable or latency > 2.5 s, stop and rethink providers before building.
Phase 1 — MVP (everything in this spec except items below)
Core call path (forwarding → GSM → SIP → ring group → agent), voice agent with tools, chat widget, two scheduling providers (Google Calendar free/busy + Partner API per Appendix B), proposal engine with provider-routed delivery (email+.ics with "Riješeno" as primary, or push into partner software), SMS (missed-call, received, confirmation, rejection), owner dashboard, super-admin panel, metering.
Phase 2 (explicitly out of MVP, design for it)
- Mid-call transfer agent → human (retry ring group on request/failure)
- SMS appointment reminders (day before)
- Cancel/reschedule full flow through the agent
- Inbound SMS handling ("DA" confirmations)
- Owner statistics ("calls saved", out-of-hours volume) as retention/upsell tool
- Additional
SchedulingProviderimplementations beyond the two MVP providers (Google Calendar, Partner API) — e.g. adapters for specific commercial booking tools, built on demand - Viber/WhatsApp channels
- SIM pooling (many salons per SIM via signaling), multi-user accounts, Cyrillic UI toggle, Serbia/Kosovo locales
15. Non-functional requirements
- Latency: agent voice response < 1.5 s target, < 2.5 s worst case.
- Concurrency: MVP sized for 10 concurrent calls (one GPU); architecture must scale by adding GPU workers.
- Privacy/GDPR-alignment: recording disclosure in greeting; audio retention 90 days default; transcripts retained; client phone numbers used only for booking communication; per-tenant data isolation; encrypted OAuth tokens at rest; right-to-delete via super-admin.
- Reliability: if the voice pipeline is down, Asterisk fallback: play a recorded message ("...pozovite kasnije ili posjetite {web}") and send the missed-call SMS. All owner-facing actions (email links) must be idempotent.
- Language: all client- and owner-facing text in Bosnian (Latin). All code, comments, and docs in English.
- Observability: structured logs, per-call trace (audio, transcript, tool calls, latency breakdown), basic health endpoints.
16. Implementation Plan for Claude Code
Golden rule: everything that can run without hardware is built AND tested first. Hardware-dependent code (GSM gateway, SIM, real operator forwarding) is written to completion but its live testing is explicitly deferred to a joint session with the founder. Milestones are ordered so each one is demonstrable on a laptop/DC with no telephony hardware.
M0 — PoC scripts (Phase 0, §14)
Standalone scripts, no product code: STT accuracy test (needs sample audio recordings from the founder), TTS bake-off, Pipecat latency smoke test using a desktop softphone registered directly to Asterisk over the network (no GSM needed). Deliverable: short results report + chosen default TTS voice. (The latency test needs Asterisk running, which is software-only — include its Docker setup here.)
M1 — Backend core + proposal engine (pure software)
FastAPI skeleton, DB schema/migrations, tenants/services/working-hours, SchedulingProvider interface with three implementations: google_calendar, partner_api, and mock (in-memory, for tests and demos). Proposal engine end-to-end: create request → email with action links + .ics → owner actions → state transitions. SMS sending behind an SmsProvider interface with a console/log implementation (real GSM-gateway implementation written in M5).
Tests: unit + integration (pytest); full proposal lifecycle against mock provider; email rendering snapshots; a fake partner API server (small FastAPI app in tests/) that implements Appendix B — used to test the partner_api provider including webhook signatures, idempotency replay, and polling fallback. This fake server doubles as a reference implementation to hand to partner IT teams.
M2 — Agent logic in text mode
Prompt composition from template + tenant data, tool definitions, conversation loop runnable as text chat in the terminal/playground (no voice). Implement the Appendix A scenarios as automated acceptance tests (assert tool calls, no "confirmed" language, ≤3 slots, closing line).
Tests: all Appendix A scenarios green against mock and fake-partner providers.
M3 — Chat widget + dashboard + super-admin panel
Embeddable widget (WebSocket) reusing M2 agent, owner dashboard (all §10), super-admin panel (§11) incl. provider config, connectivity test (dry-run against fake partner server), prompt template editor + playground, "Zalijepi cjenovnik" paste-to-parse. Tests: widget e2e (Playwright or similar) booking flow → request appears in dashboard and email.
M4 — Voice pipeline (software-only telephony)
Asterisk + Pipecat + STT/TTS/LLM wired to the M2 agent tools. Tested entirely with desktop/mobile softphones registering to Asterisk over IP — this exercises the full real call path (SIP, audio, barge-in, recording, transcripts, metering) except the GSM leg. Ring-group-first dial-plan logic included and tested with two softphones (one as "worker", one as "caller"). Tests: scripted call sessions; latency budget assertions; recording + transcript stored; minutes metered.
M5 — Hardware integration layer (WRITE ONLY — live testing deferred)
GSM gateway integration: SIP trunk config templates for the gateway, SmsProvider implementation for the gateway's HTTP SMS API, per-salon SIM/port mapping in super-admin, operator forwarding-code generator (m:tel / BH Telecom / HT Eronet). Everything written, code-reviewed, and covered by unit tests against a mocked gateway API; a HARDWARE_TESTING.md runbook is produced with the exact step-by-step live test plan (gateway setup, SIM insertion, forwarding activation, end-to-end call, SMS delivery) to execute together with the founder later.
M6 — Hardening
Fallback message when pipeline is down (§15), retention jobs, usage limit notifications, monitoring/health endpoints, backup strategy, deploy docs for the founder's DC (Docker Compose).
Definition of done for the software phase: a demo where a softphone call books a request end-to-end (agent conversation → availability from fake partner API → request pushed → webhook outcome → console-SMS logged), and the same flow via the chat widget with a Google-Calendar mock tenant — all without any hardware present.
17. Open questions (to resolve, not blockers for scaffolding)
- TTS default voice/provider — decided by Phase-0 PoC (Azure vs ElevenLabs; quality vs cost).
- Partner API adoption — Appendix B is our proposed contract for the first two customers' custom booking software; awaiting their IT teams' review (possible adjustments to field names/flows, but the shape — availability pull + request push + outcome webhook — is fixed).
- GSM gateway model — founder to select hardware (GoIP/Yeastar class, SMS API capability required); spec assumes SIP + HTTP SMS API.
- Operator terms for SIMs in gateways — verify acceptable use with m:tel/BH Telecom/HT Eronet for inbound-only + SMS usage.
- Recording consent wording — confirm exact legally appropriate phrasing for BiH (both entities) before launch.
- Exact plan limits — 300 included minutes is a starting assumption; revisit after first pilot salons.
Appendix A — Golden conversation examples (voice agent)
These dialogues are the behavioral gold standard for the agent. They feed directly into the super-admin prompt template as few-shot examples and as acceptance criteria for testing. Editable by super-admin; the hard rules in §6.4 always take precedence. Fictional salon: "Salon Merima", Banja Luka.
Style rules visible in all examples: short sentences; one question at a time; numbers and times spoken naturally ("u dva i trideset", not "14:30h"); nikad "bukirano/potvrđeno", uvijek "zahtjev/prijedlog"; agent never fills silence with chatter.
A.1 Happy path — booking with a preferred time
Gogo: Dobar dan, dobili ste salon Merima. Ja sam Gogo, virtuelni asistent — razgovor se snima. Kako vam mogu pomoći? Klijent: Dobar dan, htjela bih zakazati šišanje i feniranje. Gogo: Može. Kada bi vam odgovaralo? Klijent: Nešto u srijedu poslijepodne ako ima. Gogo: (tool: check_availability) U srijedu poslijepodne slobodno je u dva i trideset ili u pet. Šta vam više odgovara? Klijent: U pet. Gogo: Važi. Na koje ime da zavedem zahtjev? Klijent: Amra Hodžić. Gogo: Hvala, Amra. Da li je broj s kojeg zovete pravi broj za kontakt? Klijent: Jeste. Gogo: Odlično. Prosljeđujem salonu zahtjev: šišanje i feniranje, srijeda u pet. Kontaktiraće vas u najkraćem roku radi potvrde. Hvala na pozivu i prijatno!
Notes: agent offers max 2–3 slots, confirms caller-ID instead of asking to dictate the number, closes with the standard non-committal line. Target duration ~60s.
A.2 Price question only (no booking)
Gogo: Dobar dan, dobili ste salon Merima. Ja sam Gogo, virtuelni asistent — razgovor se snima. Kako vam mogu pomoći? Klijent: Koliko košta farbanje kose? Gogo: Farbanje cijele dužine je od šezdeset do devedeset maraka, zavisno od dužine kose. Želite li da zakažemo termin? Klijent: Ne, samo sam htjela cijenu. Hvala. Gogo: Nema na čemu. Prijatno!
Notes: answer from {services_table} respecting {price_mode} (here: ranges). One gentle booking offer, never a second. Call ends in ~20s — that's success, not failure.
A.3 Client unsure what they need
Klijent: Uništila mi se kosa od peglanja, ne znam šta bi mi pomoglo... Gogo: Razumijem. Salon radi keratinski tretman i dubinsku njegu — obje usluge su za oštećenu kosu. Ne mogu vam reći šta je bolje za vašu kosu, to će procijeniti frizerka. Mogu proslijediti zahtjev s napomenom da je kosa oštećena od peglanja, pa da vas salon nazove i posavjetuje. Može? Klijent: Može, super. Gogo: Kada vam uglavnom odgovara da dođete, radnim danom ili vikendom? Klijent: Radnim danom poslije četiri. Gogo: Zabilježeno. Vaše ime, molim? ...
Notes: agent NEVER gives beauty/treatment advice — it lists relevant services and routes the decision to the professional. The problem description goes into the request summary, which is exactly the "information about what the client wants or what problem they have" from the product definition.
A.4 Cancellation (MVP: message for the owner)
Klijent: Trebala bih otkazati termin za sutra. Gogo: Naravno. Recite mi vaše ime i u koliko sati je termin? Klijent: Selma Kovač, sutra u deset. Gogo: Hvala, Selma. Prosljeđujem salonu poruku da otkazujete sutrašnji termin u deset. Ako budete željeli novi termin, slobodno nazovite. Prijatno!
Notes: MVP uses take_message — the agent does not touch the calendar and does not promise the slot is freed. Full reschedule flow is Phase 2.
A.5 Out-of-hours call
Gogo: Dobar dan, dobili ste salon Merima. Ja sam Gogo, virtuelni asistent — razgovor se snima. Salon je trenutno zatvoren — radno vrijeme je od devet do osamnaest radnim danom i subotom do četrnaest. Mogu li vam pomoći da zakažete termin? Klijent: Da, može manikir u petak. Gogo: (tool: check_availability) U petak je slobodno u jedanaest i u petnaest i trideset...
Notes: out-of-hours greeting variant states working hours proactively, then proceeds with the normal booking flow — this is the core product moment ("agent radi kad salon ne radi").
A.6 Difficult call — unclear speech, then graceful exit
Klijent: (nerazgovijetno / buka) Gogo: Izvinite, nisam vas dobro razumio. Možete li ponoviti? Klijent: (i dalje nerazumljivo) Gogo: Žao mi je, veza je loša i ne razumijem vas. Ostaviću salonu poruku da vas nazove na ovaj broj. Hvala na pozivu i prijatno! (tool: take_message — "nerazumljiv poziv, nazvati klijenta")
Notes: exactly two clarification attempts (per §6.4), then a promise of a callback and a clean, polite exit. The owner gets the caller's number either way — no call is ever fully "lost".
A.7 Home-visit service
Klijent: Da li vi dolazite kući za pedikir? Majka mi je nepokretna. Gogo: Da, salon radi pedikir na kućnoj adresi. U kojem ste dijelu grada? Klijent: Lauš. Gogo: Hvala. Kada bi vam odgovaralo da ekipa dođe? Klijent: Bilo koje prijepodne ove sedmice. Gogo: Zabilježeno — pedikir kod kuće, naselje Lauš, prijepodne ove sedmice. Vaše ime i da li je ovaj broj za kontakt? ...
Notes: only offered for services flagged home_visit; collects area/address and time window rather than exact slot (travel logistics are the owner's call). The request is marked home_visit=true.
Acceptance testing: each example above becomes an automated test scenario (text-mode conversation against the composed prompt, asserting: correct tool calls, no "confirmed" language, ≤3 slots offered, closing line present). The super-admin playground (§11) runs the same scenarios on demand.
Appendix B — Partner Scheduling API (spec for the partner's IT team)
This is the HTTP contract a salon's custom booking software must implement so Gogo Telefon can (1) read free slots and (2) push booking requests into it. It is deliberately minimal: two endpoints on the partner side, one webhook on ours, plus one optional endpoint. This document can be handed to the partner's IT team as-is.
B.0 General conventions
- Transport: HTTPS only. Partner exposes a base URL, e.g.
https://booking.salon-example.ba/gogo/v1. - Auth (Gogo → partner): every request carries
Authorization: Bearer <api_key>(key issued by the partner, stored by Gogo super-admin). - Auth (partner → Gogo webhook): HMAC-SHA256 signature of the raw body using a shared secret, sent as
X-Gogo-Signature: sha256=<hex>. - Format: JSON (UTF-8). Timestamps: ISO 8601 with timezone offset (e.g.
2026-07-15T14:30:00+02:00); salon timezone isEurope/Sarajevo. - IDs: all Gogo-issued IDs are opaque strings (UUID). Partner returns its own
partner_request_id; both sides store the pair. - Idempotency:
POST /booking-requestsincludes anIdempotency-Keyheader (the Gogo request UUID). Replays with the same key MUST return the original result, not create a duplicate. - Errors: non-2xx with body
{"error": {"code": "string", "message": "human readable"}}. Gogo retries 5xx with exponential backoff (3 attempts); 4xx are not retried and are surfaced to super-admin monitoring. - Latency requirement:
GET /availabilityshould respond in < 800 ms — the voice agent is waiting on it mid-conversation. Everything else may be slower.
B.1 GET /availability (required)
Gogo asks for ready-to-offer free slots for one service in a date range. The partner computes availability (working hours, staff, existing bookings — all their logic); Gogo does no slot math for partner tenants.
Request:
GET /availability?service_id=SRV-12&from=2026-07-15&to=2026-07-22&home_visit=false
Authorization: Bearer <api_key>
Response 200:
{
"service_id": "SRV-12",
"slots": [
{"start": "2026-07-15T14:30:00+02:00", "end": "2026-07-15T15:15:00+02:00",
"staff_id": "EMP-3", "staff_name": "Merima"},
{"start": "2026-07-15T17:00:00+02:00", "end": "2026-07-15T17:45:00+02:00"}
]
}
Notes: staff_id/staff_name optional (if present, the agent may say "kod Merime"). Empty slots array is a valid answer. Return at most ~20 slots; Gogo picks up to 3 to offer.
B.2 POST /booking-requests (required)
Gogo pushes a new booking request into the partner's software, where salon staff see and handle it in their usual workflow.
Request:
POST /booking-requests
Authorization: Bearer <api_key>
Idempotency-Key: 7c9e6679-7425-40de-963d-...
{
"gogo_request_id": "7c9e6679-7425-40de-963d-...",
"created_at": "2026-07-14T19:42:10+02:00",
"source": "voice", // "voice" | "chat"
"client": {"name": "Amra Hodžić", "phone": "+38765123456"},
"service_id": "SRV-12", // null if the client couldn't be matched to a service
"service_name_raw": "šišanje i feniranje",
"requested_slots": [ // 0–3, ordered by client preference
{"start": "2026-07-15T17:00:00+02:00", "end": "2026-07-15T17:45:00+02:00"}
],
"time_preference_text": "srijeda poslijepodne", // free text when no exact slot
"home_visit": false,
"address": null, // filled when home_visit = true
"summary": "Klijentica želi šišanje i feniranje, preferira srijedu poslijepodne.",
"transcript_url": "https://app.gogotelefon.ba/t/abc123", // auth-protected
"dry_run": false // true = validate & discard (connectivity test)
}
Response 201:
{"partner_request_id": "REQ-8841", "status": "received"}
B.3 Webhook: request outcome (partner → Gogo, required)
When staff resolve the request in the partner software, the partner calls Gogo's webhook. This is what triggers the client SMS and closes the request on our side.
POST https://app.gogotelefon.ba/webhooks/partner/{tenant_id}
X-Gogo-Signature: sha256=<hmac of body>
{
"gogo_request_id": "7c9e6679-...",
"partner_request_id": "REQ-8841",
"outcome": "confirmed", // "confirmed" | "resolved" | "rejected"
"confirmed_slot": {"start": "2026-07-15T17:00:00+02:00",
"end": "2026-07-15T17:45:00+02:00"}, // required when "confirmed"
"note": "optional free text"
}
Semantics (mirrors §9.2): confirmed → Gogo sends the confirmation SMS to the client; resolved → staff contacted the client themselves, Gogo sends nothing, request closed; rejected → Gogo sends the rejection SMS. Response: 200 {"ok": true}. Gogo's webhook is idempotent per (gogo_request_id, outcome).
Polling fallback (if the partner cannot call webhooks): partner instead implements GET /booking-requests/{partner_request_id} returning {"status": "pending" | "confirmed" | "resolved" | "rejected", "confirmed_slot": ...}; Gogo polls every 5 minutes until terminal state or proposal TTL.
B.4 GET /services (optional, recommended)
Lets Gogo sync the service catalog so the salon maintains it in one place.
{"services": [
{"id": "SRV-12", "name": "Šišanje i feniranje", "duration_min": 45,
"price_min": 25, "price_max": 35, "currency": "BAM",
"home_visit": false, "active": true}
]}
Synced nightly + on-demand from super-admin panel. If absent, services are maintained manually in the Gogo dashboard and service_id mapping is configured there.
B.5 Partner checklist
- Issue an API key for Gogo; agree the base URL.
- Implement
GET /availability(< 800 ms) andPOST /booking-requests(idempotent). - Implement the outcome webhook (or the polling endpoint) with the shared HMAC secret.
- Optionally implement
GET /services. - Joint test using Gogo's connectivity test (dry-run) from the super-admin panel.