Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/app/api/openui-cloud/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { unavailableResponse } from "@/lib/openui-cloud/errors";
import { resolveRequestedModel } from "@/lib/openui-cloud/models";
import { hasAllowedOrigin, hasJsonContentType, readLimitedJson } from "@/lib/openui-cloud/request";
import { generateSystemPrompt } from "@openuidev/lang-core";
import { artifactTool } from "@openuidev/thesys-server";
import { artifactTool } from "@openuidev/lang-core/cloud";
import OpenAI from "openai";
import type { ResponseInputItem } from "openai/resources/responses/responses";

Expand Down
1 change: 0 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@openuidev/react-lang": "workspace:^",
"@openuidev/react-ui": "workspace:^",
"@openuidev/thesys": "0.3.2",
"@openuidev/thesys-server": "0.1.4",
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-tooltip": "1.2.7",
Expand Down
17 changes: 17 additions & 0 deletions packages/lang-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ const myLibraryPrompt = generateSystemPrompt({
});
```

Enable managed slides and reports with `artifactTool` from the Cloud subpath:

```ts
import { artifactTool } from "@openuidev/lang-core/cloud";

const tools = [
artifactTool({ artifacts: ["slides", "report"] }),
{ type: "web_search" },
];
```

### Merge incremental edits

```ts
Expand Down Expand Up @@ -128,6 +139,12 @@ const merged = mergeStatements(original, patch);

**`ToolSpec`** describes a tool for prompt generation (name, description, inputSchema, outputSchema). Shape inspired by MCP's tool schema.

### OpenUI Cloud

| Export | Description |
| :--- | :--- |
| `artifactTool(options?)` | From `@openuidev/lang-core/cloud`. Responses `tools[]` entry for Cloud's managed slides/report artifacts. |

## Telemetry

