All files adapter.ts

100% Statements 44/44
100% Branches 26/26
100% Functions 5/5
100% Lines 44/44

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                  1x 21x 21x 21x   21x 21x 21x   21x   21x 16x 12x 12x 16x 16x 12x 16x 16x 16x 16x 16x 16x 16x 16x   21x 16x 15x 15x   13x 13x 13x 13x 13x 13x 16x 4x 4x 2x 2x     2x 2x 2x 2x 16x 21x              
/**
 * TimesfmNodeAdapter — IForecaster implementation using @agentix-e/timesfm-node.
 *
 * Uses dynamic import to load the optional dependency at runtime.
 * If @agentix-e/timesfm-node is not installed, forecast() throws a
 * descriptive error with installation instructions.
 */
import type { IForecaster, DataPoint, ForecastResult, ForecasterType } from '@agentix-e/anomaly-detector-core'
 
export class TimesfmNodeAdapter implements IForecaster {
  readonly type: ForecasterType = 'timesfm'
  private config: TimesfmConfig
  private engine: unknown = null
 
  constructor(config?: TimesfmConfig) {
    this.config = { model: 'timesfm-2.5-200m', contextWindow: 1024, horizon: 64, ...config }
  }
 
  get modelName(): string { return `TimesFM-${this.config.model}` }
 
  async forecast(context: DataPoint[], horizon: number): Promise<ForecastResult> {
    const engine = await this.ensureEngine()
    const result = await (engine as any).forecast(
      context.map(p => p.value),
      { contextLength: this.config.contextWindow, horizon: horizon ?? this.config.horizon }
    )
    return {
      predicted: result.pointForecast ? Array.from(result.pointForecast) : [],
      q10: result.quantileForecast ? Array.from((result.quantileForecast as number[][])[2] ?? []) : [],
      q90: result.quantileForecast ? Array.from((result.quantileForecast as number[][])[8] ?? []) : [],
      horizon: horizon ?? this.config.horizon,
      modelName: this.modelName,
      predictedAt: Date.now(),
    }
  }
 
  private async ensureEngine(): Promise<unknown> {
    if (this.engine) return this.engine
    try {
      const mod = await import('@agentix-e/timesfm-node')
       
      this.engine = new (mod).TimesFMNodeEngine({
        modelName: this.config.model,
        maxContext: this.config.contextWindow,
        maxHorizon: this.config.horizon,
      })
      return this.engine
    } catch (err) {
      const msg = (err as Error).message
      if (msg.includes('Cannot find') || msg.includes('ERR_MODULE_NOT_FOUND')) {
        throw new Error(
          'TimesFM is not installed. Install it as an optional dependency:\n' +
          '  npm install @agentix-e/timesfm-node\n' +
          'Or use anofox-forecast (default): createDetector({ forecaster: { type: "anofox" } })'
        )
      }
      throw err
    }
  }
}
 
export interface TimesfmConfig {
  model?: string
  contextWindow?: number
  horizon?: number
}