UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

189 lines • 8.7 kB
import { standaloneComponents, toolNameFor } from './catalog.js'; import { toJsonSchema } from './schema.js'; import { GENUI_TREE_COMPONENT } from './tree.js'; /** * Plain text (Slack mrkdwn compatible) for a pushed component: its definition's `fallbackText`, or * the props as a JSON block when the catalog has no text for it. A `fallbackText` that returns an * empty (or blank) string means "nothing to say" — a bare layout like `Stack` — and renders nothing. * A tree frame ({@link GENUI_TREE_COMPONENT}) renders node by node, so a layout's children still * render. Never throws — a fallback that fails prints the JSON instead. */ export function componentToText(catalog, component, props) { if (component === GENUI_TREE_COMPONENT) { const root = props.root; return root === undefined ? '' : treeToText(catalog, root); } return nodeText(catalog, component, props) ?? ''; } /** * One component's own text (children aside): its `fallbackText` (`''` when blank — nothing to say), * the props as JSON when it has none or it throws, or `undefined` for a layout node without text. */ function nodeText(catalog, component, props) { const definition = catalog.get(component); if (definition?.fallbackText !== undefined) { try { const text = definition.fallbackText(props); if (typeof text === 'string') return text.trim().length > 0 ? text : ''; } catch { /* fall through to the generic rendering */ } return jsonBlock(props); } return definition?.children === true ? undefined : jsonBlock(props); } /** Plain text for a composed tree: each node's text, depth-first, blank-line free. */ export function treeToText(catalog, root) { const out = []; const visit = (node, depth) => { if (depth > 32) return; const text = nodeText(catalog, node.type, node.props); if (text !== undefined) out.push(text); for (const child of node.children ?? []) visit(child, depth + 1); }; visit(root, 0); return out.filter((line) => line.trim().length > 0).join('\n'); } function jsonBlock(props) { const title = typeof props.title === 'string' && props.title.length > 0 ? `*${props.title}*\n` : ''; return `${title}\`\`\`\n${JSON.stringify(props, null, 2)}\n\`\`\``; } /** * The catalog described for a model: one entry per model-facing component with its description and * a compact TypeScript-like signature of its props. What a tree-mode (or show) tool puts in its * description, and what a host can add to a system prompt. */ export function catalogToModelText(catalog, options = {}) { const mode = options.mode ?? 'tree'; const lines = []; if (mode === 'tree') { lines.push('Compose the UI as ONE tree of elements: { "type": <component>, "props": { … }, "children"?: [ … ] }.', 'Only components marked "takes children" accept children; `children` is a literal JSON array, never a string.', 'Every element starts with "type", then "props" — a single component is { "type": …, "props": { … } } too, never its props alone. A component\'s own field order (if its description gives one) is the order of the keys INSIDE its "props".', ...treeExamples(catalog), 'Components:'); } else if (mode === 'show') { lines.push('Pass { "component": <name>, "props": { … } } with one of these components:'); } else { lines.push('Components you can show (one tool each):'); } // Outside a tree nothing nests, so a layout (`children: true`) is not offered on its own. const components = mode === 'tree' ? catalog.modelComponents() : standaloneComponents(catalog); for (const component of components) { const schema = toJsonSchema(component.props); const head = mode === 'per-component' ? `- ${component.name} (tool \`${toolNameFor(component.name, options.namePrefix)}\`)` : `- ${component.name}`; lines.push(`${head}: ${component.description}`); if (schema !== undefined) { lines.push(` props: ${summarizeSchema(schema, schema, 0)}`); } if (mode === 'tree' && component.children === true) { lines.push(' takes children'); } } return lines.join('\n'); } /** * One or two literal calls built from the catalog's own components — the envelope shown rather * than described: a one-component tree, and a layout holding it when the catalog has one. */ function treeExamples(catalog) { const components = catalog.modelComponents(); const leaves = components.filter((component) => component.children !== true && requiredKeys(component).length > 0); // A component drawn while it is written (the sandbox) is the one a model most often sends as // bare props — long, written field by field — so it is the one worth showing wrapped. const leaf = leaves.find((component) => component.partialProps !== undefined) ?? leaves[0]; if (leaf === undefined) return []; const props = `{ ${requiredKeys(leaf) .map((key) => `"${key}": …`) .join(', ')} }`; const element = `{ "type": "${leaf.name}", "props": ${props} }`; const layout = components.find((component) => component.children === true); return [ `Example, one component: ${element}`, ...(layout !== undefined ? [ `Example, nested: { "type": "${layout.name}", "props": { … }, "children": [ ${element} ] }`, ] : []), ]; } function requiredKeys(component) { const schema = toJsonSchema(component.props); return Array.isArray(schema?.required) ? schema.required.filter((key) => typeof key === 'string') : []; } /** A JSON Schema as a one-line TypeScript-like type, e.g. `{ title?: string, rows: object[] }`. */ export function summarizeSchema(schema, root, depth) { if (typeof schema !== 'object' || schema === null) return 'unknown'; const s = schema; if (depth > 5) return '…'; if (typeof s.$ref === 'string') { return s.$ref === '#' ? 'Self' : (s.$ref.split('/').pop() ?? 'unknown'); } if (Array.isArray(s.enum)) { return s.enum.map((option) => JSON.stringify(option)).join(' | '); } if ('const' in s) return JSON.stringify(s.const); const variants = (s.anyOf ?? s.oneOf); if (Array.isArray(variants)) { return variants.map((variant) => summarizeSchema(variant, root, depth + 1)).join(' | '); } const types = Array.isArray(s.type) ? s.type : s.type !== undefined ? [s.type] : []; if (types.length > 1) { return types.map((type) => summarizeSchema({ ...s, type }, root, depth + 1)).join(' | '); } const type = types[0]; if (type === 'array') { const item = s.items === undefined ? 'unknown' : summarizeSchema(s.items, root, depth + 1); return /[|\s]/.test(item) && !item.startsWith('{') ? `(${item})[]` : `${item}[]`; } if (type === 'object' || (type === undefined && s.properties !== undefined)) { const properties = (s.properties ?? {}); const keys = Object.keys(properties); if (keys.length === 0) return 'object'; const required = new Set(Array.isArray(s.required) ? s.required : []); const fields = keys.map((key) => { const field = properties[key]; const note = typeof field?.description === 'string' && depth === 0 ? ` /* ${field.description} */` : ''; return `${key}${required.has(key) ? '' : '?'}: ${summarizeSchema(field, root, depth + 1)}${note}`; }); return `{ ${fields.join(', ')} }`; } if (type === 'integer') return 'number'; return type ?? 'unknown'; } /** Replace `{{path}}` placeholders with prop values (objects as JSON). */ export function fillTemplate(template, props) { return template.replace(/\{\{\s*([\w.[\]]+)\s*\}\}/g, (_match, path) => { const value = getPath(props, path); if (value === null || value === undefined) return ''; return typeof value === 'object' ? JSON.stringify(value) : String(value); }); } /** Read `a.b[0].c` from a value; `undefined` when any step is missing. */ export function getPath(source, path) { let current = source; for (const part of path .replace(/\[(\d+)\]/g, '.$1') .split('.') .filter(Boolean)) { if (current === null || current === undefined) return undefined; current = current[part]; } return current; } //# sourceMappingURL=text.js.map