All checks were successful
continuous-integration/drone/push Build is passing
4.2 KiB
4.2 KiB
Backend Agent Instructions
Stack
- Go 1.25.5 (
reichard.io/aethera) - air (backend hot reload for
make dev) - cobra (CLI framework)
- logrus (structured logging)
- openai-go/v3 (OpenAI API client)
- jsonschema-go (structured output schema generation)
- testify (testing assertions)
- golangci-lint (linting)
Commands
go build -o ./dist/aethera ./cmd # Build binary
golangci-lint run # Lint
go test ./... # Run all tests
Architecture
Frontend assets are embedded at compile time via web/embed.go (//go:embed static/*). The make frontend target copies built frontend output into backend/web/static/ before the Go build.
Directory Layout
cmd/ # CLI entry (main.go, config.go)
internal/
api/ # HTTP handlers, request/response types, streaming generation
client/ # OpenAI-compatible API client (chat, images, structured output)
server/ # HTTP server setup, routing, logging middleware
store/ # Store interface + FileStore (JSON files) & InMemoryStore
storage/ # Standalone file utilities (e.g. ListImages)
types/ # Shared domain types (MessageStats)
pkg/
ptr/ # Generic pointer helpers (Of, DerefOrZero)
slices/ # Generic slice helpers (Map, First, Last, FindFirst)
values/ # Generic value helpers (FirstNonZero, CountNonZero)
web/ # Embedded static assets (embed.go)
Key Patterns
- Store interface (
internal/store/interface.go): abstraction over settings + chat persistence. Two implementations:FileStore— JSON files on disk (settings.json +chats/*.json). Used in production.InMemoryStore— map-based, thread-safe withsync.RWMutex. Used in tests.
- DI via constructors:
api.New(store, dataDir, logger),client.NewClient(baseURL),store.NewFileStore(path) - Streaming:
generationManagercoordinates concurrent chat completions. One active generation per chat. Subscribers receiveMessageChunkupdates via channels over NDJSON (application/x-ndjson). - Client lazy init:
api.getClient()creates the OpenAI client on first use from stored settings, invalidated on settings change. - Env vars: all prefixed
AETHERA_(e.g.AETHERA_PORT,AETHERA_DATA_DIR). CLI flags override env.
API Routes
| Method | Path | Handler |
|---|---|---|
| GET | /api/settings |
GetSettings |
| POST | /api/settings |
PostSettings |
| GET | /api/models |
GetModels |
| GET | /api/images |
GetImages |
| POST | /api/images |
PostImage |
| DELETE | /api/images/{filename} |
DeleteImage |
| GET | /api/chats |
GetChats |
| POST | /api/chats |
PostChat |
| GET | /api/chats/{chatId} |
GetChat |
| POST | /api/chats/{chatId} |
PostChatMessage |
| DELETE | /api/chats/{chatId} |
DeleteChat |
| GET | /api/chats/{chatId}/stream |
GetChatStream |
| POST | /api/chats/{chatId}/stop |
StopChatGeneration |
Testing
- Use
testify(requirefor fatal,assertfor non-fatal) - Test both success and error paths
- Table-driven tests where appropriate
- File-based storage tests use temp directories
- Concurrency safety is verified
Non-Negotiables
- ❌ No unhandled errors — always check
err - ❌ No ignored linter warnings
- ❌ No sensitive data in logs
- ❌ No hardcoded paths — use
path.Join - ❌ No unsafe file access — use
filepath.Base - ❌ Don't skip tests or linting
Code Style
- Tab indentation, PascalCase exports, camelCase internal
- Error wrapping with context:
fmt.Errorf("...: %w", err) - Sentinel errors for domain cases (
ErrChatNotFound,ErrNilChatID) - Struct tags for JSON with
omitempty - Log with context:
log.WithField("key", val) - Clean up resources with
defer