Skip to content

Repository files navigation

🎭 Prompter

The Cratis community's docs assistant — living on Discord, grounded in the docs, allergic to making things up.

DiscordDockerBuildPublish


In theater, the prompter sits just offstage with the script and quietly feeds the line to anyone who blanks mid-scene. That's the whole idea. The script is the published documentation at cratis.io; the stage is the Cratis Discord; and when someone forgets their line — "wait, how do I append an event again?" — Prompter whispers the answer, with citations, or admits honestly when the docs don't cover it.

It is built to stick to the script: answers come from the retrieved docs, with citations — and when the docs don't cover something, it says so instead of improvising.

🎭 Why "Prompter"?

Three reasons, and they all line up:

  • The theater prompter feeds you the line you forgot from just offstage — never stealing the scene, always working from the script. That's exactly the role: the docs are the script, and the bot only speaks when someone needs the line.
  • The LLM double meaning — Prompter prompts a language model for a living. The name wears its machinery on its sleeve.
  • The Cratis storytelling family — Cratis names its products after telling a story: Chronicle records what happened, Arc shapes the plot, Narrator, Lens, StudioPrompter joins the cast.

💬 What a scene looks like

you · #help
How do I define a command in Arc?
🎭 Prompter · replies to you
In Arc a command is a record marked [Command]; its Handle() method
returns the event(s) that happened — one file, backend to event:
[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public AuthorRegistered Handle() => new(Name);
}
Arc then generates the TypeScript proxy so React calls it type-safe.
📚 Sources
• Cratis — One feature, one slice, typed end to end (cratis.io)
👍 👎 ← tell us if that helped

Ask it about something the docs don't cover and it won't improvise — it tells you it doesn't know and points you at a human. That honesty is the feature.

✨ How to summon it

  • @mention@Prompter anywhere it can see — it replies in-thread, right where you asked.
  • /ask — the slash command, for a clean one-off question; it shows a "thinking…" indicator while it looks things up, then delivers the cited answer.
  • #ask-ai — a dedicated channel where every message is treated as a question, no mention needed.
  • Help forum — open a new thread in the help forum and Prompter takes the first swing automatically, so you're never waiting on the timezone gods for a first answer.
  • 👍 / 👎 buttons — one click under any answer; the verdict is logged so the docs (and the bot) get better.
  • /issue — turn what you are describing into a GitHub issue on the right Cratis repository: Prompter drafts it, shows it to you privately, and files it only when you say so. Bugs, missing APIs, feature requests, ideas, docs gaps — anything worth tracking.

Prompter never barges into normal conversation — it only speaks when spoken to, and it rate-limits each person to a handful of questions per window so no one can spam it.

🧠 How it works

A small, honest RAG pipeline — hybrid retrieval (keyword and meaning), then a grounded answer with citations:

flowchart LR
Docs["📖 cratis.io docs<br/>(markdown mirrors)"] -->|"chunk + embed"| Voyage["Voyage AI<br/>embeddings"]
Voyage --> PG[("Postgres<br/>+ pgvector")]
Q["❓ your question"] --> Hybrid{"Hybrid search<br/>BM25 + vector · RRF"}
PG --> Hybrid
Hybrid -->|"top passages"| Claude["🤖 Claude"]
Claude -->|"cited answer<br/>or honest refusal"| You["🎭 you, on Discord"]
Loading
  • Ingest — walk cratis.io's sitemap, fetch each page's markdown mirror, strip the MDX noise, and split into heading-aware chunks. Only changed chunks are re-embedded, so re-indexing is cheap.
  • Retrieve — one SQL query fuses lexical (BM25 via tsvector) and semantic (cosine over pgvector) hits with Reciprocal Rank Fusion.
  • Answer — Claude gets the top passages and a system prompt that demands grounding and citations, and refuses when the score says the docs don't have it.

Built with C# / .NET 10, NetCord, Microsoft.Extensions.AI, Claude (Anthropic), Voyage AI embeddings, and Postgres + pgvector.

🚀 Quick start

Bring up Postgres (with pgvector):

docker compose up -d

Index the documentation, then ask a question straight from your terminal:

cd Source
dotnet run -- index # ingest cratis.io into the corpus
dotnet run -- ask "How do I append an event in Chronicle?"# answer from the CLI (add --verbose for the passages)

Run it as the Discord bot — bot mode also serves GET /healthz and the shared-secret POST /reindex webhook, and sweeps expired interactions on a daily retention job:

dotnet run

Measure answer quality against the golden question set (from the repo root; needs the keys plus an indexed corpus):

dotnet run --project Eval # groundedness, citation, and refusal scores

You'll need a (free) Voyage API key to index and an Anthropic key to answer — see the table below.

⚙️ Configuration

Configuration binds to the Cratis:Prompter section (environment variables use __ as the delimiter):

SettingEnvironment variableDefault
Postgres connection stringCratis__Prompter__ConnectionStringlocalhost, db/user/pass prompter
Docs site to ingestCratis__Prompter__DocsSiteUrlhttps://cratis.io
Embedding batch sizeCratis__Prompter__Voyage__BatchSize128
Discord bot tokenCratis__Prompter__Discord__Token
Ask channel (mention-free questions)Cratis__Prompter__Discord__AskChannelId
Help forum channel (auto-reply)Cratis__Prompter__Discord__HelpForumChannelId
Rate limit — questions per windowCratis__Prompter__Discord__RateLimit__MaxQuestions5
Rate limit — window length (minutes)Cratis__Prompter__Discord__RateLimit__WindowMinutes10
Answer timeout (seconds)Cratis__Prompter__Discord__AnswerTimeoutSeconds60
Anthropic API keyCratis__Prompter__Anthropic__ApiKey (or ANTHROPIC_API_KEY)
Answer modelCratis__Prompter__Anthropic__Modelclaude-sonnet-5
Refusal threshold (min top-passage score)Cratis__Prompter__Answering__MinScore0.02
Voyage API keyCratis__Prompter__Voyage__ApiKey
Interaction retention (days)Cratis__Prompter__RetentionDays90
Re-index webhook secretCratis__Prompter__ReindexSecret
GitHub token (file issues)Cratis__Prompter__GitHub__Token
GitHub webhook secret (answer issues)Cratis__Prompter__GitHub__WebhookSecret
Repositories whose issues may be answeredCratis__Prompter__GitHub__AnsweringRepositories__0— (none)
Channel new issues are announced inCratis__Prompter__GitHub__NotifyChannelId
Label put on issues filed from DiscordCratis__Prompter__GitHub__IssueLabelfrom-discord
Label that opts an issue out of answersCratis__Prompter__GitHub__OptOutLabelno-prompter

API keys are never committed — use environment variables or a git-ignored Source/appsettings.Development.json.

🗺️ Start here (for contributors)

✅ Quality gates

dotnet build --configuration Release # zero warnings, zero errors (warnings are errors in Release)
dotnet test --configuration Release # all specs green

And one more gate that's unusual for a bot: answer quality is measured, not vibed. A golden-question eval harness (milestone M4) scores groundedness, citation accuracy, and refusal behavior — and gates prompt and retrieval changes the same way specs gate code.

The Cratis ecosystem

This project is part of Cratis — free, MIT-licensed tools for building event-sourced and CQRS applications.

Everything Cratis publishes today is MIT licensed and free to use.


Part of the Cratis platform · Licensed under the MIT license

About

The Cratis community's documentation assistant on Discord — grounded in the docs at cratis.io, allergic to making things up

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages