@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
361 lines • 14.6 kB
JavaScript
/**
* Optional builtin component DEFINITIONS — names, props schemas, model descriptions and plain-text
* fallbacks for the components most chat apps want. No visuals: an app renders each with its own
* look, keyed by name. Use them as-is, pick some, or extend them:
*
* ```ts
* const catalog = defineCatalog([...BUILTIN_COMPONENTS, ...LAYOUT_COMPONENTS, myDealCard]);
* ```
*/
import { defineComponent } from './catalog.js';
const str = (description, extra = {}) => ({
type: 'string',
...(description !== undefined ? { description } : {}),
...extra,
});
const obj = (properties, required = []) => ({
type: 'object',
properties,
required,
additionalProperties: false,
});
const arr = (items, extra = {}) => ({
type: 'array',
items,
...extra,
});
const cell = { type: ['string', 'number', 'boolean', 'null'] };
const row = { type: 'object', additionalProperties: cell };
const s = (value) => (value === null || value === undefined ? '' : String(value));
const heading = (props) => props.title ? `*${s(props.title)}*\n` : '';
/** A fixed-width text table in a code block, at most `max` rows. */
export function textTable(columns, rows, max = 15) {
return `\`\`\`\n${tableLines(columns, rows, max).join('\n')}\n\`\`\``;
}
/** {@link textTable}'s lines, without the code block. */
function tableLines(columns, rows, max) {
const shown = rows.slice(0, max);
const widths = columns.map((column) => Math.min(28, Math.max(column.label.length, ...shown.map((r) => s(r[column.key]).length))));
const format = (values) => values
.map((value, index) => value.slice(0, widths[index]).padEnd(widths[index] ?? 0))
.join(' ');
const lines = [
format(columns.map((column) => column.label)),
widths.map((width) => '-'.repeat(width)).join(' '),
...shown.map((r) => format(columns.map((column) => s(r[column.key])))),
];
if (rows.length > max)
lines.push(`… ${rows.length - max} more rows`);
return lines;
}
/** Line diff (LCS) — good enough for snippets up to a few hundred lines. */
export function lineDiff(before, after) {
const a = before.split('\n');
const b = after.split('\n');
if (a.length * b.length > 250_000) {
return [
...a.map((text) => ({ op: 'del', text })),
...b.map((text) => ({ op: 'add', text })),
];
}
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
const at = (i, j) => dp[i]?.[j] ?? 0;
for (let i = a.length - 1; i >= 0; i--) {
for (let j = b.length - 1; j >= 0; j--) {
dp[i][j] =
a[i] === b[j] ? at(i + 1, j + 1) + 1 : Math.max(at(i + 1, j), at(i, j + 1));
}
}
const out = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
if (a[i] === b[j]) {
out.push({ op: 'same', text: a[i] });
i++;
j++;
}
else if (at(i + 1, j) >= at(i, j + 1)) {
out.push({ op: 'del', text: a[i++] });
}
else {
out.push({ op: 'add', text: b[j++] });
}
}
while (i < a.length)
out.push({ op: 'del', text: a[i++] });
while (j < b.length)
out.push({ op: 'add', text: b[j++] });
return out;
}
function bar(value, max, width = 20) {
const n = max > 0 ? Math.round((Math.abs(value) / max) * width) : 0;
return '█'.repeat(n) || '▏';
}
export const DataTable = defineComponent({
name: 'DataTable',
title: 'Data table',
description: 'Tabular data (records with the same fields) as a sortable table. Prefer it over markdown tables for more than 3 rows.',
props: obj({
title: str('Short caption'),
columns: arr(obj({
key: str('Field name in each row'),
label: str('Column header'),
align: str(undefined, { enum: ['left', 'right', 'center'] }),
format: str('How to format values', {
enum: ['text', 'number', 'currency', 'percent', 'date', 'link'],
}),
}, ['key', 'label']), { minItems: 1, maxItems: 12 }),
rows: arr(row, { maxItems: 500 }),
}, ['columns', 'rows']),
fallbackText: (props) => `${heading(props)}${textTable(props.columns ?? [], props.rows ?? [])}`,
});
export const Chart = defineComponent({
name: 'Chart',
title: 'Chart',
description: '`bar` to compare categories, `line` for a trend over time. `data` is a list of points; `xKey` names the category/time field and each series names a numeric field.',
props: obj({
type: str('Chart type', { enum: ['bar', 'line'] }),
title: str(),
xKey: str('Field of each point used for the x axis'),
series: arr(obj({ key: str('Numeric field'), label: str() }, ['key']), {
minItems: 1,
maxItems: 6,
}),
data: arr(row, { minItems: 1, maxItems: 200 }),
unit: str('Unit shown after values, e.g. `KB` or `%`'),
}, ['type', 'xKey', 'series', 'data']),
fallbackText: (props) => {
const series = props.series ?? [];
const data = props.data ?? [];
const unit = props.unit ? ` ${s(props.unit)}` : '';
const lines = props.type === 'line'
? lineChartLines(props.xKey, series, data, unit)
: barChartLines(props.xKey, series, data, unit);
return `${heading(props)}\`\`\`\n${lines.join('\n')}\n\`\`\``;
},
});
const seriesName = (each) => s(each.label || each.key);
const pad = (value, width) => s(value).slice(0, width).padEnd(width);
/**
* A bar chart as text: one bar per point (one series), or per point a bar per series, all on one
* scale. At most 20 points.
*/
function barChartLines(xKey, series, data, unit) {
const value = (point, key) => Number(point[key]) || 0;
const max = Math.max(0, ...data.flatMap((point) => series.map((each) => Math.abs(value(point, each.key)))));
const lines = [];
const width = Math.min(18, Math.max(0, ...series.map((each) => seriesName(each).length)));
for (const point of data.slice(0, 20)) {
if (series.length <= 1) {
const key = series[0]?.key ?? '';
lines.push(`${pad(point[xKey], 18)} ${bar(value(point, key), max)} ${s(point[key])}${unit}`);
continue;
}
lines.push(s(point[xKey]));
for (const each of series) {
lines.push(` ${pad(seriesName(each), width)} ${bar(value(point, each.key), max)} ${s(point[each.key])}${unit}`);
}
}
if (data.length > 20)
lines.push(`… ${data.length - 20} more points`);
return lines;
}
const SPARKS = '▁▂▃▄▅▆▇█';
/** `▁▃▅█`: one block per value, scaled between the smallest and the largest. */
function sparkline(values) {
const min = Math.min(...values);
const max = Math.max(...values);
return values
.map((value) => max === min
? SPARKS[3]
: SPARKS[Math.round(((value - min) / (max - min)) * (SPARKS.length - 1))])
.join('');
}
/** A line chart as text: a sparkline per series (first → last, min/max), then the points as a table. */
function lineChartLines(xKey, series, data, unit) {
const width = Math.min(18, Math.max(0, ...series.map((each) => seriesName(each).length)));
// At most 40 sparkline points, sampled evenly across the data.
const step = Math.max(1, Math.ceil(data.length / 40));
const lines = series.map((each) => {
const values = data.map((point) => Number(point[each.key]) || 0);
if (values.length === 0)
return seriesName(each);
const spark = sparkline(values.filter((_value, index) => index % step === 0));
const first = s(data[0]?.[each.key]);
const last = s(data[data.length - 1]?.[each.key]);
return `${pad(seriesName(each), width)} ${spark} ${first} → ${last}${unit} (min ${Math.min(...values)}, max ${Math.max(...values)})`;
});
const columns = [
{ key: xKey, label: xKey },
...series.map((each) => ({ key: each.key, label: seriesName(each) })),
];
return [...lines, '', ...tableLines(columns, data, 20)];
}
export const KpiCards = defineComponent({
name: 'KpiCards',
title: 'KPI cards',
description: 'A few key numbers (metrics, totals, counts) as cards.',
props: obj({
title: str(),
items: arr(obj({
label: str(),
value: { type: ['string', 'number'] },
delta: str('Change, e.g. `+12%`'),
trend: str(undefined, { enum: ['up', 'down', 'flat'] }),
hint: str(),
}, ['label', 'value']), { minItems: 1, maxItems: 8 }),
}, ['items']),
fallbackText: (props) => `${heading(props)}${(props.items ?? [])
.map((item) => `• *${s(item.label)}:* ${s(item.value)}${item.delta ? ` (${s(item.delta)})` : ''}`)
.join('\n')}`,
});
export const SourceCards = defineComponent({
name: 'SourceCards',
title: 'Sources',
description: 'The documents, pages or links an answer is based on, as cards.',
props: obj({
title: str(),
items: arr(obj({ title: str(), url: str(), snippet: str(), source: str('e.g. Wiki, GitHub') }, [
'title',
]), { minItems: 1, maxItems: 12 }),
}, ['items']),
fallbackText: (props) => `${heading(props)}${(props.items ?? [])
.map((item) => `• ${item.url ? `<${s(item.url)}|${s(item.title)}>` : s(item.title)}${item.source ? ` — ${s(item.source)}` : ''}`)
.join('\n')}`,
});
export const Checklist = defineComponent({
name: 'Checklist',
title: 'Checklist',
description: 'Steps or a to-do list with done/not-done state.',
props: obj({
title: str(),
items: arr(obj({ label: str(), done: { type: 'boolean' }, note: str() }, ['label']), {
minItems: 1,
maxItems: 50,
}),
}, ['items']),
fallbackText: (props) => `${heading(props)}${(props.items ?? [])
.map((item) => `${item.done ? '☑' : '☐'} ${s(item.label)}${item.note ? ` — _${s(item.note)}_` : ''}`)
.join('\n')}`,
});
export const Timeline = defineComponent({
name: 'Timeline',
title: 'Timeline',
description: 'Events in chronological order (the history of a project, an incident, a deal).',
props: obj({
title: str(),
events: arr(obj({ date: str(), title: str(), description: str() }, ['title']), {
minItems: 1,
maxItems: 50,
}),
}, ['events']),
fallbackText: (props) => `${heading(props)}${(props.events ?? [])
.map((event) => `• ${event.date ? `*${s(event.date)}* ` : ''}${s(event.title)}${event.description ? ` — ${s(event.description)}` : ''}`)
.join('\n')}`,
});
export const CodeBlock = defineComponent({
name: 'CodeBlock',
title: 'Code',
description: 'A code snippet or file excerpt with syntax highlighting.',
props: obj({ title: str(), language: str(), code: str() }, ['code']),
fallbackText: (props) => `${heading(props)}\`\`\`\n${s(props.code)}\n\`\`\``,
});
export const Diff = defineComponent({
name: 'Diff',
title: 'Diff',
description: 'A before/after change to a text or code file, as a line diff.',
props: obj({ title: str(), language: str(), before: str(), after: str() }, ['before', 'after']),
fallbackText: (props) => `${heading(props)}\`\`\`\n${lineDiff(s(props.before), s(props.after))
.map((line) => `${line.op === 'add' ? '+' : line.op === 'del' ? '-' : ' '} ${line.text}`)
.join('\n')}\n\`\`\``,
});
export const Callout = defineComponent({
name: 'Callout',
title: 'Callout',
description: 'A short note, warning or success message, emphasized.',
props: obj({
tone: str(undefined, { enum: ['info', 'success', 'warning', 'danger'] }),
title: str(),
text: str(),
}, ['text']),
fallbackText: (props) => `${props.tone === 'warning' || props.tone === 'danger' ? ':warning: ' : ':information_source: '}${heading(props)}${s(props.text)}`,
});
export const Stack = defineComponent({
name: 'Stack',
title: 'Stack',
description: 'Lays its children out vertically (or horizontally with direction=row).',
props: obj({ direction: str(undefined, { enum: ['column', 'row'] }), gap: { type: 'number' } }),
children: true,
fallbackText: () => '',
});
export const Card = defineComponent({
name: 'Card',
title: 'Card',
description: 'A box with an optional title around its children.',
props: obj({ title: str(), subtitle: str() }),
children: true,
fallbackText: (props) => props.title ? `*${s(props.title)}*${props.subtitle ? ` — ${s(props.subtitle)}` : ''}` : '',
});
export const Heading = defineComponent({
name: 'Heading',
title: 'Heading',
description: 'A heading.',
props: obj({ text: str() }, ['text']),
fallbackText: (props) => `*${s(props.text)}*`,
});
export const Text = defineComponent({
name: 'Text',
title: 'Text',
description: 'A paragraph of plain text (no markdown).',
props: obj({ text: str(), muted: { type: 'boolean' } }, ['text']),
fallbackText: (props) => s(props.text),
});
export const Badge = defineComponent({
name: 'Badge',
title: 'Badge',
description: 'A small label.',
props: obj({
text: str(),
tone: str(undefined, { enum: ['neutral', 'brand', 'success', 'warning', 'danger'] }),
}, ['text']),
fallbackText: (props) => `[${s(props.text)}]`,
});
export const Link = defineComponent({
name: 'Link',
title: 'Link',
description: 'A link to an http(s) URL.',
props: obj({ text: str(), url: str(undefined, { pattern: '^https?://' }) }, ['text', 'url']),
fallbackText: (props) => `<${s(props.url)}|${s(props.text || props.url)}>`,
});
export const Image = defineComponent({
name: 'Image',
title: 'Image',
description: 'An image from an https URL.',
props: obj({ url: str(undefined, { pattern: '^https://' }), alt: str() }, ['url']),
fallbackText: (props) => s(props.url),
});
/** Content components: data a model shows. */
export const BUILTIN_COMPONENTS = [
DataTable,
Chart,
KpiCards,
SourceCards,
Checklist,
Timeline,
CodeBlock,
Diff,
Callout,
];
/** Layout and text primitives, mostly for composing trees (`Stack`, `Card` take children). */
export const LAYOUT_COMPONENTS = [
Stack,
Card,
Heading,
Text,
Badge,
Link,
Image,
];
export { defineSandbox, Sandbox } from './sandbox.js';
//# sourceMappingURL=builtins.js.map