UNPKG

@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
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