feat: enhance local knowledge retrieval
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import type {
|
||||
RerankErrorCode,
|
||||
RerankSafeError
|
||||
} from '../../shared/rerank-contracts'
|
||||
|
||||
const MAX_SAFE_MESSAGE_LENGTH = 500
|
||||
|
||||
const descriptors: Record<RerankErrorCode, Omit<RerankSafeError, 'code'>> = {
|
||||
model_not_found: {
|
||||
message: '未找到指定的重排模型。',
|
||||
retryable: false,
|
||||
remedy: '请确认模型名称正确,并确认该模型已在服务端启用。'
|
||||
},
|
||||
authentication: {
|
||||
message: '重排服务身份验证失败。',
|
||||
retryable: false,
|
||||
remedy: '请检查访问密钥是否有效以及是否具备调用重排模型的权限。'
|
||||
},
|
||||
rate_limited: {
|
||||
message: '重排服务当前请求过多。',
|
||||
retryable: true,
|
||||
remedy: '请稍后重试,或检查服务配额与速率限制。'
|
||||
},
|
||||
timeout: {
|
||||
message: '重排服务响应超时。',
|
||||
retryable: true,
|
||||
remedy: '请检查网络和服务状态,然后重试。'
|
||||
},
|
||||
network: {
|
||||
message: '无法连接到重排服务。',
|
||||
retryable: true,
|
||||
remedy: '请检查服务地址、网络连接和代理设置。'
|
||||
},
|
||||
provider_unavailable: {
|
||||
message: '重排服务暂时不可用。',
|
||||
retryable: true,
|
||||
remedy: '请稍后重试并检查服务运行状态。'
|
||||
},
|
||||
invalid_configuration: {
|
||||
message: '重排模型配置无效。',
|
||||
retryable: false,
|
||||
remedy: '请检查服务地址、模型名称和配置参数。'
|
||||
},
|
||||
invalid_response: {
|
||||
message: '重排服务返回了无效结果。',
|
||||
retryable: false,
|
||||
remedy: '请确认服务兼容 Cohere 重排接口并返回有效分数。'
|
||||
},
|
||||
cancelled: {
|
||||
message: '重排操作已取消。',
|
||||
retryable: true
|
||||
},
|
||||
unknown: {
|
||||
message: '重排操作失败。',
|
||||
retryable: false,
|
||||
remedy: '请检查重排服务配置后重试。'
|
||||
}
|
||||
}
|
||||
|
||||
function errorText(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return `${error.name} ${error.message}`.toLowerCase()
|
||||
}
|
||||
return typeof error === 'string' ? error.toLowerCase() : ''
|
||||
}
|
||||
|
||||
function numericStatus(error: unknown): number | undefined {
|
||||
if (typeof error !== 'object' || error === null) {
|
||||
return undefined
|
||||
}
|
||||
for (const key of ['status', 'statusCode', 'code'] as const) {
|
||||
const value = Reflect.get(error, key)
|
||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string' && /^\d{3}$/u.test(value)) {
|
||||
return Number(value)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function statusFromText(text: string): number | undefined {
|
||||
const match = /\b(?:http|status(?: code)?)\s*[:=]?\s*(\d{3})\b/iu.exec(
|
||||
text
|
||||
)
|
||||
return match?.[1] ? Number(match[1]) : undefined
|
||||
}
|
||||
|
||||
function hasAny(text: string, patterns: readonly string[]): boolean {
|
||||
return patterns.some((pattern) => text.includes(pattern))
|
||||
}
|
||||
|
||||
function classifyCode(
|
||||
error: unknown,
|
||||
options: { cancelled?: boolean; timedOut?: boolean }
|
||||
): RerankErrorCode {
|
||||
const text = errorText(error)
|
||||
const status = numericStatus(error) ?? statusFromText(text)
|
||||
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
return 'timeout'
|
||||
}
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
return 'cancelled'
|
||||
}
|
||||
if (options.timedOut) {
|
||||
return 'timeout'
|
||||
}
|
||||
if (
|
||||
options.cancelled ||
|
||||
hasAny(text, ['aborterror', 'aborted', 'cancelled', 'canceled'])
|
||||
) {
|
||||
return 'cancelled'
|
||||
}
|
||||
if (hasAny(text, ['timeout', 'timed out', 'etimedout'])) {
|
||||
return 'timeout'
|
||||
}
|
||||
if (
|
||||
status === 401 ||
|
||||
status === 403 ||
|
||||
hasAny(text, [
|
||||
'unauthorized',
|
||||
'forbidden',
|
||||
'authentication',
|
||||
'invalid api key',
|
||||
'incorrect api key'
|
||||
])
|
||||
) {
|
||||
return 'authentication'
|
||||
}
|
||||
if (
|
||||
status === 404 ||
|
||||
hasAny(text, [
|
||||
'model not found',
|
||||
'model_not_found',
|
||||
'unknown model',
|
||||
'does not exist'
|
||||
])
|
||||
) {
|
||||
return 'model_not_found'
|
||||
}
|
||||
if (
|
||||
status === 429 ||
|
||||
hasAny(text, ['rate limit', 'rate_limit', 'too many requests', 'quota'])
|
||||
) {
|
||||
return 'rate_limited'
|
||||
}
|
||||
if (status === 408 || status === 504) {
|
||||
return 'timeout'
|
||||
}
|
||||
if (status !== undefined && status >= 500 && status <= 599) {
|
||||
return 'provider_unavailable'
|
||||
}
|
||||
if (
|
||||
hasAny(text, [
|
||||
'econnrefused',
|
||||
'econnreset',
|
||||
'enotfound',
|
||||
'fetch failed',
|
||||
'network',
|
||||
'failed to fetch',
|
||||
'socket'
|
||||
])
|
||||
) {
|
||||
return 'network'
|
||||
}
|
||||
if (
|
||||
hasAny(text, [
|
||||
'endpoint must',
|
||||
'model must',
|
||||
'invalid endpoint',
|
||||
'invalid configuration',
|
||||
'request body is too large'
|
||||
])
|
||||
) {
|
||||
return 'invalid_configuration'
|
||||
}
|
||||
if (
|
||||
error instanceof TypeError ||
|
||||
hasAny(text, [
|
||||
'invalid shape',
|
||||
'invalid result',
|
||||
'invalid index',
|
||||
'invalid score',
|
||||
'result count',
|
||||
'must contain exactly',
|
||||
'response item',
|
||||
'valid json',
|
||||
'response is too large',
|
||||
'invalid response'
|
||||
])
|
||||
) {
|
||||
return 'invalid_response'
|
||||
}
|
||||
if (error instanceof RangeError) {
|
||||
return 'invalid_configuration'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts provider and transport failures to bounded, localized data. Raw
|
||||
* bodies, endpoints, credentials and nested causes are never copied.
|
||||
*/
|
||||
export function classifyRerankError(
|
||||
error: unknown,
|
||||
options: { cancelled?: boolean; timedOut?: boolean } = {}
|
||||
): RerankSafeError {
|
||||
const code = classifyCode(error, options)
|
||||
const descriptor = descriptors[code]
|
||||
return {
|
||||
code,
|
||||
message: descriptor.message.slice(0, MAX_SAFE_MESSAGE_LENGTH),
|
||||
retryable: descriptor.retryable,
|
||||
...(descriptor.remedy
|
||||
? { remedy: descriptor.remedy.slice(0, MAX_SAFE_MESSAGE_LENGTH) }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
export class RerankOperationError extends Error {
|
||||
readonly code: RerankErrorCode
|
||||
readonly retryable: boolean
|
||||
readonly remedy?: string
|
||||
|
||||
constructor(error: RerankSafeError) {
|
||||
super(error.message)
|
||||
this.name = 'RerankOperationError'
|
||||
this.code = error.code
|
||||
this.retryable = error.retryable
|
||||
this.remedy = error.remedy
|
||||
}
|
||||
|
||||
toSafeError(): RerankSafeError {
|
||||
return {
|
||||
code: this.code,
|
||||
message: this.message,
|
||||
retryable: this.retryable,
|
||||
...(this.remedy ? { remedy: this.remedy } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toRerankOperationError(
|
||||
error: unknown,
|
||||
options?: { cancelled?: boolean; timedOut?: boolean }
|
||||
): RerankOperationError {
|
||||
return error instanceof RerankOperationError
|
||||
? error
|
||||
: new RerankOperationError(classifyRerankError(error, options))
|
||||
}
|
||||
Reference in New Issue
Block a user