Skip to content

Commit da18e97

Browse files
committed
Improve LLM functionality (slightly)
1 parent 6f02d88 commit da18e97

File tree

2 files changed

+144
-133
lines changed

2 files changed

+144
-133
lines changed

src/Llm.svelte

Lines changed: 77 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,12 @@
6666
let responsePromise = $state();
6767
let modelName = $state("Gemini");
6868
let template =
69-
$state(`You modify a spreadsheet by executing Markdown JavaScript code blocks. You follow these rules exactly:
70-
- You NEVER use the tool calling API or function calling API
71-
- You always output JavaScript that will be executed
72-
- You always refer to the \`llmToolFunctions\` object within that code
69+
$state(`You modify a spreadsheet by executing JavaScript code. You follow these rules exactly:
7370
- You always query for information (unless you are adding a new sheet)
74-
- You try to do as many queries as possible in each block
75-
- You always make row and column values start from 0
71+
- You try to collate as many queries as possible in each tool call
72+
- You always make row and column values start from 0 (R0C0 is the top left corner of the sheet)
7673
- You try to use formatting functions (like \`BOLD\` and \`DOLLARS\`) whenever possible
74+
- You pass string arguments wrapped in quotes like \`=BOLD("Title")\`
7775
7876
- First, you plan
7977
- Then you revise the plan to combine as many steps as possible
@@ -88,7 +86,7 @@
8886
- If the user gives new instructions, make a new plan
8987
9088
91-
You can run the following JavaScript functions in code blocks:
89+
You can run the following JavaScript functions:
9290
- \${Object.entries(llmToolFunctions).map(([name, f]) => {
9391
const args = f.toString().replaceAll("\\n", " ").replaceAll(/ */g, " ").match(/\\([^)]*\\)/)?.[0] ?? "";
9492
return "llmToolFunctions." + name + args + " " + (f.description ?? "");
@@ -116,75 +114,14 @@ Formula function definitions have access to a \`this\` object with:
116114
- this.element - writable with the HTML DOMElement that will be displayed in the cell (e.g., buttons, checkboxes, canvas, SVG, etc.)
117115
- this.style - writable with the CSS style string for the containing \`<td>\`
118116
119-
You add any formula functions you use if they do not already exist.
120-
121-
122-
Example:
123-
124-
User:
125-
Find the receipt items that contain seafood and make them red.
126-
127-
Model:
128-
Plan:
129-
- I should learn about the current sheets through a query.
130-
- I should find the items with seafood using another query.
131-
- I should check if there is already a formula to make items red.
132-
- If not, I should add one.
133-
- I should modify the formulas for the seafood item cells to wrap them in calls to the new red formula.
134-
135-
Thought: I should learn more about the current sheets using a query.
136-
137-
\`\`\`javascript
138-
// Query the current sheets
139-
const sheets = llmToolFunctions.getSheets();
140-
llmToolFunctions.query("Sheets", sheets);
141-
// Query the first rows of each sheet to learn about the columns
142-
let firstRows = {};
143-
sheets.forEach((sheet, sheetIndex) => {
144-
firstRows[sheet.name] = new Array(sheet.cols).fill().map(
145-
(_, i) => llmToolFunctions.getCell(sheetIndex, 0, i)
146-
)
147-
});
148-
llmToolFunctions.query("First rows", firstRows);
149-
\`\`\`
150-
151-
User:
152-
Sheets: [{"name": "Sheet 1", "rows": 10, "cols": 3}, {"name": "Receipt", "rows": 6, "cols": 2}]
153-
First rows: {"Sheet 1": [{}, {}, {}], "Receipt": [{"formula": "=BOLD(\\\\"Item\\\\")", "value": "Item"}, {"formula": "=BOLD(\\\\"Cost\\\\")", "value": "Cost"}]}
154-
155-
Model:
156-
Thought: I should find the cells that might contain seafood. I now know that I can identify them by the "Item" column.
157-
Thought: I can also query the formulas at the same time to check and see if there is already a formula to make items red.
158-
159-
\`\`\`javascript
160-
llmToolFunctions.query("Items", new Array(6).fill().map(
161-
(_, i) => llmToolFunctions.getCell(1, i, 0)
162-
));
163-
llmToolFunctions.query("Formulas", llmToolFunctions.getFormulaFunctionsList());
164-
\`\`\`
165-
166-
User:
167-
Items: [{"formula": "=BOLD(\\\\"Items\\\\")", "value": "Items"}, {"formula": "shrimp", "value": "shrimp"}, {"formula": "chicken", "value": "chicken"}, ... ]
168-
Formulas: [ "abs", "acos", ..., "average", "rand", "slider", "bold", "center", "dollars", "sparkbars", "checkbox" ]
169-
170-
Model:
171-
Thought: Since there is no formula to make items red, I should add one. And I can make the seafood item cells red to complete my task.
172-
173-
\`\`\`javascript
174-
llmToolFunctions.addFunction(\`
175-
functions.red = function (s) {
176-
this.style += "color: red;"
177-
return s;
178-
}
179-
\`);
180-
llmToolFunctions.setCellFormula(1, 1, 0, \`=RED(\\\${llmToolFunctions.getCell(1, 1, 0).formula})\`)
181-
llmToolFunctions.setCellFormula(1, 4, 0, \`=RED(\\\${llmToolFunctions.getCell(1, 4, 0).formula})\`)
182-
\`\`\`
183-
`);
117+
You add any formula functions you use if they do not already exist.`);
184118
185119
let conversation = $state([
186120
{ role: "system", text: "" },
187-
{ role: "user", text: "" },
121+
{
122+
role: "user",
123+
text: "",
124+
},
188125
]);
189126
190127
// Need to call eval in a separate function from derived.by to ensure globals
@@ -211,55 +148,60 @@ llmToolFunctions.setCellFormula(1, 4, 0, \`=RED(\\\${llmToolFunctions.getCell(1,
211148
conversation = conversationSlice;
212149
responsePromise = llmModels[modelName]
213150
.request(conversationSlice)
214-
.then((parts) =>
215-
parts.map((part) => {
216-
if (
217-
part.match(/^````*( *javascript *)?\n/) &&
218-
part.endsWith("\n```")
219-
) {
220-
return {
221-
role: "model",
222-
code: part
223-
.replaceAll(/(^````*( *javascript *)?\n)|(\n````*$)/g, "")
224-
.trim(),
225-
};
226-
} else {
227-
return {
228-
role: "model",
229-
text: part,
230-
};
231-
}
232-
}),
233-
)
234151
.then((parts) => {
235152
conversation = conversation.concat(parts);
236153
});
237154
}
238155
239156
function execute(llmCode, codeIndex) {
157+
const { log, warn, error } = console;
240158
llmToolFunctions.globals = globals;
241-
242-
llmToolFunctions.query = (name, value) => {
159+
llmToolFunctions.query = (value, name) => {
243160
let userResponse;
244161
let i = codeIndex;
245162
for (
246163
userResponse = conversation[++i];
247164
i < conversation.length && userResponse.role != "user";
248165
userResponse = conversation[++i]
249166
) {}
250-
if (userResponse.text.length && !userResponse.text.endsWith("\n")) {
251-
userResponse.text += "\n";
167+
let response = "";
168+
if (name) {
169+
response += `${name}: `;
170+
}
171+
response += JSON.stringify(value);
172+
if (!userResponse?.response) {
173+
conversation.splice(i, 0, { role: "user", response });
174+
} else {
175+
userResponse.response += "\n" + response;
252176
}
253-
userResponse.text += `${name}: ${JSON.stringify(value)}`;
254177
};
178+
console.log = (value) => {
179+
log(value);
180+
llmToolFunctions.query(value, "LOG");
181+
};
182+
console.warn = (value) => {
183+
warn(value);
184+
llmToolFunctions.query(value, "WARN");
185+
};
186+
console.error = (value) => {
187+
error(value);
188+
llmToolFunctions.query(value, "ERROR");
189+
};
190+
191+
try {
192+
eval(
193+
llmCode +
194+
// Allows user code to show up in the devtools debugger as "llm-code.js"
195+
"\n//# sourceURL=llm-code.js",
196+
);
197+
} catch (e) {
198+
console.error(e?.message ?? e?.toString() ?? e);
199+
}
255200
256-
// TODO: Display the error
257-
eval(
258-
llmCode +
259-
// Allows user code to show up in the devtools debugger as "llm-code.js"
260-
"\n//# sourceURL=llm-code.js",
261-
);
262201
delete llmToolFunctions.globals;
202+
console.log = log;
203+
console.warn = warn;
204+
console.error = error;
263205
}
264206
265207
function scrollIntoView(e) {
@@ -321,21 +263,37 @@ llmToolFunctions.setCellFormula(1, 4, 0, \`=RED(\\\${llmToolFunctions.getCell(1,
321263
<div style="margin-left: 10%;">
322264
<Details open>
323265
{#snippet summary()}User{/snippet}
324-
<textarea
325-
onkeydown={(e) => {
326-
if (
327-
e.key.toLocaleLowerCase() == "enter" &&
328-
(e.ctrlKey || e.metaKey)
329-
) {
330-
e.target.blur();
331-
submit(conversation.slice(0, i + 1));
332-
}
333-
}}
334-
placeholder={i == 1
335-
? "Make a comprehensive budget spreadsheet for a 25 year old living in Manhattan and making $75k per year"
336-
: ""}
337-
bind:value={part.text}
338-
></textarea>
266+
{#if part.response}
267+
<CodeEditor
268+
onkeydown={(e) => {
269+
if (
270+
e.key.toLocaleLowerCase() == "enter" &&
271+
(e.ctrlKey || e.metaKey)
272+
) {
273+
e.target.blur();
274+
submit(conversation.slice(0, i + 1));
275+
}
276+
}}
277+
bind:code={part.response}
278+
style="min-height: 10em; resize: vertical;"
279+
></CodeEditor>
280+
{:else}
281+
<textarea
282+
onkeydown={(e) => {
283+
if (
284+
e.key.toLocaleLowerCase() == "enter" &&
285+
(e.ctrlKey || e.metaKey)
286+
) {
287+
e.target.blur();
288+
submit(conversation.slice(0, i + 1));
289+
}
290+
}}
291+
placeholder={i == 1
292+
? "Make a comprehensive budget spreadsheet for a 25 year old living in Manhattan and making $75k per year"
293+
: ""}
294+
bind:value={part.text}
295+
></textarea>
296+
{/if}
339297
<div class="buttons">
340298
<Button onclick={() => submit(conversation.slice(0, i + 1))}
341299
>Submit</Button

src/llm.svelte.js

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ llmToolFunctions.newSheet = function (name, cells) {
99
);
1010
};
1111
llmToolFunctions.newSheet.description =
12-
"takes a 2D array of strings containing formulas";
12+
"cells is a 2D array of strings containing formulas, and a required argument";
1313

1414
llmToolFunctions.addRow = function (sheetIndex, offset) {
1515
this.globals.sheets[sheetIndex]?.addRows(1, offset);
@@ -70,7 +70,7 @@ llmToolFunctions.query = function (value) {
7070
export const llmModels = $state({});
7171

7272
llmModels.Gemini = {
73-
model: "gemini-2.5-pro-exp-03-25",
73+
model: "gemini-flash-latest",
7474
async request(conversation) {
7575
const response = await fetch(
7676
`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`,
@@ -82,7 +82,31 @@ llmModels.Gemini = {
8282
body: JSON.stringify({
8383
generation_config: {
8484
temperature: 0.5,
85+
thinkingConfig: {
86+
thinkingBudget: -1,
87+
},
8588
},
89+
tools: [
90+
// { google_search: {}, url_context: {}, },
91+
{
92+
functionDeclarations: [
93+
{
94+
name: "evalJavaScript",
95+
description:
96+
"Evaluate JavaScript code in the spreadsheet context to read and modify spreadsheet state",
97+
parameters: {
98+
type: "object",
99+
properties: {
100+
code: {
101+
type: "string",
102+
},
103+
},
104+
required: ["code"],
105+
},
106+
},
107+
],
108+
},
109+
],
86110
system_instruction: {
87111
parts: conversation
88112
.filter(({ role }) => role == "system")
@@ -91,17 +115,38 @@ llmModels.Gemini = {
91115
)
92116
.flat(Infinity),
93117
},
94-
tools: [
95-
{
96-
google_search: {},
97-
},
98-
],
99118
contents: conversation
100119
.filter(({ role }) => role != "system")
101-
.map(({ role, text, code }) => ({
102-
role,
103-
parts: [{ text: text ?? "```javascript\n" + code + "\n```" }],
104-
})),
120+
.filter(
121+
// Text can be null or set, but not empty string
122+
({ text }) => text !== "",
123+
)
124+
.map(({ role, text, code, response }) => {
125+
if (text) {
126+
return { role, parts: [{ text }] };
127+
} else if (code) {
128+
return {
129+
role,
130+
parts: [
131+
{
132+
functionCall: { name: "evalJavaScript", args: { code } },
133+
},
134+
],
135+
};
136+
} else if (response) {
137+
return {
138+
role,
139+
parts: [
140+
{
141+
function_response: {
142+
name: "evalJavaScript",
143+
response: { output: response },
144+
},
145+
},
146+
],
147+
};
148+
}
149+
}),
105150
}),
106151
},
107152
)
@@ -113,8 +158,16 @@ llmModels.Gemini = {
113158
console.error(e);
114159
throw e;
115160
});
116-
return response?.candidates?.[0]?.content?.parts
117-
?.map(({ text }) => text.trim())
118-
.filter((text) => text);
161+
const result = response?.candidates?.[0]?.content?.parts?.map(
162+
({ text, functionCall }) => {
163+
if (functionCall) {
164+
return { role: "model", code: functionCall?.args?.code };
165+
} else {
166+
return { role: "model", text };
167+
}
168+
},
169+
);
170+
console.log(result);
171+
return result;
119172
},
120173
};

0 commit comments

Comments
 (0)