@endlessriver/optimaiz
Version:
Client SDK for interacting with the Optimaiz logging & trace system.
483 lines (480 loc) • 19.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OptimaizClient = exports.OptimaizServerError = exports.OptimaizValidationError = exports.OptimaizAuthenticationError = exports.OptimaizError = void 0;
const crypto_1 = __importDefault(require("crypto"));
const DEFAULT_BASE_URL = "https://www.optimaiz.io";
// Custom error types for better error handling
class OptimaizError extends Error {
constructor(message, status, details, type) {
super(message);
this.name = 'OptimaizError';
this.status = status;
this.details = details;
this.type = type;
}
}
exports.OptimaizError = OptimaizError;
class OptimaizAuthenticationError extends OptimaizError {
constructor(message, details) {
super(message, 401, details, 'AUTHENTICATION_ERROR');
this.name = 'OptimaizAuthenticationError';
}
}
exports.OptimaizAuthenticationError = OptimaizAuthenticationError;
class OptimaizValidationError extends OptimaizError {
constructor(message, details) {
super(message, 400, details, 'VALIDATION_ERROR');
this.name = 'OptimaizValidationError';
}
}
exports.OptimaizValidationError = OptimaizValidationError;
class OptimaizServerError extends OptimaizError {
constructor(message, details) {
super(message, 500, details, 'SERVER_ERROR');
this.name = 'OptimaizServerError';
}
}
exports.OptimaizServerError = OptimaizServerError;
// Tool helper functions
function validateToolDefinitions(tools) {
const errors = [];
for (const tool of tools) {
if (!tool.name || typeof tool.name !== 'string') {
errors.push(`Tool must have a valid name: ${tool.name}`);
}
if (!tool.description || typeof tool.description !== 'string') {
errors.push(`Tool ${tool.name} must have a valid description`);
}
if (!tool.parameters || typeof tool.parameters !== 'object') {
errors.push(`Tool ${tool.name} must have valid parameters`);
}
else {
if (tool.parameters.type !== 'object') {
errors.push(`Tool ${tool.name} parameters must have type 'object'`);
}
if (!tool.parameters.properties || typeof tool.parameters.properties !== 'object') {
errors.push(`Tool ${tool.name} must have properties defined`);
}
}
}
return { valid: errors.length === 0, errors };
}
function convertProviderToolsToUnified(tools, provider) {
switch (provider.toLowerCase()) {
case 'openai':
return tools.map((tool) => {
if (!tool.function || !tool.function.name || !tool.function.description) {
throw new Error('Invalid OpenAI tool format: missing function or required fields');
}
return {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
};
});
case 'anthropic':
return tools.map((tool) => {
if (!tool.name || !tool.description || !tool.input_schema) {
throw new Error('Invalid Anthropic tool format: missing required fields');
}
return {
name: tool.name,
description: tool.description,
parameters: {
type: 'object',
properties: tool.input_schema.properties,
required: tool.input_schema.required,
},
};
});
case 'mistral':
return tools.map((tool) => {
if (!tool.function || !tool.function.name || !tool.function.description) {
throw new Error('Invalid Mistral tool format: missing function or required fields');
}
return {
name: tool.function.name,
description: tool.function.description,
parameters: tool.function.parameters,
};
});
default:
// Assume already in unified format
return tools;
}
}
function getToolsInProviderFormat(tools, provider) {
switch (provider.toLowerCase()) {
case 'openai':
return tools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}));
case 'anthropic':
return tools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: {
type: 'object',
properties: tool.parameters.properties,
required: tool.parameters.required,
},
}));
case 'mistral':
return tools.map(tool => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}));
default:
// Return in unified format
return tools;
}
}
class OptimaizClient {
constructor(config) {
this.config = config;
if (!config.token || typeof config.token !== 'string') {
throw new Error('OptimaizClient requires a valid token');
}
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
}
async post(path, body) {
try {
const res = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.token}`,
},
body: JSON.stringify({ ...body }),
});
/* if (!res.ok) {
let errorMessage: string;
let errorDetails: string | undefined;
try {
const errorData = await res.json();
errorMessage = errorData.error || `HTTP ${res.status}`;
errorDetails = errorData.details;
} catch {
// Fallback to text if JSON parsing fails
const errorText = await res.text();
errorMessage = errorText || `HTTP ${res.status}`;
}
// Create specific error types based on status codes
if (res.status === 401) {
throw new OptimaizAuthenticationError(`Authentication failed: ${errorMessage}`, errorDetails);
} else if (res.status === 403) {
const authError = new OptimaizError(`Access forbidden: ${errorMessage}`, 403, errorDetails, 'AUTHORIZATION_ERROR');
authError.name = 'OptimaizAuthorizationError';
throw authError;
} else if (res.status === 400) {
throw new OptimaizValidationError(`Validation error: ${errorMessage}`, errorDetails);
} else if (res.status === 500) {
throw new OptimaizServerError(`Server error: ${errorMessage}`, errorDetails);
} else {
const error = new OptimaizError(errorMessage, res.status, errorDetails);
throw error;
}
} */
return res.json();
}
catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
const networkError = new Error(`Network error: Unable to connect to ${this.baseUrl}. Please check your internet connection.`);
networkError.type = 'NETWORK_ERROR';
throw networkError;
}
throw error;
}
}
async startTrace(data) {
// Client-side validation matching server patterns
if (!data.promptTemplate && !data.promptVariables) {
throw new Error("No prompts provided: either promptTemplate or promptVariables must be provided");
}
if (data.tools && !data.provider) {
throw new Error("Tool definitions provided but no provider specified for conversion");
}
if (data.tools) {
const validation = this.validateTools(data.tools);
if (!validation.valid) {
throw new Error(`Invalid tool definitions: ${validation.errors.join(', ')}`);
}
}
return this.post("/api/v1/interactions/start", data);
}
async appendResponse(data) {
if (!data.traceId) {
throw new Error("Missing traceId");
}
return this.post("/api/v1/interactions/append", data);
}
async finalizeTrace(traceId) {
if (!traceId) {
throw new Error("Missing traceId");
}
return this.post("/api/v1/interactions/finalize", { traceId });
}
async logError(traceId, error) {
if (!traceId || !error) {
throw new Error("Missing traceId or error");
}
return this.post("/api/v1/interactions/error", { traceId, error });
}
async sendFeedback(traceId, feedback) {
if (!traceId || !feedback) {
throw new Error("Missing traceId or feedback");
}
return this.post("/api/v1/interactions/feedback", { traceId, feedback });
}
async addToolExecution(data) {
if (!data.traceId) {
throw new Error("Missing traceId");
}
if (!data.toolId || !data.toolName) {
throw new Error("Missing toolId or toolName");
}
return this.post("/api/v1/interactions/tool-execution", data);
}
async addToolResults(data) {
if (!data.traceId) {
throw new Error("Missing traceId");
}
if (!Array.isArray(data.toolResults) || data.toolResults.length === 0) {
throw new Error("Missing or invalid toolResults array");
}
return this.post("/api/v1/interactions/tool-results", data);
}
composePrompts(templates, variables) {
if (!Array.isArray(templates)) {
throw new Error('Templates must be an array');
}
if (!variables || typeof variables !== 'object') {
throw new Error('Variables must be an object');
}
const prompts = [];
const promptTemplate = [];
for (const { role, content, type = "text" } of templates) {
const resolved = Object.entries(variables).reduce((acc, [k, v]) => acc.replace(`{${k}}`, String(v || '')), content);
prompts.push({ type, role, value: resolved });
promptTemplate.push({ type, role, value: content });
}
return { prompts, promptTemplate, promptVariables: variables };
}
async wrapLLMCall({ traceId = crypto_1.default.randomUUID(), agentId, flowId, threadId, sessionId, userId, promptTemplate, promptVariables, tools, provider, model, modelParams, metadata, tags, call, }) {
try {
await this.startTrace({
traceId,
agentId,
flowId,
threadId,
sessionId,
userId,
promptTemplate,
promptVariables,
tools,
provider,
model,
modelParams,
metadata,
tags,
});
}
catch (err) {
console.warn("[Optimaiz] Failed to start trace", err);
}
try {
const response = await call();
try {
await this.appendResponse({ traceId, rawResponse: response, provider, model });
}
catch (err) {
console.warn("[Optimaiz] Failed to append response", err);
}
try {
await this.finalizeTrace(traceId);
}
catch (err) {
console.warn("[Optimaiz] Failed to finalize trace", err);
}
return { response, traceId };
}
catch (err) {
try {
await this.logError(traceId, {
message: err.message,
code: err.code || "unknown_error",
details: err.stack || err,
});
}
catch (logErr) {
console.warn("[Optimaiz] Failed to log error:", logErr);
}
console.error("[Optimaiz] Error in wrapLLMCall", err);
throw err;
}
}
generatePromptFromTools({ toolInfo, toolInput, }) {
if (!Array.isArray(toolInfo)) {
throw new Error('ToolInfo must be an array');
}
if (!toolInput || typeof toolInput !== 'object') {
throw new Error('ToolInput must be an object');
}
if (!toolInput.name || typeof toolInput.name !== 'string') {
throw new Error('ToolInput must have a valid name');
}
const fn = toolInfo.find(f => f.name === toolInput.name);
if (!fn)
throw new Error(`Tool ${toolInput.name} not found in toolInfo`);
// Use the properly typed properties from StandardToolDefinition
const properties = fn.parameters.properties;
const arguments_ = toolInput.arguments || {};
const argKeys = Object.keys(arguments_);
const promptText = [
`Use the tool: {toolName}.`,
fn.description ? `Tool Description: {description}.` : "",
...argKeys.map(key => `${key}: {${key}}${properties[key]?.description ? ` // ${properties[key].description}` : ""}`),
]
.filter(Boolean)
.join("\n");
const promptTemplate = [
{
role: "user",
type: "text",
value: promptText,
},
];
const promptVariables = {
toolName: fn.name,
description: fn.description || "",
...Object.fromEntries(argKeys.map(k => [k, String(arguments_[k] || '')])),
};
return { promptTemplate, promptVariables };
}
async call(data) {
if (!data || typeof data !== 'object') {
throw new Error('Call data must be an object');
}
if (!data.promptVariables || typeof data.promptVariables !== 'object') {
throw new Error('promptVariables must be provided and must be an object');
}
data.traceId = data.traceId || crypto_1.default.randomUUID();
try {
if (!data.promptTemplate && !data.agentId) {
throw new Error("Either promptTemplate or agentId must be provided");
}
const { data: response, error } = await this.post("/api/v1/call", data);
return { data: response, error };
}
catch (err) {
if (data.traceId) {
try {
await this.logError(data.traceId, {
message: err.message,
code: err.code || "unknown_error",
details: err.stack || err,
});
}
catch (logErr) {
console.warn("[Optimaiz] Failed to log error:", logErr);
}
}
return { data: null, error: err.message };
}
}
// Tool conversion methods
convertToolsToProviderFormat(tools, provider) {
if (!Array.isArray(tools)) {
throw new Error('Tools must be an array');
}
// First convert to unified format
const unifiedTools = tools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
category: tool.category,
tags: tool.tags,
version: tool.version,
deprecated: tool.deprecated,
}));
// Then convert to provider-specific format
return getToolsInProviderFormat(unifiedTools, provider);
}
convertProviderToolsToStandard(tools, provider) {
if (!Array.isArray(tools)) {
throw new Error('Tools must be an array');
}
const unifiedTools = convertProviderToolsToUnified(tools, provider);
return unifiedTools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
category: tool.category,
tags: tool.tags,
version: tool.version,
deprecated: tool.deprecated,
}));
}
validateTools(tools) {
if (!Array.isArray(tools)) {
throw new Error('Tools must be an array');
}
const unifiedTools = tools.map(tool => ({
name: tool.name,
description: tool.description,
parameters: tool.parameters,
category: tool.category,
tags: tool.tags,
version: tool.version,
deprecated: tool.deprecated,
}));
return validateToolDefinitions(unifiedTools);
}
/**
* Utility method to handle errors with proper error types
*/
static handleError(error) {
if (error instanceof OptimaizError) {
throw error;
}
// Convert generic errors to OptimaizError
if (error.message && typeof error.message === 'string') {
if (error.message.includes('Authentication failed') || error.message.includes('401')) {
throw new OptimaizAuthenticationError(error.message);
}
else if (error.message.includes('Validation error') || error.message.includes('400')) {
throw new OptimaizValidationError(error.message);
}
else if (error.message.includes('Server error') || error.message.includes('500')) {
throw new OptimaizServerError(error.message);
}
}
// Default to generic OptimaizError
throw new OptimaizError(error.message || 'Unknown error occurred');
}
/**
* Check if an error is a specific type
*/
static isAuthenticationError(error) {
return error instanceof OptimaizAuthenticationError || error.type === 'AUTHENTICATION_ERROR';
}
static isValidationError(error) {
return error instanceof OptimaizValidationError || error.type === 'VALIDATION_ERROR';
}
static isServerError(error) {
return error instanceof OptimaizServerError || error.type === 'SERVER_ERROR';
}
}
exports.OptimaizClient = OptimaizClient;