/**
* AgentForms SDK — lightweight client for the AI Workflow Builder SSE endpoint.
*
* Usage:
* const af = new AgentForms({ baseUrl: 'https://agentforms.io', apiKey: 'afk_live_...' });
* await af.buildWorkflow({ prompt: 'Contact form with name, email, message, send to webhook' }, {
* onToken: (token) => console.log(token),
* onComplete: (workflow) => console.log('Built:', workflow),
* onError: (error) => console.error('Failed:', error),
* });
*
* No dependencies. Works in browser and Node.js (with event-source-polyfill for Node).
*/
class AgentForms {
/**
* @param {Object} options
* @param {string} options.baseUrl - API base URL (e.g. 'https://agentforms.io')
* @param {string} options.apiKey - API key for authentication
*/
constructor(options) {
if (!options || !options.baseUrl || !options.apiKey) {
throw new Error('AgentForms requires baseUrl and apiKey');
}
this.baseUrl = options.baseUrl.replace(/\/$/, '');
this.apiKey = options.apiKey;
}
/**
* Build a workflow using the streaming SSE endpoint.
*
* @param {Object} params
* @param {string} params.prompt - Natural language description of the workflow
* @param {string} [params.form_name] - Optional form name override
* @param {Object} callbacks
* @param {Function} [callbacks.onToken] - Called with each token chunk from LLM
* @param {Function} [callbacks.onFields] - Called when fields are parsed (subset of complete)
* @param {Function} [callbacks.onActions] - Called when actions are parsed (subset of complete)
* @param {Function} [callbacks.onComplete] - Called with full workflow definition
* @param {Function} [callbacks.onError] - Called on error with error message
* @returns {Promise<void>}
*/
buildWorkflow(params, callbacks) {
const { prompt, form_name } = params || {};
const { onToken, onFields, onActions, onComplete, onError } = callbacks || {};
if (!prompt || prompt.length < 5) {
const err = new Error('Prompt must be at least 5 characters');
if (onError) onError(err.message);
throw err;
}
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/api/v2/forms/build/stream`;
// Use EventSource with POST support via fetch + ReadableStream
// since EventSource only supports GET
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ prompt, form_name }),
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
const msg = err.error || `HTTP ${response.status}`;
if (onError) onError(msg);
throw new Error(msg);
});
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const readChunk = () => {
return reader.read().then(({ done, value }) => {
if (done) {
resolve();
return;
}
buffer += decoder.decode(value, { stream: true });
this._parseSSE(buffer, onToken, onFields, onActions, onComplete, onError, () => {
// After parsing, clear processed buffer
const lastDoubleNewline = buffer.lastIndexOf('\n\n');
if (lastDoubleNewline !== -1) {
buffer = buffer.slice(lastDoubleNewline + 2);
}
readChunk();
});
});
};
readChunk();
})
.catch(err => {
if (onError) onError(err.message);
reject(err);
});
});
}
/**
* Parse SSE events from a buffer string.
* @private
*/
_parseSSE(buffer, onToken, onFields, onActions, onComplete, onError, onMore) {
const events = [];
const lines = buffer.split('\n');
let currentEvent = null;
let currentData = '';
for (const line of lines) {
if (line.startsWith('event: ')) {
// Save previous event if exists
if (currentEvent && currentData) {
events.push({ type: currentEvent, data: currentData });
}
currentEvent = line.slice(7).trim();
currentData = '';
} else if (line.startsWith('data: ')) {
currentData += line.slice(6);
}
}
// Save last event
if (currentEvent && currentData) {
events.push({ type: currentEvent, data: currentData });
}
for (const event of events) {
this._handleEvent(event, onToken, onFields, onActions, onComplete, onError);
}
if (onMore) onMore();
}
/**
* Handle a single SSE event.
* @private
*/
_handleEvent(event, onToken, onFields, onActions, onComplete, onError) {
try {
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
switch (event.type) {
case 'progress':
if (onToken) onToken(data);
break;
case 'complete':
// Fire field/action callbacks if present
if (onFields && data.fields) onFields(data.fields);
if (onActions && data.actions) onActions(data.actions);
if (onComplete) onComplete(data);
break;
case 'error':
if (onError) onError(data);
break;
}
} catch (err) {
if (onError) onError(`Failed to parse SSE event: ${err.message}`);
}
}
}
// Export for different environments
if (typeof module !== 'undefined' && module.exports) {
module.exports = { AgentForms };
} else if (typeof window !== 'undefined') {
window.AgentForms = AgentForms;
}