All files / apps/work-orders-service/src/triage anthropic-triage.classifier.ts

100% Statements 31/31
100% Branches 16/16
100% Functions 3/3
100% Lines 26/26

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 991x 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;
    }
  }
}