@future-agi/sdk
Version:
We help GenAI teams maintain high-accuracy for their Models in production.
462 lines • 19.3 kB
JavaScript
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