diff --git a/.changeset/empty-a2ui-cli-prompt.md b/.changeset/empty-a2ui-cli-prompt.md new file mode 100644 index 0000000000..e4907d82cd --- /dev/null +++ b/.changeset/empty-a2ui-cli-prompt.md @@ -0,0 +1,5 @@ +--- + +--- + +No package release is required because this change wires A2UI prompt/catalog generation tooling for the GenUI workspace without changing published runtime behavior. diff --git a/biome.jsonc b/biome.jsonc index 76db89d25f..2e95caf670 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -62,6 +62,8 @@ // REPL examples use Lynx platform globals and are not subject to lint rules "packages/repl/src/examples/**", + // REPL generated artifacts can exceed Biome's file-size limit + "packages/repl/src/generated/**", ], "rules": { // We are migrating from ESLint to Biome diff --git a/packages/genui/a2ui-catalog-extractor/README.md b/packages/genui/a2ui-catalog-extractor/README.md index 50bf7f04e0..bea6083197 100644 --- a/packages/genui/a2ui-catalog-extractor/README.md +++ b/packages/genui/a2ui-catalog-extractor/README.md @@ -38,6 +38,7 @@ It can also wrap those generated components with a `catalogId`, - It does not ask you to write JSON Schema in comments. - It does not expand arbitrary imported type aliases or external interfaces. +- It does not call an LLM or choose a model. The package consumes TypeDoc reflection data. This keeps the implementation small, but it also means catalog-facing shapes should be written inline in @@ -53,7 +54,7 @@ the marked interface. ### Package manager -Install it as a development dependency: +Install the extractor as a development dependency: ```bash pnpm add -D @lynx-js/a2ui-catalog-extractor @@ -385,8 +386,16 @@ export interface CardProps { ## CLI Reference +The package exposes the standalone `a2ui-catalog-extractor` binary. The +separate `@lynx-js/a2ui-cli` package also exposes this flow as +`a2ui-cli generate catalog`. + +### Generate catalog artifacts + ```bash a2ui-catalog-extractor [options] +# or +a2ui-cli generate catalog [options] ``` | Option | Description | Default | diff --git a/packages/genui/a2ui-catalog-extractor/package.json b/packages/genui/a2ui-catalog-extractor/package.json index 57c077df57..b4a7ecb07d 100644 --- a/packages/genui/a2ui-catalog-extractor/package.json +++ b/packages/genui/a2ui-catalog-extractor/package.json @@ -1,8 +1,12 @@ { "name": "@lynx-js/a2ui-catalog-extractor", "version": "0.0.0", - "private": true, "description": "TypeDoc-driven A2UI catalog extractor for TypeScript interfaces.", + "repository": { + "type": "git", + "url": "https://github.com/lynx-family/lynx-stack.git", + "directory": "packages/genui/a2ui-catalog-extractor" + }, "license": "Apache-2.0", "type": "module", "exports": { @@ -10,6 +14,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./cli": { + "types": "./dist/cli.d.ts", + "default": "./dist/cli.js" + }, "./skill": "./skills/a2ui-catalog-extractor/SKILL.md", "./package.json": "./package.json" }, diff --git a/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md index 314874d32e..a2ade3aac2 100644 --- a/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md +++ b/packages/genui/a2ui-catalog-extractor/readme.zh_cn.md @@ -36,6 +36,7 @@ agent 哪些 props 合法、哪些 props 必填、哪些 enum 值可用,以及 - 它不直接使用 TypeScript compiler API。 - 它不要求你在注释里写 JSON Schema。 - 它不会展开任意导入的 type alias 或外部 interface。 +- 它不会调用 LLM,也不会替你选择模型。 这个包消费 TypeDoc reflection 数据。这样实现更小,但也意味着面向 catalog 的类型形状应该直接内联写在被标记的 interface 中。 @@ -50,7 +51,7 @@ agent 哪些 props 合法、哪些 props 必填、哪些 enum 值可用,以及 ### 包管理器 -把它安装为开发依赖: +把 extractor 安装为开发依赖: ```bash pnpm add -D @lynx-js/a2ui-catalog-extractor @@ -377,8 +378,16 @@ export interface CardProps { ## CLI 参考 +这个包暴露独立的 `a2ui-catalog-extractor` binary。单独的 +`@lynx-js/a2ui-cli` 包也把这个流程暴露为 +`a2ui-cli generate catalog`。 + +### 生成 catalog artifacts + ```bash a2ui-catalog-extractor [options] +# 或 +a2ui-cli generate catalog [options] ``` | 选项 | 说明 | 默认值 | diff --git a/packages/genui/a2ui-catalog-extractor/src/cli.ts b/packages/genui/a2ui-catalog-extractor/src/cli.ts index 32834b58fe..1d12f59a25 100644 --- a/packages/genui/a2ui-catalog-extractor/src/cli.ts +++ b/packages/genui/a2ui-catalog-extractor/src/cli.ts @@ -34,9 +34,9 @@ Options: --typedoc-json Read an existing TypeDoc JSON project instead of running TypeDoc conversion. - --out-dir Output directory for component catalog.json files. - --version Print the package version. - --help Print this help message. + --out-dir Output directory for component catalog.json files. + --version Print the package version. + --help Print this help message. Defaults: --catalog-dir src/catalog @@ -55,6 +55,8 @@ export function parseCliArgs(args: string[]): CliOptions { for (let index = 0; index < args.length; index += 1) { const arg = args[index]!; switch (arg) { + case 'catalog-extractor': + break; case '--catalog-dir': options.catalogDirs.push(readValue(args, ++index, arg)); break; diff --git a/packages/genui/a2ui-cli/AGENTS.md b/packages/genui/a2ui-cli/AGENTS.md new file mode 100644 index 0000000000..ab6dc09ada --- /dev/null +++ b/packages/genui/a2ui-cli/AGENTS.md @@ -0,0 +1,23 @@ +# a2ui-cli + +Keep this package as the single public CLI entry point for A2UI setup. It should +orchestrate other packages instead of implementing catalog extraction or prompt +construction itself. + +`generate catalog` should delegate to `@lynx-js/a2ui-catalog-extractor`. +`generate prompt` should delegate to `@lynx-js/a2ui-prompt`. + +Do not require users to pass `--catalog-dir` for the common prompt path. A prompt +without `--catalog-dir` should use the built-in A2UI basic catalog from +`@lynx-js/a2ui-prompt`; use `--catalog-dir` only for custom generated catalog +artifacts. + +When `--catalog-dir` is provided, empty catalog directories should fail clearly +instead of producing a prompt with an empty component catalog. + +The executable file is `bin/cli.js`; keep `package.json` `bin.a2ui-cli` pointed +at that path. + +When testing local CLI changes, run the bin directly with Node. If testing +changes in `@lynx-js/a2ui-prompt`, rebuild that package first because this CLI +imports it through package exports. diff --git a/packages/genui/a2ui-cli/README.md b/packages/genui/a2ui-cli/README.md new file mode 100644 index 0000000000..de2326f905 --- /dev/null +++ b/packages/genui/a2ui-cli/README.md @@ -0,0 +1,65 @@ +# A2UI CLI + +`@lynx-js/a2ui-cli` provides one command line entry point for A2UI agent setup. +It can generate catalog artifacts from TypeScript catalog definitions and build +a system prompt for an A2UI generation agent. + +## Usage + +Generate a system prompt with the built-in A2UI basic catalog: + +```bash +npx @lynx-js/a2ui-cli@latest generate prompt --out dist/a2ui-system-prompt.txt +``` + +Generate catalog artifacts for a custom catalog: + +```bash +npx @lynx-js/a2ui-cli@latest generate catalog \ + --catalog-dir src/catalog \ + --source src/functions \ + --out-dir dist/catalog +``` + +Generate a system prompt for a custom catalog: + +```bash +npx @lynx-js/a2ui-cli@latest generate prompt \ + --catalog-dir dist/catalog \ + --catalog-id https://example.com/catalogs/custom/v1/catalog.json \ + --out dist/a2ui-system-prompt.txt +``` + +`generate prompt` uses the built-in A2UI basic catalog by default. Pass +`--catalog-dir` only when generating a prompt for custom generated catalog +artifacts. When `--catalog-dir` is provided, the directory must contain files +like `/catalog.json`. + +## Commands + +### `generate catalog` + +Delegates catalog extraction to `@lynx-js/a2ui-catalog-extractor`. + +Useful options: + +- `--catalog-dir `: directory to scan for TypeScript component catalog + interfaces. Defaults to `src/catalog`. +- `--source `: source file or directory to scan for catalog functions. + Repeatable. +- `--typedoc-json `: read an existing TypeDoc JSON project. +- `--out-dir `: output directory for generated catalog artifacts. Defaults + to `dist/catalog`. + +### `generate prompt` + +Delegates prompt construction to `@lynx-js/a2ui-prompt`. + +Useful options: + +- `--catalog-dir `: generated catalog artifact directory. Omit this option + to use the built-in A2UI basic catalog. +- `--catalog-id `: catalog id to require in `createSurface` messages. + Defaults to the built-in A2UI basic catalog id. +- `--out `: write the prompt to a file instead of stdout. +- `--appendix `: append extra instructions to the generated prompt. diff --git a/packages/genui/a2ui-cli/bin/cli.js b/packages/genui/a2ui-cli/bin/cli.js new file mode 100755 index 0000000000..4b233ecae0 --- /dev/null +++ b/packages/genui/a2ui-cli/bin/cli.js @@ -0,0 +1,197 @@ +#!/usr/bin/env node +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import * as fs from 'node:fs'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; + +const usage = `Usage: a2ui-cli generate [options] + +Targets: + catalog Generate A2UI component and function catalog files. + prompt Generate an A2UI system prompt from catalog files. +`; + +const generateCatalogUsage = `Usage: a2ui-cli generate catalog [options] + +Options: + --catalog-dir Directory to scan for TypeScript catalog interfaces. + --source Source file or directory to scan. Repeatable. + --typedoc-json + Read an existing TypeDoc JSON project instead of + running TypeDoc conversion. + --out-dir Output directory for component catalog.json files. + --version Print the package version. + --help Print this help message. + +Defaults: + --catalog-dir src/catalog + --out-dir dist/catalog +`; + +const generatePromptUsage = `Usage: a2ui-cli generate prompt [options] + +Options: + --catalog-dir Directory containing generated catalog files. When + omitted, use the built-in A2UI basic catalog. + --catalog-id Catalog id to require in createSurface messages. + --out Write the prompt to a file instead of stdout. + --appendix Append extra instructions to the generated prompt. + --version Print the package version. + --help Print this help message. + +Defaults: + --catalog-id built-in A2UI basic catalog id +`; + +try { + process.exitCode = await runCli(process.argv.slice(2)); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} + +async function runCli(args, cwd = process.cwd()) { + const command = args[0]; + if (command === undefined || command === '--help' || command === '-h') { + console.info(usage); + return 0; + } + if (command === '--version' || command === '-v') { + printPackageVersion(); + return 0; + } + if (command !== 'generate') { + throw new Error(`Unknown command: ${command}`); + } + + const target = args[1]; + const targetArgs = args.slice(2); + if (target === undefined || target === '--help' || target === '-h') { + console.info(usage); + return 0; + } + if (target === '--version' || target === '-v') { + printPackageVersion(); + return 0; + } + if (target === 'catalog') { + if (targetArgs.includes('--help') || targetArgs.includes('-h')) { + console.info(generateCatalogUsage); + return 0; + } + const { runCli: runCatalogExtractorCli } = await import( + '@lynx-js/a2ui-catalog-extractor/cli' + ); + return await runCatalogExtractorCli(targetArgs, cwd); + } + if (target === 'prompt') { + return runGeneratePromptCli(targetArgs, cwd); + } + throw new Error(`Unknown generate target: ${target}`); +} + +async function runGeneratePromptCli(args, cwd = process.cwd()) { + const options = parseGeneratePromptArgs(args); + if (options.help) { + console.info(generatePromptUsage); + return 0; + } + if (options.version) { + printPackageVersion(); + return 0; + } + + const { + BASIC_CATALOG, + BASIC_CATALOG_ID, + buildA2UISystemPrompt, + readA2UICatalogFromDirectory, + } = await import('@lynx-js/a2ui-prompt'); + const catalog = options.catalogDir + ? readA2UICatalogFromDirectory({ + catalogDir: options.catalogDir, + catalogId: options.catalogId ?? BASIC_CATALOG_ID, + cwd, + }) + : (options.catalogId + ? { ...BASIC_CATALOG, id: options.catalogId } + : undefined); + if (options.catalogDir && catalog && isEmptyCatalog(catalog)) { + throw new Error( + `[a2ui-cli] No components or functions found in generated catalog directory: ${options.catalogDir}`, + ); + } + const systemPrompt = buildA2UISystemPrompt({ + ...(catalog ? { catalog } : {}), + ...(options.appendix ? { appendix: options.appendix } : {}), + }); + + if (options.out) { + const outPath = path.resolve(cwd, options.out); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, systemPrompt); + console.info(`Generated A2UI system prompt at ${options.out}.`); + } else { + process.stdout.write(systemPrompt); + } + + return 0; +} + +function parseGeneratePromptArgs(args) { + const options = { + help: false, + version: false, + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + switch (arg) { + case '--catalog-dir': + options.catalogDir = readValue(args, ++index, arg); + break; + case '--catalog-id': + options.catalogId = readValue(args, ++index, arg); + break; + case '--out': + options.out = readValue(args, ++index, arg); + break; + case '--appendix': + options.appendix = readValue(args, ++index, arg); + break; + case '--help': + case '-h': + options.help = true; + break; + case '--version': + case '-v': + options.version = true; + break; + default: + throw new Error(`Unknown option: ${arg}`); + } + } + + return options; +} + +function isEmptyCatalog(catalog) { + return (!Array.isArray(catalog.components) || catalog.components.length === 0) + && (!Array.isArray(catalog.functions) || catalog.functions.length === 0); +} + +function readValue(args, index, option) { + const value = args[index]; + if (!value || value.startsWith('-')) { + throw new Error(`Missing value for ${option}.`); + } + return value; +} + +function printPackageVersion() { + const require = createRequire(import.meta.url); + const packageJson = require('../package.json'); + console.info(packageJson.version); +} diff --git a/packages/genui/a2ui-cli/package.json b/packages/genui/a2ui-cli/package.json new file mode 100644 index 0000000000..671a72ae50 --- /dev/null +++ b/packages/genui/a2ui-cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "@lynx-js/a2ui-cli", + "version": "0.0.0", + "description": "CLI for generating A2UI catalog artifacts and system prompts.", + "repository": { + "type": "git", + "url": "https://github.com/lynx-family/lynx-stack.git", + "directory": "packages/genui/a2ui-cli" + }, + "license": "Apache-2.0", + "type": "module", + "bin": { + "a2ui-cli": "./bin/cli.js" + }, + "files": [ + "bin", + "README.md" + ], + "dependencies": { + "@lynx-js/a2ui-catalog-extractor": "workspace:*", + "@lynx-js/a2ui-prompt": "workspace:*" + }, + "engines": { + "node": ">=22" + } +} diff --git a/packages/genui/a2ui-prompt/AGENTS.md b/packages/genui/a2ui-prompt/AGENTS.md new file mode 100644 index 0000000000..8a1c32d111 --- /dev/null +++ b/packages/genui/a2ui-prompt/AGENTS.md @@ -0,0 +1,21 @@ +# a2ui-prompt + +Keep `packages/genui/server/agent` as the source of truth for built-in A2UI +prompt and catalog logic. Do not copy the built-in catalog JSON, prompt text, or +example definitions into this package. + +This package may contain thin package-specific adapters such as +`readA2UICatalogFromDirectory`, but shared prompt rendering and built-in catalog +behavior should live in the server agent source and be re-exported from here. + +The server package must remain self-contained for package-root Vercel +deployments. Do not make `packages/genui/server` depend on `@lynx-js/a2ui-prompt` +at runtime. + +When editing exported functions or constants reachable through this package, +remember that `isolatedDeclarations` requires explicit types for exported APIs. + +Run `pnpm -C packages/genui/a2ui-prompt build` after changing server agent +prompt/catalog sources or this package's adapter code. The CLI imports this +package through its published exports, so local CLI tests may need this build +step before changes are visible. diff --git a/packages/genui/a2ui-prompt/README.md b/packages/genui/a2ui-prompt/README.md new file mode 100644 index 0000000000..467676692a --- /dev/null +++ b/packages/genui/a2ui-prompt/README.md @@ -0,0 +1,65 @@ +# A2UI Prompt + +`@lynx-js/a2ui-prompt` provides A2UI system prompt construction utilities for +CLI and backend usage. + +The source of truth for the built-in prompt and catalog is +`packages/genui/server/agent`. This package re-exports and bundles those server +agent sources for publishing, so the server package stays self-contained for +package-root deployments while CLI users can still install a standalone prompt +package. + +## Usage + +Build a prompt with the built-in A2UI basic catalog: + +```ts +import { buildA2UISystemPrompt } from '@lynx-js/a2ui-prompt'; + +const prompt = buildA2UISystemPrompt(); +``` + +Read generated catalog artifacts and build a prompt for a custom catalog: + +```ts +import { + buildA2UISystemPrompt, + readA2UICatalogFromDirectory, +} from '@lynx-js/a2ui-prompt'; + +const catalog = readA2UICatalogFromDirectory({ + catalogDir: 'dist/catalog', + catalogId: 'https://example.com/catalogs/custom/v1/catalog.json', +}); + +const prompt = buildA2UISystemPrompt({ catalog }); +``` + +`readA2UICatalogFromDirectory` expects generated files such as +`/catalog.json` and optional function definitions under `functions/`. +Use `@lynx-js/a2ui-cli generate catalog` or +`@lynx-js/a2ui-catalog-extractor` to create those artifacts. + +## Exports + +- `buildA2UISystemPrompt` +- `A2UI_SYSTEM_PROMPT` +- `BASIC_CATALOG` +- `BASIC_CATALOG_ID` +- `renderCatalogReference` +- `createA2UICatalogFromManifests` +- `readA2UICatalogFromDirectory` + +## Local Development + +Build the publishable bundle: + +```bash +pnpm -C packages/genui/a2ui-prompt build +``` + +Run the package type check: + +```bash +pnpm -C packages/genui/a2ui-prompt exec tsc -p tsconfig.json --noEmit +``` diff --git a/packages/genui/a2ui-prompt/package.json b/packages/genui/a2ui-prompt/package.json new file mode 100644 index 0000000000..8a0994f2b6 --- /dev/null +++ b/packages/genui/a2ui-prompt/package.json @@ -0,0 +1,32 @@ +{ + "name": "@lynx-js/a2ui-prompt", + "version": "0.0.0", + "description": "A2UI prompt and catalog rendering utilities for CLI prompt generation.", + "repository": { + "type": "git", + "url": "https://github.com/lynx-family/lynx-stack.git", + "directory": "packages/genui/a2ui-prompt" + }, + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "rslib build" + }, + "devDependencies": { + "@types/node": "^24.10.13" + }, + "engines": { + "node": ">=22" + } +} diff --git a/packages/genui/a2ui-prompt/rslib.config.ts b/packages/genui/a2ui-prompt/rslib.config.ts new file mode 100644 index 0000000000..45f478c40b --- /dev/null +++ b/packages/genui/a2ui-prompt/rslib.config.ts @@ -0,0 +1,19 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import type { RslibConfig } from '@rslib/core'; +import { defineConfig } from '@rslib/core'; + +const config: RslibConfig = defineConfig({ + lib: [ + { format: 'esm', syntax: 'es2022', dts: { bundle: true, tsgo: true } }, + ], + source: { + entry: { + index: './src/index.ts', + }, + tsconfigPath: './tsconfig.build.json', + }, +}); + +export default config; diff --git a/packages/genui/a2ui-prompt/src/index.ts b/packages/genui/a2ui-prompt/src/index.ts new file mode 100644 index 0000000000..77b5d3005c --- /dev/null +++ b/packages/genui/a2ui-prompt/src/index.ts @@ -0,0 +1,146 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { + createA2UICatalogFromManifests, +} from '../../server/agent/a2ui-catalog.js'; +import type { + A2UICatalog, + A2UIFunctionSpec, + JsonSchema, +} from '../../server/agent/a2ui-catalog.js'; + +export * from '../../server/agent/a2ui-catalog.js'; +export * from '../../server/agent/a2ui-examples.js'; +export * from '../../server/agent/a2ui-prompt.js'; + +export interface ReadA2UICatalogDirectoryOptions { + catalogDir: string; + catalogId: string; + cwd?: string; + label?: string; + version?: string; +} + +export function readA2UICatalogFromDirectory( + options: ReadA2UICatalogDirectoryOptions, +): A2UICatalog { + const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd(); + const catalogDir = path.resolve(cwd, options.catalogDir); + if (!fs.existsSync(catalogDir)) { + throw new Error( + `[a2ui-prompt] Catalog directory does not exist: ${options.catalogDir}`, + ); + } + if (!fs.statSync(catalogDir).isDirectory()) { + throw new Error( + `[a2ui-prompt] Catalog path is not a directory: ${options.catalogDir}`, + ); + } + + const componentManifests: Record[] = []; + for (const entry of fs.readdirSync(catalogDir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name === 'functions') { + continue; + } + const catalogJsonPath = path.join(catalogDir, entry.name, 'catalog.json'); + if (fs.existsSync(catalogJsonPath)) { + componentManifests.push(readCatalogManifest(catalogJsonPath)); + } + } + if (componentManifests.length === 0) { + throw new Error( + `[a2ui-prompt] No component catalog files found in ${options.catalogDir}. Expected files like /catalog.json. Run "a2ui-cli generate catalog" first or pass --catalog-dir to the generated catalog directory.`, + ); + } + + return createA2UICatalogFromManifests({ + catalogId: options.catalogId, + componentManifests, + functions: readFunctionDefinitions(catalogDir), + ...(options.label ? { label: options.label } : {}), + ...(options.version ? { version: options.version } : {}), + }); +} + +function readFunctionDefinitions(catalogDir: string): A2UIFunctionSpec[] { + const functionsDir = path.join(catalogDir, 'functions'); + if (!fs.existsSync(functionsDir)) { + return []; + } + if (!fs.statSync(functionsDir).isDirectory()) { + throw new Error( + `[a2ui-prompt] Expected functions directory at ${functionsDir}.`, + ); + } + + const functions: A2UIFunctionSpec[] = []; + for (const entry of fs.readdirSync(functionsDir, { withFileTypes: true })) { + if (!entry.isFile() || !entry.name.endsWith('.json')) { + continue; + } + const functionRecord = readJsonObject(path.join(functionsDir, entry.name)); + for (const [name, value] of Object.entries(functionRecord)) { + if (!isRecord(value)) { + continue; + } + const description = value['description']; + const parameters = value['parameters']; + const returnType = value['returnType']; + functions.push({ + name, + ...(typeof description === 'string' ? { description } : {}), + parameters: isRecord(parameters) + ? parameters as JsonSchema + : { type: 'object', properties: {}, additionalProperties: false }, + returnType: isReturnType(returnType) ? returnType : 'any', + }); + } + } + + return functions.sort((left, right) => left.name.localeCompare(right.name)); +} + +function readJsonObject(filePath: string): Record { + const value = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown; + if (!isRecord(value)) { + throw new Error(`[a2ui-prompt] Expected JSON object in ${filePath}.`); + } + return value; +} + +function readCatalogManifest(filePath: string): Record { + const manifest = readJsonObject(filePath); + const keys = Object.keys(manifest); + if (keys.length !== 1) { + throw new Error( + `[a2ui-prompt] Expected exactly one component manifest in ${filePath}, found ${keys.length}.`, + ); + } + const componentName = keys[0]!; + const schema = manifest[componentName]; + if (!isRecord(schema)) { + throw new Error( + `[a2ui-prompt] Expected JSON schema object for ${componentName} in ${filePath}.`, + ); + } + return { [componentName]: schema as JsonSchema }; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isReturnType(value: unknown): value is A2UIFunctionSpec['returnType'] { + return value === 'string' + || value === 'number' + || value === 'boolean' + || value === 'array' + || value === 'object' + || value === 'any' + || value === 'void'; +} diff --git a/packages/genui/a2ui-prompt/tsconfig.build.json b/packages/genui/a2ui-prompt/tsconfig.build.json new file mode 100644 index 0000000000..fed4574d75 --- /dev/null +++ b/packages/genui/a2ui-prompt/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "src", + "../server/agent/a2ui-catalog.ts", + "../server/agent/a2ui-examples.ts", + "../server/agent/a2ui-prompt.ts", + "../server/agent/catalog/**/*.json", + ], +} diff --git a/packages/genui/a2ui-prompt/tsconfig.json b/packages/genui/a2ui-prompt/tsconfig.json new file mode 100644 index 0000000000..9c53a55a0d --- /dev/null +++ b/packages/genui/a2ui-prompt/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": "..", + "noEmit": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["node"], + }, + "include": [ + "src", + "rslib.config.ts", + "../server/agent/a2ui-catalog.ts", + "../server/agent/a2ui-examples.ts", + "../server/agent/a2ui-prompt.ts", + "../server/agent/catalog/**/*.json", + ], + "references": [], +} diff --git a/packages/genui/a2ui-prompt/turbo.json b/packages/genui/a2ui-prompt/turbo.json new file mode 100644 index 0000000000..fa17b63f01 --- /dev/null +++ b/packages/genui/a2ui-prompt/turbo.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": [ + "//" + ], + "tasks": { + "build": { + "dependsOn": [ + "^build" + ], + "inputs": [ + "src/**", + "../server/agent/a2ui-catalog.ts", + "../server/agent/a2ui-examples.ts", + "../server/agent/a2ui-prompt.ts", + "../server/agent/catalog/**/*.json", + "package.json", + "rslib.config.ts", + "tsconfig.build.json", + "tsconfig.json" + ], + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/packages/genui/a2ui/package.json b/packages/genui/a2ui/package.json index c9a3e6a442..77b00ca983 100644 --- a/packages/genui/a2ui/package.json +++ b/packages/genui/a2ui/package.json @@ -123,7 +123,7 @@ "types": "./dist/index.d.ts", "scripts": { "build": "tsc --project tsconfig.build.json && npm run build:catalog", - "build:catalog": "a2ui-catalog-extractor --catalog-dir src/catalog --out-dir dist/catalog", + "build:catalog": "a2ui-cli generate catalog --catalog-dir src/catalog --out-dir dist/catalog", "test": "rstest" }, "dependencies": { @@ -131,7 +131,7 @@ "@preact/signals": "^2.5.1" }, "devDependencies": { - "@lynx-js/a2ui-catalog-extractor": "workspace:*", + "@lynx-js/a2ui-cli": "workspace:*", "@lynx-js/lynx-ui": "^3.133.0", "@lynx-js/react": "workspace:*", "@lynx-js/types": "3.7.0", diff --git a/packages/genui/a2ui/tsconfig.build.json b/packages/genui/a2ui/tsconfig.build.json index 4e664712ac..f46b0dea72 100644 --- a/packages/genui/a2ui/tsconfig.build.json +++ b/packages/genui/a2ui/tsconfig.build.json @@ -3,6 +3,7 @@ "compilerOptions": { "noEmit": false, "rootDir": "./src", + "tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo", }, "include": ["src"], "exclude": ["test"], diff --git a/packages/genui/server/agent/a2ui-catalog.ts b/packages/genui/server/agent/a2ui-catalog.ts index cbbb415876..8794b8bc4d 100644 --- a/packages/genui/server/agent/a2ui-catalog.ts +++ b/packages/genui/server/agent/a2ui-catalog.ts @@ -45,6 +45,21 @@ export interface A2UICatalog { components: A2UIComponentSpec[]; extraRules?: string[]; examples?: A2UIExample[]; + functions?: A2UIFunctionSpec[]; +} + +export interface A2UIFunctionSpec { + description?: string; + name: string; + parameters: JsonSchema; + returnType: + | 'string' + | 'number' + | 'boolean' + | 'array' + | 'object' + | 'any' + | 'void'; } export const BASIC_CATALOG_ID = @@ -203,6 +218,30 @@ function componentFromManifest( }; } +export function createA2UICatalogFromManifests(options: { + catalogId: string; + componentManifests: Record[]; + examples?: A2UIExample[]; + extraRules?: string[]; + functions?: A2UIFunctionSpec[]; + label?: string; + version?: string; +}): A2UICatalog { + return { + id: options.catalogId, + label: options.label ?? `A2UI catalog (${options.catalogId})`, + ...(options.version ? { version: options.version } : {}), + components: options.componentManifests + .map((manifest) => componentFromManifest(manifest as CatalogManifest)) + .filter((component): component is A2UIComponentSpec => + component !== null + ), + ...(options.extraRules ? { extraRules: options.extraRules } : {}), + ...(options.examples ? { examples: options.examples } : {}), + ...(options.functions ? { functions: options.functions } : {}), + }; +} + export const BASIC_CATALOG: A2UICatalog = { id: BASIC_CATALOG_ID, label: 'Lynx A2UI basic catalog (v0.9)', @@ -249,5 +288,16 @@ export function renderCatalogReference(catalog: A2UICatalog): string { for (const r of catalog.extraRules) lines.push(`- ${r}`); lines.push(''); } + if (catalog.functions !== undefined && catalog.functions.length > 0) { + lines.push('### Available functions'); + for (const fn of catalog.functions) { + lines.push(`- ${fn.name}: returns ${fn.returnType}`); + if (fn.description) { + lines.push(` ${fn.description}`); + } + lines.push(` parameters: ${JSON.stringify(fn.parameters)}`); + } + lines.push(''); + } return lines.join('\n'); } diff --git a/packages/genui/server/agent/a2ui-prompt.ts b/packages/genui/server/agent/a2ui-prompt.ts index ff01495413..eca343472a 100644 --- a/packages/genui/server/agent/a2ui-prompt.ts +++ b/packages/genui/server/agent/a2ui-prompt.ts @@ -173,4 +173,4 @@ export function buildA2UISystemPrompt( return parts.join('\n'); } -export const A2UI_SYSTEM_PROMPT = buildA2UISystemPrompt(); +export const A2UI_SYSTEM_PROMPT: string = buildA2UISystemPrompt(); diff --git a/packages/genui/tsconfig.json b/packages/genui/tsconfig.json index 7f8d6b6ae9..ad7396c501 100644 --- a/packages/genui/tsconfig.json +++ b/packages/genui/tsconfig.json @@ -10,6 +10,7 @@ "references": [ /** packages-start */ { "path": "./a2ui-catalog-extractor/tsconfig.json" }, + { "path": "./a2ui-prompt/tsconfig.json" }, { "path": "./a2ui/tsconfig.build.json" }, { "path": "./openui/tsconfig.json" }, { "path": "./ui-judge/tsconfig.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0b11de74f..078a2fd0b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -608,9 +608,9 @@ importers: specifier: ^2.5.1 version: 2.5.1(preact@10.29.1) devDependencies: - '@lynx-js/a2ui-catalog-extractor': + '@lynx-js/a2ui-cli': specifier: workspace:* - version: link:../a2ui-catalog-extractor + version: link:../a2ui-cli '@lynx-js/lynx-ui': specifier: ^3.133.0 version: 3.133.0(@lynx-js/react@packages+react)(@lynx-js/types@3.7.0)(@types/react@18.3.28)(react-dom@19.2.4(react@19.2.5))(react@19.2.5) @@ -640,6 +640,15 @@ importers: specifier: ^24.10.13 version: 24.10.13 + packages/genui/a2ui-cli: + dependencies: + '@lynx-js/a2ui-catalog-extractor': + specifier: workspace:* + version: link:../a2ui-catalog-extractor + '@lynx-js/a2ui-prompt': + specifier: workspace:* + version: link:../a2ui-prompt + packages/genui/a2ui-playground: dependencies: '@codemirror/lang-json': @@ -716,6 +725,12 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/genui/a2ui-prompt: + devDependencies: + '@types/node': + specifier: ^24.10.13 + version: 24.10.13 + packages/genui/openui: dependencies: '@openuidev/lang-core':