AI Classification
How it works
When a task is created, Doto fires an async classification job (non-blocking — the task is returned immediately):
classifyTask(provider, { title, description })is called- Two requests run in parallel:
classify()for tags,suggestPriority()for priority - Results are written back to the task:
tagsandaiClassification
If the provider is null (not configured) or any request fails or times out (30s), the task is left with empty tags and aiClassification: null. Task creation never fails due to AI errors.
Configuration
Set these environment variables:
# Explicit provider and model
AI_PROVIDER=openai
AI_MODEL=gpt-4o-mini # optional, auto-selects if omitted
OPENAI_API_KEY=sk-...
# Or use Anthropic
AI_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-ant-...
# Optional — override API endpoints (e.g. a proxy or gateway)
OPENAI_BASE_URL=https://my-proxy.example.com/v1
ANTHROPIC_BASE_URL=https://my-proxy.example.comIf AI_PROVIDER is not set, the factory auto-detects based on which key is present (OpenAI → Anthropic). OPENAI_BASE_URL / ANTHROPIC_BASE_URL are optional; when unset, each provider's default endpoint is used.
The AI layer is powered by the Vercel AI SDK, using the first-party OpenAI and Anthropic providers.
The provider interface
// packages/server/src/ai/provider.ts
export interface AiProvider {
classify(task: { title: string; description?: string }): Promise<ClassificationResult | null>;
suggestPriority(task: { title: string; description?: string }): Promise<TaskPriority | null>;
}Both methods must return null on any failure — never throw.
Adding a new provider
- Create
packages/server/src/ai/providers/<name>.tsimplementingAiProvider - Handle the 30s timeout using
AbortController - Catch all errors and return
null - Register in
src/ai/factory.ts:
if (provider === 'myprovider') {
const apiKey = process.env.MYPROVIDER_API_KEY;
if (!apiKey) return null;
return new MyProvider(apiKey);
}Stored data
aiClassification is a JSON field on the task:
{
"tags": ["bug", "frontend", "auth"],
"suggestedPriority": "P1",
"reasoning": "Login issues are high priority user-facing bugs"
}suggestedPriority is stored but never auto-applied to the task's priority field. The user decides whether to act on it.