lume-ai
Version:
A powerful yet simple library to build your own AI applications.
317 lines (302 loc) • 10.3 kB
text/typescript
// ===============================
// SECTION | IMPORTS
// ===============================
import AnthropicProvider from '@anthropic-ai/sdk'
import { LLM, Message, Tool } from '../interfaces'
import { VoyageAIClient } from 'voyageai'
// ===============================
// ===============================
// SECTION | Anthropic
// ===============================
/**
* Implementation of the LLM interface for Anthropic's Claude models.
* Handles message formatting and API interaction for Anthropic.
*/
export class Anthropic extends LLM {
/**
* The Anthropic SDK client instance.
*/
protected llm: AnthropicProvider
/**
* The Voyage AI client instance.
*/
private voyage: VoyageAIClient
/**
* Whether to log debug information.
*/
private debug: boolean
/**
* Constructs a new Anthropic LLM instance.
* @param apiKey - The API key for authenticating with Anthropic.
* @param debug - Optional flag to enable debug logging.
*/
constructor(apiKey: string, debug: boolean = false) {
super()
if (!apiKey || typeof apiKey !== 'string') {
throw new Error('Anthropic: apiKey must be a non-empty string')
}
this.llm = new AnthropicProvider({ apiKey })
this.voyage = new VoyageAIClient({ apiKey })
this.debug = debug
}
private logDebug(message: string, ...args: any[]) {
if (this.debug) {
// eslint-disable-next-line no-console
console.debug(`[Anthropic DEBUG] ${message}`, ...args)
}
}
/**
* Gets a response from the Anthropic Claude model based on the provided text and options.
* @param text - The user's input message.
* @param options - Optional parameters including message history and tags for context.
* @returns A promise that resolves to the model's response as a string.
*/
async getResponse(
text: string,
options: {
history?: Message[]
tags?: string[]
vectorMatches?: string[]
tools?: Tool[]
llmOptions: {
systemPrompt: string
model?: string
temperature?: number
maxTokens?: number
topP?: number
}
toolCallId?: string
toolCall?: any
toolCallDepth?: number
toolResult?: string
}
): Promise<string> {
if (!text || typeof text !== 'string') {
throw new Error('Anthropic.getResponse: text must be a non-empty string')
}
if (!options || typeof options !== 'object') {
throw new Error(
'Anthropic.getResponse: options must be provided as an object'
)
}
if (!options.llmOptions || typeof options.llmOptions !== 'object') {
throw new Error('Anthropic.getResponse: llmOptions must be provided')
}
try {
this.logDebug('Requesting response', { text, options })
const response = await this.llm.messages.create({
model: options.llmOptions.model || 'claude-3-5-sonnet-latest',
max_tokens: options.llmOptions.maxTokens || 1000,
system: options.llmOptions.systemPrompt,
messages: [
...(options.history || []),
{ role: 'user', content: text },
// --> Tool calls
...((options.toolCallId && options.toolCall
? [
{ role: 'assistant', content: options.toolCall },
{
role: 'user',
content: [
{
tool_use_id: options.toolCallId,
content: options.toolResult,
type: 'tool_result',
},
],
},
]
: []) as AnthropicProvider.Messages.MessageParam[]),
],
temperature: options.llmOptions.temperature || 0.5,
top_p: options.llmOptions.topP || 1,
tools: options.tools?.map((tool) => this.parseTool(tool)),
})
this.logDebug('Received response', response)
// Defensive: check response structure
if (!response || typeof response !== 'object') {
throw new Error('Anthropic.getResponse: Invalid response from API')
}
// --> Process tools
if (response.stop_reason === 'pause_turn') {
this.logDebug('stop_reason: pause_turn, retrying...')
return await this.getResponse(text, {
...options,
})
} else if (response.stop_reason === 'tool_use') {
const toolCall = response.content
if (!Array.isArray(toolCall)) {
throw new Error('Anthropic.getResponse: toolCall is not an array')
}
const toolCallContent = toolCall.find((c) => c && c.type === 'tool_use')
if (toolCallContent) {
const tool = options.tools?.find(
(t) => t?.metadata?.name === toolCallContent?.name
)
if (!tool) {
this.logDebug('Tool not found for tool_call', toolCallContent)
return 'Tool not found'
}
let result: any
try {
result = await tool.execute(toolCallContent.input as object)
} catch (err) {
this.logDebug('Error executing tool', err)
return `Error executing tool: ${
err instanceof Error ? err.message : String(err)
}`
}
return await this.getResponse(text, {
...options,
toolCallId: toolCallContent.id,
toolCall,
toolCallDepth: options.toolCallDepth || 0,
toolResult: result,
})
}
}
// Defensive: check content structure
if (!Array.isArray(response.content) || response.content.length === 0) {
this.logDebug('No content in response')
return 'No response from the model'
}
if ('text' in response.content[0]) {
return response.content[0].text
}
this.logDebug('No text in response content')
return 'No response from the model'
} catch (err) {
this.logDebug('Error in getResponse', err)
return `Anthropic.getResponse error: ${
err instanceof Error ? err.message : String(err)
}`
}
}
/**
* Streams a response from the Anthropic Claude model based on the provided text and options.
* @param text - The user's input message.
* @param options - Optional parameters including message history and tags for context.
* @returns A promise that resolves to the model's response as a string.
*/
async *streamResponse(
text: string,
options: {
history?: Message[]
tags?: string[]
vectorMatches?: string[]
tools?: Tool[]
llmOptions: {
systemPrompt: string
model?: string
temperature?: number
maxTokens?: number
topP?: number
}
}
) {
if (!text || typeof text !== 'string') {
throw new Error(
'Anthropic.streamResponse: text must be a non-empty string'
)
}
if (!options || typeof options !== 'object') {
throw new Error(
'Anthropic.streamResponse: options must be provided as an object'
)
}
if (!options.llmOptions || typeof options.llmOptions !== 'object') {
throw new Error('Anthropic.streamResponse: llmOptions must be provided')
}
try {
this.logDebug('Requesting stream response', { text, options })
const response = await this.llm.messages.create({
model: options.llmOptions.model || 'claude-3-5-sonnet-latest',
max_tokens: options.llmOptions.maxTokens || 1000,
system: options.llmOptions.systemPrompt,
messages: [...(options.history || []), { role: 'user', content: text }],
temperature: options.llmOptions.temperature || 0.5,
top_p: options.llmOptions.topP || 1,
stream: true,
})
for await (const chunk of response) {
if (chunk && typeof chunk === 'object') {
if ('delta' in chunk && chunk.delta && 'text' in chunk.delta) {
yield chunk.delta.text as string
} else if ('text' in chunk) {
yield chunk.text as string
}
}
}
} catch (err) {
this.logDebug('Error in streamResponse', err)
yield `Anthropic.streamResponse error: ${
err instanceof Error ? err.message : String(err)
}`
}
}
/**
* Gets an embedding from the Anthropic Claude model based on the provided text.
* @param text - The input text to get an embedding for.
* @returns A promise that resolves to the model's embedding as an array of numbers.
*/
async getEmbedding(text: string) {
if (!text || typeof text !== 'string') {
throw new Error('Anthropic.getEmbedding: text must be a non-empty string')
}
try {
this.logDebug('Requesting embedding', { text })
const response = await this.voyage.embed({
input: text,
model: 'voyage-3',
})
if (
!response ||
typeof response !== 'object' ||
!Array.isArray(response.data)
) {
throw new Error(
'Anthropic.getEmbedding: Invalid response from VoyageAI'
)
}
return response.data?.[0]?.embedding || []
} catch (err) {
this.logDebug('Error in getEmbedding', err)
return []
}
}
/**
* Parses a tool into an object.
* @param tool - The tool to parse.
* @returns An object representing the tool compatible with the LLM.
*/
parseTool(tool: Tool): AnthropicProvider.Messages.ToolUnion {
if (!tool || typeof tool !== 'object' || !tool.metadata) {
throw new Error('Anthropic.parseTool: tool must be a valid Tool object')
}
const meta = tool.metadata
if (!meta.name || !meta.parameters) {
throw new Error(
'Anthropic.parseTool: tool metadata must have name and parameters'
)
}
const properties: Record<string, any> = {}
const required: string[] = []
for (const param of meta.parameters) {
properties[param.name] = {
type: param.type,
description: param.description,
}
if (param.required) required.push(param.name)
}
return {
name: meta.name,
description: meta.description,
input_schema: {
type: 'object',
properties,
required,
},
}
}
}
// ===============================