From d4180e06aa57bdc669652eb7eb6525885512416f Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 17 Jun 2025 13:02:26 -0700 Subject: [PATCH 1/8] start of port --- deno.lock | 22 +++ packages/api/index.ts | 14 ++ packages/jumble/deno.json | 1 + packages/llm/deno.json | 1 + packages/llm/src/client.ts | 33 ++++- packages/llm/src/types.ts | 16 ++ packages/runner/src/builder/built-in.ts | 16 ++ packages/runner/src/builder/factory.ts | 2 + packages/runner/src/builder/types.ts | 2 + packages/runner/src/builtins/index.ts | 3 +- packages/runner/src/builtins/llm.ts | 138 +++++++++++++++++- packages/static/assets/types/commontools.d.ts | 10 ++ packages/toolshed/deno.json | 1 + .../toolshed/routes/ai/llm/generateObject.ts | 73 +++++++++ .../toolshed/routes/ai/llm/llm.handlers.ts | 32 ++++ packages/toolshed/routes/ai/llm/llm.index.ts | 3 +- packages/toolshed/routes/ai/llm/llm.routes.ts | 70 +++++++++ recipes/test-generate-object.tsx | 128 ++++++++++++++++ 18 files changed, 561 insertions(+), 4 deletions(-) create mode 100644 packages/toolshed/routes/ai/llm/generateObject.ts create mode 100644 recipes/test-generate-object.tsx diff --git a/deno.lock b/deno.lock index a1f383d64..17414d1c8 100644 --- a/deno.lock +++ b/deno.lock @@ -105,6 +105,7 @@ "npm:@web/test-runner@*": "0.20.2", "npm:ai@^4.3.10": "4.3.16_react@18.3.1_zod@3.25.49", "npm:ai@^4.3.9": "4.3.16_react@18.3.1_zod@3.25.49", + "npm:ajv@^8.17.1": "8.17.1", "npm:cmdk@^1.0.4": "1.1.1_react@18.3.1_react-dom@18.3.1__react@18.3.1_@types+react@18.3.23_@types+react-dom@18.3.7__@types+react@18.3.23", "npm:csstype@^3.1.3": "3.1.3", "npm:emoji-picker-react@^4.12.0": "4.12.2_react@18.3.1", @@ -3043,6 +3044,15 @@ "react" ] }, + "ajv@8.17.1": { + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": [ + "fast-deep-equal", + "fast-uri", + "json-schema-traverse", + "require-from-string" + ] + }, "ansi-escapes@4.3.2": { "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dependencies": [ @@ -3843,6 +3853,9 @@ "fast-copy@3.0.2": { "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==" }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fast-fifo@1.3.2": { "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" }, @@ -3862,6 +3875,9 @@ "fast-safe-stringify@2.1.1": { "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-uri@3.0.6": { + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==" + }, "fastq@1.19.1": { "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dependencies": [ @@ -4434,6 +4450,9 @@ "bignumber.js" ] }, + "json-schema-traverse@1.0.0": { + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "json-schema@0.4.0": { "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, @@ -6844,6 +6863,7 @@ "npm:@uiw/react-json-view@2.0.0-alpha.30", "npm:@use-gesture/react@^10.3.1", "npm:@vitejs/plugin-react@^4.3.4", + "npm:ajv@^8.17.1", "npm:cmdk@^1.0.4", "npm:csstype@^3.1.3", "npm:emoji-picker-react@^4.12.0", @@ -6863,6 +6883,7 @@ }, "packages/llm": { "dependencies": [ + "npm:ajv@^8.17.1", "npm:json5@^2.2.3" ] }, @@ -6903,6 +6924,7 @@ "npm:@sentry/deno@^9.3.0", "npm:@vercel/otel@^1.10.1", "npm:ai@^4.3.10", + "npm:ajv@^8.17.1", "npm:gcp-metadata@6.1.0", "npm:hono-pino@0.7", "npm:jsonschema@^1.5.0", diff --git a/packages/api/index.ts b/packages/api/index.ts index f91de411b..57ee585ae 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -229,6 +229,15 @@ export interface BuiltInLLMState { error: unknown; } +export interface BuiltInGenerateObjectParams { + prompt?: string; + schema?: JSONSchema; + system?: string; + cache?: boolean; + maxTokens?: number; + metadata?: Record; +} + export interface BuiltInCompileAndRunParams { files: Record; main: string; @@ -340,6 +349,10 @@ export type LLMFunction = ( params: Opaque, ) => OpaqueRef>; +export type GenerateObjectFunction = ( + params: Opaque, +) => OpaqueRef>; + export type FetchDataFunction = ( params: Opaque<{ url: string; @@ -404,6 +417,7 @@ export declare const render: RenderFunction; export declare const str: StrFunction; export declare const ifElse: IfElseFunction; export declare const llm: LLMFunction; +export declare const generateObject: GenerateObjectFunction; export declare const fetchData: FetchDataFunction; export declare const streamData: StreamDataFunction; export declare const compileAndRun: CompileAndRunFunction; diff --git a/packages/jumble/deno.json b/packages/jumble/deno.json index 02c8dcc32..5fa561ebb 100644 --- a/packages/jumble/deno.json +++ b/packages/jumble/deno.json @@ -39,6 +39,7 @@ "@codemirror/lang-json": "npm:@codemirror/lang-json@^6.0.1", "@use-gesture/react": "npm:@use-gesture/react@^10.3.1", "@vitejs/plugin-react": "npm:@vitejs/plugin-react@^4.3.4", + "ajv": "npm:ajv@^8.17.1", "cmdk": "npm:cmdk@^1.0.4", "csstype": "npm:csstype@^3.1.3", "emoji-picker-react": "npm:emoji-picker-react@^4.12.0", diff --git a/packages/llm/deno.json b/packages/llm/deno.json index f6c928101..8f0e65bd1 100644 --- a/packages/llm/deno.json +++ b/packages/llm/deno.json @@ -8,6 +8,7 @@ "./types": "./src/types.ts" }, "imports": { + "ajv": "npm:ajv@^8.17.1", "json5": "npm:json5@^2.2.3" } } diff --git a/packages/llm/src/client.ts b/packages/llm/src/client.ts index 5e73b9bce..6c5a1016d 100644 --- a/packages/llm/src/client.ts +++ b/packages/llm/src/client.ts @@ -1,4 +1,11 @@ -import { LLMContent, LLMMessage, LLMRequest, LLMResponse } from "./types.ts"; +import { + LLMContent, + LLMMessage, + LLMRequest, + LLMResponse, + LLMGenerateObjectRequest, + LLMGenerateObjectResponse +} from "./types.ts"; type PartialCallback = (text: string) => void; @@ -14,6 +21,30 @@ export const setLLMUrl = (toolshedUrl: string) => { }; export class LLMClient { + async generateObject( + request: LLMGenerateObjectRequest, + ): Promise { + const response = await fetch(llmApiUrl + "/generateObject", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `HTTP error! status: ${response.status}, body: ${errorText}`, + ); + } + + if (!response.body) { + throw new Error("No response body"); + } + + const data = await response.json(); + return data; + } + /** * Sends a request to the LLM service. * diff --git a/packages/llm/src/types.ts b/packages/llm/src/types.ts index 5ee47d02c..2a62e68e5 100644 --- a/packages/llm/src/types.ts +++ b/packages/llm/src/types.ts @@ -6,6 +6,7 @@ export const DEFAULT_MODEL_NAME: ModelName = // NOTE(ja): This should be an array of models, the first model will be tried, if it // fails, the second model will be tried, etc. export const DEFAULT_IFRAME_MODELS: ModelName = "openai:gpt-4.1-nano"; +export const DEFAULT_GENERATE_OBJECT_MODELS: ModelName = "openai:gpt-4.1-nano"; export type LLMResponse = { content: string; @@ -37,6 +38,21 @@ export interface LLMRequest { metadata?: LLMRequestMetadata; } +export interface LLMGenerateObjectRequest { + schema: Record; + prompt: string; + model?: ModelName; + system?: string; + cache?: boolean; + maxTokens?: number; + metadata?: LLMRequestMetadata; +} + +export interface LLMGenerateObjectResponse { + object: Record; + id?: string; +} + function isArrayOf( callback: (data: any) => boolean, input: any, diff --git a/packages/runner/src/builder/built-in.ts b/packages/runner/src/builder/built-in.ts index 1bddf816a..aa9eb09ba 100644 --- a/packages/runner/src/builder/built-in.ts +++ b/packages/runner/src/builder/built-in.ts @@ -48,6 +48,15 @@ export interface BuiltInLLMState { error: unknown; } +export interface BuiltInGenerateObjectParams { + prompt?: string; + schema?: JSONSchema; + system?: string; + cache?: boolean; + maxTokens?: number; + metadata?: Record; +} + export const llm = createNodeFactory({ type: "ref", implementation: "llm", @@ -55,6 +64,13 @@ export const llm = createNodeFactory({ params: Opaque, ) => OpaqueRef>; +export const generateObject = createNodeFactory({ + type: "ref", + implementation: "generateObject", +}) as ( + params: Opaque, +) => OpaqueRef>; + export const fetchData = createNodeFactory({ type: "ref", implementation: "fetchData", diff --git a/packages/runner/src/builder/factory.ts b/packages/runner/src/builder/factory.ts index 56fa861a7..e4be664bc 100644 --- a/packages/runner/src/builder/factory.ts +++ b/packages/runner/src/builder/factory.ts @@ -25,6 +25,7 @@ import { byRef, compute, derive, handler, lift, render } from "./module.ts"; import { compileAndRun, fetchData, + generateObject, ifElse, llm, navigateTo, @@ -91,6 +92,7 @@ export const createBuilder = ( str, ifElse, llm, + generateObject, fetchData, streamData, compileAndRun, diff --git a/packages/runner/src/builder/types.ts b/packages/runner/src/builder/types.ts index e1e29b87d..46215ee90 100644 --- a/packages/runner/src/builder/types.ts +++ b/packages/runner/src/builder/types.ts @@ -9,6 +9,7 @@ import type { CreateCellFunction, DeriveFunction, FetchDataFunction, + GenerateObjectFunction, GetRecipeEnvironmentFunction, HandlerFunction, IfElseFunction, @@ -270,6 +271,7 @@ export interface BuilderFunctionsAndConstants { str: StrFunction; ifElse: IfElseFunction; llm: LLMFunction; + generateObject: GenerateObjectFunction; fetchData: FetchDataFunction; streamData: StreamDataFunction; compileAndRun: CompileAndRunFunction; diff --git a/packages/runner/src/builtins/index.ts b/packages/runner/src/builtins/index.ts index 9133bddcd..b746aa17b 100644 --- a/packages/runner/src/builtins/index.ts +++ b/packages/runner/src/builtins/index.ts @@ -2,7 +2,7 @@ import { raw } from "../module.ts"; import { map } from "./map.ts"; import { fetchData } from "./fetch-data.ts"; import { streamData } from "./stream-data.ts"; -import { llm } from "./llm.ts"; +import { llm, generateObject } from "./llm.ts"; import { ifElse } from "./if-else.ts"; import type { IRuntime } from "../runtime.ts"; import { compileAndRun } from "./compile-and-run.ts"; @@ -19,4 +19,5 @@ export function registerBuiltins(runtime: IRuntime) { moduleRegistry.addModuleByRef("llm", raw(llm)); moduleRegistry.addModuleByRef("ifElse", raw(ifElse)); moduleRegistry.addModuleByRef("compileAndRun", raw(compileAndRun)); + moduleRegistry.addModuleByRef("generateObject", raw(generateObject)); } diff --git a/packages/runner/src/builtins/llm.ts b/packages/runner/src/builtins/llm.ts index 6413cf253..4ed9380d7 100644 --- a/packages/runner/src/builtins/llm.ts +++ b/packages/runner/src/builtins/llm.ts @@ -1,5 +1,11 @@ import { type DocImpl } from "../doc.ts"; -import { DEFAULT_MODEL_NAME, LLMClient, LLMRequest } from "@commontools/llm"; +import { + DEFAULT_MODEL_NAME, + DEFAULT_GENERATE_OBJECT_MODELS, + LLMClient, + LLMRequest, + LLMGenerateObjectRequest, +} from "@commontools/llm"; import { type Action } from "../scheduler.ts"; import type { IRuntime } from "../runtime.ts"; import { refer } from "merkle-reference"; @@ -7,6 +13,7 @@ import { type ReactivityLog } from "../scheduler.ts"; import { BuiltInLLMParams, BuiltInLLMState, + BuiltInGenerateObjectParams, } from "@commontools/api"; const client = new LLMClient(); @@ -151,3 +158,132 @@ export function llm( }); }; } + +/** + * Generate structured data via an LLM using JSON mode. + * + * Returns the complete result as `result` and the incremental result as + * `partial`. `pending` is true while a request is pending. + * + * @param prompt - The prompt to send to the LLM. + * @param schema - JSON Schema to validate the response against. + * @param system - Optional system message. + * @param maxTokens - Maximum number of tokens to generate. + * @param model - Model to use (defaults to DEFAULT_GENERATE_OBJECT_MODELS). + * @param cache - Whether to cache the response (defaults to true). + * @param metadata - Additional metadata to pass to the LLM. + * + * @returns { pending: boolean, result?: object, partial?: string } - As individual + * docs, representing `pending` state, final `result` and incrementally + * updating `partial` result. + */ +export function generateObject( + inputsCell: DocImpl, + sendResult: (result: any) => void, + _addCancel: (cancel: () => void) => void, + cause: any, + parentDoc: DocImpl, + runtime: IRuntime, +): Action { + const pending = runtime.documentMap.getDoc( + false, + { generateObject: { pending: cause } }, + parentDoc.space, + ); + const result = runtime.documentMap.getDoc | undefined>( + undefined, + { + generateObject: { result: cause }, + }, + parentDoc.space, + ); + const partial = runtime.documentMap.getDoc( + undefined, + { + generateObject: { partial: cause }, + }, + parentDoc.space, + ); + const requestHash = runtime.documentMap.getDoc( + undefined, + { + generateObject: { requestHash: cause }, + }, + parentDoc.space, + ); + + sendResult({ pending, result, partial, requestHash }); + + let currentRun = 0; + let previousCallHash: string | undefined = undefined; + + return (log: ReactivityLog) => { + const thisRun = ++currentRun; + + const { prompt, maxTokens, model, schema, system, cache, metadata } = + inputsCell.getAsQueryResult([], log) ?? {}; + + if (!prompt || !schema) { + pending.setAtPath([], false, log); + return; + } + + const readyMetadata = metadata ? JSON.parse(JSON.stringify(metadata)) : {}; + + const generateObjectParams: LLMGenerateObjectRequest = { + prompt, + maxTokens: maxTokens ?? 8192, + schema: JSON.parse(JSON.stringify(schema)), + model: model ?? DEFAULT_GENERATE_OBJECT_MODELS, + metadata: { + ...readyMetadata, + context: "charm", + }, + cache: cache ?? true, + }; + + if (system) { + generateObjectParams.system = system; + } + + const hash = refer(generateObjectParams).toString(); + + // Return if the same request is being made again, either concurrently (same + // as previousCallHash) or when rehydrated from storage (same as the + // contents of the requestHash doc). + if (hash === previousCallHash || hash === requestHash.get()) return; + previousCallHash = hash; + + result.setAtPath([], undefined, log); + partial.setAtPath([], undefined, log); + pending.setAtPath([], true, log); + + const resultPromise = client.generateObject(generateObjectParams); + + resultPromise + .then(async (response) => { + if (thisRun !== currentRun) return; + + await runtime.idle(); + + pending.setAtPath([], false, log); + result.setAtPath([], response.object, log); + requestHash.setAtPath([], hash, log); + }) + .catch(async (error) => { + if (thisRun !== currentRun) return; + + console.error("Error generating object", error); + + await runtime.idle(); + + pending.setAtPath([], false, log); + result.setAtPath([], undefined, log); + partial.setAtPath([], undefined, log); + + // TODO(seefeld): Not writing now, so we retry the request after failure. + // Replace this with more fine-grained retry logic. + // requestHash.setAtPath([], hash, log); + }); + }; +} diff --git a/packages/static/assets/types/commontools.d.ts b/packages/static/assets/types/commontools.d.ts index b487030d4..c75ea33d1 100644 --- a/packages/static/assets/types/commontools.d.ts +++ b/packages/static/assets/types/commontools.d.ts @@ -131,6 +131,14 @@ export interface BuiltInLLMState { partial?: string; error: unknown; } +export interface BuiltInGenerateObjectParams { + prompt?: string; + schema?: JSONSchema; + system?: string; + cache?: boolean; + maxTokens?: number; + metadata?: Record; +} export interface BuiltInCompileAndRunParams { files: Record; main: string; @@ -166,6 +174,7 @@ export type RenderFunction = (fn: () => T) => OpaqueRef; export type StrFunction = (strings: TemplateStringsArray, ...values: any[]) => OpaqueRef; export type IfElseFunction = (condition: Opaque, ifTrue: Opaque, ifFalse: Opaque) => OpaqueRef; export type LLMFunction = (params: Opaque) => OpaqueRef>; +export type GenerateObjectFunction = (params: Opaque) => OpaqueRef>; export type FetchDataFunction = (params: Opaque<{ url: string; mode?: "json" | "text"; @@ -208,6 +217,7 @@ export declare const render: RenderFunction; export declare const str: StrFunction; export declare const ifElse: IfElseFunction; export declare const llm: LLMFunction; +export declare const generateObject: GenerateObjectFunction; export declare const fetchData: FetchDataFunction; export declare const streamData: StreamDataFunction; export declare const compileAndRun: CompileAndRunFunction; diff --git a/packages/toolshed/deno.json b/packages/toolshed/deno.json index a9c8bc14d..089102bfe 100644 --- a/packages/toolshed/deno.json +++ b/packages/toolshed/deno.json @@ -13,6 +13,7 @@ "@ai-sdk/google-vertex": "npm:@ai-sdk/google-vertex@^2.2.17", "@ai-sdk/xai": "npm:@ai-sdk/xai@^1.2.15", "@std/cli": "jsr:@std/cli@^1.0.12", + "ajv": "npm:ajv@^8.17.1", "gcp-metadata": "npm:gcp-metadata@6.1.0", "@ai-sdk/groq": "npm:@ai-sdk/groq@^1.2.8", "@ai-sdk/openai": "npm:@ai-sdk/openai@^1.3.20", diff --git a/packages/toolshed/routes/ai/llm/generateObject.ts b/packages/toolshed/routes/ai/llm/generateObject.ts new file mode 100644 index 000000000..2f51f0932 --- /dev/null +++ b/packages/toolshed/routes/ai/llm/generateObject.ts @@ -0,0 +1,73 @@ +import { + type LLMGenerateObjectRequest, + type LLMGenerateObjectResponse, +} from "@commontools/llm/types"; +import { findModel } from "./models.ts"; +import { generateObject as generateObjectCore, jsonSchema } from "ai"; +import { Ajv } from "ajv"; +import { DEFAULT_GENERATE_OBJECT_MODELS } from "@commontools/llm"; +import { trace } from "@opentelemetry/api"; + +export async function generateObject( + params: LLMGenerateObjectRequest, +): Promise { + try { + const model = findModel(params.model ?? DEFAULT_GENERATE_OBJECT_MODELS); + const ajv = new Ajv({ allErrors: true, strict: false }); + const validator = ajv.compile(params.schema); + + const activeSpan = trace.getActiveSpan(); + const spanId = activeSpan?.spanContext().spanId; + + // Attach metadata directly to the root span + if (activeSpan) { + // Add the metadata from params if available + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + // Only set attributes with valid values (not undefined) + if (value !== undefined) { + // Handle different types to ensure we only use valid AttributeValue types + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + activeSpan.setAttribute(`metadata.${key}`, value); + } else if (typeof value === "object") { + // Convert objects to JSON strings + activeSpan.setAttribute(`metadata.${key}`, JSON.stringify(value)); + } + } + }); + } + } + + const { object } = await generateObjectCore({ + model: model.model, + prompt: params.prompt, + mode: "json", + schema: jsonSchema(params.schema, { + validate: (value: unknown) => { + if (!validator(value)) { + return { + success: false, + error: new Error(JSON.stringify(validator.errors)), + }; + } + return { + success: true, + value, + }; + }, + }), + }); + + return { + object: object as Record, + id: spanId, + }; + } catch (error) { + console.error("Error generating object:", error); + throw new Error(`Failed to generate object: ${error}`); + } +} \ No newline at end of file diff --git a/packages/toolshed/routes/ai/llm/llm.handlers.ts b/packages/toolshed/routes/ai/llm/llm.handlers.ts index 04ca298c3..a03675545 100644 --- a/packages/toolshed/routes/ai/llm/llm.handlers.ts +++ b/packages/toolshed/routes/ai/llm/llm.handlers.ts @@ -4,11 +4,13 @@ import type { FeedbackRoute, GenerateTextRoute, GetModelsRoute, + GenerateObjectRoute, } from "./llm.routes.ts"; import { ALIAS_NAMES, ModelList, MODELS, TASK_MODELS } from "./models.ts"; import { hashKey, loadFromCache, saveToCache } from "./cache.ts"; import type { Context } from "@hono/hono"; import { generateText as generateTextCore } from "./generateText.ts"; +import { generateObject as generateObjectCore } from "./generateObject.ts"; import { findModel } from "./models.ts"; import env from "@/env.ts"; import { isLLMRequest, type LLMMessage } from "@commontools/llm/types"; @@ -240,3 +242,33 @@ export const submitFeedback: AppRouteHandler = async (c) => { return c.json({ error: message }, HttpStatusCodes.BAD_REQUEST); } }; + +/** + * Handler for POST /generateObject endpoint + * Generates structured JSON objects using specified LLM model + */ +export const generateObject: AppRouteHandler = async (c) => { + const payload = await c.req.json(); + + if (!payload.prompt || !payload.schema) { + return c.json({ error: "Missing required fields: prompt and schema" }, HttpStatusCodes.BAD_REQUEST); + } + + if (!payload.metadata) { + payload.metadata = {}; + } + + const user = c.req.header("Tailscale-User-Login"); + if (user) { + payload.metadata.user = user; + } + + try { + const result = await generateObjectCore(payload); + return c.json(result); + } catch (error) { + console.error("Error in generateObject:", error); + const message = error instanceof Error ? error.message : "Unknown error"; + return c.json({ error: message }, HttpStatusCodes.BAD_REQUEST); + } +}; diff --git a/packages/toolshed/routes/ai/llm/llm.index.ts b/packages/toolshed/routes/ai/llm/llm.index.ts index d1c55d067..05013356a 100644 --- a/packages/toolshed/routes/ai/llm/llm.index.ts +++ b/packages/toolshed/routes/ai/llm/llm.index.ts @@ -7,7 +7,8 @@ import { cors } from "@hono/hono/cors"; const router = createRouter() .openapi(routes.getModels, handlers.getModels) .openapi(routes.generateText, handlers.generateText) - .openapi(routes.feedback, handlers.submitFeedback); + .openapi(routes.feedback, handlers.submitFeedback) + .openapi(routes.generateObject, handlers.generateObject); router.use( "/api/ai/llm/*", diff --git a/packages/toolshed/routes/ai/llm/llm.routes.ts b/packages/toolshed/routes/ai/llm/llm.routes.ts index 568a5e1d2..1bedf1beb 100644 --- a/packages/toolshed/routes/ai/llm/llm.routes.ts +++ b/packages/toolshed/routes/ai/llm/llm.routes.ts @@ -6,6 +6,7 @@ import { toZod } from "@commontools/utils/zod-utils"; import { type LLMMessage, type LLMRequest, + type LLMGenerateObjectRequest, LLMTypedContent, } from "@commontools/llm/types"; @@ -40,6 +41,17 @@ export const LLMRequestSchema = toZod().with({ cache: z.boolean().default(true).optional(), }); +export const GenerateObjectRequestSchema = toZod() + .with({ + prompt: z.string(), + schema: z.record(z.string(), z.any()), + system: z.string().optional(), + cache: z.boolean().default(true).optional(), + maxTokens: z.number().optional(), + model: z.string().optional(), + metadata: z.record(z.string(), z.any()).optional(), + }); + export const ModelInfoSchema = z.object({ capabilities: z.object({ contextWindow: z.number(), @@ -231,6 +243,64 @@ export const feedback = createRoute({ }, }); +export const generateObject = createRoute({ + path: "/api/ai/llm/generateObject", + method: "post", + tags, + request: { + body: { + content: { + "application/json": { + schema: GenerateObjectRequestSchema.openapi({ + example: { + prompt: + "What is the first thing that comes to mind when I say 'apple'?", + schema: { + type: "object", + properties: { + idea: { type: "string" }, + reason: { type: "string" }, + silliness: { type: "number" }, + }, + required: ["idea", "reason", "silliness"], + }, + }, + }), + }, + }, + }, + }, + responses: { + [HttpStatusCodes.OK]: jsonContent( + z.object({ + object: z.any(), + id: z.string().optional(), + }).openapi({ + example: { + object: { + idea: "apple", + reason: "It's a fruit", + silliness: 0.5, + }, + id: "123", + }, + }), + "Generated object", + ), + [HttpStatusCodes.BAD_REQUEST]: jsonContent( + z.object({ + error: z.string(), + }).openapi({ + example: { + error: "idea is missing", + }, + }), + "Invalid request parameters", + ), + }, +}); + export type GetModelsRoute = typeof getModels; export type GenerateTextRoute = typeof generateText; export type FeedbackRoute = typeof feedback; +export type GenerateObjectRoute = typeof generateObject; diff --git a/recipes/test-generate-object.tsx b/recipes/test-generate-object.tsx new file mode 100644 index 000000000..98dc4fc81 --- /dev/null +++ b/recipes/test-generate-object.tsx @@ -0,0 +1,128 @@ +import { + derive, + generateObject, + h, + handler, + ifElse, + type JSONSchema, + lift, + NAME, + recipe, + schema, + str, + UI, +} from "commontools"; + +// Input schema with a number that can be incremented +const inputSchema = schema({ + type: "object", + properties: { + number: { type: "number", default: 0, asCell: true }, + }, + default: { number: 0 }, +}); + +// Output schema for the generated object +const outputSchema = { + type: "object", + properties: { + number: { type: "number" }, + story: { type: "string" }, + title: { type: "string" }, + storyOrigin: { type: "string" }, + seeAlso: { type: "array", items: { type: "number" } }, + imagePrompt: { type: "string" }, + }, +} as const satisfies JSONSchema; + +// Handler to increment the number +const adder = handler({}, inputSchema, (_, state) => { + console.log("incrementing number"); + state.number.set(state.number.get() + 1); +}); + +// Handler to set a specific number +const setNumber = handler({ + type: "object", + properties: {}, +}, { + type: "object", + properties: { + number: { type: "number", asCell: true }, + n: { type: "number" }, + }, +}, (_, state) => { + if (state.number && state.n) { + state.number.set(state.n); + } +}); + +// Generate the prompt for the LLM +const generatePrompt = lift(({ number: number }) => { + return { + prompt: + `You are the parent of a young child who loves to learn about numbers. Luckily for your child, you are a historian of numbers and when the child says a number you make up an interesting story about it, including the history of the number. The child is currently at ${number}. Also return a recommendation for other numbers that might be interesting to the child to learn about next.`, + schema: outputSchema, + }; +}); + +// Generate an image URL from the prompt +const generateImageUrl = lift(({ imagePrompt }) => { + return `/api/ai/img?prompt=${encodeURIComponent(imagePrompt)}`; +}); + +export default recipe(inputSchema, outputSchema, (cell) => { + // Use generateObject to get structured data from the LLM + const { result: object, pending } = generateObject(generatePrompt({ number: cell.number })); + + return { + [NAME]: str`Number Story: ${object?.title || "Loading..."}`, + [UI]: ( +
+ + Current number: {cell.number} (click to increment) + + {ifElse( + pending, +

Generating story...

, +
+ {ifElse(object?.title,

{object.title}

,

No title

)} + {ifElse( + object?.imagePrompt, +

+ +

, +

No image prompt

, + )} + {ifElse(object?.story,

{object.story}

,

No story yet

)} + {ifElse( + object?.storyOrigin, +

+ {object.storyOrigin} +

, +

No story origin

, + )} + {ifElse( + object?.seeAlso, +
+

See also these interesting numbers:

+
    + {object.seeAlso.map((n: number) => ( +
  • + + {n} + +
  • + ))} +
+
, +

No related numbers

, + )} +
+ )} +
+ ), + number: cell.number, + ...object, + }; +}); \ No newline at end of file From ca07883861915dd15c7f88dbafdbcdc874db7d4c Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 17 Jun 2025 14:14:14 -0700 Subject: [PATCH 2/8] cleanups --- packages/llm/src/client.ts | 4 --- .../toolshed/routes/ai/llm/generateObject.ts | 8 +++-- .../toolshed/routes/ai/llm/llm.handlers.ts | 29 +++++++++++++++++++ packages/toolshed/routes/ai/llm/models.ts | 5 ++-- recipes/test-generate-object.tsx | 24 ++++++++------- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/packages/llm/src/client.ts b/packages/llm/src/client.ts index 6c5a1016d..86a2d5e27 100644 --- a/packages/llm/src/client.ts +++ b/packages/llm/src/client.ts @@ -37,10 +37,6 @@ export class LLMClient { ); } - if (!response.body) { - throw new Error("No response body"); - } - const data = await response.json(); return data; } diff --git a/packages/toolshed/routes/ai/llm/generateObject.ts b/packages/toolshed/routes/ai/llm/generateObject.ts index 2f51f0932..2797af548 100644 --- a/packages/toolshed/routes/ai/llm/generateObject.ts +++ b/packages/toolshed/routes/ai/llm/generateObject.ts @@ -12,7 +12,7 @@ export async function generateObject( params: LLMGenerateObjectRequest, ): Promise { try { - const model = findModel(params.model ?? DEFAULT_GENERATE_OBJECT_MODELS); + const modelConfig = findModel(params.model ?? DEFAULT_GENERATE_OBJECT_MODELS); const ajv = new Ajv({ allErrors: true, strict: false }); const validator = ajv.compile(params.schema); @@ -43,7 +43,7 @@ export async function generateObject( } const { object } = await generateObjectCore({ - model: model.model, + model: modelConfig.model, prompt: params.prompt, mode: "json", schema: jsonSchema(params.schema, { @@ -60,6 +60,8 @@ export async function generateObject( }; }, }), + maxTokens: params.maxTokens, + ...(params.system && { system: params.system }), }); return { @@ -68,6 +70,6 @@ export async function generateObject( }; } catch (error) { console.error("Error generating object:", error); - throw new Error(`Failed to generate object: ${error}`); + throw error instanceof Error ? error : new Error(`Failed to generate object: ${error}`); } } \ No newline at end of file diff --git a/packages/toolshed/routes/ai/llm/llm.handlers.ts b/packages/toolshed/routes/ai/llm/llm.handlers.ts index a03675545..306e8ee0e 100644 --- a/packages/toolshed/routes/ai/llm/llm.handlers.ts +++ b/packages/toolshed/routes/ai/llm/llm.handlers.ts @@ -263,8 +263,37 @@ export const generateObject: AppRouteHandler = async (c) => payload.metadata.user = user; } + const cacheKey = await hashKey( + JSON.stringify(removeNonCacheableFields(payload)), + ); + + // Check cache if enabled + if (payload.cache !== false) { + const cachedResult = await loadFromCache(cacheKey); + if (cachedResult) { + return c.json({ + object: cachedResult.object, + id: cachedResult.id, + }); + } + } + try { const result = await generateObjectCore(payload); + + // Save to cache if enabled + if (payload.cache !== false) { + try { + await saveToCache(cacheKey, { + ...removeNonCacheableFields(payload), + object: result.object, + id: result.id, + }); + } catch (e) { + console.error("Error saving generateObject response to cache:", e); + } + } + return c.json(result); } catch (error) { console.error("Error in generateObject:", error); diff --git a/packages/toolshed/routes/ai/llm/models.ts b/packages/toolshed/routes/ai/llm/models.ts index 317dbb006..e0d114e61 100644 --- a/packages/toolshed/routes/ai/llm/models.ts +++ b/packages/toolshed/routes/ai/llm/models.ts @@ -4,6 +4,7 @@ import { createGroq, groq } from "@ai-sdk/groq"; import { openai } from "@ai-sdk/openai"; import { createVertex, vertex } from "@ai-sdk/google-vertex"; import { createXai, xai } from "@ai-sdk/xai"; +import type { LanguageModelV1 } from "ai"; import env from "@/env.ts"; @@ -20,7 +21,7 @@ export type Capabilities = { }; type ModelConfig = { - model: string; // FIXME(ja): this type is wrong! it isn't a string + model: LanguageModelV1; name: string; capabilities: Capabilities; aliases: string[]; @@ -77,7 +78,7 @@ const addModel = ({ : provider(modelName); const config: ModelConfig = { - model: model as unknown as string, // FIXME(ja): this type is wrong! it isn't a string + model, name, capabilities, aliases, diff --git a/recipes/test-generate-object.tsx b/recipes/test-generate-object.tsx index 98dc4fc81..27bd50d83 100644 --- a/recipes/test-generate-object.tsx +++ b/recipes/test-generate-object.tsx @@ -37,15 +37,11 @@ const outputSchema = { // Handler to increment the number const adder = handler({}, inputSchema, (_, state) => { - console.log("incrementing number"); state.number.set(state.number.get() + 1); }); // Handler to set a specific number -const setNumber = handler({ - type: "object", - properties: {}, -}, { +const setNumber = handler({}, { type: "object", properties: { number: { type: "number", asCell: true }, @@ -73,13 +69,15 @@ const generateImageUrl = lift(({ imagePrompt }) => { export default recipe(inputSchema, outputSchema, (cell) => { // Use generateObject to get structured data from the LLM - const { result: object, pending } = generateObject(generatePrompt({ number: cell.number })); + const { result: object, pending } = generateObject( + generatePrompt({ number: cell.number }), + ); return { [NAME]: str`Number Story: ${object?.title || "Loading..."}`, [UI]: (
- + Current number: {cell.number} (click to increment) {ifElse( @@ -90,7 +88,9 @@ export default recipe(inputSchema, outputSchema, (cell) => { {ifElse( object?.imagePrompt,

- +

,

No image prompt

, )} @@ -109,7 +109,9 @@ export default recipe(inputSchema, outputSchema, (cell) => {
    {object.seeAlso.map((n: number) => (
  • - + {n}
  • @@ -118,11 +120,11 @@ export default recipe(inputSchema, outputSchema, (cell) => {
,

No related numbers

, )} - + , )} ), number: cell.number, ...object, }; -}); \ No newline at end of file +}); From 6a1367899a25dc9be5995beeceba2644fb761c67 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 13:51:12 -0700 Subject: [PATCH 3/8] add type information --- packages/runner/src/builtins/llm.ts | 31 +++++++++----- recipes/test-generate-object.tsx | 65 +++++++++++++---------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/runner/src/builtins/llm.ts b/packages/runner/src/builtins/llm.ts index 4ed9380d7..5af1ba26f 100644 --- a/packages/runner/src/builtins/llm.ts +++ b/packages/runner/src/builtins/llm.ts @@ -1,19 +1,19 @@ import { type DocImpl } from "../doc.ts"; -import { - DEFAULT_MODEL_NAME, +import { DEFAULT_GENERATE_OBJECT_MODELS, - LLMClient, - LLMRequest, + DEFAULT_MODEL_NAME, + LLMClient, LLMGenerateObjectRequest, + LLMRequest, } from "@commontools/llm"; import { type Action } from "../scheduler.ts"; import type { IRuntime } from "../runtime.ts"; import { refer } from "merkle-reference"; import { type ReactivityLog } from "../scheduler.ts"; import { + BuiltInGenerateObjectParams, BuiltInLLMParams, BuiltInLLMState, - BuiltInGenerateObjectParams, } from "@commontools/api"; const client = new LLMClient(); @@ -177,9 +177,14 @@ export function llm( * docs, representing `pending` state, final `result` and incrementally * updating `partial` result. */ -export function generateObject( +export function generateObject>( inputsCell: DocImpl, - sendResult: (result: any) => void, + sendResult: (docs: { + pending: DocImpl; + result: DocImpl; + partial: DocImpl; + requestHash: DocImpl; + }) => void, _addCancel: (cancel: () => void) => void, cause: any, parentDoc: DocImpl, @@ -190,7 +195,7 @@ export function generateObject( { generateObject: { pending: cause } }, parentDoc.space, ); - const result = runtime.documentMap.getDoc | undefined>( + const result = runtime.documentMap.getDoc( undefined, { generateObject: { result: cause }, @@ -254,11 +259,15 @@ export function generateObject( if (hash === previousCallHash || hash === requestHash.get()) return; previousCallHash = hash; - result.setAtPath([], undefined, log); + result.setAtPath([], {}, log); // FIXME(ja): setting result to undefined causes a storage conflict partial.setAtPath([], undefined, log); pending.setAtPath([], true, log); - const resultPromise = client.generateObject(generateObjectParams); + const resultPromise = client.generateObject( + generateObjectParams, + ) as Promise<{ + object: T; + }>; resultPromise .then(async (response) => { @@ -278,7 +287,7 @@ export function generateObject( await runtime.idle(); pending.setAtPath([], false, log); - result.setAtPath([], undefined, log); + result.setAtPath([], {}, log); // FIXME(ja): setting result to undefined causes a storage conflict partial.setAtPath([], undefined, log); // TODO(seefeld): Not writing now, so we retry the request after failure. diff --git a/recipes/test-generate-object.tsx b/recipes/test-generate-object.tsx index 27bd50d83..41a40b563 100644 --- a/recipes/test-generate-object.tsx +++ b/recipes/test-generate-object.tsx @@ -8,6 +8,7 @@ import { lift, NAME, recipe, + Schema, schema, str, UI, @@ -35,6 +36,8 @@ const outputSchema = { }, } as const satisfies JSONSchema; +type OutputSchema = Schema; + // Handler to increment the number const adder = handler({}, inputSchema, (_, state) => { state.number.set(state.number.get() + 1); @@ -69,7 +72,7 @@ const generateImageUrl = lift(({ imagePrompt }) => { export default recipe(inputSchema, outputSchema, (cell) => { // Use generateObject to get structured data from the LLM - const { result: object, pending } = generateObject( + const { result: object, pending } = generateObject( generatePrompt({ number: cell.number }), ); @@ -84,42 +87,30 @@ export default recipe(inputSchema, outputSchema, (cell) => { pending,

Generating story...

,
- {ifElse(object?.title,

{object.title}

,

No title

)} - {ifElse( - object?.imagePrompt, -

- -

, -

No image prompt

, - )} - {ifElse(object?.story,

{object.story}

,

No story yet

)} - {ifElse( - object?.storyOrigin, -

- {object.storyOrigin} -

, -

No story origin

, - )} - {ifElse( - object?.seeAlso, -
-

See also these interesting numbers:

-
    - {object.seeAlso.map((n: number) => ( -
  • - - {n} - -
  • - ))} -
-
, -

No related numbers

, - )} +

{object?.title}

+

+ +

+

{object?.story}

+

+ {object?.storyOrigin} +

+
+

See also these interesting numbers:

+
    + {object?.seeAlso?.map((n: number) => ( +
  • + + {n} + +
  • + ))} +
+
, )} From bd8da0c82f44bebcb1d087544d81adf9cd0471ab Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 14:32:05 -0700 Subject: [PATCH 4/8] deno fmt --- packages/toolshed/routes/ai/llm/generateObject.ts | 10 +++++++--- packages/toolshed/routes/ai/llm/llm.handlers.ts | 15 ++++++++++----- packages/toolshed/routes/ai/llm/llm.routes.ts | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/toolshed/routes/ai/llm/generateObject.ts b/packages/toolshed/routes/ai/llm/generateObject.ts index 2797af548..29d12ba08 100644 --- a/packages/toolshed/routes/ai/llm/generateObject.ts +++ b/packages/toolshed/routes/ai/llm/generateObject.ts @@ -12,7 +12,9 @@ export async function generateObject( params: LLMGenerateObjectRequest, ): Promise { try { - const modelConfig = findModel(params.model ?? DEFAULT_GENERATE_OBJECT_MODELS); + const modelConfig = findModel( + params.model ?? DEFAULT_GENERATE_OBJECT_MODELS, + ); const ajv = new Ajv({ allErrors: true, strict: false }); const validator = ajv.compile(params.schema); @@ -70,6 +72,8 @@ export async function generateObject( }; } catch (error) { console.error("Error generating object:", error); - throw error instanceof Error ? error : new Error(`Failed to generate object: ${error}`); + throw error instanceof Error + ? error + : new Error(`Failed to generate object: ${error}`); } -} \ No newline at end of file +} diff --git a/packages/toolshed/routes/ai/llm/llm.handlers.ts b/packages/toolshed/routes/ai/llm/llm.handlers.ts index 306e8ee0e..005cbf99b 100644 --- a/packages/toolshed/routes/ai/llm/llm.handlers.ts +++ b/packages/toolshed/routes/ai/llm/llm.handlers.ts @@ -2,9 +2,9 @@ import * as HttpStatusCodes from "stoker/http-status-codes"; import type { AppRouteHandler } from "@/lib/types.ts"; import type { FeedbackRoute, + GenerateObjectRoute, GenerateTextRoute, GetModelsRoute, - GenerateObjectRoute, } from "./llm.routes.ts"; import { ALIAS_NAMES, ModelList, MODELS, TASK_MODELS } from "./models.ts"; import { hashKey, loadFromCache, saveToCache } from "./cache.ts"; @@ -247,11 +247,16 @@ export const submitFeedback: AppRouteHandler = async (c) => { * Handler for POST /generateObject endpoint * Generates structured JSON objects using specified LLM model */ -export const generateObject: AppRouteHandler = async (c) => { +export const generateObject: AppRouteHandler = async ( + c, +) => { const payload = await c.req.json(); if (!payload.prompt || !payload.schema) { - return c.json({ error: "Missing required fields: prompt and schema" }, HttpStatusCodes.BAD_REQUEST); + return c.json( + { error: "Missing required fields: prompt and schema" }, + HttpStatusCodes.BAD_REQUEST, + ); } if (!payload.metadata) { @@ -280,7 +285,7 @@ export const generateObject: AppRouteHandler = async (c) => try { const result = await generateObjectCore(payload); - + // Save to cache if enabled if (payload.cache !== false) { try { @@ -293,7 +298,7 @@ export const generateObject: AppRouteHandler = async (c) => console.error("Error saving generateObject response to cache:", e); } } - + return c.json(result); } catch (error) { console.error("Error in generateObject:", error); diff --git a/packages/toolshed/routes/ai/llm/llm.routes.ts b/packages/toolshed/routes/ai/llm/llm.routes.ts index 1bedf1beb..9190d8fb1 100644 --- a/packages/toolshed/routes/ai/llm/llm.routes.ts +++ b/packages/toolshed/routes/ai/llm/llm.routes.ts @@ -4,9 +4,9 @@ import { jsonContent } from "stoker/openapi/helpers"; import { z } from "zod"; import { toZod } from "@commontools/utils/zod-utils"; import { + type LLMGenerateObjectRequest, type LLMMessage, type LLMRequest, - type LLMGenerateObjectRequest, LLMTypedContent, } from "@commontools/llm/types"; From 98465e8e546920eb7c87822e8b3e98a38b05f36d Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 17:16:40 -0700 Subject: [PATCH 5/8] closer to type completion --- packages/toolshed/routes/ai/llm/cache.ts | 1 + packages/toolshed/routes/ai/llm/llm.handlers.ts | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/toolshed/routes/ai/llm/cache.ts b/packages/toolshed/routes/ai/llm/cache.ts index f392bf614..1571582dc 100644 --- a/packages/toolshed/routes/ai/llm/cache.ts +++ b/packages/toolshed/routes/ai/llm/cache.ts @@ -6,6 +6,7 @@ export const CACHE_DIR = `${env.CACHE_DIR}/llm-api-cache`; interface CacheItem { messages: Array<{ role: string; content: string }>; + object?: Record; model?: string; system?: string; stopSequences?: string[]; diff --git a/packages/toolshed/routes/ai/llm/llm.handlers.ts b/packages/toolshed/routes/ai/llm/llm.handlers.ts index 005cbf99b..b5398334a 100644 --- a/packages/toolshed/routes/ai/llm/llm.handlers.ts +++ b/packages/toolshed/routes/ai/llm/llm.handlers.ts @@ -278,8 +278,7 @@ export const generateObject: AppRouteHandler = async ( if (cachedResult) { return c.json({ object: cachedResult.object, - id: cachedResult.id, - }); + }, HttpStatusCodes.OK); } } @@ -292,14 +291,13 @@ export const generateObject: AppRouteHandler = async ( await saveToCache(cacheKey, { ...removeNonCacheableFields(payload), object: result.object, - id: result.id, }); } catch (e) { console.error("Error saving generateObject response to cache:", e); } } - return c.json(result); + return c.json(result, HttpStatusCodes.OK); } catch (error) { console.error("Error in generateObject:", error); const message = error instanceof Error ? error.message : "Unknown error"; From 1b01687391be246e1a3d1c6329d7f5353e9453c5 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 17:21:06 -0700 Subject: [PATCH 6/8] claude says this... --- packages/runner/src/builtins/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/runner/src/builtins/index.ts b/packages/runner/src/builtins/index.ts index b746aa17b..f818f5c0c 100644 --- a/packages/runner/src/builtins/index.ts +++ b/packages/runner/src/builtins/index.ts @@ -6,6 +6,8 @@ import { llm, generateObject } from "./llm.ts"; import { ifElse } from "./if-else.ts"; import type { IRuntime } from "../runtime.ts"; import { compileAndRun } from "./compile-and-run.ts"; +import type { DocImpl } from "../doc.ts"; +import type { BuiltInGenerateObjectParams } from "@commontools/api"; /** * Register all built-in modules with a runtime's module registry @@ -19,5 +21,10 @@ export function registerBuiltins(runtime: IRuntime) { moduleRegistry.addModuleByRef("llm", raw(llm)); moduleRegistry.addModuleByRef("ifElse", raw(ifElse)); moduleRegistry.addModuleByRef("compileAndRun", raw(compileAndRun)); - moduleRegistry.addModuleByRef("generateObject", raw(generateObject)); + moduleRegistry.addModuleByRef("generateObject", raw; + result: DocImpl | undefined>; + partial: DocImpl; + requestHash: DocImpl; + }>(generateObject)); } From 7edf13997a35c89d5fe428c406f7f193e159a42d Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 17:28:06 -0700 Subject: [PATCH 7/8] fix ajv --- deno.lock | 2 -- packages/jumble/deno.json | 1 - packages/llm/deno.json | 1 - 3 files changed, 4 deletions(-) diff --git a/deno.lock b/deno.lock index 17414d1c8..3ddcd9c66 100644 --- a/deno.lock +++ b/deno.lock @@ -6863,7 +6863,6 @@ "npm:@uiw/react-json-view@2.0.0-alpha.30", "npm:@use-gesture/react@^10.3.1", "npm:@vitejs/plugin-react@^4.3.4", - "npm:ajv@^8.17.1", "npm:cmdk@^1.0.4", "npm:csstype@^3.1.3", "npm:emoji-picker-react@^4.12.0", @@ -6883,7 +6882,6 @@ }, "packages/llm": { "dependencies": [ - "npm:ajv@^8.17.1", "npm:json5@^2.2.3" ] }, diff --git a/packages/jumble/deno.json b/packages/jumble/deno.json index 5fa561ebb..02c8dcc32 100644 --- a/packages/jumble/deno.json +++ b/packages/jumble/deno.json @@ -39,7 +39,6 @@ "@codemirror/lang-json": "npm:@codemirror/lang-json@^6.0.1", "@use-gesture/react": "npm:@use-gesture/react@^10.3.1", "@vitejs/plugin-react": "npm:@vitejs/plugin-react@^4.3.4", - "ajv": "npm:ajv@^8.17.1", "cmdk": "npm:cmdk@^1.0.4", "csstype": "npm:csstype@^3.1.3", "emoji-picker-react": "npm:emoji-picker-react@^4.12.0", diff --git a/packages/llm/deno.json b/packages/llm/deno.json index 8f0e65bd1..f6c928101 100644 --- a/packages/llm/deno.json +++ b/packages/llm/deno.json @@ -8,7 +8,6 @@ "./types": "./src/types.ts" }, "imports": { - "ajv": "npm:ajv@^8.17.1", "json5": "npm:json5@^2.2.3" } } From 929c23d97932e5eb0b4626b38a2dc7a35340b0c1 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 18 Jun 2025 21:22:31 -0700 Subject: [PATCH 8/8] add model to generateObject --- packages/api/index.ts | 1 + packages/runner/src/builder/built-in.ts | 1 + packages/static/assets/types/commontools.d.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/api/index.ts b/packages/api/index.ts index 57ee585ae..ac8d82332 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -230,6 +230,7 @@ export interface BuiltInLLMState { } export interface BuiltInGenerateObjectParams { + model?: string; prompt?: string; schema?: JSONSchema; system?: string; diff --git a/packages/runner/src/builder/built-in.ts b/packages/runner/src/builder/built-in.ts index aa9eb09ba..9802fff55 100644 --- a/packages/runner/src/builder/built-in.ts +++ b/packages/runner/src/builder/built-in.ts @@ -49,6 +49,7 @@ export interface BuiltInLLMState { } export interface BuiltInGenerateObjectParams { + model?: string; prompt?: string; schema?: JSONSchema; system?: string; diff --git a/packages/static/assets/types/commontools.d.ts b/packages/static/assets/types/commontools.d.ts index c75ea33d1..83901c262 100644 --- a/packages/static/assets/types/commontools.d.ts +++ b/packages/static/assets/types/commontools.d.ts @@ -132,6 +132,7 @@ export interface BuiltInLLMState { error: unknown; } export interface BuiltInGenerateObjectParams { + model?: string; prompt?: string; schema?: JSONSchema; system?: string;