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:

  1. Build and rungo run main.go
  2. Try itcurl 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:

Bash
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.

Go
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.

Go
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.

Go
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.

Go
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.

Go
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:

Bash
# 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