Skip to content

Commit 9925c02

Browse files
committed
chore: checkpoint in-flight slop-fix work [skip ci]
Wave-2 snapshot. Agents are still editing these files; unverified.
1 parent 296476b commit 9925c02

5 files changed

Lines changed: 90 additions & 50 deletions

File tree

packages/llm-nodes/src/nodes/agent-utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,27 @@ export function buildUserMessage(
420420
return { role: "user", content };
421421
}
422422

423+
/**
424+
* A user message that keeps `content` a plain string unless a media ref is
425+
* actually attached. The Summarizer/Extractor/Classifier nodes declare `image`
426+
* and `audio` props; wiring them through {@link buildUserMessage} unconditionally
427+
* would also turn every text-only request into a parts array and run the prompt
428+
* through `expandAssetReferences`, changing what existing graphs send. This
429+
* keeps the old string shape whenever no ref resolves.
430+
*/
431+
export function userMessageWithMedia(
432+
text: string,
433+
images: unknown,
434+
audios: unknown
435+
): Message {
436+
const message = buildUserMessage(text, images, audios);
437+
const parts = Array.isArray(message.content) ? message.content : [];
438+
const hasMedia = parts.some(
439+
(part) => part.type === "image_url" || part.type === "audio"
440+
);
441+
return hasMedia ? message : { role: "user", content: text };
442+
}
443+
423444
/**
424445
* A single classified item from a provider's {@link BaseProvider.generateLoop}
425446
* stream. Both {@link runAgentLoop} and the AgentNode's genProcess drive the

packages/llm-nodes/src/nodes/agents.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
parseCategory,
3434
normalizeMessage,
3535
buildUserMessage,
36+
userMessageWithMedia,
3637
uniqueToolName,
3738
normalizeTools,
3839
gateAgentTools,
@@ -275,10 +276,11 @@ export class SummarizerNode extends BaseNode {
275276
for await (const item of streamProviderMessages(provider, {
276277
messages: [
277278
{ role: "system", content: systemPrompt },
278-
{
279-
role: "user",
280-
content: `Summarize the following text in about ${maxSentences} sentence(s):\n\n${text}`
281-
}
279+
userMessageWithMedia(
280+
`Summarize the following text in about ${maxSentences} sentence(s):\n\n${text}`,
281+
this.image,
282+
this.audio
283+
)
282284
],
283285
model: modelId,
284286
maxTokens: Math.max(64, maxSentences * 128)
@@ -642,7 +644,7 @@ export class ExtractorNode extends BaseNode {
642644
content:
643645
asText(this.system_prompt ?? "").trim() || EXTRACTOR_SYSTEM_PROMPT
644646
},
645-
{ role: "user", content: text }
647+
userMessageWithMedia(text, this.image, this.audio)
646648
],
647649
toolName: "extraction_result",
648650
toolDescription: "Submit the extracted data.",
@@ -787,10 +789,11 @@ export class ClassifierNode extends BaseNode {
787789
content:
788790
asText(this.system_prompt ?? "").trim() || CLASSIFIER_SYSTEM_PROMPT
789791
},
790-
{
791-
role: "user",
792-
content: `Allowed categories: ${categories.join(", ")}\n\nText: ${text}`
793-
}
792+
userMessageWithMedia(
793+
`Allowed categories: ${categories.join(", ")}\n\nText: ${text}`,
794+
this.image,
795+
this.audio
796+
)
794797
],
795798
toolName: "classification_result",
796799
toolDescription: "Submit the classification result.",

packages/llm-nodes/src/nodes/generators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ export class StructuredOutputGeneratorNode extends BaseNode {
441441
const result = await runMeteredMessage(context, providerId, modelId, {
442442
model: modelId,
443443
messages,
444+
max_tokens: Number(this.max_tokens ?? 4096),
444445
response_format: {
445446
type: "json_schema",
446447
json_schema: {

packages/llm-nodes/src/nodes/openai.ts

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type {
33
StreamingInputs,
44
StreamingOutputs
55
} from "@nodetool-ai/node-sdk";
6+
import type { ImageRef, AudioRef } from "@nodetool-ai/node-sdk";
7+
import type { Chunk } from "@nodetool-ai/protocol";
68
import type { ProcessingContext } from "@nodetool-ai/runtime";
79
import { fetchExternalMedia } from "@nodetool-ai/runtime";
810
import { tagAsServer } from "@nodetool-ai/nodes-utils";
@@ -57,9 +59,9 @@ export class EmbeddingNode extends BaseNode {
5759

5860
async process(): Promise<Record<string, unknown>> {
5961
const apiKey = getApiKey(this._secrets);
60-
const text = String(this.input ?? "");
61-
const model = String(this.model ?? "text-embedding-3-small");
62-
const chunkSize = Number(this.chunk_size ?? 4096);
62+
const text = this.input;
63+
const model = this.model;
64+
const chunkSize = this.chunk_size;
6365

6466
const chunks: string[] = [];
6567
for (let i = 0; i < text.length; i += chunkSize) {
@@ -123,7 +125,7 @@ export class WebSearchNode extends BaseNode {
123125

124126
async process(): Promise<WebSearchNodeOutputs> {
125127
const apiKey = getApiKey(this._secrets);
126-
const query = String(this.query ?? "");
128+
const query = this.query;
127129
if (!query) throw new Error("Search query cannot be empty");
128130

129131
const res = await fetch(`${OPENAI_API_BASE}/chat/completions`, {
@@ -195,8 +197,8 @@ export class ModerationNode extends BaseNode {
195197

196198
async process(): Promise<Record<string, unknown>> {
197199
const apiKey = getApiKey(this._secrets);
198-
const text = String(this.input ?? "");
199-
const model = String(this.model ?? "omni-moderation-latest");
200+
const text = this.input;
201+
const model = this.model;
200202
if (!text) throw new Error("Input text cannot be empty");
201203

202204
const res = await fetch(`${OPENAI_API_BASE}/moderations`, {
@@ -292,13 +294,13 @@ export class CreateImageNode extends BaseNode {
292294

293295
async process(): Promise<CreateImageNodeOutputs> {
294296
const apiKey = getApiKey(this._secrets);
295-
const prompt = String(this.prompt ?? "");
297+
const prompt = this.prompt;
296298
if (!prompt) throw new Error("Prompt cannot be empty");
297299

298-
const model = String(this.model ?? "gpt-image-1");
299-
const size = String(this.size ?? "1024x1024");
300-
const quality = String(this.quality ?? "high");
301-
const background = String(this.background ?? "auto");
300+
const model = this.model;
301+
const size = this.size;
302+
const quality = this.quality;
303+
const background = this.background;
302304

303305
const res = await fetch(`${OPENAI_API_BASE}/images/generations`, {
304306
method: "POST",
@@ -419,17 +421,17 @@ export class EditImageNode extends BaseNode {
419421

420422
async process(): Promise<EditImageNodeOutputs> {
421423
const apiKey = getApiKey(this._secrets);
422-
const prompt = String(this.prompt ?? "");
424+
const prompt = this.prompt;
423425
if (!prompt) throw new Error("Edit prompt cannot be empty");
424426

425-
const image = this.image as Record<string, unknown> | undefined;
427+
const image = this.image;
426428
if (!image || (!image.data && !image.uri)) {
427429
throw new Error("Input image is required");
428430
}
429431

430-
const model = String(this.model ?? "gpt-image-1");
431-
const size = String(this.size ?? "1024x1024");
432-
const quality = String(this.quality ?? "high");
432+
const model = this.model;
433+
const size = this.size;
434+
const quality = this.quality;
433435

434436
// gpt-image-1 always returns base64 and rejects `response_format`, so it
435437
// must not be sent (unlike the legacy dall-e-2 edits endpoint).
@@ -444,7 +446,7 @@ export class EditImageNode extends BaseNode {
444446
formData.append("image", imageBlob, "image.png");
445447

446448
// Optional mask
447-
const mask = this.mask as Record<string, unknown> | undefined;
449+
const mask = this.mask;
448450
if (mask && (mask.data || mask.uri)) {
449451
const maskBlob = await refToBlob(mask);
450452
formData.append("mask", maskBlob, "mask.png");
@@ -521,11 +523,11 @@ export class ImageVariationNode extends BaseNode {
521523

522524
async process(): Promise<ImageVariationNodeOutputs> {
523525
const apiKey = getApiKey(this._secrets);
524-
const image = this.image as Record<string, unknown> | undefined;
526+
const image = this.image;
525527
if (!image || (!image.data && !image.uri)) {
526528
throw new Error("Input image is required");
527529
}
528-
const size = String(this.size ?? "1024x1024");
530+
const size = this.size;
529531

530532
// The variations endpoint only supports dall-e-2, which requires the
531533
// `response_format` to request base64 output.
@@ -577,7 +579,9 @@ function audioRefFromB64(b64: string, contentType: string) {
577579
}
578580

579581
/** Convert an image/audio ref object to a Blob for multipart upload. */
580-
async function refToBlob(ref: Record<string, unknown>): Promise<Blob> {
582+
type MediaRefLike = { uri?: string; data?: unknown };
583+
584+
async function refToBlob(ref: MediaRefLike): Promise<Blob> {
581585
if (ref.data && isString(ref.data)) {
582586
const dataStr = ref.data;
583587
// Handle data: URI
@@ -666,12 +670,12 @@ export class TextToSpeechNode extends BaseNode {
666670

667671
async process(): Promise<TextToSpeechNodeOutputs> {
668672
const apiKey = getApiKey(this._secrets);
669-
const text = String(this.input ?? "");
673+
const text = this.input;
670674
if (!text) throw new Error("Input text cannot be empty");
671-
const model = String(this.model ?? "tts-1");
672-
const voice = String(this.voice ?? "alloy");
673-
const speed = Number(this.speed ?? 1.0);
674-
const instructions = String(this.instructions ?? "");
675+
const model = this.model;
676+
const voice = this.voice;
677+
const speed = this.speed;
678+
const instructions = this.instructions;
675679

676680
const isMiniTts = model === "gpt-4o-mini-tts";
677681
const body: Record<string, unknown> = {
@@ -750,11 +754,11 @@ export class TranslateNode extends BaseNode {
750754

751755
async process(): Promise<TranslateNodeOutputs> {
752756
const apiKey = getApiKey(this._secrets);
753-
const audio = this.audio as Record<string, unknown> | undefined;
757+
const audio = this.audio;
754758
if (!audio || (!audio.data && !audio.uri)) {
755759
throw new Error("Audio input is required");
756760
}
757-
const temperature = Number(this.temperature ?? 0.0);
761+
const temperature = this.temperature;
758762

759763
const audioBlob = await refToBlob(audio);
760764
const formData = new FormData();
@@ -926,16 +930,16 @@ export class TranscribeNode extends BaseNode {
926930

927931
async process(): Promise<TranscribeNodeOutputs> {
928932
const apiKey = getApiKey(this._secrets);
929-
const audio = this.audio as Record<string, unknown> | undefined;
933+
const audio = this.audio;
930934
if (!audio || (!audio.data && !audio.uri)) {
931935
throw new Error("Audio input is required");
932936
}
933937

934-
const model = String(this.model ?? "whisper-1");
935-
const language = String(this.language ?? "auto_detect");
936-
const timestamps = Boolean(this.timestamps ?? false);
937-
const promptText = String(this.prompt ?? "");
938-
const temperature = Number(this.temperature ?? 0);
938+
const model = this.model;
939+
const language = this.language;
940+
const timestamps = this.timestamps;
941+
const promptText = this.prompt;
942+
const temperature = this.temperature;
939943

940944
const isNewModel =
941945
model === "gpt-4o-transcribe" || model === "gpt-4o-mini-transcribe";
@@ -1124,11 +1128,11 @@ export class RealtimeAgentNode extends BaseNode {
11241128
if (!apiKey) apiKey = process.env.OPENAI_API_KEY ?? "";
11251129
if (!apiKey) throw new Error("OPENAI_API_KEY is not configured");
11261130

1127-
const model = String(this.model ?? "gpt-4o-mini-realtime-preview");
1128-
const voice = String(this.voice ?? "alloy");
1129-
const system = String(this.system ?? "");
1130-
const temperature = Number(this.temperature ?? 0.8);
1131-
const speed = Number(this.speed ?? 1);
1131+
const model = this.model;
1132+
const voice = this.voice;
1133+
const system = this.system;
1134+
const temperature = this.temperature;
1135+
const speed = this.speed;
11321136
const wantAudio = voice !== "none";
11331137

11341138
const { WebSocket } = await import("ws");
@@ -1406,8 +1410,8 @@ export class RealtimeTranscriptionNode extends BaseNode {
14061410
if (!apiKey) apiKey = process.env.OPENAI_API_KEY ?? "";
14071411
if (!apiKey) throw new Error("OPENAI_API_KEY is not configured");
14081412

1409-
const model = String(this.model ?? "gpt-4o-mini-realtime-preview");
1410-
const temperature = Number(this.temperature ?? 0.8);
1413+
const model = this.model;
1414+
const temperature = this.temperature;
14111415

14121416
const { WebSocket } = await import("ws");
14131417
const wsUrl = `wss://api.openai.com/v1/realtime?model=${encodeURIComponent(model)}`;
@@ -1429,7 +1433,7 @@ export class RealtimeTranscriptionNode extends BaseNode {
14291433
type: "session.update",
14301434
session: {
14311435
modalities: ["text"],
1432-
instructions: this.system ? String(this.system) : undefined,
1436+
instructions: this.system || undefined,
14331437
input_audio_format: "pcm16",
14341438
input_audio_transcription: { model: "gpt-4o-mini-transcribe" },
14351439
turn_detection: {

web/tsconfig.slopcheck.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": [
4+
"src/components/ui_primitives",
5+
"src/components/assets",
6+
"src/components/menus",
7+
"src/components/appbuilder",
8+
"src/components/workspace",
9+
"src/vite-env.d.ts"
10+
]
11+
}

0 commit comments

Comments
 (0)