Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 1x 9x 8x 8x 5x 3x 3x 2x 2x 1x 3x 1x 2x 3x | import { Injectable, Logger } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import {
TRIAGE_CATEGORIES,
TRIAGE_URGENCIES,
WorkOrderTriage,
} from '@app/contracts';
import { TriageClassifier, TriageInput } from './triage-classifier';
/**
* The JSON schema does double duty: it constrains generation (the API
* guarantees the response parses and the enums hold) and it documents the
* contract — the same closed vocabularies consumers see in @app/contracts.
*/
const TRIAGE_SCHEMA = {
type: 'object',
properties: {
category: { type: 'string', enum: [...TRIAGE_CATEGORIES] },
urgency: { type: 'string', enum: [...TRIAGE_URGENCIES] },
reasoning: {
type: 'string',
description: 'One sentence justifying the classification.',
},
},
required: ['category', 'urgency', 'reasoning'],
additionalProperties: false,
} as const;
const SYSTEM_PROMPT = `You triage maintenance requests for residential properties.
Classify each request into a category and an urgency level.
Urgency guidance: "emergency" means active danger or damage in progress (gas leak, flooding, no heat in winter); "high" means the home is significantly impaired; "medium" means inconvenient but livable; "low" means cosmetic or routine.
The tenant-reported priority is a hint, not ground truth — tenants overstate and understate.`;
@Injectable()
export class AnthropicTriageClassifier extends TriageClassifier {
private readonly logger = new Logger(AnthropicTriageClassifier.name);
private readonly client: Anthropic | null;
private readonly model = process.env.TRIAGE_MODEL ?? 'claude-opus-4-8';
constructor() {
super();
// Explicit key check instead of letting the SDK resolve credentials:
// a server workload should be deterministic about whether triage is on.
this.client = process.env.ANTHROPIC_API_KEY ? new Anthropic() : null;
if (!this.client) {
this.logger.warn('ANTHROPIC_API_KEY not set, triage is disabled');
}
}
async classify(input: TriageInput): Promise<WorkOrderTriage | null> {
if (!this.client) return null;
try {
const response = await this.client.messages.create({
model: this.model,
max_tokens: 1024,
// Simple classification: skip thinking, spend little.
output_config: {
effort: 'low',
format: { type: 'json_schema', schema: TRIAGE_SCHEMA },
},
system: SYSTEM_PROMPT,
messages: [
{
role: 'user',
content: [
`Title: ${input.title}`,
`Description: ${input.description}`,
`Tenant-reported priority: ${input.priority}`,
].join('\n'),
},
],
});
if (response.stop_reason === 'refusal') {
this.logger.warn('classification refused by the model');
return null;
}
const text = response.content.find((block) => block.type === 'text');
if (!text) return null;
return JSON.parse(text.text) as WorkOrderTriage;
} catch (error) {
// Triage must never break the event flow: rate limits, outages and
// bad requests all degrade to "no classification".
if (error instanceof Anthropic.APIError) {
this.logger.error(
`Anthropic API error ${error.status}: ${error.message}`,
);
} else {
this.logger.error(
'unexpected triage failure',
error instanceof Error ? error.stack : String(error),
);
}
return null;
}
}
}
|