UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

352 lines 14.6 kB
"use strict"; 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