donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
307 lines • 12.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AnthropicGptClient = void 0;
const Logger_1 = require("../utils/Logger");
const JsonUtils_1 = require("../utils/JsonUtils");
const GptClient_1 = require("./GptClient");
const GptModelNotFoundException_1 = require("../exceptions/GptModelNotFoundException");
const GptPlatformAuthenticationFailedException_1 = require("../exceptions/GptPlatformAuthenticationFailedException");
const GptPlatformInternalErrorException_1 = require("../exceptions/GptPlatformInternalErrorException");
const GptPlatformNotReachableException_1 = require("../exceptions/GptPlatformNotReachableException");
/**
* A GPT client implemented using the Anthropic API
* @see https://docs.anthropic.com/en/api/messages
*/
class AnthropicGptClient extends GptClient_1.GptClient {
/**
* Create a new instance.
*/
constructor(anthropicConfig) {
super(anthropicConfig);
this.anthropicConfig = anthropicConfig;
this.headers = new Headers({
[AnthropicGptClient.API_KEY_HEADER_NAME]: anthropicConfig.apiKey,
[AnthropicGptClient.API_VERSION_HEADER_NAME]: AnthropicGptClient.API_VERSION_HEADER_VALUE,
'Content-Type': AnthropicGptClient.CONTENT_TYPE_HEADER_VALUE,
});
}
async ping() {
const resp = await this.makeRequest(`/v1/models/${this.anthropicConfig.modelName}`);
if (resp.status === 404) {
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, this.anthropicConfig.modelName);
}
else if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
}
async getMessage(messages) {
const systemPrompts = messages
.filter((msg) => msg.type === 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage);
const body = {
model: this.anthropicConfig.modelName,
max_tokens: AnthropicGptClient.MAX_TOKENS,
temperature: 0.0,
system: systemPrompts,
messages: messages
.filter((msg) => msg.type !== 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage),
};
AnthropicGptClient.shenanigansUserMessageMerge(body.messages);
const resp = await this.makeRequest('/v1/messages', 'POST', body);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
const data = await resp.json();
const text = data.content[0].text;
const promptTokensUsed = data.usage.input_tokens;
const completionTokensUsed = data.usage.output_tokens;
return {
type: 'assistant',
text,
promptTokensUsed,
completionTokensUsed,
};
}
async getStructuredOutput(messages, jsonSchema) {
const systemPrompts = messages
.filter((msg) => msg.type === 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage);
const body = {
model: this.anthropicConfig.modelName,
max_tokens: AnthropicGptClient.MAX_TOKENS,
temperature: 0.0,
system: systemPrompts,
messages: messages
.filter((msg) => msg.type !== 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage),
tools: [
{
name: 'StructuredOutputTool',
description: 'Call this tool with the described parameters',
input_schema: jsonSchema,
},
],
tool_choice: {
name: 'StructuredOutputTool',
type: 'tool',
disable_parallel_tool_use: true,
},
};
AnthropicGptClient.shenanigansUserMessageMerge(body.messages);
const resp = await this.makeRequest('/v1/messages', 'POST', body);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
const data = await resp.json();
const item = data.content[0];
const contentType = item.type;
let respObj;
if (contentType === 'tool_use') {
respObj = item.input;
}
else if (contentType === 'text') {
throw new Error('Unsupported content type: text');
}
else {
throw new Error(`Unexpected content type: ${contentType}`);
}
const promptTokensUsed = data.usage.input_tokens;
const completionTokensUsed = data.usage.output_tokens;
return {
type: 'structured_output',
output: respObj,
promptTokensUsed,
completionTokensUsed,
};
}
async getToolCalls(messages, tools) {
const systemPrompts = messages
.filter((msg) => msg.type === 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage);
if (systemPrompts.length > 0) {
const lastPrompt = systemPrompts[systemPrompts.length - 1];
lastPrompt.cache_control = { type: 'ephemeral' };
}
const body = {
model: this.anthropicConfig.modelName,
max_tokens: AnthropicGptClient.MAX_TOKENS,
temperature: 0.0,
tool_choice: { type: 'any' },
tools: tools.length
? tools.map(AnthropicGptClient.toolChoiceFromTool)
: undefined,
system: systemPrompts,
messages: messages
.filter((msg) => msg.type !== 'system')
.map(AnthropicGptClient.chatRequestMessageFromGptMessage),
};
AnthropicGptClient.shenanigansUserMessageMerge(body.messages);
// Note the special header for this endpoint
const headers = new Headers(this.headers);
headers.append('anthropic-beta', 'prompt-caching-2024-07-31');
const resp = await this.makeRequest('/v1/messages', 'POST', body, headers);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
const data = await resp.json();
const proposedToolCalls = data.content.map((item) => {
const contentType = item.type;
if (contentType === 'tool_use') {
const tool = tools.find((t) => t.name === item.name);
if (!tool) {
throw new Error('Unable to find matching tool for tool call');
}
return {
name: item.name,
parameters: item.input,
toolCallId: item.id,
};
}
else if (contentType === 'text') {
throw new Error('Unsupported content type: text');
}
else {
throw new Error(`Unexpected content type: ${contentType}`);
}
});
const promptTokensUsed = data.usage.input_tokens;
const completionTokensUsed = data.usage.output_tokens;
return {
type: 'proposed_tool_calls',
proposedToolCalls,
promptTokensUsed,
completionTokensUsed,
};
}
async mapErrorResponseToDonobuException(error) {
try {
const errorData = await error.json();
Logger_1.appLogger.error(`Anthropic error response: ${JSON.stringify(JsonUtils_1.JsonUtils.objectToJson(errorData))}`);
if (errorData.error?.code === 'authentication_error') {
return new GptPlatformAuthenticationFailedException_1.GptPlatformAuthenticationFailedException(this.config.type);
}
return new GptPlatformInternalErrorException_1.GptPlatformInternalErrorException(errorData.error?.message || `HTTP ${error.status}: ${error.statusText}`);
}
catch (_) {
Logger_1.appLogger.error(`Failed to parse Anthropic error response: HTTP ${error.status}: ${error.statusText}`);
return new GptPlatformInternalErrorException_1.GptPlatformInternalErrorException(`HTTP ${error.status}: ${error.statusText}`);
}
}
async makeRequest(endpoint, method = 'GET', body, customHeaders) {
try {
return await fetch(`${AnthropicGptClient.API_URL}${endpoint}`, {
method,
headers: customHeaders || this.headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(AnthropicGptClient.REQUEST_TIMEOUT_MILLISECONDS),
});
}
catch (error) {
if (error instanceof TypeError) {
throw new GptPlatformNotReachableException_1.GptPlatformNotReachableException(this.config.type);
}
else {
throw error;
}
}
}
/**
* Merges adjacent user messages because Anthropic will reject requests that do not delicately
* flip-flop between "user" and "assistant" roles.
*/
static shenanigansUserMessageMerge(messages) {
for (let i = messages.length - 1; i > 0; i--) {
const message = messages[i];
const adjacentMessage = messages[i - 1];
if (message.role === 'user' && adjacentMessage.role === 'user') {
adjacentMessage.content.push(...message.content);
messages.splice(i, 1);
}
}
}
static chatRequestMessageFromGptMessage(gptMessage) {
if (gptMessage.type === 'assistant') {
return {
role: 'assistant',
content: gptMessage.text,
};
}
if (gptMessage.type === 'structured_output') {
const output = gptMessage.output;
return {
role: 'assistant',
content: JSON.stringify(JsonUtils_1.JsonUtils.objectToJson(output), null, 2),
};
}
if (gptMessage.type === 'proposed_tool_calls') {
return {
role: 'assistant',
content: gptMessage.proposedToolCalls.map((tc) => ({
type: 'tool_use',
id: tc.toolCallId,
name: tc.name,
input: JsonUtils_1.JsonUtils.objectToJson(tc.parameters),
})),
};
}
if (gptMessage.type === 'system') {
return {
type: 'text',
text: gptMessage.text,
};
}
if (gptMessage.type === 'user') {
return {
role: 'user',
content: gptMessage.items.map((item) => {
if (item.type === 'png') {
return {
type: 'image',
source: {
type: 'base64',
media_type: 'image/png',
data: Buffer.from(item.bytes).toString('base64'),
},
};
}
else {
return {
type: 'text',
text: item.text,
};
}
}),
};
}
if (gptMessage.type === 'tool_call_result') {
return {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: gptMessage.toolCallId,
content: gptMessage.data,
},
],
};
}
throw new Error(`Unsupported message type: ${JsonUtils_1.JsonUtils.objectToJson(gptMessage)}`);
}
static toolChoiceFromTool(tool) {
return {
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
};
}
}
exports.AnthropicGptClient = AnthropicGptClient;
AnthropicGptClient.API_URL = 'https://api.anthropic.com';
AnthropicGptClient.REQUEST_TIMEOUT_MILLISECONDS = 120000;
AnthropicGptClient.API_KEY_HEADER_NAME = 'x-api-key';
AnthropicGptClient.API_VERSION_HEADER_NAME = 'anthropic-version';
AnthropicGptClient.API_VERSION_HEADER_VALUE = '2023-06-01';
AnthropicGptClient.CONTENT_TYPE_HEADER_VALUE = 'application/json';
AnthropicGptClient.MAX_TOKENS = 8192;
//# sourceMappingURL=AnthropicGptClient.js.map