So here is a base openAPI definition to be able to work with your own model via the setup I discussed:
openapi: 3.1.0
info:
title: Aiden Sage API - AI Writing Assistant by TechSpokes
description: >
API for leveraging the advanced AI writing assistant functionalities from "Aiden Sage" by TechSpokes.
version: 1.0.0
contact:
name: TechSpokes
url: https://www.techspokes.com
email: contact@techspokes.com
termsOfService: https://www.techspokes.com/privacy-policy/
servers:
- url: https://your-worker-subdomain.workers.dev
description: production server
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
RequestItem:
type: object
required:
- payload
properties:
payload:
oneOf:
- type: string
description: >
If the type is `text`, the payload is a simple text content to process by the remote server.
- type: array
items:
type: string
description: >
If the type is `list`, the payload is a list of strings to process by the remote server.
The items will be presented as a text block containing an ordered list of items.
- type: object
additionalProperties:
type: string
description: >
If the type is `map`, the payload is a key-value list of items to process by the remote server.
The items will be presented as a text block containing a list of key: value pairs.
- type: object
additionalProperties:
type: string
description: >
If the type is `object`, the payload is a complex object to process by the remote server.
The object will be json encoded and presented as a JSON string.
type:
type: string
enum: [ text, list, map, object ]
default: text
description: >
Type of the payload. Defaults to `text`.
Text is used for simple text content, list for a list of items (strings), map for a key-value list of items, and object for a complex object.
Note: all the types will be converted to string value before processing, but their formatting will vary:
- text: simple text content, passed as is;
- list: list of strings, the list will be presented as a text block containing an ordered list of string values;
- map: key-value pairs, the elements will be presented as a text block containing a list of key-value pairs formatted as key: value;
- object: complex object, the object will be json encoded and presented as a JSON string (pretty printed).
This allows an easy way to pass both: simple text content and structured data to the remote server when necessary.
metadata:
type: object
description: >
Additional metadata for the item, e.g., the item's title, author, etc. Defaults to an empty object.
This allows passing additional information about the item to the remote server if needed.
default: { }
instructions:
type: string
description: >
Additional instructions from the user passed to the remote server for processing the item. Defaults to an empty string.
Please note that the remote server already has the instructions for each of the operations. Use this field only if specific item instructions are needed.
When used, the instructions will be prepended to to the user message formatted as following: [additional instructions: {instructions}]\n\n{user message}
default: ""
numberOfVariants:
type: integer
minimum: 1
maximum: 25
description: >
Number of variants to generate for the item. Defaults to 1. Allows to generate multiple variants of the reply.
default: 1
RequestBody:
type: object
required:
- data
properties:
data:
type: array
description: List of items to process.
items:
$ref: '#/components/schemas/RequestItem'
instructions:
type: string
description: >
Additional instructions from the user passed to the remote server for processing the items (as part of the system message). Defaults to an empty string.
Please note that the remote server already has the instructions for each of the operations. Use this field only if specific system instructions are needed.
When used, the instructions will be appended to the system message formatted as following: {system message}\n\n[additional instructions: {instructions}]
default: ""
ResponseItem:
type: object
required:
- requestOrder
- requestShortContent
- responseVariants
properties:
requestOrder:
type: integer
description: The order of the item in the request sequence, starting from 1.
requestShortContent:
type: string
description: >
The parsed content of the request item, presented as a short text block truncated to 150 characters including the ellipsis.
Helps to better understand the request history and map responses to the original items.
responseVariants:
type: array
description: >
List of response variants generated for the requested item. Primary response is the first item in the list.
The list may contain multiple variants of the response to provide different options for the user.
If the processing fails at the item level, the response variants will contain the error message starting with `ITEM_ERROR: `. In this case, you may retry the item processing or skip the item, depending on the context.
items:
type: string
ResponseBody:
type: object
required:
- status
- message
- data
properties:
status:
type: string
enum: [ success, error ]
description: Indicates whether the request was successful or resulted in an error. Note, item errors will set the status to `error`.
message:
oneOf:
- type: string
enum: [ OK, Unauthorized ]
description: One of the predefined messages for the status.
- type: string
description: Custom message for the status containing additional information.
- type: string
description: >
Error message containing the error details. If the error occurred at the item level, the message will concatenate the error message from individual items.
data:
type: array
description: List of response items returned by the remote server.
items:
$ref: '#/components/schemas/ResponseItem'
responses:
SuccessResponse:
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseBody'
ErrorResponse:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseBody'
security:
- BearerAuth: [ ]
paths:
/brainstormIdea:
post:
operationId: BrainstormIdea
summary: Based on the initial user input about the content, this endpoint generates a list of content ideas.
description: >
Based on the initial user input about the content, this endpoint generates a list of content ideas.
The user input can be a simple text, a list of items, a key-value map, or a complex object.
The response will contain a list of content ideas generated by the AI writing assistant.
security:
- BearerAuth: [ ]
requestBody:
required: true
description: Request payload for generating the content main subject.
content:
application/json:
schema:
$ref: '#/components/schemas/RequestBody'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ResponseBody'
'400':
$ref: '#/components/responses/ErrorResponse'
'401':
$ref: '#/components/responses/ErrorResponse'
'500':
$ref: '#/components/responses/ErrorResponse'
and an example of the assistant code (located in ./assistants/brainstormIdea.js file, donāt pay attention to the system prompt, I didnāt even read that one, AI did some first drafts for me)
// brainstormIdea.js
export const brainstormIdeaConfig = {
modelName: "gpt-4o-mini",
temperature: 0.9,
store: true,
metadata: {
operation: "brainstormIdea"
},
system_message: `
You are the Subject Idea Brainstorming Assistant, a creative content strategist with extensive experience in generating engaging and innovative ideas for writers across various industries. Your specialized area is helping writers develop compelling subject ideas that align with their interests, expertise, and the expectations of the publishing platform.
Context of the Application:
The application is a writing assistant designed to support writers in the initial stage of content creation by brainstorming and refining subject ideas. Your role is to collaborate with the writer to generate a list of potential subject ideas and help refine these ideas into a clear, concise statement for the post subject that resonates with the target audience of the publishing resource.
Task:
Assist the writer in brainstorming subject ideas based on their interests, expertise, and the description of the publishing website or resource. Your goal is to generate a variety of relevant and engaging ideas and help the writer select and refine the most suitable one into a clear subject statement.
Goals of the Task:
- Generate Creative and Relevant Ideas:
- Provide a diverse set of potential subject ideas that align with the writer's interests and the publishing resource's focus.
- Align with Publishing Resource and Audience:
- Ensure the ideas are suitable for the publishing platform's content style, tone, and target audience.
- Refine and Clarify the Subject Idea:
- Help the writer select the most promising idea and refine it into a clear, compelling subject statement.
Inputs:
- Writer's Interests and Expertise:
- Topics the writer is passionate about or knowledgeable in.
- Areas the writer wishes to explore or has unique insights on.
- Publishing Resource Description:
- Purpose and mission of the platform.
- Types of content typically published (topics, formats, styles).
- Tone and voice of the content.
- Target audience characteristics (demographics, interests, preferences).
- Specific Constraints or Guidelines (if any):
- Word count limits.
- Preferred content formats (e.g., listicles, how-to guides).
- Themes or topics to focus on or avoid.
Outputs:
1. List of Potential Subject Ideas:
- A curated list of creative and relevant ideas.
- Brief descriptions or angles for each idea to illustrate their potential.
2. Recommended Subject Idea:
- The selected idea that best fits the writer's goals and the publishing resource's requirements.
- Justification for why this idea is the most suitable.
3. Clear Subject Statement:
- A concise and compelling statement for the post subject, suitable as a working title or thesis.
Rules and Special Areas to Pay Attention To:
1. Relevance and Alignment:
- Ensure all ideas are relevant to the writer's interests and expertise.
- Align ideas with the publishing resource's content focus and audience expectations.
2. Originality and Creativity:
- Encourage unique perspectives and innovative ideas.
- Avoid clichƩs or overdone topics unless presenting them with a fresh angle.
3. Feasibility:
- Consider the writer's ability to effectively develop the idea within the given constraints.
- Ensure the scope is appropriate for the intended length and depth of the content.
4. Audience Engagement:
- Focus on ideas that would captivate and provide value to the target audience.
- Tailor topics to address the audience's interests, needs, or pain points.
5. Clarity and Specificity:
- Avoid vague or overly broad subjects.
- Refine ideas to be clear and specific, facilitating focused content development.
6. Adherence to Guidelines:
- Respect any constraints provided, such as avoiding certain topics or adhering to style guidelines.
- Ensure compliance with the publishing resource's standards.
7. Positive and Collaborative Approach:
- Present ideas in an encouraging manner.
- Be open to the writer's feedback and ready to iterate on ideas as needed.
8. Cultural Sensitivity and Appropriateness:
- Be mindful of cultural nuances and avoid sensitive or controversial topics unless appropriate.
- Ensure content ideas are suitable for the platform's audience.
Example Format for the Output:
1. Introduction:
- A brief overview of the brainstorming session and its objectives.
2. List of Potential Subject Ideas:
- Idea 1: [Title or Topic]
- Brief description or angle explaining the idea.
- Idea 2: [Title or Topic]
- Brief description or angle explaining the idea.
- Idea 3: [Title or Topic]
- Brief description or angle explaining the idea.
- (Include additional ideas as appropriate.)
3. Recommended Subject Idea:
- Selected Idea: [Title or Topic]
- Justification for selection, highlighting its alignment with the writer's goals and the publishing resource's audience.
4. Clear Subject Statement:
- Provide the refined subject statement ready to be used as a working title or thesis.
Final Note:
Your collaboration is crucial in setting a strong foundation for the writing process. Ensure that the ideas generated are inspiring, actionable, and tailored to both the writer's strengths and the publishing platform's audience. Your creative input will help the writer produce content that is engaging and successful.
`
};
And the beast of the worker.js (If someone knows JS, please feel free to add notes etc, especially on data validation, as Iām a php guy and didnāt even do anything about the security here, just a proof of concept in private env):
// noinspection JSUnusedGlobalSymbols
import {brainstormIdeaConfig} from './assistants/brainstormIdea.js';
/**
* Configuration for each supported operation.
* Extend this object to include additional operations.
*/
const CONFIG = {
brainstormIdea: brainstormIdeaConfig
};
export default {
async fetch(request, env) {
// Handle CORS preflight requests
if (request.method === "OPTIONS") {
return handleOptions();
}
// Only allow POST requests
if (request.method !== "POST") {
return new Response(JSON.stringify({
status: "error",
message: "Method not allowed. Only POST requests are accepted.",
data: []
}), {
status: 405,
headers: {"Content-Type": "application/json"}
});
}
// Verify Bearer token
if (!verifyBearerToken(request, env.AUTH_BEARER_TOKEN)) {
return new Response(JSON.stringify({
status: "error",
message: "Unauthorized",
data: []
}), {
status: 401,
headers: {"Content-Type": "application/json"}
});
}
// Extract the operation from the URL path
const url = new URL(request.url);
const pathSegments = url.pathname.split("/").filter(seg => seg);
if (pathSegments.length !== 1) {
return new Response(JSON.stringify({
status: "error",
message: "Invalid endpoint",
data: []
}), {
status: 404,
headers: {"Content-Type": "application/json"}
});
}
const operation = pathSegments[0];
// Check if operation is supported
const config = CONFIG[operation];
if (!config) {
return new Response(JSON.stringify({
status: "error",
message: "Operation not supported",
data: []
}), {
status: 400,
headers: {"Content-Type": "application/json"}
});
}
// Parse and validate the request body
let requestBody;
try {
requestBody = await request.json();
} catch (error) {
return new Response(JSON.stringify({
status: "error",
message: "Invalid JSON payload",
data: []
}), {
status: 400,
headers: {"Content-Type": "application/json"}
});
}
const {instructions = "", data = []} = requestBody;
// Validate required fields
if (typeof instructions !== "string" || !Array.isArray(data)) {
return new Response(JSON.stringify({
status: "error",
message: "Invalid request structure",
data: []
}), {
status: 400,
headers: {"Content-Type": "application/json"}
});
}
// Process each item concurrently with retries and ordering
const processingPromises = data.map((item, index) => processItem(item, index + 1, instructions, config, env));
// Wait for all processing to complete
const processedItems = await Promise.all(processingPromises);
// Check for any errors
const failedItems = processedItems.filter(item => item.responseVariants.some(variant => variant.startsWith("ITEM_ERROR:")));
if (failedItems.length > 0) {
const errorDetails = failedItems.map(item =>
`Item ${item.requestOrder}: ${item.responseVariants.filter(v => v.startsWith("ITEM_ERROR:")).join("; ")}`
).join("; ");
return new Response(JSON.stringify({
status: "error",
message: errorDetails,
data: processedItems
}), {
status: 200,
headers: {"Content-Type": "application/json"}
});
}
// If all items succeeded
return new Response(JSON.stringify({
status: "success",
message: "OK",
data: processedItems
}), {
status: 200,
headers: {"Content-Type": "application/json"}
});
},
};
/**
* Utility function to handle CORS preflight requests.
*/
function handleOptions() {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
},
});
}
/**
* Utility function to verify Bearer token.
* @param {Request} request - The incoming request.
* @param {string} expectedToken - The expected Bearer token from environment variables.
* @returns {boolean} - Whether the token is valid.
*/
function verifyBearerToken(request, expectedToken) {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return false;
}
const token = authHeader.split(" ")[1];
return token === expectedToken;
}
/**
* Utility function to delay execution (for retries).
* @param {number} ms - Milliseconds to delay.
* @returns {Promise} - A promise that resolves after the specified delay.
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Function to process a single item with retries.
* @param {Object} item - The item to process.
* @param {number} order - The order of the item in the request (starts from 1).
* @param {string} generalInstructions - General instructions for the request.
* @param {Object} config - The configuration for the operation.
* @param {Object} env - Environment variables.
* @returns {Promise<Object>} - The processed item.
*/
async function processItem(item, order, generalInstructions, config, env) {
const maxRetries = 2;
const {
payload = "",
type = "text",
metadata = {},
instructions = "",
numberOfVariants = 1
} = item;
let attempt = 0;
let success = false;
let responseData = null;
let errorMessage = "";
// Construct the system message with general instructions if any
let systemMessage = config.system_message;
if (generalInstructions.trim().length > 0) {
systemMessage += `\n\n[additional instructions: ${generalInstructions}]`;
}
// Parse the payload based on its type
let parsedPayload;
try {
parsedPayload = parsePayload(payload, type);
} catch (parseError) {
// If payload parsing fails, return an error variant immediately
return {
requestOrder: order,
requestShortContent: truncateText(`Failed to parse payload: ${parseError.message}`),
responseVariants: [`ITEM_ERROR: Failed to parse payload: ${parseError.message}`]
};
}
// Construct the user message with item-specific instructions if any
let userMessage = parsedPayload;
if (instructions.trim().length > 0) {
userMessage = `[additional instructions: ${instructions}]\n\n${userMessage}`;
}
// Prepend metadata if present and not empty
if (metadata && Object.keys(metadata).length > 0) {
const prettyMetadata = JSON.stringify(metadata, null, 2);
userMessage = `[metadata]\n${prettyMetadata}\n[/metadata]\n\n${userMessage}`;
}
// Prepare messages array
const messages = [
{
role: "system",
content: systemMessage
},
{
role: "user",
content: userMessage
}
];
// OpenAI API request payload with dynamic property assignment and null filtering
const openAIPayload = removeNulls({
model: config.modelName !== undefined ? config.modelName : null,
messages: messages,
max_completion_tokens: config.max_completion_tokens !== undefined ? config.max_completion_tokens : null,
top_p: config.top_p !== undefined ? config.top_p : null,
temperature: config.temperature !== undefined ? config.temperature : null,
presence_penalty: config.presence_penalty !== undefined ? config.presence_penalty : null,
frequency_penalty: config.frequency_penalty !== undefined ? config.frequency_penalty : null,
store: config.store !== undefined ? config.store : null,
metadata: config.metadata !== undefined ? config.metadata : null,
n: Math.min(Math.max(parseInt(String(numberOfVariants).trim(), 10), 1), 25)
});
while (attempt <= maxRetries && !success) {
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${env.OPENAI_API_KEY}`,
"OpenAI-Organization": env.OPENAI_ORG,
"OpenAI-Project": env.OPENAI_PROJECT_ID
},
body: JSON.stringify(openAIPayload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error?.message || "Unknown OpenAI API error");
}
const data = await response.json();
// Extract variants from the response
responseData = data.choices.map(choice => choice.message.content.trim());
success = true;
} catch (error) {
errorMessage = error.message || "Unknown error";
attempt += 1;
if (attempt > maxRetries) {
// Mark as failed after exceeding retries
responseData = [`ITEM_ERROR: ${errorMessage}`];
} else {
// Exponential backoff before retrying
await delay(1000 * attempt);
}
}
}
// Prepare the response item
return {
requestOrder: order,
requestShortContent: truncateText(parsedPayload),
responseVariants: responseData
};
}
/**
* Parses the payload based on its type and converts it to a string.
* @param {any} payload - The payload to parse.
* @param {string} type - The type of the payload (text, list, map, object).
* @returns {string} - The parsed payload as a string.
* @throws {Error} - If the payload cannot be parsed based on its type.
*/
function parsePayload(payload, type) {
switch (type) {
case "text":
if (typeof payload !== "string") {
throw new Error("Payload must be a string for type 'text'.");
}
return payload;
case "list":
if (!Array.isArray(payload) || !payload.every(item => typeof item === "string")) {
throw new Error("Payload must be an array of strings for type 'list'.");
}
return payload.map((item, index) => `${index + 1}. ${item}`).join("\n");
case "map":
if (typeof payload !== "object" || Array.isArray(payload) || payload === null) {
throw new Error("Payload must be a key-value object for type 'map'.");
}
return Object.entries(payload).map(([key, value]) => `${key}: ${value}`).join("\n");
case "object":
if (typeof payload !== "object" || Array.isArray(payload) || payload === null) {
throw new Error("Payload must be a JSON object for type 'object'.");
}
return JSON.stringify(payload, null, 2); // Pretty-printed JSON string
default:
throw new Error(`Unsupported payload type: ${type}`);
}
}
/**
* Truncates a text to 150 characters, appending an ellipsis if truncated.
* @param {string} text - The text to truncate.
* @returns {string} - The truncated text.
*/
function truncateText(text) {
if (text.length > 150) {
return text.substring(0, 147) + "...";
}
return text;
}
/**
* Removes properties with null values from an object.
* @param {Object} obj - The object to filter.
* @returns {Object} - The filtered object without null values.
*/
function removeNulls(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value !== null)
);
}
The goal of the setup is to be able to generate the assistants and the api endpoints in semi-automated way, the API is easy and DTOs are flexible enough to start working with pretty much anything as input/output + parallel processing for multiple items submitted to the assistant (someone was talking about the proofreading recently, this is how your custom GPT can split the text in separate sections and process them all at once through a specialized assistant)
Have fun, Iāll work on the app later in the week to have something decent out.
PS: who the heck is Aiden Sage: Echoes of Thought