Lang Core sends pseudonymous installation telemetry during `postinstall`.
Expand Down
19 changes: 18 additions & 1 deletion packages/lang-core/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
{
"name": "@openuidev/lang-core",
"version": "0.2.17",
"version": "0.2.18",
"description": "Framework-agnostic core for OpenUI Lang: parser, prompt generation, validation, and type definitions",
"license": "MIT",
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.cts",
"typesVersions": {
"*": {
"cloud": [
"dist/cloud.d.cts"
]
}
},
"sideEffects": false,
"files": [
"dist",
Expand All @@ -23,6 +30,16 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./cloud": {
"import": {
"types": "./dist/cloud.d.mts",
"default": "./dist/cloud.mjs"
},
"require": {
"types": "./dist/cloud.d.cts",
"default": "./dist/cloud.cjs"
}
}
},
"scripts": {
Expand Down
77 changes: 77 additions & 0 deletions packages/lang-core/src/__tests__/cloud.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { artifactTool, REPORT_LIBRARY_VERSION, SLIDES_LIBRARY_VERSION } from "../cloud";

const CLOUD_SUPPORTED_ARTIFACT_TYPES = ["slides", "report"];

describe("artifactTool — defaults", () => {
const ALL_WITH_VERSION = {
type: "artifact",
artifacts: [
{ artifact_type: "slides", library_version: SLIDES_LIBRARY_VERSION },
{ artifact_type: "report", library_version: REPORT_LIBRARY_VERSION },
],
};

it("no options → all types WITH library_version", () => {
expect(artifactTool()).toEqual(ALL_WITH_VERSION);
});

it("empty options → all types WITH library_version too", () => {
expect(artifactTool({})).toEqual(ALL_WITH_VERSION);
});
});

describe("artifactTool — restriction", () => {
it("'slides' shorthand → entry with pinned version", () => {
expect(artifactTool({ artifacts: ["slides"] })).toEqual({
type: "artifact",
artifacts: [{ artifact_type: "slides", library_version: SLIDES_LIBRARY_VERSION }],
});
});

it("'report' shorthand → entry with pinned version", () => {
expect(artifactTool({ artifacts: ["report"] })).toEqual({
type: "artifact",
artifacts: [{ artifact_type: "report", library_version: REPORT_LIBRARY_VERSION }],
});
});

it("mixed shorthand + object entries, order preserved", () => {
const entry = artifactTool({
artifacts: [{ type: "slides", instruction: "Use the corporate template." }, "report"],
});
expect(entry.artifacts?.map((a) => a.artifact_type)).toEqual(["slides", "report"]);
expect(entry.artifacts?.[0]?.instruction).toBe("Use the corporate template.");
expect(entry.artifacts?.[1]).not.toHaveProperty("instruction");
});

it("every emitted artifact_type is Cloud-supported", () => {
const entry = artifactTool({ artifacts: ["slides", "report"] });
for (const a of entry.artifacts ?? []) {
expect(CLOUD_SUPPORTED_ARTIFACT_TYPES).toContain(a.artifact_type);
}
});

it("libraryVersion override wins over the pinned constant", () => {
const entry = artifactTool({
artifacts: [{ type: "report", libraryVersion: "2.3.0" }],
});
expect(entry.artifacts?.[0]?.library_version).toBe("2.3.0");
});
});

describe("artifactTool — validation", () => {
it("empty artifacts array throws (omit to enable all)", () => {
expect(() => artifactTool({ artifacts: [] })).toThrow(/must not be empty/);
});

it("duplicate artifact kinds throw", () => {
expect(() => artifactTool({ artifacts: ["report", { type: "report" }] })).toThrow(/duplicate/);
});

it("unknown artifact kind throws — incl. 'presentation' (not wire vocabulary)", () => {
expect(() => artifactTool({ artifacts: ["presentation" as never] })).toThrow(
/unknown artifact type 'presentation'/,
);
});
});
95 changes: 95 additions & 0 deletions packages/lang-core/src/cloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export type ArtifactKind = "slides" | "report";

export interface ArtifactOption {
type: ArtifactKind;
/** Per-artifact instruction, folded into the Cloud tool description. */
instruction?: string;
/** Override this package's pinned library version for this artifact. */
libraryVersion?: string;
}

export interface ArtifactToolOptions {
/** Which artifacts to enable. Omit to enable all artifact types. */
artifacts?: Array<ArtifactKind | ArtifactOption>;
}

export interface ArtifactWireEntry {
artifact_type: ArtifactKind;
instruction?: string;
library_version?: string;
}

/** The Responses `tools[]` entry enabling OpenUI Cloud's managed artifact tool. */
export interface ResponsesArtifactToolEntry {
type: "artifact";
/** Absent → all supported artifact types enabled. */
artifacts?: ArtifactWireEntry[];
}

/**
* Wire pins for OpenUI Cloud's managed artifact libraries. Cloud rejects a
* non-numeric or too-old version.
*/
export const SLIDES_LIBRARY_VERSION = "0.1.0";
export const REPORT_LIBRARY_VERSION = "0.1.0";

const DEFAULT_LIBRARY_VERSION: Record<ArtifactKind, string> = {
slides: SLIDES_LIBRARY_VERSION,
report: REPORT_LIBRARY_VERSION,
};

/**
* Build the Responses `tools[]` entry that enables Cloud's managed artifact tool.
*
* tools: [artifactTool()] // all artifact types
* tools: [artifactTool({ artifacts: ["report"] })] // report only
* tools: [artifactTool({
* artifacts: [
* { type: "slides", instruction: "Use the corporate template." },
* "report",
* ],
* })]
*
* Pass at most one artifactTool() entry per request — Cloud keys the
* artifact config by tool type, so a second entry silently replaces the first.
*/
export function artifactTool(options: ArtifactToolOptions = {}): ResponsesArtifactToolEntry {
const { artifacts } = options;
if (artifacts === undefined) {
// Emit every supported type WITH its library_version so Cloud resolves to
// the OpenUI Lang format. A bare `{ type: "artifact" }` with no
// library_version is the legacy shape — not what a lang-core caller wants.
return {
type: "artifact",
artifacts: (Object.keys(DEFAULT_LIBRARY_VERSION) as ArtifactKind[]).map((kind) => ({
artifact_type: kind,
library_version: DEFAULT_LIBRARY_VERSION[kind],
})),
};
}
if (artifacts.length === 0) {
throw new Error(
"artifactTool: `artifacts` must not be empty — omit it to enable all artifact types.",
);
}

const seen = new Set<ArtifactKind>();
const entries: ArtifactWireEntry[] = artifacts.map((artifact) => {
const opt: ArtifactOption = typeof artifact === "string" ? { type: artifact } : artifact;
if (!(opt.type in DEFAULT_LIBRARY_VERSION)) {
throw new Error(
`artifactTool: unknown artifact type '${opt.type}'. Supported: ${Object.keys(DEFAULT_LIBRARY_VERSION).join(", ")}.`,
);
}
if (seen.has(opt.type)) {
throw new Error(`artifactTool: duplicate artifact '${opt.type}'.`);
}
seen.add(opt.type);
return {
artifact_type: opt.type,
...(opt.instruction && { instruction: opt.instruction }),
library_version: opt.libraryVersion ?? DEFAULT_LIBRARY_VERSION[opt.type],
};
});
return { type: "artifact", artifacts: entries };
}
2 changes: 1 addition & 1 deletion packages/lang-core/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineConfig } from "tsdown";
import packageJson from "./package.json" with { type: "json" };

export default defineConfig({
entry: ["src/index.ts", "src/postinstall.ts"],
entry: ["src/index.ts", "src/postinstall.ts", "src/cloud.ts"],
format: ["esm", "cjs"],
dts: true,
sourcemap: true,
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading