Examples
Knowledge Base Manager: Go (net/http)
A complete Knowledge Base Manager built with Go's standard library and the Aether Go SDK — no frameworks, just net/http.
This example implements the full Knowledge Base Manager API using idiomatic Go. It uses http.NewServeMux with Go 1.22+ pattern-based routing, encoding/json for serialization, and the Aether Go SDK for all document and search operations. The chat endpoint calls the Anthropic API directly via net/http — no external LLM SDK required.
Quick start
Three steps to get the app running:
- Build and run —
go run main.go - Try it —
curl http://localhost:8080/api/stats
Environment variables
AETHER_API_KEY— your Aether API key, passed to the client (required).ANTHROPIC_API_KEY— Anthropic API key for the chat feature (optional).
Project setup
Initialize a Go module and install the Aether SDK:
mkdir kb-manager && cd kb-manager
go mod init kb-manager
go get github.com/quintessence-group/aether-sdk-go
Create a single main.go file — the entire application lives in one file.
Server setup
Start with the imports, the Aether client initialization, and the HTTP server. Environment variables configure the connection, with sensible defaults for local development.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
aether "github.com/quintessence-group/aether-sdk-go"
)
var client *aether.Client
func main() {
client = aether.New(aether.WithAPIKey(os.Getenv("AETHER_API_KEY")))
mux := http.NewServeMux()
// Document endpoints
mux.HandleFunc("GET /api/documents", handleListDocuments)
mux.HandleFunc("POST /api/documents", handleInsertDocument)
mux.HandleFunc("GET /api/documents/{id}", handleGetDocument)
mux.HandleFunc("DELETE /api/documents/{id}", handleDeleteDocument)
mux.HandleFunc("POST /api/documents/{id}/restore", handleRestoreDocument)
// Search and chat
mux.HandleFunc("GET /api/search", handleSearch)
mux.HandleFunc("POST /api/chat", handleChat)
// Stats
mux.HandleFunc("GET /api/stats", handleStats)
log.Println("Knowledge Base Manager listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
The envOr helper keeps the config logic clean. The mux uses Go 1.22's method-prefixed patterns so each route is bound to exactly one HTTP verb.
Document handlers
These handlers cover the full document lifecycle: listing, inserting, fetching (with content), deleting, and restoring.
func handleListDocuments(w http.ResponseWriter, r *http.Request) {
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 {
limit = 20
}
result, err := client.List(r.Context(), &aether.ListOptions{
Offset: offset,
Limit: limit,
})
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"documents": result.Documents,
"total": result.Total,
"has_more": result.HasMore,
})
}
func handleInsertDocument(w http.ResponseWriter, r *http.Request) {
var body struct {
Text string `json:"text"`
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Text == "" {
writeError(w, http.StatusBadRequest, "text is required")
return
}
filename := body.Title
if filename == "" {
filename = "untitled.txt"
}
doc, err := client.InsertText(r.Context(), body.Text, filename)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, doc)
}
func handleGetDocument(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
doc, err := client.Get(r.Context(), id)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
content, err := client.DownloadText(r.Context(), id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"document": doc,
"content": content,
})
}
func handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := client.Delete(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
func handleRestoreDocument(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := client.Restore(r.Context(), id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "restored"})
}
Each handler reads path parameters with r.PathValue("id") — another Go 1.22 addition that eliminates the need for third-party routers. The insert endpoint accepts a JSON body with text and an optional title that becomes the filename.
Search and chat
The search endpoint wraps client.Search with query-string parameters. The chat endpoint uses client.Retrieve to fetch relevant passages, then sends them to the Anthropic API for a grounded answer.
func handleSearch(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
writeError(w, http.StatusBadRequest, "q parameter is required")
return
}
k := 5
if v, err := strconv.Atoi(r.URL.Query().Get("k")); err == nil && v > 0 {
k = v
}
results, err := client.Search(r.Context(), query, k)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"results": results})
}
func handleChat(w http.ResponseWriter, r *http.Request) {
apiKey := os.Getenv("ANTHROPIC_API_KEY")
if apiKey == "" {
writeError(w, http.StatusServiceUnavailable,
"ANTHROPIC_API_KEY is not set — chat is unavailable")
return
}
var body struct {
Message string `json:"message"`
K int `json:"k"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Message == "" {
writeError(w, http.StatusBadRequest, "message is required")
return
}
if body.K <= 0 {
body.K = 5
}
// Retrieve relevant passages from Aether
passages, err := client.Retrieve(r.Context(), body.Message, body.K)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
// Build context from retrieved passages
var contextBuf bytes.Buffer
for i, p := range passages {
fmt.Fprintf(&contextBuf, "[Source %d]\n%s\n\n", i+1, p.Content)
}
// Call Anthropic API directly via net/http
answer, err := callAnthropic(r.Context(), apiKey, contextBuf.String(), body.Message)
if err != nil {
writeError(w, http.StatusBadGateway, "LLM request failed: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"answer": answer,
"sources": passages,
})
}
func callAnthropic(ctx context.Context, apiKey, contextStr, question string) (string, error) {
reqBody, _ := json.Marshal(map[string]any{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"system": fmt.Sprintf(
"Answer the user's question using only the following context. "+
"Cite source numbers in brackets.\n\n%s", contextStr),
"messages": []map[string]string{
{"role": "user", "content": question},
},
})
req, err := http.NewRequestWithContext(ctx, "POST",
"https://api.anthropic.com/v1/messages", bytes.NewReader(reqBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Anthropic-Version", "2023-06-01")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("anthropic API returned %d: %s", resp.StatusCode, body)
}
var result struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if len(result.Content) == 0 {
return "", fmt.Errorf("empty response from Anthropic")
}
return result.Content[0].Text, nil
}
The callAnthropic function builds a raw HTTP request to the Anthropic Messages API. This avoids pulling in an external SDK — the only dependency beyond the standard library is the Aether Go SDK. If ANTHROPIC_API_KEY is missing, the chat endpoint returns a clear error while every other endpoint continues to work.
Stats handler
The stats endpoint reports the total number of stored documents. client.List returns pagination metadata with every page, so requesting a single document is enough to read the total.
func handleStats(w http.ResponseWriter, r *http.Request) {
result, err := client.List(r.Context(), &aether.ListOptions{Limit: 1})
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{
"total_documents": result.Total,
})
}
JSON helpers
Two small helpers keep the handler code focused on business logic.
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
Try it out
With the server running, exercise the API:
# Insert a document
curl -X POST http://localhost:8080/api/documents \
-H "Content-Type: application/json" \
-d '{"text": "Employees accrue 20 days of PTO per year.", "title": "pto-policy.txt"}'
# List documents
curl http://localhost:8080/api/documents
# Search
curl "http://localhost:8080/api/search?q=vacation+days&k=3"
# Chat (requires ANTHROPIC_API_KEY)
curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "How many vacation days do I get?", "k": 3}'
# Check usage stats
curl http://localhost:8080/api/stats
Next steps
- Overview — see how this compares to the Python, TypeScript, and C# implementations
- API Reference: Documents — full endpoint documentation for the Aether API
- API Reference: Search & Retrieval — details on search and retrieve operations
- Anthropic Claude integration — advanced patterns for RAG with Claude