PRIX / src /bot.ts
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
8.44 kB
// Node 20 includes native fetch support
// AI Client for Groq with Failover Support + Circuit Breaker
// Fallback shims for Probot runtime
/* eslint-disable no-console */
import {
ChatGPTAPI as AIClient,
ChatGPTError as AIError,
ChatMessage,
SendMessageOptions
} from 'chatgpt'
import pRetry from 'p-retry'
import CircuitBreaker from 'opossum'
import {AIOptions, Options} from './options'
import {TokenLimits} from './limits'
import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
// Circuit breaker configuration
const CIRCUIT_BREAKER_OPTIONS = {
timeout: 60000, // 60 seconds
errorThresholdPercentage: 50,
resetTimeout: 30000,
volumeThreshold: 5,
}
const logInfo = (msg: string) => logger.info(msg)
const logWarning = (msg: string) => logger.warn(msg)
const logError = (msg: string) => logger.error(msg)
const setFailed = (msg: string) => {
logger.error(msg)
process.exit(1)
}
export interface Ids {
parentMessageId?: string
conversationId?: string
}
export class Bot {
private api: AIClient | null = null
private readonly options: Options
private readonly aiOptions: AIOptions
private currentModel: string
private fallbackModels: string[]
private circuitBreaker: CircuitBreaker | null = null
constructor(options: Options, aiOptions: AIOptions) {
this.options = options
this.aiOptions = aiOptions
this.currentModel = aiOptions.model
// Setup fallback chain based on tier
if (Options.HEAVY_MODELS.includes(this.currentModel)) {
this.fallbackModels = [...Options.HEAVY_MODELS]
} else {
this.fallbackModels = [...Options.LIGHT_MODELS]
}
// Remove current model from fallbacks to avoid immediate repeat
this.fallbackModels = this.fallbackModels.filter(
m => m !== this.currentModel
)
this.initClient()
this.initCircuitBreaker()
}
private initCircuitBreaker(): void {
if (!this.api) return
// Wrap the sendMessage method with circuit breaker
this.circuitBreaker = new CircuitBreaker(
async ({ message, opts }: { message: string; opts: SendMessageOptions }) => {
return this.api!.sendMessage(message, opts)
},
CIRCUIT_BREAKER_OPTIONS
)
// Circuit breaker event handlers
this.circuitBreaker.on('open', () => {
logWarning(`Circuit breaker opened for model: ${this.currentModel}`)
})
this.circuitBreaker.on('halfOpen', () => {
logInfo(`Circuit breaker half-open for model: ${this.currentModel}`)
})
this.circuitBreaker.on('close', () => {
logInfo(`Circuit breaker closed for model: ${this.currentModel}`)
})
this.circuitBreaker.on('fallback', (_result: unknown) => {
logWarning(`Circuit breaker fallback executed for model: ${this.currentModel}`)
})
}
private initClient(): void {
const apiKey = process.env.GROQ_API_KEY || process.env.AI_API_KEY
if (apiKey) {
const currentDate = new Date().toISOString().split('T')[0]
const limits = new TokenLimits(this.currentModel)
const systemMessage = `${this.options.systemMessage}
Knowledge cutoff: ${limits.knowledgeCutOff}
Current date: ${currentDate}
IMPORTANT: Entire response must be in ISO code: ${this.options.language}
`
this.api = new AIClient({
apiBaseUrl: this.options.apiBaseUrl,
systemMessage,
apiKey,
apiOrg: process.env.AI_API_ORG || undefined,
debug: this.options.debug,
maxModelTokens: limits.maxTokens,
maxResponseTokens: limits.responseTokens,
completionParams: {
temperature: this.options.modelTemperature,
model: this.currentModel
}
})
logInfo(`Initialized AI client with model: ${this.currentModel}`)
} else {
throw new Error("Missing 'GROQ_API_KEY' or 'AI_API_KEY'")
}
}
chat = async (message: string, ids: Ids): Promise<[string, Ids]> => {
let res: [string, Ids] = ['', {}]
try {
res = await this.chat_(message, ids)
return res
} catch (e: unknown) {
if (e instanceof AIError) {
logWarning(`AI communication error: ${e.message}`)
}
return res
}
}
private readonly chat_ = async (
message: string,
ids: Ids
): Promise<[string, Ids]> => {
const start = Date.now()
if (!message) return ['', {}]
let response: ChatMessage | undefined
if (this.api != null) {
const opts: SendMessageOptions = {
timeoutMs: this.options.timeoutMS
}
if (ids.parentMessageId) {
opts.parentMessageId = ids.parentMessageId
}
try {
// Check if circuit breaker is open
if (this.circuitBreaker?.opened) {
logWarning(`Circuit breaker is OPEN for ${this.currentModel}, attempting fallback...`)
if (this.fallbackModels.length > 0) {
const nextModel = this.fallbackModels.shift()
if (nextModel) {
this.currentModel = nextModel
this.initClient()
this.initCircuitBreaker()
// Recursive retry with new model
return this.chat_(message, ids)
}
}
throw new Error(`Circuit breaker open and no fallback models available for ${this.currentModel}`)
}
const result = await pRetry<ChatMessage>(
async () => {
if (!this.circuitBreaker) {
throw new Error('Circuit breaker not initialized')
}
try {
const messageResult = await this.circuitBreaker.fire({ message, opts })
return messageResult as ChatMessage
} catch (e: any) {
// Check if circuit breaker opened
if (this.circuitBreaker.opened && this.fallbackModels.length > 0) {
const nextModel = this.fallbackModels.shift()
if (nextModel) {
logWarning(
`Circuit breaker opened for ${this.currentModel}. Switching to fallback: ${nextModel}`
)
this.currentModel = nextModel
this.initClient()
this.initCircuitBreaker()
// Throw to trigger retry with new model
throw new Error(`Model switched to ${nextModel}, retrying...`)
}
}
// Handle rate limiting / context size
if ((e?.status === 429 || e?.status === 413) && this.fallbackModels.length > 0) {
const nextModel = this.fallbackModels.shift()
if (nextModel) {
logWarning(
`Rate limit or context size error (${e.status}) hit for ${this.currentModel}. Switching to fallback: ${nextModel}`
)
this.currentModel = nextModel
this.initClient()
this.initCircuitBreaker()
throw new Error(`Model switched to ${nextModel}, retrying...`)
}
}
throw e
}
},
{
retries: this.options.retries,
minTimeout: 20000, // 20s delay to allow TPM refresh
onFailedAttempt: (err: any) => {
logInfo(
`Attempt ${err.attemptNumber} failed. ${err.retriesLeft} retries left. Error: ${err.message}`
)
}
}
)
response = result
} catch (e: unknown) {
if (e instanceof AIError) {
logWarning(
`AI Error after retries: ${e.message} (Last model used: ${this.currentModel})`
)
} else if (e instanceof Error) {
logError(`Failed to get AI response: ${e.message}`)
}
}
const end = Date.now()
logInfo(`AI response time: ${end - start} ms`)
} else {
setFailed('AI client is not initialized')
}
let responseText = ''
if (response != null) {
responseText = response.text
}
const newIds: Ids = {
parentMessageId: response?.id,
conversationId: response?.conversationId
}
return [responseText, newIds]
}
}