Skip to content

Commit 4931e75

Browse files
authored
generateData -> llm + lift methods (#195)
generateData -> llm simplified generate code to just be about LLM conversations moves all the logic around prep / parsing / ... to recipe ergo - json / html mode is now handled by lift inside recipes example of using Zod / schemas to parse json results llm improvements: streaming stop sequences support for multiple messages (user -> assistant -> user -> assistant) support for putting words into LLMs mouth (forcing it to generate json) support for (base64) images common file input: added ability to load images / json data from file input
1 parent 321bd00 commit 4931e75

File tree

27 files changed

+1238
-1118
lines changed

27 files changed

+1238
-1118
lines changed

typescript/package-lock.json

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/packages/common-builder/src/built-in.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { lift, createNodeFactory } from "./module.js";
22
import { Value, NodeFactory, CellProxy } from "./types.js";
33

4-
export function generateData<T>(
4+
export function llm(
55
params: Value<{
6-
prompt: string;
7-
result?: T;
8-
schema?: any;
6+
messages?: string[];
7+
prompt?: string;
98
system?: string;
10-
mode?: "json" | "html";
9+
stop?: string;
10+
max_tokens?: number;
1111
}>
12-
): CellProxy<{ pending: boolean; result: T; partial: any; error: any }> {
13-
generateDataFactory ||= createNodeFactory({
12+
): CellProxy<{ pending: boolean; result?: string; partial?: string; error: any }> {
13+
llmFactory ||= createNodeFactory({
1414
type: "builtin",
15-
implementation: "generateData",
15+
implementation: "llm",
1616
});
17-
return generateDataFactory(params);
17+
return llmFactory(params);
1818
}
1919

2020
export function fetchData<T>(
@@ -74,10 +74,10 @@ export function ifElse<T, U, V>(
7474

7575
let ifElseFactory: NodeFactory<[any, any, any], any> | undefined = undefined;
7676

77-
let generateDataFactory:
77+
let llmFactory:
7878
| NodeFactory<
79-
{ prompt: string; result?: any; schema?: any; system?: string },
80-
{ pending: boolean; result: any; partial: any; error: any }
79+
{ messages?: string[]; prompt?: string; system?: string; stop?: string; max_tokens?: number },
80+
{ pending: boolean; result?: string; partial?: string; error: any }
8181
>
8282
| undefined = undefined;
8383

typescript/packages/common-builder/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export {
66
isolated,
77
} from "./module.js";
88
export { recipe } from "./recipe.js";
9-
export { streamData, fetchData, generateData, ifElse, str } from "./built-in.js";
9+
export { streamData, fetchData, llm, ifElse, str } from "./built-in.js";
1010
export {
1111
ID,
1212
TYPE,

typescript/packages/common-runner/src/builtins/generate-data.ts

Lines changed: 0 additions & 129 deletions
This file was deleted.

typescript/packages/common-runner/src/builtins/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { type CellImpl } from "../cell.js";
33
import { map } from "./map.js";
44
import { fetchData } from "./fetch-data.js";
55
import { streamData } from "./stream-data.js";
6-
import { generateData } from "./generate-data.js";
6+
import { llm } from "./llm.js";
77
import { ifElse } from "./if-else.js";
88
export const builtins: {
99
[key: string]: (recipeCell: CellImpl<any>, node: Node) => void;
1010
} = {
1111
map,
1212
fetchData,
1313
streamData,
14-
generateData,
14+
llm,
1515
ifElse,
1616
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { type Node } from "@commontools/common-builder";
2+
import { cell, CellImpl, ReactivityLog } from "../cell.js";
3+
import { sendValueToBinding, findAllAliasedCells } from "../utils.js";
4+
import { schedule, Action } from "../scheduler.js";
5+
import { mapBindingsToCell } from "../utils.js";
6+
import { makeClient, SimpleMessage, SimpleContent } from "../llm-client.js";
7+
8+
// TODO(ja): investigate if generateText should be replaced by
9+
// fetchData with streaming support
10+
11+
/**
12+
* Generate data via an LLM.
13+
*
14+
* Returns the complete result as `result` and the incremental result as
15+
* `partial`. `pending` is true while a request is pending.
16+
*
17+
* @param prompt - A cell to store the prompt message - if you only have a single message
18+
* @param messages - list of messages to send to the LLM. - alternating user and assistant messages.
19+
* - if you end with an assistant message, the LLM will continue from there.
20+
* - if both prompt and messages are empty, no LLM call will be made,
21+
* result and partial will be undefined.
22+
* @param system - A cell to store the system message.
23+
* @param stop - A cell to store (optional) stop sequence.
24+
* @param max_tokens - A cell to store the maximum number of tokens to generate.
25+
*
26+
* @returns { pending: boolean, result?: string, partial?: string } - As individual
27+
* cells, representing `pending` state, final `result` and incrementally
28+
* updating `partial` result.
29+
*/
30+
export function llm(
31+
recipeCell: CellImpl<any>,
32+
{ inputs, outputs }: Node
33+
) {
34+
const inputBindings = mapBindingsToCell(inputs, recipeCell) as {
35+
messages?: SimpleContent[] | SimpleMessage[];
36+
prompt?: SimpleContent;
37+
stop?: string;
38+
system?: string;
39+
max_tokens?: number;
40+
};
41+
const inputsCell = cell(inputBindings);
42+
43+
const pending = cell(false);
44+
const result = cell<string | undefined>(undefined);
45+
const partial = cell<string | undefined>(undefined);
46+
47+
const outputCell = cell({ pending, result, partial });
48+
49+
const outputBindings = mapBindingsToCell(outputs, recipeCell) as any[];
50+
sendValueToBinding(recipeCell, outputBindings, outputCell);
51+
52+
let currentRun = 0;
53+
54+
const startGeneration: Action = (log: ReactivityLog) => {
55+
const thisRun = ++currentRun;
56+
57+
const { system, messages, prompt, stop, max_tokens } = inputsCell.getAsProxy([], log) ?? {};
58+
59+
result.setAtPath([], undefined, log);
60+
partial.setAtPath([], undefined, log);
61+
62+
if (((prompt === undefined || prompt.length === 0) && (messages === undefined || messages.length === 0)) || system === undefined) {
63+
pending.setAtPath([], false, log);
64+
return;
65+
}
66+
pending.setAtPath([], true, log);
67+
68+
const updatePartial = (text: string) => {
69+
if (thisRun != currentRun) return;
70+
partial.setAtPath([], text, log);
71+
}
72+
73+
let resultPromise = makeClient().sendRequest({
74+
messages: messages || [prompt as SimpleContent],
75+
system,
76+
model: "claude-3-5-sonnet-20240620",
77+
max_tokens: max_tokens || 4096,
78+
stop
79+
}, updatePartial)
80+
81+
resultPromise
82+
.then((text) => {
83+
if (thisRun !== currentRun) return;
84+
// normalizeToCells(result, undefined, log);
85+
86+
pending.setAtPath([], false, log);
87+
result.setAtPath([], text, log);
88+
partial.setAtPath([], text, log);
89+
}).catch((error) => {
90+
if (thisRun !== currentRun) return;
91+
92+
console.error("Error generating data", error);
93+
pending.setAtPath([], false, log);
94+
result.setAtPath([], undefined, log);
95+
partial.setAtPath([], undefined, log);
96+
});
97+
};
98+
99+
schedule(startGeneration, {
100+
reads: findAllAliasedCells(inputBindings, recipeCell),
101+
writes: findAllAliasedCells(outputBindings, recipeCell),
102+
});
103+
}
Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
import { LLMClient } from "@commontools/llm-client";
2-
export const LLM_SERVER_URL =
3-
window.location.protocol + "//" + window.location.host + "/api/llm";
42

5-
export const suggestionClient = new LLMClient({
6-
serverUrl: LLM_SERVER_URL,
7-
system:
8-
"You are an assistant that helps match user queries to relevant data gems based on their names and types.",
9-
tools: [],
10-
});
3+
export { SimpleMessage, SimpleContent } from "@commontools/llm-client";
114

12-
export const mockResultClient = new LLMClient({
13-
serverUrl: LLM_SERVER_URL,
14-
system: `Generate dummy data as JSON as per the provided spec. Use the input to imagine what an API response would look like for a request.`,
15-
tools: [],
16-
});
5+
export const suggestSystem = "You are an assistant that helps match user queries to relevant data gems based on their names and types."
6+
export const jsonDataRequest = `Generate dummy data as JSON as per the provided spec. Use the input to imagine what an API response would look like for a request.`
177

18-
export const makeClient = (system: string) =>
19-
new LLMClient({
20-
serverUrl: LLM_SERVER_URL,
21-
system,
22-
tools: [],
23-
});
8+
export const LLM_SERVER_URL = window.location.protocol + "//" + window.location.host + "/api/llm";
9+
export const makeClient = (url?: string) => new LLMClient(url || LLM_SERVER_URL);
10+
11+
export function dataRequest({
12+
description,
13+
inputData,
14+
jsonSchema,
15+
}: {
16+
description: string;
17+
inputData: any;
18+
jsonSchema: any;
19+
}) {
20+
return `You specialize in generating believable and useful data for testing applications during development. Take the provided input parameters and use them to hallucinate a plausible result that conforms to the following JSON schema:
21+
22+
<schema>${JSON.stringify(jsonSchema)}</schema>
23+
24+
<description>${description}</description>
25+
<input>${JSON.stringify(inputData)}</input>
26+
27+
Respond with only the generated data in a JSON block.`;
28+
}

0 commit comments

Comments
 (0)