donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
352 lines • 14.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAiGptClient = void 0;
const GptClient_1 = require("./GptClient");
const Logger_1 = require("../utils/Logger");
const JsonUtils_1 = require("../utils/JsonUtils");
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 OpenAI API.
* @see https://platform.openai.com/docs/api-reference/chat
*/
class OpenAiGptClient extends GptClient_1.GptClient {
/**
* Create a new instance.
* @param apiKey The OpenAI API key to use for all requests with this client.
* @param modelName See https://platform.openai.com/docs/models for the list of models.
* @param apiUrl The URL of the API to use for all requests with this client.
*/
constructor(openAiConfig) {
super(openAiConfig);
this.openAiConfig = openAiConfig;
this.headers = new Headers({
Authorization: `Bearer ${openAiConfig.apiKey}`,
'Content-Type': 'application/json',
});
}
async ping() {
const resp = await this.makeRequest(`/v1/models/${this.openAiConfig.modelName}`);
if (resp.status === 404) {
throw new GptModelNotFoundException_1.GptModelNotFoundException(this.config.type, this.openAiConfig.modelName);
}
else if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
}
async getMessage(messages) {
const body = {
model: this.openAiConfig.modelName,
temperature: 0.0,
messages: messages.map((msg) => this.chatRequestMessageFromGptMessage(msg)),
};
const resp = await this.makeRequest('/v1/chat/completions', 'POST', body);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
else {
const data = await resp.json();
return this.parseAssistantMessage(data);
}
}
async getStructuredOutput(messages, jsonSchema) {
const body = {
model: this.openAiConfig.modelName,
temperature: 0.0,
response_format: {
type: 'json_schema',
json_schema: {
name: jsonSchema.title || 'output',
strict: true,
schema: OpenAiGptClient.createOpenAiCompatibleJsonSchema(jsonSchema),
},
},
messages: messages.map((msg) => this.chatRequestMessageFromGptMessage(msg)),
};
const resp = await this.makeRequest('/v1/chat/completions', 'POST', body);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
else {
const data = await resp.json();
return this.parseStructuredOutputMessage(data);
}
}
async getToolCalls(messages, tools) {
const body = {
model: this.openAiConfig.modelName,
temperature: 0.0,
tool_choice: 'required',
tools: tools.length > 0
? tools.map((tool) => this.toolChoiceFromTool(tool))
: undefined,
messages: messages.map((msg) => this.chatRequestMessageFromGptMessage(msg)),
};
const resp = await this.makeRequest('/v1/chat/completions', 'POST', body);
if (resp.status !== 200) {
throw await this.mapErrorResponseToDonobuException(resp);
}
else {
const data = await resp.json();
return this.parseProposedToolCallsMessage(tools, data);
}
}
chatRequestMessageFromGptMessage(gptMessage) {
if (gptMessage.type === 'assistant') {
return {
role: 'assistant',
content: { type: 'text', text: gptMessage.text },
};
}
if (gptMessage.type === 'structured_output') {
return {
role: 'assistant',
content: {
type: 'text',
text: JSON.stringify(gptMessage.output, null, 2),
},
};
}
if (gptMessage.type === 'proposed_tool_calls') {
return {
role: 'assistant',
tool_calls: gptMessage.proposedToolCalls.map((tc) => ({
id: tc.toolCallId,
type: 'function',
function: {
name: tc.name,
arguments: JSON.stringify(tc.parameters),
},
})),
};
}
if (gptMessage.type === 'system') {
return {
role: 'system',
content: gptMessage.text,
};
}
if (gptMessage.type === 'user') {
return {
role: 'user',
content: gptMessage.items.map((item) => {
if ('bytes' in item) {
// PNG
return {
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${Buffer.from(item.bytes).toString('base64')}`,
},
};
}
else {
// Text
return {
type: 'text',
text: item.text,
};
}
}),
};
}
if (gptMessage.type === 'tool_call_result') {
return {
role: 'tool',
content: gptMessage.data,
tool_call_id: gptMessage.toolCallId,
};
}
throw new Error(`Unsupported message type: ${JsonUtils_1.JsonUtils.objectToJson(gptMessage)}`);
}
toolChoiceFromTool(tool) {
return {
type: 'function',
function: {
name: tool.name,
description: tool.description.trim(),
parameters: OpenAiGptClient.createOpenAiCompatibleJsonSchema(tool.inputSchema),
},
};
}
parseAssistantMessage(response) {
const text = response.choices[0].message.content;
const promptTokensUsed = response.usage.prompt_tokens;
const completionTokensUsed = response.usage.completion_tokens;
return {
type: 'assistant',
text: text,
promptTokensUsed: promptTokensUsed,
completionTokensUsed: completionTokensUsed,
};
}
parseStructuredOutputMessage(response) {
const text = response.choices[0].message.content;
const output = JsonUtils_1.JsonUtils.jsonStringToJsonObject(text);
if (!output) {
throw new Error('Failed to parse structured output');
}
const promptTokensUsed = response.usage.prompt_tokens;
const completionTokensUsed = response.usage.completion_tokens;
return {
type: 'structured_output',
output: output,
promptTokensUsed: promptTokensUsed,
completionTokensUsed: completionTokensUsed,
};
}
parseProposedToolCallsMessage(tools, response) {
const toolCalls = response.choices[0].message.tool_calls;
if (!toolCalls?.length) {
throw new Error('No tool calls found in response');
}
const proposedCalls = toolCalls.map((tc) => {
const functionName = tc.function.name;
const parameters = JsonUtils_1.JsonUtils.jsonStringToJsonObject(tc.function.arguments);
const toolCallId = tc.id;
const tool = tools.find((t) => t.name === functionName);
if (!tool) {
throw new Error(`Tool not found: ${functionName}`);
}
return {
name: functionName,
parameters: parameters,
toolCallId: toolCallId,
};
});
const promptTokensUsed = response.usage.prompt_tokens;
const completionTokensUsed = response.usage.completion_tokens;
return {
type: 'proposed_tool_calls',
proposedToolCalls: proposedCalls,
promptTokensUsed: promptTokensUsed,
completionTokensUsed: completionTokensUsed,
};
}
async mapErrorResponseToDonobuException(error) {
try {
const errorData = await error.json();
Logger_1.appLogger.error(`OpenAI error response: ${JSON.stringify(JsonUtils_1.JsonUtils.objectToJson(errorData))}`);
// Handle authentication errors
if (errorData.error?.code === 'invalid_api_key') {
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 ${this.config.type} error response: HTTP ${error.status}: ${error.statusText}`);
return new GptPlatformInternalErrorException_1.GptPlatformInternalErrorException(`HTTP ${error.status}: ${error.statusText}`);
}
}
/**
* Makes an HTTP request to the OpenAI API with standard configuration.
*/
async makeRequest(endpoint, method = 'GET', body) {
try {
return await fetch(`${OpenAiGptClient.API_URL}${endpoint}`, {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(OpenAiGptClient.REQUEST_TIMEOUT_MILLISECONDS),
});
}
catch (error) {
if (error instanceof TypeError) {
throw new GptPlatformNotReachableException_1.GptPlatformNotReachableException(this.config.type);
}
else {
throw error;
}
}
}
/**
* Creates a new JSON schema compatible with OpenAI's API by:
* - Adding additionalProperties: false to all objects.
* - Making all properties required (but nullable if necessary).
* - Removing unsupported keywords.
*/
static createOpenAiCompatibleJsonSchema(schema) {
// Deep clone the schema to avoid modifying the original
const clonedSchema = JSON.parse(JSON.stringify(schema));
// Helper function to recursively process schema and its nested schemas
const processSchema = (current) => {
if (!current || typeof current !== 'object') {
return current;
}
// Handle array of schemas (e.g., in oneOf, anyOf, allOf)
if (Array.isArray(current)) {
return current.map((item) => processSchema(item));
}
// Process object type schemas
if (current.type === 'object' || current.properties) {
// Set additionalProperties to false
current.additionalProperties = false;
// Make all properties required
if (current.properties) {
current.required = Object.keys(current.properties);
// Process each property
for (const key in current.properties) {
current.properties[key] = processSchema(current.properties[key]);
}
}
// Remove unsupported object keywords
delete current.patternProperties;
delete current.unevaluatedProperties;
delete current.propertyNames;
delete current.minProperties;
delete current.maxProperties;
}
// Process array type schemas
if (current.type === 'array') {
// Process items schema if it exists
if (current.items) {
current.items = processSchema(current.items);
}
// Remove unsupported array keywords
delete current.unevaluatedItems;
delete current.contains;
delete current.minContains;
delete current.maxContains;
delete current.minItems;
delete current.maxItems;
delete current.uniqueItems;
}
// Process string type schemas
if (current.type === 'string') {
// Remove unsupported string keywords
delete current.minLength;
delete current.maxLength;
delete current.pattern;
delete current.format;
}
// Process number/integer type schemas
if (current.type === 'number' || current.type === 'integer') {
// Remove unsupported number keywords
delete current.minimum;
delete current.maximum;
delete current.multipleOf;
}
// Process combiners (oneOf, anyOf, allOf)
if (current.oneOf)
current.oneOf = processSchema(current.oneOf);
if (current.anyOf)
current.anyOf = processSchema(current.anyOf);
if (current.allOf)
current.allOf = processSchema(current.allOf);
// Process nested schemas
if (current.not)
current.not = processSchema(current.not);
if (current.then)
current.then = processSchema(current.then);
if (current.else)
current.else = processSchema(current.else);
return current;
};
return processSchema(clonedSchema);
}
}
exports.OpenAiGptClient = OpenAiGptClient;
OpenAiGptClient.API_URL = 'https://api.openai.com';
OpenAiGptClient.REQUEST_TIMEOUT_MILLISECONDS = 120000;
//# sourceMappingURL=OpenAiGptClient.js.map