Examples
Python (FastAPI)
Build a knowledge base manager with FastAPI and the Aether Python SDK.
This guide walks through a complete FastAPI application that exposes a REST API for document management, semantic search, and RAG-powered chat. The async Aether client fits naturally into FastAPI's async-first architecture, so every request is non-blocking end to end.
Quick start
Three commands to get the app running:
# 1. Install dependencies
pip install aether-ai fastapi uvicorn anthropic
# 2. Run the server
uvicorn kb_manager:app --reload --port 8000
The API is now live at http://localhost:8000. The chat endpoint requires an ANTHROPIC_API_KEY environment variable — every other feature works without it.
Project setup
Install the dependencies into a virtual environment:
python -m venv .venv
source .venv/bin/activate
pip install aether-ai fastapi uvicorn anthropic
The project is a single file:
kb-manager/
kb_manager.py
API key required
Set AETHER_API_KEY in your environment (or a .env file) before starting the server — the app loads it and passes it to the client. See the Installation guide for details.
Server setup
Start with imports, the Aether client lifecycle, and the FastAPI app. The asynccontextmanager lifespan ensures the async client is properly initialized at startup and closed on shutdown.
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
from aether import AsyncAetherClient
aether: AsyncAetherClient
@asynccontextmanager
async def lifespan(app: FastAPI):
global aether
aether = AsyncAetherClient(api_key=os.environ["AETHER_API_KEY"])
yield
await aether.close()
app = FastAPI(title="Knowledge Base Manager", lifespan=lifespan)
The global aether client is shared across all request handlers. AsyncAetherClient uses connection pooling under the hood, so a single instance handles concurrency safely.
Document endpoints
These endpoints cover the full document lifecycle: listing, inserting, viewing, deleting, and restoring.
class InsertRequest(BaseModel):
text: str
title: str
@app.get("/api/documents")
async def list_documents(offset: int = 0, limit: int = 50):
docs = await aether.list(offset=offset, limit=limit)
return {
"documents": [
{
"id": doc.id,
"filename": doc.filename,
"created_at": doc.created_at,
"status": doc.status,
}
for doc in docs
]
}
@app.post("/api/documents", status_code=201)
async def insert_document(req: InsertRequest):
filename = f"{req.title}.txt"
doc = await aether.insert_text(text=req.text, filename=filename)
return {"id": doc.id, "filename": doc.filename, "created_at": doc.created_at}
@app.get("/api/documents/{doc_id}")
async def get_document(doc_id: str):
try:
doc = await aether.get(doc_id)
except Exception:
raise HTTPException(status_code=404, detail="Document not found")
content = await aether.download_text(doc_id)
return {
"id": doc.id,
"filename": doc.filename,
"created_at": doc.created_at,
"status": doc.status,
"content": content,
}
@app.delete("/api/documents/{doc_id}", status_code=204)
async def delete_document(doc_id: str):
try:
await aether.delete(doc_id)
except Exception:
raise HTTPException(status_code=404, detail="Document not found")
return None
@app.post("/api/documents/{doc_id}/restore")
async def restore_document(doc_id: str):
try:
await aether.restore(doc_id)
except Exception:
raise HTTPException(status_code=404, detail="Document not found")
doc = await aether.get(doc_id)
return {"id": doc.id, "filename": doc.filename, "status": doc.status}
A few things worth noting:
insert_documentderives the filename from the user-supplied title, so documents are easy to identify when browsing.get_documentcalls bothget()for metadata anddownload_text()for the full content, returning everything the frontend needs in one round trip.delete_documentperforms a soft delete. The document can be brought back with the restore endpoint.
Search and chat
The search endpoint wraps Aether's semantic search. The chat endpoint adds a RAG layer: it retrieves relevant passages with retrieve(), then sends them to Claude as grounding context.
class ChatRequest(BaseModel):
message: str
@app.get("/api/search")
async def search_documents(
q: str = Query(..., description="Search query"),
k: int = Query(default=10, ge=1, le=100),
):
results = await aether.search(query=q, k=k)
return {
"results": [
{
"doc_id": r.doc_id,
"score": r.score,
"passage": r.passage,
}
for r in results
]
}
@app.post("/api/chat")
async def chat(req: ChatRequest):
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise HTTPException(
status_code=503,
detail="Chat is unavailable. Set the ANTHROPIC_API_KEY environment variable to enable it.",
)
# Retrieve grounding context from Aether
passages = await aether.retrieve(query=req.message, k=5)
context = "\n\n".join(
f"[Source {i + 1}] {p.content}" for i, p in enumerate(passages)
)
# Generate a grounded answer with Claude
import anthropic
claude = anthropic.AsyncAnthropic(api_key=api_key)
response = await claude.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=(
"You are a helpful assistant. Answer the user's question using "
"only the following context. Cite sources by number.\n\n"
f"{context}"
),
messages=[{"role": "user", "content": req.message}],
)
return {
"answer": response.content[0].text,
"sources": [
{"doc_id": p.doc_id, "content": p.content} for p in passages
],
}
The anthropic import is inside the handler so the module loads even when the package is not installed. If the API key is missing, the endpoint returns a 503 with a clear message instead of crashing.
Swapping LLM providers
The RAG pattern here is provider-agnostic. Replace the Anthropic call with any LLM client and the rest of the code stays the same. See the Integrations section for examples with OpenAI, Azure, and others.
Stats endpoint
Expose a usage stat so monitoring tools or a dashboard can check the system at a glance. list() returns pagination metadata with every page, so a single call with limit=1 is enough to read the total document count.
@app.get("/api/stats")
async def stats():
page = await aether.list(limit=1)
return {"total_documents": page.total}
That's the complete application. Run it with uvicorn kb_manager:app --reload --port 8000 and point any HTTP client at http://localhost:8000.
Next steps
- TypeScript version -- the same app built with Express
- Go version -- idiomatic Go using only the standard library
- API Reference -- full Aether endpoint documentation
- Integrations -- connect to other LLM providers