Open sourceReleased

docs-assistant

An AI support assistant that answers from your documents, cites every answer, and hands the conversation to a person when it doesn't know.

An embeddable RAG chatbot in .NET 10: hybrid search in PostgreSQL with pgvector, Claude Haiku behind a swappable port, and a 9 KB widget that installs with one line.

Role:
Design and development
Year:
2026
retrieval hit@5 on a 54-question bilingual eval
98%
prompt-injection attacks resisted
12/12
model cost per 1,000 questions
$0.33
embeddable widget
9 KB

Demo

75 seconds on a demo store. On-screen text in Spanish, answers by Claude Haiku 5.5.

The problem

Small businesses answer the same questions all day, and generic chatbots make up answers that the business then has to correct. The goal was an assistant a customer can trust: it answers only from the business’s own documents, shows where each answer came from, and when it cannot answer, it says so and passes the conversation to a person.

Constraints

  • No invented answers: abstaining beats guessing
  • Questions and documents in different languages, Spanish and English
  • A small, visible cost per answer, with hard spending caps
  • It has to embed in any website without breaking its styles
  • Several businesses on one deployment, each seeing only its own data

How it works

  1. Documents (PDF, DOCX, Markdown and web pages) are split along their structure and indexed twice in PostgreSQL: as vectors, for meaning across languages, and as full text, for exact words, numbers and names.
  2. A question searches both indexes and the two rankings are fused with Reciprocal Rank Fusion. Passages shaped like injected instructions are quarantined before they reach the model.
  3. Below a measured similarity floor the assistant abstains without calling the model. Otherwise Claude answers from those passages only and cites them as [1], [2]; the citations are validated before the answer streams to the widget over SSE.
  4. The widget is framework-free TypeScript inside a Shadow DOM: 9 KB, one script tag, and styles that never leak into the host page.

Key decisions

Abstain before calling the model

When retrieval finds nothing similar enough, the answer is “I can’t find that” plus a person, decided in code at zero cost.

Trade-off: A few answerable questions are refused when retrieval misses. The eval tracks that rate, so the floor is tuned on data rather than taste.

Hybrid search, not vectors alone

Vectors handle meaning and cross-language questions; full text catches the prices, codes and names that embeddings blur. Guaranteeing one vector hit in the fused list raised cross-lingual hit@5 from 86% to 95%.

Trade-off: Two indexes to maintain and a fusion step to tune, though both live in the PostgreSQL the app already needs.

The model behind a port, with spending guards

Claude sits behind Microsoft.Extensions.AI, so the model is a configuration choice, and an extractive mode works with no model at all. The evals refuse to spend without an explicit flag, a call cap and a cost cap.

Trade-off: A thin abstraction layer to keep current, in exchange for never being locked to one provider or surprised by a bill.

Results

  • Retrieval finds the right passage for 98% of the 54 eval questions, and for 95% when the question and the document are in different languages.
  • With Claude Haiku 5.5: 0 invalid citations, abstention on 100% of the unanswerable questions, and USD 0.33 per 1,000 questions.
  • 12 of 12 prompt-injection attacks resisted (8 direct, 4 hidden inside documents), while 0 of the 54 real questions were wrongly refused.
  • Every number comes from an eval in the repository that anyone can rerun, and CI fails if retrieval drops below its thresholds.

Stack

  • .NET 10
  • C#
  • PostgreSQL
  • pgvector
  • Aspire
  • Claude Haiku 5.5
  • TypeScript
  • Playwright