fleet/website/api/helpers/ai/prompt.js
kilo-code-bot[bot] f7c20c4731
Switch query generator to use Anthropic Sonnet on the backend (#41521)
## Summary

Closes #41466

- Adds Anthropic Claude API support to the `prompt.js` AI helper,
detecting `claude-*` model names and routing to the Anthropic Messages
API (`https://api.anthropic.com/v1/messages`) with proper authentication
headers
- Switches both LLM calls in the query generator
(`get-llm-generated-sql.js`) from OpenAI models
(`gpt-4o-mini-2024-07-18` for schema filtration, `o3-mini-2025-01-31`
for SQL generation) to `claude-sonnet-4-6-20260218`
- Adds `anthropicSecret` config placeholder in `custom.js` (set via
`sails_custom__anthropicSecret` env var in production)
- Updates the query generator UI to reference "Anthropic" instead of
"OpenAI"

### Changes

| File | What changed |
|------|-------------|
| `website/api/helpers/ai/prompt.js` | Added Anthropic API branch
alongside existing OpenAI logic; system prompts use Anthropic's
top-level `system` parameter |
| `website/api/controllers/query-generator/get-llm-generated-sql.js` |
Both model references changed to `claude-sonnet-4-6-20260218` |
| `website/config/custom.js` | Added `anthropicSecret` config
placeholder |
| `website/views/pages/admin/query-generator.ejs` | Updated copy from
"OpenAI" to "Anthropic" |

### Deployment notes

The `sails_custom__anthropicSecret` environment variable must be set
with an Anthropic API key before deploying this change.

---

Built for
[mikermcneil](https://fleetdm.slack.com/archives/D0AFASLRHNU/p1773278374183489?thread_ts=1773271495.702919&cid=D0AFASLRHNU)
by [Kilo for Slack](https://kilo.ai/features/slack-integration)

---------

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-authored-by: Eric <[email protected]>
2026-03-16 11:48:33 -05:00

167 lines
6.8 KiB
JavaScript
Vendored

module.exports = {
friendlyName: 'Prompt',
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: {
type: 'string',
description: 'The base model to use.',
example: 'gpt-4o',
// OpenAI models:
// 'o4-mini-2025-04-16'
// 'o3-2025-04-16'
// 'o1-preview'
// 'o3-mini-2025-01-31'
// 'gpt-4o-2024-08-06'
// 'gpt-4o-mini-2024-07-18'
// 'gpt-4.1-2025-04-14'
// Anthropic models:
// 'claude-sonnet-4-6-20260218'
// 'claude-opus-4-6-20260218'
moreInfoUrl: 'https://platform.openai.com/docs/models',
defaultsTo: 'gpt-3.5-turbo',
},
expectJson: { type: 'boolean', defaultsTo: false },
systemPrompt: { type: 'string', example: 'Here is data about each computer, as JSON: ```[ … ]```' },
},
exits: {
success: {
description: 'All done.',
outputDescription: 'The output from the model, parsed as JSON, if appropriate.',
outputExample: '*',
},
jsonExpectationFailed: {
description: 'The model was supposed to respond with valid JSON, but it didn\'t.',
extendedDescription: `It can be useful to call .prompt.with({expectJson: true, prompt:'How many fingers am I holding up?'}).retry('jsonExpectationFailed')`
}
},
fn: async function ({prompt, baseModel, expectJson, systemPrompt}) {
// TODO: Write a comprehensive test suite that prompts hundreds of times in parallel to see which combo
// of JSON prompt suffix + base model works the best, through actual experimentation. Then document
// those results, have them included in a benchmark script whose usage is documented here in the code
// for this .prompt() helper, and edit the prompt helper to automatically suggest using the correct
// base model when using `expectJson: true` (and of course, change it to use the best JSON prompt suffix).
// (^This would be a good starter task for a summer internship project)
let JSON_PROMPT_SUFFIX = `
Please do not add any text outside of the JSON or wrap it in a code fence. Never use newline characters within double quotes.`;
let isAnthropicModel = baseModel.startsWith('claude-');
let rawPromptResponse;
if (isAnthropicModel) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Anthropic API [?]: https://docs.anthropic.com/en/api/messages
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (!sails.config.custom.anthropicSecret) {
throw new Error('sails.config.custom.anthropicSecret not set. (To play around, run `sails_custom__anthropicSecret=\'…\' sails console`. You can get your API secret at https://console.anthropic.com/settings/keys.)');
}//•
let requestData = {
model: baseModel,
max_tokens: 4096,// eslint-disable-line camelcase
messages: [
{ role: 'user', content: prompt+(expectJson? JSON_PROMPT_SUFFIX : '') }
]
};
if (systemPrompt) {
requestData.system = systemPrompt;
}
let anthropicResponse = await sails.helpers.http.post('https://api.anthropic.com/v1/messages', requestData, {
'x-api-key': sails.config.custom.anthropicSecret,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
})
.intercept('non200Response', (serverResponse)=>{
return new Error('Failed to generate result. Error details from LLM: '+serverResponse);
})
.intercept((err)=>{
return new Error('Failed to generate result. Error communicating with LLM: '+err.stack);
});
rawPromptResponse = anthropicResponse.content[0].text;
} else {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// OpenAI API [?]: https://platform.openai.com/docs/api-reference/chat/create
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (!sails.config.custom.openAiSecret) {
throw new Error('sails.config.custom.openAiSecret not set. (To play around, run `sails_custom__openAiSecret=\'…\' sails console`. You can get your API secret at https://platform.openai.com/settings/organization/api-keys.)');
}//•
let openAiResponse = await sails.helpers.http.post('https://api.openai.com/v1/chat/completions', {
model: baseModel,
messages: ((await sails.helpers.flow.build(()=>{
if(systemPrompt && [// The specified baseModel might not support system prompts.
'o1-preview',
'o3-mini-2025-01-31'
].includes(baseModel)){
sails.log.warn(`The prompt helper recieved a system prompt input, but the specified baseModel (${baseModel}) does not support a system prompt. This input will be ignored in this LLM generation, please remove the system prompt or use a different base model.`);
} else if (systemPrompt) {// But it also might.
return [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt+(expectJson? JSON_PROMPT_SUFFIX : '') }
];
} else {//There might not BE a system prompt.
return [
{ role: 'user', content: prompt+(expectJson? JSON_PROMPT_SUFFIX : '') }
];
}
})))
}, {
Authorization: `Bearer ${sails.config.custom.openAiSecret}`
})
.intercept('non200Response', (serverResponse)=>{
return new Error('Failed to generate result. Error details from LLM: '+serverResponse);
})
.intercept((err)=>{
return new Error('Failed to generate result. Error communicating with LLM: '+err.stack);
});
rawPromptResponse = openAiResponse.choices[0].message.content;
}
// The response to our prompt might be JSON.
let parsedPromptResponse;
if (expectJson) {
// If the JSON response is wrapped in a code fence, remove it before trying to parse it.
let jsonResponse = rawPromptResponse.trim();
if (jsonResponse.startsWith('```')) {
jsonResponse = jsonResponse.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
}
try {
parsedPromptResponse = JSON.parse(jsonResponse);
} catch (err) {
throw new Error('Expecting JSON result from LLM, but when attemting to JSON.parse(…) it, an error occurred: '+err.stack+'\n P.S. Here is what the LLM returned (and what we were *trying* to parse as valid JSON):'+rawPromptResponse);
}
} else {
parsedPromptResponse = rawPromptResponse;
}
return parsedPromptResponse;
}
};