@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
131 lines • 6.53 kB
JavaScript
import { isTypedQuestion, questionOptions, validateElicitationAnswer, validateElicitationValue, } from '../elicitation-input.js';
export const DEFAULT_CHANNEL_QUESTION_TEXTS = {
counter: (index, total) => `(${index + 1}/${total})`,
pickOne: 'Reply with the number of your choice.',
pickMany: 'Reply with one or more numbers, separated by commas.',
orWrite: 'Or write your own answer.',
skipWord: 'skip',
keep: (skipWord, labels) => `Reply *${skipWord}* to keep: ${labels}.`,
invalid: (problem) => `I could not read that answer (${problem}).`,
};
/** The checks' English problems ({@link parseChannelAnswer}), in Brazilian Portuguese. */
const PT_BR_PROBLEMS = [
[/^empty answer$/, () => 'resposta vazia'],
[/^not one of the options$/, () => 'não é uma das opções'],
[/^must be one of the offered options$/, () => 'precisa ser uma das opções'],
[/^requires an answer$/, () => 'precisa de uma resposta'],
[/^must be "true" or "false"$/, () => 'precisa ser "true" ou "false"'],
[/^must be a number$/, () => 'precisa ser um número'],
[/^must be at least (.+) characters$/, (n) => `precisa ter pelo menos ${n} caracteres`],
[/^must be at most (.+) characters$/, (n) => `precisa ter no máximo ${n} caracteres`],
[/^must be at least (.+)$/, (n) => `precisa ser no mínimo ${n}`],
[/^must be at most (.+)$/, (n) => `precisa ser no máximo ${n}`],
[/^must be a date as YYYY-MM-DD$/, () => 'precisa ser uma data no formato AAAA-MM-DD'],
[/^must be on or after (.+)$/, (d) => `precisa ser a partir de ${d}`],
[/^must be on or before (.+)$/, (d) => `precisa ser até ${d}`],
[/^must be an email address$/, () => 'precisa ser um endereço de e-mail'],
[/^must be an http\(s\) URL$/, () => 'precisa ser um link http(s)'],
[/^does not match the expected format$/, () => 'não está no formato esperado'],
[/^takes a single value$/, () => 'aceita só um valor'],
];
const ptBrProblem = (problem) => {
for (const [pattern, say] of PT_BR_PROBLEMS) {
const match = pattern.exec(problem);
if (match)
return say(...match.slice(1).map((group) => group ?? ''));
}
return null;
};
/** Brazilian Portuguese question texts — part of `ptBrChannelTexts`. */
export const ptBrChannelQuestionTexts = {
counter: (index, total) => `(${index + 1}/${total})`,
pickOne: 'Responda com o número da sua escolha.',
pickMany: 'Responda com um ou mais números, separados por vírgula.',
orWrite: 'Ou escreva sua própria resposta.',
skipWord: 'pular',
keep: (skipWord, labels) => `Responda *${skipWord}* para manter: ${labels}.`,
invalid: (problem) => {
const said = ptBrProblem(problem);
return said === null ? 'Não entendi essa resposta.' : `Não entendi essa resposta (${said}).`;
},
};
const labelOf = (question, value) => questionOptions(question).find((option) => option.value === value)?.label ?? value;
/** One question as a text message: its prompt, numbered options, and how to answer. */
export function formatChannelQuestion(question, position, texts = DEFAULT_CHANNEL_QUESTION_TEXTS) {
const lines = [];
if (position.index === 0 && position.preamble)
lines.push(position.preamble, '');
const counter = position.total > 1 ? `${texts.counter(position.index, position.total)} ` : '';
lines.push(`*${counter}${question.prompt}*`);
if (question.description)
lines.push(question.description);
const options = isTypedQuestion(question) ? [] : questionOptions(question);
if (options.length > 0) {
lines.push('');
for (const [index, option] of options.entries())
lines.push(`${index + 1}. ${option.label}`);
lines.push('', question.multiple === true ? texts.pickMany : texts.pickOne);
if (question.allowFreeText === true)
lines.push(texts.orWrite);
}
const defaults = question.defaults ?? [];
if (defaults.length > 0) {
lines.push(texts.keep(texts.skipWord, defaults.map((value) => labelOf(question, value)).join(', ')));
}
return lines.join('\n');
}
const fold = (value) => value.trim().toLowerCase();
/**
* Read a chat message as the answer to `question`: an option's number or label (several, separated
* by commas, for a multiple choice), a typed value for a typed question or one that allows free
* text, or the skip word. Checked against the question's own rules.
*/
export function parseChannelAnswer(question, text, skipWord = DEFAULT_CHANNEL_QUESTION_TEXTS.skipWord) {
const answer = text.trim();
if (fold(answer) === fold(skipWord))
return { status: 'skip' };
if (answer === '')
return { status: 'invalid', problem: 'empty answer' };
let values;
if (isTypedQuestion(question)) {
values = [answer];
}
else {
const options = questionOptions(question);
const pick = (token) => {
const bare = token.trim().replace(/[.)]$/, '');
if (/^\d+$/.test(bare)) {
const option = options[Number(bare) - 1];
if (option)
return option.value;
}
const match = options.find((option) => fold(option.label) === fold(token) || fold(option.value) === fold(token));
return match?.value ?? null;
};
const whole = pick(answer);
if (whole !== null)
values = [whole];
else if (question.multiple === true) {
const tokens = answer.split(/\s*[,;]\s*|\s+/).filter((token) => token !== '');
const picked = tokens.map(pick);
if (picked.every((value) => value !== null))
values = [...new Set(picked)];
else if (question.allowFreeText === true)
values = [answer];
else
return { status: 'invalid', problem: 'not one of the options' };
}
else if (question.allowFreeText === true || options.length === 0)
values = [answer];
else
return { status: 'invalid', problem: 'not one of the options' };
}
for (const value of values) {
const problem = validateElicitationValue(question, value);
if (problem !== null)
return { status: 'invalid', problem };
}
const problem = validateElicitationAnswer(question, values);
return problem === null ? { status: 'answer', values } : { status: 'invalid', problem };
}
//# sourceMappingURL=questions.js.map