Skip to content

ziadwaelai/standy

Repository files navigation

Standy

Standy — Daily standups. AI that calls.

Voice AI that runs your team's daily standup. Invite Standy to a calendar event, walk away. Each guest gets a 3-minute 1:1 voice call. The organizer gets the digest emailed when everyone's done.

How it works · Quickstart · Configuration · Architecture · Roadmap

Python 3.11+ LiveKit Agents Gemini Live MIT License MVP

If this saves your team 75 minutes a week, drop a star — that's how I keep building it in the open.


Why?

Daily standups cost a team 15 min × 5 days × N people every week. Most of it is everyone listening to status they don't need. The manager wants the summary, not the meeting.

This replaces the meeting with a voice call per person, conducted by an AI agent. Same three questions (yesterday / today / blockers), same accountability, none of the Zoom Tetris.

What makes it different

  • Zero new tool to install. You schedule it the way you schedule any other meeting — in Google Calendar, Outlook, or Apple Calendar. The agent receives the .ics invite, reads it, and runs the standup at the start time.
  • No OAuth dance. No "Connect your Google account" step. The agent has its own email address; you just add it as a guest.
  • Real voice, not chat. Built on LiveKit Agents + Google Gemini Live for sub-second speech-to-speech latency.
  • Bilingual. Each member can be set to English or Arabic; the agent speaks their language end-to-end.
  • Self-hosted, two containers. Single Docker Compose file, SQLite on a bind-mounted volume, no managed services required.

