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
24 changes: 18 additions & 6 deletions apps/backend/src/services/generate/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { GenerationError } from "./errors.js";
import type { Patch } from "../vdom/index.js";
import type { Action } from "@cuttlekit/common/client";
import type { LLMResponse } from "./types.js";

export const MAX_RETRY_ATTEMPTS = 3;

Expand All @@ -20,6 +20,7 @@ COMPONENTS: Register reusable UI components with define, then use custom tags in
- Use custom tags in patches: {"selector":"#root","append":"<my-card id='c1' title='Hello' status='Active'></my-card>"}
- Define components BEFORE first use. One define per line.
- Components persist across requests — do NOT re-emit define unless restyling. Check [COMPONENTS] for already-defined tags.
- When creating 3+ similar elements, ALWAYS define a component first. Repeated inline HTML wastes context — components keep page state compact.

JSON ESCAPING: Use single quotes for HTML attributes to avoid escaping.
CORRECT: {"html":"<div class='flex'>"}
Expand Down Expand Up @@ -50,6 +51,7 @@ INTERACTIVITY - NO JavaScript/onclick (won't work):
Use &quot; for JSON in data-action-data. Input values auto-sent with actions.

ACTIONS: Update data only — don't redesign or restyle the UI. Exception: inherently visual actions (color pickers, theme toggles).
RESTYLING: Visual-only changes must preserve existing data — never silently replace content.

IDs REQUIRED: All interactive/dynamic elements need unique id. Containers: id="todo-list". Items: id="todo-1". Buttons: id="add-btn".

Expand Down Expand Up @@ -105,12 +107,22 @@ export const buildSystemPrompt = (deps?: PackageInfo[]): string =>
// Build corrective prompt for retry after error
export const buildCorrectivePrompt = (
error: GenerationError,
successfulPatches: readonly Patch[] = [],
successfulOps: readonly LLMResponse[] = [],
currentHtml?: string,
): string => {
const defines = successfulOps.filter((o) => o.op === "define");
const patches = successfulOps.flatMap((o) =>
o.op === "patches" ? o.patches : [],
);

const defined =
defines.length > 0
? `\nDEFINED: ${defines.map((d) => d.tag).join(", ")} (already registered — do NOT re-emit)`
: "";

const applied =
successfulPatches.length > 0
? `\nAPPLIED: ${JSON.stringify(successfulPatches)}\nContinue from here.`
patches.length > 0
? `\nAPPLIED: ${JSON.stringify(patches)}\nContinue from here.`
: "";

const pageState = currentHtml
Expand All @@ -120,11 +132,11 @@ export const buildCorrectivePrompt = (
if (error._tag === "JsonParseError") {
return `${pageState}JSON ERROR: ${error.message}
Bad: ${error.line.slice(0, 100)}
Fix: valid JSONL, one JSON/line, single quotes in HTML attrs${applied}`;
Fix: valid JSONL, one JSON/line, single quotes in HTML attrs${defined}${applied}`;
}

return `${pageState}PATCH ERROR "${error.patch.selector}": ${error.reason}
Fix: selector must exist, use #id only${applied}`;
Fix: selector must exist, use #id only${defined}${applied}`;
};

// Build a compact, LLM-readable description of a single action.
Expand Down
43 changes: 25 additions & 18 deletions apps/backend/src/services/generate/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { z } from "zod";
import type { LanguageModelConfig } from "@cuttlekit/common/server";
import { MemoryService, type MemorySearchResult } from "../memory/index.js";
import { accumulateLinesWithFlush } from "../../stream/utils.js";
import { PatchValidator, renderCETree, getCompactHtmlFromCtx, type Patch, type ValidationContext } from "../vdom/index.js";
import { PatchValidator, renderCETree, getCompactHtmlFromCtx, type ValidationContext } from "../vdom/index.js";
import { ModelRegistry } from "../model-registry.js";
import { loadAppConfig } from "../app-config.js";
import {
PatchSchema,
LLMResponseSchema,
JsonParseError,
type LLMResponse,
type UnifiedResponse,
type UnifiedGenerateOptions,
type Message,
Expand Down Expand Up @@ -255,7 +256,7 @@ export class GenerateService extends Effect.Service<GenerateService>()(
validationCtx: ValidationContext,
usageRef: Ref.Ref<Usage[]>,
ttftRef: Ref.Ref<number>,
patchesRef: Ref.Ref<Patch[]>,
opsRef: Ref.Ref<LLMResponse[]>,
modeRef: Ref.Ref<"patches" | "full">,
attempt: number,
modelConfig: LanguageModelConfig,
Expand All @@ -277,15 +278,13 @@ export class GenerateService extends Effect.Service<GenerateService>()(
requestTools,
),

// Track successful patches, mode, and log
// Track successful operations, mode, and log
Stream.tap((response) =>
Effect.gen(function* () {
if (response.op === "patches") {
yield* Ref.update(patchesRef, (ps) => [
...ps,
...response.patches,
]);
} else if (response.op === "full") {
if (response.op !== "stats") {
yield* Ref.update(opsRef, (ops) => [...ops, response]);
}
if (response.op === "full") {
yield* Ref.set(modeRef, "full");
}
yield* Effect.log(`[Attempt ${attempt}] Emitting response`, {
Expand All @@ -304,14 +303,18 @@ export class GenerateService extends Effect.Service<GenerateService>()(
const genError = error as GenerationError;
return Stream.unwrap(
Effect.gen(function* () {
const successfulPatches = yield* Ref.get(patchesRef);
yield* Ref.set(patchesRef, []); // Reset for next attempt
const successfulOps = yield* Ref.get(opsRef);
// Keep defines (persist in registry across retries), reset patches/full
yield* Ref.set(
opsRef,
successfulOps.filter((o) => o.op === "define"),
);
const compactHtml = yield* getCompactHtmlFromCtx(validationCtx);
yield* Effect.log(
`[Attempt ${attempt}] ${genError._tag}, retrying...`,
{
error: genError.message,
successfulPatches: successfulPatches.length,
successfulOps: successfulOps.length,
},
);

Expand All @@ -324,15 +327,15 @@ export class GenerateService extends Effect.Service<GenerateService>()(
role: "user",
content: buildCorrectivePrompt(
genError,
successfulPatches,
successfulOps,
compactHtml,
),
},
],
validationCtx,
usageRef,
ttftRef,
patchesRef,
opsRef,
modeRef,
attempt + 1,
modelConfig,
Expand Down Expand Up @@ -507,7 +510,7 @@ export class GenerateService extends Effect.Service<GenerateService>()(
// Create Refs to track state across retries
const usageRef = yield* Ref.make<Usage[]>([]);
const ttftRef = yield* Ref.make<number>(0);
const patchesRef = yield* Ref.make<Patch[]>([]);
const opsRef = yield* Ref.make<LLMResponse[]>([]);
const modeRef = yield* Ref.make<"patches" | "full">("patches");
const startTime = yield* DateTime.now;

Expand All @@ -517,7 +520,7 @@ export class GenerateService extends Effect.Service<GenerateService>()(
validationCtx,
usageRef,
ttftRef,
patchesRef,
opsRef,
modeRef,
0,
modelConfig,
Expand Down Expand Up @@ -577,15 +580,19 @@ export class GenerateService extends Effect.Service<GenerateService>()(
});

const mode = yield* Ref.get(modeRef);
const patches = yield* Ref.get(patchesRef);
const ops = yield* Ref.get(opsRef);
const patchCount = ops.reduce(
(n, o) => n + (o.op === "patches" ? o.patches.length : 0),
0,
);
const ttft = yield* Ref.get(ttftRef);

return {
op: "stats" as const,
cacheRate: Math.round(cacheRate),
tokensPerSecond: Math.round(tokensPerSecond),
mode,
patchCount: patches.length,
patchCount,
ttft: Math.round(ttft),
ttc: Math.round(elapsedMs),
};
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/src/services/generate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const LLMResponseSchema = z.union([
DefineOpSchema,
]);

export type LLMResponse = z.infer<typeof LLMResponseSchema>;

// Full response schema includes stats (generated by code, not LLM)
export const UnifiedResponseSchema = z.union([
LLMResponseSchema,
Expand Down
1 change: 1 addition & 0 deletions apps/webpage/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const app = {
`TTC ${ttc}`,
`${s.tokensPerSecond} tok/s`,
`${s.cacheRate}% cache`,
`${s.patchCount} patches`,
].join(" · ");
statsEl.style.display = "flex";
},
Expand Down
Loading