UNPKG

@future-agi/sdk

Version:

We help GenAI teams maintain high-accuracy for their Models in production.

462 lines 19.3 kB
import { APIKeyAuth, ResponseHandler, } from '../api/auth.js'; import { HttpMethod } from '../api/types.js'; import { Routes } from '../utils/routes.js'; import { ModelConfig, PromptTemplate, } from './types.js'; import { InvalidAuthError, SDKException, TemplateAlreadyExists, TemplateNotFound, } from '../utils/errors.js'; /** * Simple JSON handler – returns parsed JSON on success, handles common auth/not-found errors. */ class SimpleJsonResponseHandler extends ResponseHandler { static _parseSuccess(response) { return response.data; } static _handleError(response) { if (response.status === 403) { throw new InvalidAuthError(); } if (response.status === 404) { throw new TemplateNotFound('unknown'); } // Fallback – use generic logic from base class return super._handleError(response); } } /** * Response handler for prompt-template related endpoints. Converts backend payloads into * strongly-typed `PromptTemplate` instances where appropriate. */ class PromptResponseHandler extends ResponseHandler { static _buildModelConfig(cfgSrc) { if (!cfgSrc) return new ModelConfig(); return new ModelConfig({ model_name: cfgSrc.modelName ?? cfgSrc.model, temperature: cfgSrc.temperature ?? 0, frequency_penalty: cfgSrc.frequencyPenalty ?? cfgSrc.frequency_penalty ?? 0, presence_penalty: cfgSrc.presencePenalty ?? cfgSrc.presence_penalty ?? 0, max_tokens: cfgSrc.maxTokens ?? cfgSrc.max_tokens, top_p: cfgSrc.topP ?? cfgSrc.top_p ?? 0, response_format: cfgSrc.responseFormat ?? cfgSrc.response_format, tool_choice: cfgSrc.toolChoice ?? cfgSrc.tool_choice, tools: cfgSrc.tools ?? null, }); } static _toPromptTemplate(data) { const promptConfigRaw = data.promptConfig || data.prompt_config; let modelConfiguration; let messages = []; if (promptConfigRaw && Array.isArray(promptConfigRaw) && promptConfigRaw.length > 0) { const pc = promptConfigRaw[0]; modelConfiguration = this._buildModelConfig(pc.configuration || {}); messages = pc.messages || []; } return new PromptTemplate({ id: data.id, name: data.name, description: data.description ?? '', messages, model_configuration: modelConfiguration, variable_names: data.variableNames ?? data.variable_names ?? {}, version: data.version, is_default: data.isDefault ?? true, evaluation_configs: data.evaluationConfigs ?? [], status: data.status, error_message: data.errorMessage, }); } static _parseSuccess(response) { const { data } = response; const url = response.config.url ?? ''; const method = response.config.method?.toUpperCase() ?? 'GET'; // Search endpoint: /prompt-templates/?search=<name> if (url.includes('search=')) { const results = data.results ?? []; const name = url.split('search=')[1]; const found = results.find((it) => it.name === name); if (found) return found.id; throw new SDKException(`No template found with the given name: ${name}`); } // GET template by ID endpoint if (method === HttpMethod.GET && !url.endsWith('/')) { // Heuristic: treat as single-template retrieval by ID return this._toPromptTemplate(data); } // GET template by name endpoint – keep parity with Python SDK behaviour if (method === HttpMethod.GET && url.includes(Routes.get_template_by_name)) { return this._toPromptTemplate(data); } // POST create template endpoint returns { result: {...} } if (method === HttpMethod.POST && url.endsWith(Routes.create_template)) { return data.result ?? data; } // Fallback to raw payload return data; } static _handleError(response) { // Optional debug output if (process.env.FI_SDK_DEBUG === '1' || process.env.FI_SDK_DEBUG === 'true') { /* eslint-disable no-console */ console.error('[PromptResponseHandler] HTTP', response.status, 'payload:', response.data); /* eslint-enable no-console */ } if (response.status === 403) { throw new InvalidAuthError(); } if (response.status === 404) { // Attempt to parse the template name from query string if present const url = response.config.url ?? ''; const match = url.match(/name=([^&]+)/); const name = match ? decodeURIComponent(match[1]) : 'unknown'; throw new TemplateNotFound(name); } if (response.status === 400) { const detail = response.data ?? {}; const errorCode = detail.errorCode; // Map "template not found" phrasing used by backend to our TemplateNotFound exception if (detail?.result && typeof detail.result === 'string') { const lowercase = detail.result.toLowerCase(); if (lowercase.includes('no prompttemplate matches') || lowercase.includes('failed to retrieve template')) { throw new TemplateNotFound('unknown'); } } if (errorCode === 'TEMPLATE_ALREADY_EXIST') { throw new TemplateAlreadyExists(detail.name ?? '<unknown>'); } const msg = detail.result || detail.message || detail.error || (() => { try { return JSON.stringify(detail); } catch { return undefined; } })() || 'Bad request – please verify request body.'; throw new SDKException(msg); } return super._handleError(response); } } /** * Main Prompt client – allows programmatic CRUD and execution of prompt templates. */ export class Prompt extends APIKeyAuth { // ---------- Static helpers ---------- /** * List all prompt templates (raw JSON payload). */ static async listTemplates(options = {}) { const client = new APIKeyAuth(options); try { const res = (await client.request({ method: HttpMethod.GET, url: `${client.baseUrl}/${Routes.list_templates}`, })); return res.data ?? res; } finally { await client.close(); } } /** * Convenience: fetch template by exact name. */ static async getTemplateByName(name, options = {}) { const client = new APIKeyAuth(options); try { return (await client.request({ method: HttpMethod.GET, url: `${client.baseUrl}/${Routes.get_template_by_name}`, params: { name }, }, PromptResponseHandler)); } finally { await client.close(); } } /** * Delete template by name (helper). */ static async deleteTemplateByName(name, options = {}) { const client = new APIKeyAuth(options); try { const tmpl = (await client.request({ method: HttpMethod.GET, url: `${client.baseUrl}/${Routes.get_template_by_name}`, params: { name }, }, PromptResponseHandler)); await client.request({ method: HttpMethod.DELETE, url: `${client.baseUrl}/${Routes.delete_template.replace('{template_id}', tmpl.id ?? '')}`, }); return true; } finally { await client.close(); } } // ---------- Instance ---------- constructor(template, options = {}) { super(options); if (template) { this.template = template; } } /** * Create a new draft prompt template. */ async open() { if (!this.template) { throw new SDKException('template must be set'); } // If template already has an ID it's already created – just return the client. if (this.template.id) { return this; } // Attempt to fetch existing template by name; propagate any errors. if (this.template.name) { try { const remote = await Prompt.getTemplateByName(this.template.name, { fiApiKey: this.fiApiKey, fiSecretKey: this.fiSecretKey, fiBaseUrl: this.baseUrl, }); // Found existing template – adopt it and return immediately this.template = remote; return this; } catch (err) { // In production, treat any error during lookup as "not found" and proceed to create // This handles cases where the lookup endpoint is unstable but create works // Template truly does not exist – proceed to create below } } // Transform messages into backend-friendly format const messages = this._prepareMessages(); const jsonPayload = { name: this.template.name, prompt_config: [ { messages, configuration: { model: this.template.model_configuration?.model_name ?? 'gpt-4o-mini', temperature: this.template.model_configuration?.temperature, max_tokens: this.template.model_configuration?.max_tokens, top_p: this.template.model_configuration?.top_p, frequency_penalty: this.template.model_configuration?.frequency_penalty, presence_penalty: this.template.model_configuration?.presence_penalty, }, }, ], variable_names: this.template.variable_names, evaluation_configs: this.template.evaluation_configs ?? [], }; let response; try { response = await this.request({ method: HttpMethod.POST, url: `${this.baseUrl}/${Routes.create_template}`, json: jsonPayload, timeout: 60000, // 60 second timeout for create operations }, PromptResponseHandler); } catch (error) { throw error; } // Update template with returned info this.template.id = response.id; this.template.name = response.name; this.template.version = response.templateVersion ?? response.createdVersion ?? 'v1'; return this; } async _createNewDraft() { if (!this.template || !this.template.id) { throw new SDKException('Template must be created before creating a new version.'); } const messages = this._prepareMessages(); const modelConfig = { model: this.template.model_configuration?.model_name ?? 'gpt-4o-mini', temperature: this.template.model_configuration?.temperature, max_tokens: this.template.model_configuration?.max_tokens, top_p: this.template.model_configuration?.top_p, frequency_penalty: this.template.model_configuration?.frequency_penalty, presence_penalty: this.template.model_configuration?.presence_penalty, }; const body = { new_prompts: [ { prompt_config: [{ messages, configuration: modelConfig }], variable_names: this.template.variable_names, evaluation_configs: this.template.evaluation_configs ?? [], }, ], }; const url = `${this.baseUrl}/${Routes.add_new_draft.replace('{template_id}', this.template.id)}`; const resp = await this.request({ method: HttpMethod.POST, url, json: body, timeout: 60000, // 60 second timeout for create operations }, PromptResponseHandler); // The backend typically returns { result: [ { templateVersion: 'vX', ... } ] } if (resp && typeof resp === 'object' && resp.result && Array.isArray(resp.result) && resp.result.length > 0) { const newVersion = resp.result[0]?.templateVersion; if (newVersion && this.template) { this.template.version = newVersion; } } } /* ------------------------------------------------------------------ * Version-history helpers (parity with Python SDK) * ------------------------------------------------------------------ */ /** Fetch raw version history (array of objects) */ async _fetchTemplateVersionHistory() { if (!this.template || !this.template.id) { throw new SDKException('Template must be created before fetching history.'); } const res = (await this.request({ method: HttpMethod.GET, url: `${this.baseUrl}/${Routes.get_template_version_history}`, params: { template_id: this.template.id }, timeout: 60000, // 60 second timeout for history operations })); return (res.data ?? res).results ?? []; } /** Public: list full version history */ async listTemplateVersions() { return this._fetchTemplateVersionHistory(); } /** Internal draft-status helper */ async _currentVersionIsDraft() { const history = await this._fetchTemplateVersionHistory(); return history.some((e) => e.templateVersion === this.template?.version && e.isDraft === true); } /** Apply selected fields from another PromptTemplate to current */ _applyTemplateUpdates(tpl) { if (!this.template) return; const mutable = [ 'messages', 'description', 'variable_names', 'model_configuration', 'evaluation_configs', ]; for (const field of mutable) { const val = tpl[field]; if (val !== undefined) this.template[field] = val; } } /** Save current draft state to backend */ async saveCurrentDraft() { if (!this.template || !this.template.id) { throw new SDKException('Template must be created before it can be updated.'); } if (!(await this._currentVersionIsDraft())) { throw new SDKException('Current version is already committed; create a new draft version first.'); } const messages = this._prepareMessages(); const modelCfg = { model: this.template.model_configuration?.model_name ?? 'gpt-4o-mini', temperature: this.template.model_configuration?.temperature, max_tokens: this.template.model_configuration?.max_tokens, top_p: this.template.model_configuration?.top_p, frequency_penalty: this.template.model_configuration?.frequency_penalty, presence_penalty: this.template.model_configuration?.presence_penalty, }; const body = { is_run: 'draft', is_sdk: true, version: this.template.version, prompt_config: [{ messages, configuration: modelCfg }], variable_names: this.template.variable_names, evaluation_configs: this.template.evaluation_configs ?? [], }; await this.request({ method: HttpMethod.POST, url: `${this.baseUrl}/${Routes.run_template.replace('{template_id}', this.template.id)}`, json: body, timeout: 60000, // 60 second timeout for save operations }, SimpleJsonResponseHandler); return true; } /** Commit current draft version */ async commitCurrentVersion(message = '', set_default = false) { if (!this.template || !this.template.id || !this.template.version) { throw new SDKException('Template and version must be set before commit.'); } const body = { version_name: this.template.version, message, set_default, is_draft: false, }; await this.request({ method: HttpMethod.POST, url: `${this.baseUrl}/${Routes.commit_template.replace('{template_id}', this.template.id)}`, json: body, timeout: 60000, // 60 second timeout for commit operations }, SimpleJsonResponseHandler); return true; } /** Commit current draft if needed then open a new draft version */ async createNewVersion({ template, commit_message = 'Auto-commit via SDK', set_default = false, } = {}) { if (!this.template) throw new SDKException('Client.template must be set'); if (template) this._applyTemplateUpdates(template); if (await this._currentVersionIsDraft()) { await this.commitCurrentVersion(commit_message, set_default); } await this._createNewDraft(); return this; } /* ------------------------------------------------------------------ * Deprecated execution method * ------------------------------------------------------------------ */ /** * Execution support removed – this SDK variant focuses solely on template management. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars /** * Delete the current template. */ async delete() { if (!this.template || !this.template.id) { throw new SDKException('Template ID missing; cannot delete.'); } await this.request({ method: HttpMethod.DELETE, url: `${this.baseUrl}/${Routes.delete_template.replace('{template_id}', this.template.id)}`, timeout: 60000, // 60 second timeout for delete operations }); // Clear local ref this.template = undefined; return true; } // Helper to transform messages into backend format (adds variable_names if missing for user role) _prepareMessages() { if (!this.template) return []; const varNames = Object.keys(this.template.variable_names ?? {}); return this.template.messages.map((msg) => { const obj = { ...msg }; if (typeof obj.content === 'string') { obj.content = [{ type: 'text', text: obj.content }]; } if (obj.role === 'user') { // Ensure variable_names field exists for user messages if (!obj.variable_names || obj.variable_names.length === 0) { obj.variable_names = varNames; } } return obj; }); } } export default { Prompt }; //# sourceMappingURL=client.js.map