How it works

   ┌─────────────────────────┐
   │ Manager opens Calendar  │
   │ → New event, 9 AM       │
   │ → Adds team + agent     │
   │   as a guest            │
   └────────────┬────────────┘
                │  (calendar invite by email)
                ▼
   ┌─────────────────────────┐         ┌──────────────────┐
   │ Agent's mailbox (IMAP)  │ ──────► │ Inbox poller     │
   │ standup@yourdomain      │         │ parses .ics      │
   └─────────────────────────┘         └────────┬─────────┘
                                                ▼
                                       ┌──────────────────┐
                                       │ CalendarEvent    │
                                       │ row in SQLite    │
                                       └────────┬─────────┘
                                                │ at start_at, fire one
                                                │ standup per attendee
                                                ▼
   ┌─────────────────────────┐         ┌──────────────────┐
   │ Each member opens       │ ◄────── │ Per-guest        │
   │ their join email,       │  email  │ join link        │
   │ clicks → 1:1 voice call │         │ (LiveKit room)   │
   └────────────┬────────────┘         └──────────────────┘
                │
                ▼
   ┌─────────────────────────┐
   │ Agent asks 3 questions  │
   │yesterday/today/blockers,|
   │ digs into vague answers │
   │ then says goodbye       │
   └────────────┬────────────┘
                │
                │  (after every guest's call wraps)
                ▼
   ┌─────────────────────────┐
   │ Digest email to the     │
   │ event organizer         │
   └─────────────────────────┘

The whole flow is automatic. The dashboard exists for observation (who's done their standup today, what did they say) and light tuning (tone, language, custom instructions for the agent).


Quickstart

You'll need:

  • Docker + Docker Compose
  • A LiveKit Cloud project (free tier)
  • A Google AI Studio API key with Gemini Live access (here)
  • A Gmail account with 2-factor auth on (we'll use it for both SMTP outbound and IMAP inbound)
git clone https://github.com/ziadwaelai/standy.git
cd standy
cp .env.example .env

Fill in .env — at minimum:

LIVEKIT_URL=wss://your-project.livekit.cloud
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...
GOOGLE_API_KEY=...

SMTP_USER=standup@yourdomain.com
SMTP_PASSWORD=<gmail-app-password>      # https://myaccount.google.com/apppasswords
IMAP_USER=standup@yourdomain.com
IMAP_PASSWORD=<gmail-app-password>      # same one
INBOUND_EMAIL=standup@yourdomain.com    # what managers add as a guest
PUBLIC_APP_URL=http://localhost:8000

Then:

docker compose up --build -d
docker compose logs -f api

Open http://localhost:8000. The dashboard shows the agent's email address with a copy button — paste that as a guest in any calendar event with your team, and watch the logs:

inbox: new event uid=... attendees=4 title='Daily standup'
calendar tick: dispatching 3 standup(s)
event digest sent: uid=... -> manager@yourcompany.com (3 items)

That's the full loop.


Configuration

Required env vars

Variable What it's for
LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET LiveKit Cloud project. Free tier is enough for a small team.
LIVEKIT_AGENT_NAME The worker's registered name. Default standup-agent — keep unless you run multiple agents.
GOOGLE_API_KEY Gemini API key. Must be on a project with Live access.
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD Outbound: join links + digest emails. Gmail App Password works.
IMAP_HOST, IMAP_USER, IMAP_PASSWORD Inbound: where calendar invites land. Usually the same Gmail account as SMTP.
INBOUND_EMAIL The address managers add as a guest. Defaults to IMAP_USER if blank.
PUBLIC_APP_URL Public URL of the dashboard. Must be reachable from the email recipients' browsers — for hosted deployments, set this to your https://... domain.

Optional env vars

Variable Default Effect
INBOX_POLL_SECONDS 30 How often the IMAP poller checks for new invites.
IMAP_MAILBOX INBOX Mailbox to scan.
DATABASE_URL sqlite:////app/data/standup.db Switch to postgresql+psycopg://... for Postgres.
API_PORT 8000 Host port mapped to the api container.

Per-team settings (in the dashboard)

Field Default Effect
Team name My Team Used in email subject lines and the agent's opening line.
Tone friendly-concise Free-form prompt input. Try formal, playful, etc.
Follow-ups / topic 2 Cap on how many follow-up questions the agent will ask per topic.
Max session (sec) 600 Hard cap on call length — agent ends the call if it overruns.
Custom instructions empty Extra context for the agent. Example: "Ask specifically about the OAuth migration this week."

Per-member settings (in the roster row)

  • Active toggle — inactive members are skipped when their email shows up in an invite.
  • LanguageEnglish or العربية. The agent speaks the chosen language end-to-end (greeting, follow-ups, summary).
  • Members are auto-created the first time their email appears in a calendar invite. You don't add them by hand.

Architecture

Two long-running processes, one shared SQLite database:

docker-compose.yml
├─ api    → FastAPI + Jinja2 dashboard, APScheduler, IMAP poller, digest mailer
└─ agent  → LiveKit Agents worker (registers with LiveKit Cloud, waits for dispatches)
src/standup/
├─ core/           pure domain logic (no FastAPI, no LiveKit)
│  ├─ models.py    Member · AgentConfig · StandupSession · CalendarEvent
│  ├─ db.py        SQLite engine, WAL pragmas, additive migrations + data backfills
│  ├─ ics.py       RFC 5545 parser (RRULE expansion via recurring_ical_events)
│  ├─ inbox.py     IMAP poller, UID-cursor based (immune to \Seen flips)
│  ├─ email_util.py SMTP via aiosmtplib + Jinja2 email templates
│  ├─ livekit_util.py token mint + agent dispatch
│  ├─ time_util.py tz coercion + duration humanizers
│  └─ config.py    pydantic-settings
│
├─ api/            HTTP surface
│  ├─ main.py      FastAPI app, lifespan, static mount
│  ├─ routes.py    dashboard, member CRUD, /join/{id}, history
│  └─ scheduler.py APScheduler jobs: inbox poll, calendar tick, event completion
│
├─ agent/          LiveKit worker (separate process)
│  ├─ main.py      job entrypoint — wait_for_participant, AEC warm-up, greeting
│  ├─ standup_agent.py the Agent subclass + function tools (mark_topic_done, end_call)
│  ├─ prompts.py   bilingual system prompt + summarizer prompt
│  └─ finalize.py  shutdown hook — Gemini summarizer, transcript persistence
│
└─ web/            templates + static assets (vanilla JS, no build step)

Three periodic jobs

  • Inbox poll (every 30s, configurable) — IMAP UID SEARCH UID >cursor, parses any .ics, upserts CalendarEvent rows. Tracks UIDs so it survives the manager opening the invite in Gmail (which would flip \Seen).
  • Calendar tick (every 30s) — for events whose start_at ∈ [now-15min, now+45s] and not yet processed: dispatches a 1:1 standup per attendee (skipping the agent itself, the organizer, and DECLINED guests). Auto-creates Member rows for new emails.
  • Event completion (every 60s) — for events past end_at: flips any pending sessions to missed, then once every session is terminal, emails the digest to the organizer.

Data model

Four tables:

  • Member — name, email, role, language (en/ar), active flag.
  • AgentConfig — single-row team settings + IMAP UID cursor.
  • StandupSession — one per member-call, with status (scheduled/notified/joined/completed/missed/errored), transcript JSON, summary markdown.
  • CalendarEvent — parsed from .ics. last_processed_at claims an event for dispatch; digest_sent_at claims it for the digest. Recurring events expand to one row per occurrence over a 60-day horizon.

Tech stack

  • Python 3.11 · FastAPI · SQLModel · APScheduler · pydantic-settings
  • LiveKit Agents + livekit-plugins-google (Gemini Live, gemini-2.5-flash-native-audio-latest)
  • Silero VAD for endpointing
  • Gemini 2.5 Flash for the summarizer (separate from the live model)
  • icalendar + recurring_ical_events for invite parsing
  • aiosmtplib for outbound, stdlib imaplib for inbound
  • Jinja2 + vanilla JS — no build step, no framework

No managed dependencies beyond LiveKit Cloud and Gemini.


Roadmap

Things that are deliberately not done so this stays an MVP:

  • Bearer-token auth on the dashboard (currently wide open — gate before exposing publicly)
  • Multi-tenant (one agent address per team, currently single-team)
  • Postmark/Mailgun inbound webhook (faster than IMAP polling at scale)
  • Slack / Teams digest delivery in addition to email
  • Postgres support for hosted deployments (SQLite is fine for one team)
  • OpenAI Realtime as an alternative to Gemini Live (one-line provider swap)
  • CI + a real test suite (currently only the prompt-compose unit test)

PRs welcome on any of the above — or open an issue first if you want to discuss the shape.


Contributing

python -m venv .venv && source .venv/bin/activate
pip install -e .[dev]
pytest                                  # runs the existing tests

# In two terminals for live development:
uvicorn standup.api.main:app --reload --port 8000
python -m standup.agent.main dev        # agent worker with hot reload

Tests, screenshots, and English-language proofreading are especially welcome contributions. The bilingual prompts in src/standup/agent/prompts.py are the spot for native Arabic speakers to suggest improvements.

License

MIT — see LICENSE.


If this is useful, a star helps it find the next person who needs it. Thanks for reading.

About

AI voice agent that runs your team's standup. Invite it to a calendar event, walk away.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors