About the project

Inspiration

For 25 years OpenmindProjects has run as a 100% volunteer-led nonprofit — every recruiter, admin, and community coordinator is themselves a volunteer or a student intern. And that creates a cruel irony: the exact people we depend on to operate our programs — screening new applicants, writing welcome emails, scheduling interviews, building onboarding plans, chasing follow-ups — are the most transient. Every handover means weeks of re-training. Meanwhile, the commercial volunteer-placement agencies we compete with have entire paid ops teams. Small grassroots NGOs — the hosts that need support the most — get priced out of the platforms and left doing the same copy-paste job by hand. Hosts drown in repetitive messaging while the real work of matching and onboarding stalls.

One day we watched a coordinator rewrite the same interview invite for the twelfth time that week — by hand, in Gmail — and thought: this is a human approving a button, not a human doing work. That's the split Mindy attacks. We already had a text-generator assistant behind a button. The project became: make that button act — safely, audibly, in the background — while keeping the human as the final approver of every outbound action.

What it does

Mindy is a Taskmaster agent, not a chatbot: it runs the entire volunteer recruitment and onboarding lifecycle end-to-end, behind a single human-approval gate.

  • Ops Autopilot. A BookingRequest status change fires a background job that fans out to nine specialist sub-agentsscreening_questioner, welcome_writer, interview_inviter, onboarding_task_planner, followup_drafter, invoice_reminder, project_writer, field_polisher, seo_writer — and merges their drafts into a single Approve & send button for the host. One tap dispatches a real email.
  • Onboarding Pipeline. A deterministic graph workflow routes a lifecycle stage token (screen → interview → accept → onboard → followup) to the matching specialist — no LLM guesswork, no skipping steps.
  • Mindy, the volunteer-facing assistant. Grounded Q&A, airport & flight guidance, personalized project recommendations (ranking real database rows, not hallucinated listings), pgvector-backed cross-session memory recall, photo Q&A via vision, web-grounded search with citation filtering, speech in/out, and a gamified mission/XP/tiers onboarding journey.
  • Jettie, the host-facing assistant. Answers only from the organization's own facts (projects, bookings, volunteers, categories) so it never invents host data.
  • Long-term memory with GDPR erasure. Every interaction is embedded and persisted in AlloyDB via pgvector; the recall_memory tool does cosine-similarity recall over a volunteer's history and injects prior preferences into the next turn. A data-deletion request erases the vectors, not just the text.
  • Guardrails and observability. Deterministic PII redaction and prompt-injection blocking run before the model call (they cannot be talked out of by prompt wording). OpenTelemetry traces every run with slog JSON audit logs.
  • Accessibility. An axe + Pa11y WCAG 2.1 gate runs across the assistant UI.

Impact-first: every hour Mindy saves a host is an hour redirected toward real community work. Across 25 years OpenmindProjects has educated 32,000+ students (SDG 4), supported 10,000+ beneficiaries in the poorest regions of SE Asia, trained 500+ women through rural support networks. Mindy will removes the admin drag that was quietly capping how many volunteers each host can support.

How we built it

Runtime. A Rails 8.1 monolith (Falcon async fiber server, Ruby 3.3) owns durable state — domain models, Solid Queue background jobs, Solid Cache/Cable, Devise auth, and the Rails-facing UI. The agent runtime runs as a sidecar service written in Go 1.26 on Google ADK 2.0; four agents (mindy, ops_autopilot, onboarding_pipeline, jettie) are registered at startup in main.go, each wrapped in an in-memory ADK runner. Rails talks HTTP JSON + SSE to the agent via MindyClient; the agent is stateless.

Workflow routing. For Ops: a BookingRequest status change enqueues OpsAutopilotJob, which calls POST /v1/ops with a deterministic action + context payload. For Onboarding: a stage token flows through POST /v1/onboarding onto explicit graph edges — no LLM selector.

Grounding. Project recommendations serialize real DB rows into the prompt as [Project N] fact blocks; [project:SLUG] tokens emitted by the agent are post-sanitized against the grounded candidate slugs so a hallucinated project never becomes a broken link. Answers to hosts in Jettie are grounded by an injected "Organization context (grounded facts — use ONLY these)" block.

Data. Google Cloud AlloyDB for PostgreSQL (enterprise Postgres 18) holds every table, every Solid Queue job, every cache entry, every Action Cable message, and every pgvector embedding. The app connects through the AlloyDB Auth Proxy sidecar (mTLS over port 443) using a GCP service account — no direct public credential logins. In dev we run the same proxy on 127.0.0.1:5433 and prove the tunnel with bin/alloydb_proof, which prints inet_server_addr() returning the AlloyDB public IP.

Model. A single Gemini 3.6 Flash powers both multi-turn, tool-orchestrated chat (mindy/jettie) and single-shot drafting (ops_autopilot / onboarding specialists). Streaming uses server-sent events with deltas, progress status frames, citation frames, and a final authoritative frame — critical for sub-agent merged output.

Guardrails. guardrails.go runs pre-model: regex-based PII redaction (email, cards, phones) + prompt-injection signal detection. Failures return 422s before any Gemini call.

Deployment. Rails + agent sidecar under Podman Quadlet + systemd, TLS terminated by Caddy. The agent image is CGO_ENABLED=0 static, shipped on gcr.io/distroless/static-debian12:nonroot (no shell, no apt — tiny attack surface). For GCP-first deploys it can also ship to Cloud Run with gcloud run deploy mindy-agent-service --source agent.

Tests. Go unit tests: go test ./... across every agent (mindy_test.go, knowledge_test.go, tokens_test.go, onboarding_pipeline_test.go, server_test.go, guardrails_test.go, observability_test.go), plus Go benchmarks on the tokenizer. Rails tests + Brakeman + bundler-audit + RuboCop on the monolith side.

Challenges we ran into

  • "Chat vs. act" was a UX battle. Everyone on the team intuitively reaches for a chat bubble. But real operators do not want to chat — they want to approve a draft and move on. Remaking the UI around a single Approve & send button instead of a chat assistant was the hardest design pivot, and the one that made "agent that acts" stop being a slogan and become a product.
  • Hallucinated project slugs break the platform. We had a week of demo failures where the agent would invent a project name that sounded right but didn't exist in the DB. The fix wasn't "prompt the model harder" — it was deterministic post-processing: emit slugs as explicit tokens, then strip any token whose slug isn't in the grounded candidate set. That eliminated the failure mode.
  • Lifecycle routing via LLM choice was non-deterministic. Early on, the onboarding pipeline asked Gemini "which specialist should handle this?" and about 12% of the time it picked the wrong stage. Replacing that with a hard graph over a stage token (screen | interview | accept | onboard | followup) plus explicit edges removed all of that variance — the cost was less "autonomy," the gain was a real, reliable production system.
  • SSE partial deltas truncate sub-agent output. When ops_autopilot merged nine specialists, the final merged text came through on a final (non-partial) event. Naive client code that only rendered deltas lost half the answer. We added an authoritative {"final": true, "text": "..."} frame and told the client to replace the delta buffer with that frame on arrival.
  • Distroless images have no shell. We shipped the agent image on distroless/static-debian12:nonroot and then spent an hour confused when Easypanel "Open terminal" failed with exec: "/bin/sh": stat /bin/sh: no such file or directory. It turns out that's the correct security posture — we just had to switch to the debug base during diagnostics, and use the /healthz + curl surface for routine proof instead of shelling in.
  • AlloyDB Auth Proxy mount gotcha. Our initial compose file mounted the volume to /var/lib/postgresql/data, but Postgres 18+ Debian trixie images store data in a versioned subdirectory under /var/lib/postgresql, so the container looped startup. Mounting the parent directory fixed it.
  • Solid Queue recurring tasks did not auto-register by existing in config/recurring.yml. After a day of debugging "why isn't the 8am follow-up sweep firing?" we realized that the :async development adapter doesn't run a scheduler, and Procfile.dev wasn't launching bin/jobs at all. Adding the Solid Queue adapter + a jobs: proc line fixed the whole automation stack.

Accomplishments that we're proud of

  • We made the approve→send gate real. A human taps one button and Mindy dispatches the right communication to the right volunteer at the right stage — not just writes it. That's the line between a text generator and an agent, and we crossed it.
  • A single model does everything. Gemini 3.6 Flash handles the tool-using assistants, the drafting specialists, the graph workflow language-modeling nodes, and the vision Q&A. We didn't need to juggle a zoo of fine-tunes; one model + deterministic routing and guardrails was enough.
  • Zero hallucinated project links in production. We haven't had a single broken [project:slug] token leak to a user since the sanitizer landed.
  • GDPR-complete memory. When a volunteer asks to be forgotten, the vectors get forgotten too. That's table-stakes for EU ops and we built it in from day one, not as an afterthought.
  • Mindy is deployed and serving. gcloud alloydb instances list shows the cluster. bin/alloydb_proof reports the AlloyDB server_addr. The agent /healthz reports gemini-3.6-flash. The Rails app on Easypanel is live. This isn't a local demo — it's a real production system operating against real AlloyDB data.
  • 25 years of institutional impact riding on a zero-platform-fee agent. Small community hosts that could never afford a booking agency now get the same (actually better) drafting, scheduling, and follow-up capability — for free. That's the one we care about most.

What we learned

  • The approve→send gate is the product. Making "action vs. chat" real — the single button that actually dispatches — was the difference between a demo and a product. Everything else orbits it.
  • Grounding beats hallucination, but token-sanitization beats grounding. A model can still slip bad output past a grounding prefix if it's seen enough examples. Deterministic post-processing of structured tokens (and dropping anything that doesn't match) eliminates the tail.
  • Deterministic routing beats LLM choice for lifecycles. "Use the LLM to decide the next step" is tempting. "Use a token + explicit graph edges" is boring, testable, and 100% reliable. That's what runs in production.
  • Model-independent guardrails can't be talked out of. If prompt-injection checks live inside a system prompt, users will find a way around. If they're regex + heuristics running before the model call, they hold.
  • Streaming status frames make autonomy visible. Without the "Retrieving memory / Searching the web / Looking up airports" frames, users told us Mindy "felt frozen." Once we surfaced tool calls as progress steps, they reported "it feels like it's working on something" — same latency, completely different perception.
  • Stateless Go + durable Rails memory is a clean split. A lot of agent projects put orchestration and persistence in the same process. We put stateless agents on one side, durable domain state on the other. They never fight.
  • Gamification and the approval gate reinforce each other. The onboarding engine reuses the same HostTask verification step as the ops autopilot — so the AI agent earns badges for the work it actually completes, not just for chat turns.
  • Small hosts do not need another chatbot. They need operations capacity. Every host we showed the prototype to skipped the assistant demo and asked "can it send the follow-up email for me?" That conversation permanently oriented the project around Taskmaster, not chat.

What's next for Mindy the AI Volunteer Agent

  1. Institutional knowledge ingestion. Migrate 25 years of accumulated organizational knowledge into a structured, provenance-tagged retrieval corpus: teaching & learning lesson plans, cross-cultural awareness books, volunteer and host-organization handbooks, local language and food references. Today the assistant works from curated chunks — next it works from the whole library.
  2. Semantic knowledge-base retrieval. Upgrade the KB retriever from lexical token matching to embedding-based (pgvector cosine) semantic search, so a volunteer can ask "what's the best way to say hello to host families in Isan?" and get the right answer even when the exact words never appear in the text.
  3. Durable cross-process session resumability. Checkpoint ADK sessions to AlloyDB so a long-running workflow can restart after a deploy without replaying its history. This enables multi-day drafting (e.g. Mindy drafts an onboarding plan over a weekend as data arrives).
  4. Managed Google Cloud compute. Migrate the Rails + agent compute onto Cloud Run for full GCP residency — AlloyDB + Cloud Run + Gemini, all first-party, no third-party hosting in the path.
  5. Deeper Google Workspace tooling. Direct Gmail / Calendar / Tasks tool access from the agents, so "approve and send" can also block a 30-minute calendar slot for the interview and add the ToDo item — not just fire an email.
  6. Automatic context compaction. Summarize long host↔volunteer threads before each model call so Mindy stays focused across a 3-month placement (today we rely on explicit memory recall; compaction adds "what's the last thing we decided" as a native artifact).
  7. Enterprise agent catalog & gateway. Agent registry/versioning, per-agent zero-trust identity, and a unified policy-enforcing gateway — so Jettie, Mindy, ops_autopilot, and the onboarding pipeline can be surfaced to third-party partner NGOs safely.
  8. Evaluation harness. A golden-set regression suite that continuously measures grounding accuracy, citation not-404 rate, hallucinated-slug leak rate, and prompt-injection bypass rate — so we can prove numerically, with every deploy, that Mindy keeps getting better without losing the safety guarantees we built in.

Technologies used

Layer Stack
Frontend Rails views, Turbo 8, Stimulus 3, Tailwind CSS 4 + SCSS (BEM), Importmap
Backend Ruby 3.3.11, Rails 8.1.3, Falcon (async Rack server), Solid Queue (background jobs)
AI agents Go 1.26+, Google ADK 2.0 (google.golang.org/adk/v2), OpenTelemetry OTLP
Model Gemini 3.6 Flash (assistant + drafting)
Database Google Cloud AlloyDB for PostgreSQL 16 with pgvector
Infra Podman + systemd + Caddy, AlloyDB Auth Proxy (mTLS), agent sidecar (optional Cloud Run)
Protocols HTTP JSON, Server-Sent Events (SSE), OTLP/HTTP

Other data sources used

  • OpenmindProjects project & booking catalog — PostgreSQL/AlloyDB rows for grounded project recommendations and lifecycle state.
  • Embedded knowledge base — provenance-tagged chunks (FAQs, project details, cultural tips) migrated from OpenmindProjects' legacy Globie chatbot and compiled into the Go binary, so the assistant stays grounded with no runtime data dependency.
  • Volunteer interaction corpus — pgvector embeddings powering cross-session memory recall.
  • IATA airport reference data — deterministic fact lookup for travel-related questions.
  • Google Search — web grounding with citation surfacing and dead-link filtering.
  • Gemini Embeddings — semantic recall over the volunteer interaction log (via Rails).
  • Gemini 3.6 Flash — a single Google model powering both the assistants and the lightweight drafting specialists.

Architecture Diagrams

Overall system flow

flowchart TB
    subgraph Frontend["Frontend — Rails views (Turbo · Stimulus · SSE)"]
        UI["Mindy chat · Host tasks · Booking pipeline"]
    end

    subgraph Backend["Backend — Rails 8 Monolith (Falcon)"]
        MODELS["Domain models · Solid Queue jobs"]
        MEMORY["AgentMemory (pgvector)"]
        CLIENT["MindyClient"]
    end

    subgraph Agents["AI Agents — Google ADK 2.0 (Go)"]
        RUNNER["ADK Runner"]
        MINDY["mindy"]
        OPS["ops_autopilot"]
        ONB["onboarding_pipeline"]
        JETTIE["jettie"]
        GUARD["Guardrails (PII + injection)"]
        TRACE["OpenTelemetry"]
    end

    subgraph Models["Google AI"]
        GEM["Gemini 3.6 Flash"]
    end

    subgraph Data["Data"]
        DB[("Google Cloud AlloyDB (pgvector)")]
    end

    UI --> CLIENT
    CLIENT -->|"HTTP JSON / SSE"| RUNNER
    RUNNER --> MINDY --> GEM
    RUNNER --> OPS --> GEM
    RUNNER --> ONB --> GEM
    RUNNER --> JETTIE --> GEM
    RUNNER --> GUARD
    RUNNER --> TRACE
    MINDY -->|"memory recall / knowledge / mission"| MODELS
    MODELS --> DB

1. The "takes action" loop (host-facing)

sequenceDiagram
    participant BR as BookingRequest
    participant JOB as OpsAutopilotJob
    participant AGENT as ops_autopilot
    participant TASK as HostTask
    participant HOST as Host
    participant MAIL as VolunteerMailer

    BR->>JOB: status change (e.g. deposit_paid)
    JOB->>AGENT: route to specialist sub-agent
    AGENT-->>TASK: draft next action (pending)
    TASK-->>HOST: single Approve & send button
    HOST->>MAIL: Approve & send → email
    MAIL-->>TASK: mark_dispatched

2. Long-term memory — write & recall

flowchart TB
    subgraph Write["Write path — every interaction"]
        DEC["Agent interaction"] --> EMB["Embed text (pgvector)"]
        EMB --> ME["assistant_interactions<br/>embedding · feedback"]
    end

    subgraph Read["Read path — recall_memory tool"]
        QUERY["recall_memory"] --> SEM["Cosine-similarity recall"]
        SEM --> OUT["Prior preferences"]
        OUT --> INJ["Injected into agent context"]
    end

    ME --> SEM

Built With

Share this project:

Updates

Submission history