M1: backend core + proposal engine

- 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>
This commit is contained in:
2026-07-11 09:45:06 +02:00
commit e855650f09
48 changed files with 4941 additions and 0 deletions

606
docs/SPEC.md Normal file
View File

@@ -0,0 +1,606 @@
# 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:
1. **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.
2. **Chat agent** — an embeddable web widget on the salon's website that does the same over text, using the same backend logic.
3. **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.
4. **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
1. **Propose, don't book.** The agent gathers intent + availability and produces a structured proposal. The owner is always the decision-maker.
2. **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.
3. **Voice is the primary product.** Chat is a secondary channel sharing the same backend.
4. **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).
5. **Short calls by design.** Target call length 60120 seconds. The agent is efficient and polite, not chatty.
6. **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 1015 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 |
| Email | 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
1. 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.
2. 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).
3. 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:
1. **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."
2. **Request received** (optional, config): "Primili smo vaš zahtjev za termin. Javićemo vam potvrdu u najkraćem roku."
3. **Confirmation**: "Potvrđen termin: {usluga}, {dan} {datum} u {vrijeme}h — {salon}."
4. **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: 23 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)
1. Greet + recording disclosure (template with salon name).
2. 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.
3. 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**.
4. 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.
5. 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 60120s; 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, 13 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.
```python
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) and `max_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_availability` probe 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_calendar` tenants** → email to owner (below).
- **`partner_api` tenants** → 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]`
- **`.ics` attachment** 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; `.ics` is 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), status `expired`, 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**
1. Salon profile: name, address, city, phone, working hours per weekday incl. breaks.
2. 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.
3. 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`); for `partner_api` — read-only view of connection status and last sync (all configuration is super-admin-only, §8.3).
4. 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.
5. Agent: voice selection (23 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).
6. Notifications: proposal email recipients; SMS template tweaks; "request received" SMS on/off.
**Review**
1. Booking requests list with statuses + same action buttons as email.
2. Call history: timestamp, caller number, duration, outcome (`human_answered / request_created / info_only / message_taken / abandoned`), transcript, audio player.
3. Chat history (same format, text only).
4. 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_minutes` per 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-BA` default), 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_id
- `workers`: tenant_id, name, sip_username, sip_secret (hashed/managed), active bool
- `phone_numbers`: tenant_id, msisdn, gateway_port, sim_status
- `calls`: tenant_id, started_at, caller_msisdn, duration_s, outcome, recording_path, transcript, agent_minutes_charged
- `chat_sessions` / `chat_messages`
- `booking_requests`: tenant_id, source (voice/chat), client_name, client_phone, service_id, slots (jsonb, ordered), home_visit + address, summary, status, action tokens, timestamps
- `messages_for_owner` (non-booking messages)
- `sms_log`, `email_log`
- `usage_counters` (per tenant per month)
- `users` (owner accounts) + `admins`
---
## 14. Phasing
### Phase 0 — Proof of Concept (before any product code)
Small scripts, ~12 days, kill the biggest risk first:
1. **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.
2. **TTS bake-off:** generate 56 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.
3. **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 `SchedulingProvider` implementations 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)
1. **TTS default voice/provider** — decided by Phase-0 PoC (Azure vs ElevenLabs; quality vs cost).
2. **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).
3. **GSM gateway model** — founder to select hardware (GoIP/Yeastar class, SMS API capability required); spec assumes SIP + HTTP SMS API.
4. **Operator terms for SIMs in gateways** — verify acceptable use with m:tel/BH Telecom/HT Eronet for inbound-only + SMS usage.
5. **Recording consent wording** — confirm exact legally appropriate phrasing for BiH (both entities) before launch.
6. **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 23 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 is `Europe/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-requests` includes an `Idempotency-Key` header (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 /availability` should 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`:
```json
{
"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:
```json
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": [ // 03, 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`:
```json
{"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.
```json
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.
```json
{"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
1. Issue an API key for Gogo; agree the base URL.
2. Implement `GET /availability` (< 800 ms) and `POST /booking-requests` (idempotent).
3. Implement the outcome webhook (or the polling endpoint) with the shared HMAC secret.
4. Optionally implement `GET /services`.
5. Joint test using Gogo's connectivity test (dry-run) from the super-admin panel.