More ai helpers (#29027)

I will upstream these ahead of next week into sails-hook-organics.
This commit is contained in:
Mike McNeil
2025-05-12 17:01:43 -05:00
committed by GitHub
parent 5bc56f31fa
commit 94aa200424
9 changed files with 411 additions and 44 deletions
+59
View File
@@ -0,0 +1,59 @@
module.exports = {
friendlyName: 'Compile',
description: 'Automatically generate code for use in a Sails app based on a human specification.',
extendedDescription: '',
inputs: {
humanSpecification: {
required: true,
type: 'string',
example: 'Sign up: Handle a signup form by creating a user in the database and set the `.userId` key in the session.',
},
purpose: {
type: 'string',
isIn: ['action', 'helper'],
defaultsTo: 'action'
},
},
exits: {
success: {
outputFriendlyName: 'Code file',
outputDescription: 'The code for a Sails action or helper that implements the given human specification.',
extendedDescription: 'The generated code is formatted for Sails v1 and above (aka using "actions2", aka the node-machine spec).'
},
},
fn: async function ({ humanSpecification, purpose }) {
return await ƒ.prompt.with({
baseModel: 'o4-mini-2025-04-16',
prompt:
'Generate code for a sails app '+
(purpose === 'action' ?
'action (actions2 in sails v1+)'
: 'helper (in sails v1+)'
)+
' to accomplish the following specification:\n```\n'+
humanSpecification+
'\n```\nRespond only with pure JavaScript code, without code fences. It is ok to use try/catch as needed, but never use .catch(). When it helps result in less code with equivalent rigor, take advantage of .intercept() or .tolerate() for error handling. (Remember never to throw inside of the intercept or tolerate functions - instead for .intercept(), return the error to throw, and for .tolerate(), return the value the function should return) Do not make a separate exit for 5xx server errors-- instead just throw to take advatage of the built-in error exit. Do not include a second `exits` argument to `fn` -- instead just return or throw. Instead of using the `inputs` argument to `fn`, specify it using syntax like `fn: ({ foo, bar })=>{ /* implementation */ }. If writing or reading to the session, use the convention of `req.session.userId`. Never use `fetch` for making outgoing HTTP requests -- instead, use sails.helpers.http if needed.',
});
}
};
+92
View File
@@ -0,0 +1,92 @@
module.exports = {
friendlyName: 'Decide',
description: 'Make an intelligent determination about some data from the provided choices.',
extendedDescription: 'e.g. for sentiment analysis for social network, or implementing "top posts" or featured content.',
sideEffects: 'cacheable',
inputs: {
data: {
type: 'json',
required: true
},
choices: {
type: {},
required: true,
description: 'The choices to pick from.',
extendedDescription: 'Each choice consists of a key (a string that will be returned if this choice is selected) and a value (the "predicate", i.e. a phrase describing the data that could either be true or false). You can think of this similar to how many popular `<select>`/UI dropdown components work in web frontend libraries. One of the choices *MUST MATCH*, so be sure to include an "Anything else" option, lest you run into errors for any data that doesn\'t match your other provided choices.',
example: {
'Top post': 'A social media post…',
'n/a': 'Anything else'
}
},
},
exits: {
success: {
outputFriendlyName: 'Decision',
outputType: 'string',
outputDescription: 'The choice that was decided upon.',
outputExample: 'Top post',
extendedDescription: 'The LLM will pick the choice that is the "most true", using the key from the provided dictionary.'
},
},
fn: async function ({data, choices: predicatesByValue}) {
let prompt = 'Given some data and a set of possible choices, decide which choice most accurately classifies the data.';
// FUTURE: Add an option to first validate `choices` (e.g. for non-production envs or where accuracy is critical and the massive trade-off in increased response time is worthwhile) using a prompt that verifies it is an appropriately-formatted predicate.
prompt += 'Data: ```\n';
prompt += `${JSON.stringify(data)}\n`;
prompt += '```\n';
prompt += '\n';
prompt += 'Choices:\n';
for (let value in predicatesByValue) {
prompt += `${predicatesByValue[value]}\n`;
}//∞
prompt += '\n';
prompt += 'Decide based on which choice is the most correct for the given data. Respond only with the exact string value for the choice provided.';
let decision = await sails.helpers.flow.build(async ()=>{
let parsedPromptResponse = await sails.helpers.ai.prompt.with({
baseModel: 'o4-mini-2025-04-16',
prompt: prompt,
});
let chosenValue;
for (let value in predicatesByValue) {
if (predicatesByValue[value] === parsedPromptResponse) {
chosenValue = value;
}
}//∞
if (!chosenValue) {
throw new Error('Response from LLM does not match provided choices. The LLM said: \n```\n'+require('util').inspect(parsedPromptResponse,{depth:null})+'\n```\n\nBut the provided choices to pick from were: \n```\n'+require('util').inspect(predicatesByValue, {depth: null})+'\n```');
}
return chosenValue;
}).retry();
return decision;
}
};
+6
View File
@@ -7,6 +7,12 @@ module.exports = {
description: 'Prompt a large language model (LLM).',
extendedDescription: 'e.g. chatbot, automatically fill out metadata on a user profile',
sideEffects: 'cacheable',
inputs: {
prompt: { type: 'string', required: true, example: 'Who is running macOS 15?' },
baseModel: {
+13 -6
View File
@@ -7,7 +7,18 @@ module.exports = {
description: 'Modify some data such that it satisfies one or more constraints.',
extendedDescription: 'e.g. wedding seating chart, generate work schedule',
sideEffects: 'cacheable',
inputs: {
data: {
type: 'json',
required: true
},
constraints: {
description: 'A list of constraints to impose upon the provided data and any changes to it.',
type: [ 'string' ],
@@ -15,11 +26,6 @@ module.exports = {
example: [ `Every table must have no more than 2 empty seats.`, `Couples with the same last name should sit together at the same table.` ]
},
data: {
type: 'json',
required: true
},
changes: {
description: 'An optional list of changes to make to the data, in order, keeping with the constraints all the while.',
type: [ 'string' ],
@@ -33,12 +39,13 @@ module.exports = {
success: {
outputType: 'json',
outputDescription: 'The modified data.',
extendedDescription: 'Note that this is a deep clone returned from the LLM. (The original data is not modified in-place.)'
},
},
fn: async function ({constraints, data, changes}) {
fn: async function ({data, constraints, changes}) {
let prompt = `Given some data and a set of constraints, make sure the data matches all of those constraints.`;
+94
View File
@@ -0,0 +1,94 @@
module.exports = {
friendlyName: 'Weigh',
description: 'Score the provided data along multiple custom dimensions.',
extendedDescription: 'e.g. build an index for a "recommended product" feature in an ecommerce site by scoring a product for future searching/querying on product detail pages in the "Recommended for you" section.',
sideEffects: 'cacheable',
inputs: {
data: {
type: 'json',
required: true
},
dimensions: {
type: [ 'string' ],
required: true,
example: [
'night on the town',
'formal',
'polyester fabric',
'wool fabric'
]
},
},
exits: {
success: {
outputFriendlyName: 'Weights',
outputDescription: 'The weights/scores of this data along each dimension, expressed as a number from 0 to 1.',
outputType: {},
outputExample: {
'night on the town': 0.3,
'formal': 0.1,
'polyester fabric': 1,
'wool fabric': 0,
'cotton fabric': 1,
},
},
},
fn: async function ({ data, dimensions }) {
// TODO: Limit (round) the precision of decimal places for better userland experience.
let prompt = 'Given some data and a set of dimensions, score the data on a scale from 0 to 1 along each dimension, using a decimal precision of no more than one decimal place.';
prompt += 'Data: ```\n';
prompt += `${JSON.stringify(data)}\n`;
prompt += '```\n';
prompt += '\n';
prompt += 'Dimensions:\n';
for (let dimension of dimensions) {
prompt += `${dimension}\n`;
}//∞
prompt += '\n';
prompt += 'Respond only with JSON in this data shape: `{"foo": 0.8, "bar": 0.4 }`';
let weights = await sails.helpers.flow.build(async ()=>{
let parsedPromptResponse = await sails.helpers.ai.prompt.with({
expectJson: true,
baseModel: 'o4-mini-2025-04-16',
prompt: prompt,
})
.retry('jsonExpectationFailed');
if (!_.isObject(parsedPromptResponse) || _.isArray(parsedPromptResponse) || _.intersection(dimensions,Object.keys(parsedPromptResponse)).length !== dimensions.length) {
throw new Error('Response from LLM does not match the expected format for weights derived from the provided dimensions. The LLM said: \n```\n'+require('util').inspect(parsedPromptResponse,{depth:null})+'\n```\n\nBut the provided dimensions were: \n```\n'+require('util').inspect(dimensions, {depth: null})+'\n```');
}
return parsedPromptResponse;
}).retry();
return weights;
}
};
+27
View File
@@ -0,0 +1,27 @@
module.exports = {
friendlyName: 'Test ai compile',
description: '',
fn: async function () {
sails.log('Running custom shell script... (`sails run test-ai-compile`)');
let goal1 = 'Fibonnacci: Respond with an array of a fibonacci sequence';
sails.log(await ƒ.compile(goal1, 'helper'));
let goal2 = 'Sign up: Handle a signup form.';
sails.log(await ƒ.compile(goal2));
let goal3 = 'Receive from Fleet: Handle a webhook sent by Fleet whenever a policy fails, such that, if the policy is critical, we send an email to the person\'s email. Reach out to the Fleet API as needed to map the incoming data\'s hostname to the human email identity using the originating host.';
sails.log(await sails.helpers.ai.compile(goal3));
}
};
+11 -38
View File
@@ -11,49 +11,22 @@ module.exports = {
sails.log('Running custom shell script... (`sails run test-ai-constraint-satisfaction`)');
return await ƒ.satisfy([
let seatingChart = {
elevenTop1: [ 'Rachael McNeil', 'Mike McNeil', 'Andrew Peterson', 'Ally Peterson', 'Tina Morales', 'Luke Morales', 'Becky Simon', 'Charlie Simon', ],
elevenTop2: [ 'Ella Thompson', 'Jack Thompson', 'Laura Kim', 'Daniel Kim', 'Samantha Ortiz', 'Victor Ortiz', 'Annie Benson', 'Matt Benson', 'Michelle Reeves', 'Oscar Reeves', 'Pamela Frost' ],
tenTop4: [ 'Ava Pruitt', 'Mason Pruitt', 'Harper Sloan', 'Logan Sloan', 'Peyton Sellers', 'Griffin Sellers', 'Kayla Lowe', 'Trevor Lowe', 'Eliza Pratt', 'Dean Pratt' ],
//…etc
};
let newSeatingChart = await ƒ.satisfy(seatingChart, [
'People with the same last name are married and should sit together.',
'No table can have fewer than 8 people seated at it.'
], {
elevenTop1: [
'Rachael McNeil',
'Mike McNeil',
'Andrew Peterson',
'Ally Peterson',
'Tina Morales',
'Luke Morales',
'Becky Simon',
'Charlie Simon',
],
elevenTop2: [
'Ella Thompson',
'Jack Thompson',
'Laura Kim',
'Daniel Kim',
'Samantha Ortiz',
'Victor Ortiz',
'Annie Benson',
'Matt Benson',
'Michelle Reeves',
'Oscar Reeves',
'Pamela Frost'
],
tenTop4: [
'Ava Pruitt',
'Mason Pruitt',
'Harper Sloan',
'Logan Sloan',
'Peyton Sellers',
'Griffin Sellers',
'Kayla Lowe',
'Trevor Lowe',
'Eliza Pratt',
'Dean Pratt'
]
}, [
], [
'Add another, special 2-person table for the bride and groom, Ally and Andrew Peterson, and move them to it'
]);
return newSeatingChart;
}
+54
View File
@@ -0,0 +1,54 @@
module.exports = {
friendlyName: 'Test ai decision',
description: '',
fn: async function () {
sails.log('Running custom shell script... (`sails run test-ai-decision`)');
let posts = [
{
id: 1,
author: 'mikermcneil',
tweet: 'I fed this one stray cat and now I have 20 stray cats coming to my house',
},
{
id: 2,
author: 'fancydoilies',
tweet: 'My cat is named Rory'
},
{
id: 3,
author: 'koo',
tweet: 'Sails.js is the best JavaScript framework'
},
{
id: 4,
author: 'koo',
tweet: 'The 4th annual SailsConf is coming up in May in Abuja!'
},
];
let topPosts = [];
await ƒ.simultaneouslyForEach(posts, async (post)=>{
let postClassification = await ƒ.decide(post, {
'Top post': 'A social media post that is both (a) VERY interesting and (b) in reasonably good taste',
'n/a': 'Anything else',
});
if (postClassification === 'Top post') {
topPosts.push(post);
}
});//∞
return topPosts;
}
};
+55
View File
@@ -0,0 +1,55 @@
module.exports = {
friendlyName: 'Test ai weights',
description: '',
fn: async function () {
sails.log('Running custom shell script... (`sails run test-ai-weights`)');
let posts = [
{
id: 1,
author: 'mikermcneil',
tweet: 'I fed this one stray cat and now I have 20 stray cats coming to my house',
},
{
id: 2,
author: 'fancydoilies',
tweet: 'My cat is named Rory'
},
{
id: 3,
author: 'koo',
tweet: 'Sails.js is the best JavaScript framework'
},
{
id: 4,
author: 'koo',
tweet: 'The 4th annual SailsConf is coming up in May in Abuja!'
},
];
let weighedPosts = [];
await ƒ.simultaneouslyForEach(posts, async (post)=>{
let postWeights = await ƒ.weigh(post, [
'related to cats',
'related to javascript',
'A social media post that is both (a) VERY interesting and (b) in reasonably good taste'
]);
weighedPosts.push(Object.assign({
scoresByTopic: postWeights
}, post));
});//∞
return weighedPosts;
}
};