import { hydratePrompt, llmPrompt, parseTagFromResponse } from "./prompting.ts";
import { LLMClient } from "../client.ts";
import { DEFAULT_MODEL_NAME, extractTextFromLLMResponse } from "../types.ts";
import type { JSONSchema, JSONSchemaMutable } from "@commontools/runner";
import { WorkflowForm } from "@commontools/charm";
import { JSONSchemaObj } from "@commontools/api";
import { isObject } from "@commontools/utils/types";
// Prompt for generating schema and specification from a goal
export const SCHEMA_FROM_GOAL_PROMPT = llmPrompt(
"schema-from-goal",
`
You are creating a simple minimal viable product (MVP) based on a user's goal. Focus on the simplest implementation that works.
Given a user's feature request, you will:
1. Create a short title (2-5 words) that names the artifact
2. Create a one-sentence description in the format "A to "
3. Create a concise specification (3-5 sentences max)
4. Generate a brief implementation plan (3 steps max)
5. Design a minimal JSON schema that represents the core data model
Your response must be structured as follows:
Freeform reasoning for you to consider the solution.
[Short title for the artifact, 2-5 words]
[One-sentence description in the format "A to "]
[Concise specification that captures only the essential requirements]
[Brief 3-step implementation plan]
[Minimal JSON Schema in valid JSON format that represents the core data model]
[Simple example data that conforms to the schema]
1. Keep it minimal:
- Include only essential fields (5-7 properties max)
- Focus on the core functionality
- If user requested complex features, simplify for this first version
2. Each property should have:
- A descriptive "title" field
- A brief "description" field
- A sensible default value where appropriate
3. Example of a simple schema:
\`\`\`json
{
"type": "object",
"title": "Note List",
"description": "A simple note list for the user",
"properties": {
"notes": {
"type": "array",
"title": "Notes",
"description": "List of user notes",
"default": [],
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Title of the note",
"default": "New Note"
},
"content": {
"type": "string",
"title": "Content",
"description": "Content of the note"
},
"created": {
"type": "string",
"format": "date-time",
"title": "Created Date",
"description": "When the note was created"
}
},
"required": ["title", "content"]
}
}
},
"required": ["title", "content"]
}
\`\`\`
IMPORTANT:
- Focus on the simplest working version
- Aim for fewer fields rather than more
- But still capture all the important state the user is creating
- Remember, the user can always iterate and improve the solution later
`,
);
// Prompt for generating specification from a goal and existing schema
export const SPEC_FROM_SCHEMA_PROMPT = llmPrompt(
"spec-from-schema",
`
You are creating a simple MVP based on the user's goal, using an existing data schema. Focus on the simplest implementation that works with the provided schema.
Given a user's feature request and an existing data schema, you will:
1. Create a short title (2-5 words) that names the artifact
2. Create a one-sentence description in the format "A to "
3. Create a concise specification (3-5 sentences max) that works with the existing schema
4. Generate a brief implementation plan (3 steps max)
5. Design a minimal JSON schema that represents the core data model
Your response must be structured as follows:
Freeform reasoning for you to consider the solution.
[Short title for the artifact, 2-5 words]
[One-sentence description in the format "A to "]
[Concise specification that captures only the essential requirements]
[Brief 3-step implementation plan using the existing schema]
[Minimal JSON Schema in valid JSON format that represents data created by the artifact]
1. Keep it minimal:
- Include only essential fields (5-7 properties max)
- Focus on the core functionality
- If user requested complex features, simplify for this first version
2. Each property should have:
- A descriptive "title" field
- A brief "description" field
- A sensible default value where appropriate
3. Example of a simple schema:
\`\`\`json
{
"type": "object",
"title": "Note List",
"description": "A simple note list for the user",
"properties": {
"notes": {
"type": "array",
"title": "Notes",
"description": "List of user notes",
"default": [],
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"title": "Title",
"description": "Title of the note",
"default": "New Note"
},
"content": {
"type": "string",
"title": "Content",
"description": "Content of the note"
},
"created": {
"type": "string",
"format": "date-time",
"title": "Created Date",
"description": "When the note was created"
}
},
"required": ["title", "content"]
}
}
},
"required": ["title", "content"]
}
\`\`\`
GUIDELINES:
- Aim for the simplest possible solution that works with the existing schema
- The specification should take into account the existing schema structure
- Focus on what can be achieved quickly with the existing data model
- Avoid suggesting modifications to the schema if possible
IMPORTANT:
- Focus on the simplest working version
- Aim for fewer fields rather than more
- But still capture all the important state the user is creating
- The user can always iterate and improve the solution later
`,
);
export function formatForm(form: WorkflowForm) {
return `
${form.input.processedInput}
${
form.plan?.features?.length
? `${
form.plan.features.map((step) => `${step}`).join(
"\n",
)
}`
: ""
}
${form.plan?.description}
`;
}
/**
* Generates a complete specification, schema, and plan from a goal.
* @param goal The user's goal or request
* @param existingSchema Optional existing schema to use as a basis
* @param model Optional model identifier to use (defaults to claude-sonnet-4-5)
* @returns Object containing title, description, specification, schema
*/
export async function generateSpecAndSchema(
form: WorkflowForm,
existingSchema?: JSONSchema,
model: string = "anthropic:claude-sonnet-4-5",
): Promise<{
spec: string;
plan: string;
title: string;
description: string;
resultSchema: JSONSchemaObj;
argumentSchema: JSONSchemaObj;
}> {
let systemPrompt, userContent;
if (!form.plan) {
throw new Error("Plan is required");
}
// If the existing schema is boolean, pretend it doesn't exist, since a
// boolean schema isn't useful to the llm.
if (isObject(existingSchema) && Object.keys(existingSchema).length > 0) {
// When we have an existing schema, focus on generating specification
systemPrompt = SPEC_FROM_SCHEMA_PROMPT;
userContent = hydratePrompt(
llmPrompt(
"spec-from-schema-user",
`
{{FORM}}
\`\`\`json
{{EXISTING_SCHEMA}}
\`\`\`
Based on this goal and the existing schema, please provide a title, description, any additional schema, detailed specification, and implementation plan.
`,
),
{
FORM: formatForm(form),
EXISTING_SCHEMA: JSON.stringify(existingSchema, null, 2),
},
);
} else {
// When generating from scratch, use the full schema generation prompt
systemPrompt = SCHEMA_FROM_GOAL_PROMPT;
userContent = llmPrompt("schema-from-goal-user", formatForm(form));
}
// Send the request to the LLM using the specified model or default
const response = await new LLMClient().sendRequest({
model: model,
system: systemPrompt.text,
stream: false,
messages: [
{
role: "user",
content: userContent.text,
},
],
cache: form.meta.cache,
metadata: {
context: "workflow",
workflow: "spec-and-schema-gen",
generationId: form.meta.generationId,
systemPrompt: systemPrompt.version,
userPrompt: userContent.version,
space: form.meta.charmManager.getSpaceName(),
},
});
// Extract sections from the response
const title =
parseTagFromResponse(extractTextFromLLMResponse(response), "title") ||
"New Charm";
const description = parseTagFromResponse(
extractTextFromLLMResponse(response),
"description",
);
const spec = parseTagFromResponse(
extractTextFromLLMResponse(response),
"spec",
);
const plan = parseTagFromResponse(
extractTextFromLLMResponse(response),
"plan",
);
// If we have an existing schema, use it; otherwise parse the generated schema
let resultSchema: JSONSchemaMutable;
let argumentSchema: JSONSchemaMutable;
try {
const resultSchemaJson = parseTagFromResponse(
extractTextFromLLMResponse(response),
"result_schema",
);
resultSchema = resultSchemaJson ? JSON.parse(resultSchemaJson) : {};
} catch (error) {
console.warn("Error parsing schema:", error);
// Fallback to an empty schema
resultSchema = {};
}
try {
const argumentSchemaJson = parseTagFromResponse(
extractTextFromLLMResponse(response),
"argument_schema",
);
argumentSchema = argumentSchemaJson ? JSON.parse(argumentSchemaJson) : {};
} catch (error) {
console.warn("Error parsing schema:", error);
// Fallback to an empty schema
argumentSchema = {};
}
if (!argumentSchema && resultSchema) {
// HACK(bf): for iframes, this is ok, it will not last forever
argumentSchema = resultSchema;
}
// Add title and description to schema
argumentSchema.title = title;
argumentSchema.description = description;
return {
spec,
resultSchema,
title,
description,
argumentSchema,
plan,
};
}
/**
* Generates a complete specification, schema, and plan from a goal.
* @param goal The user's goal or request
* @param existingSchema Optional existing schema to use as a basis
* @param model Optional model identifier to use (defaults to claude-sonnet-4-5)
* @returns Object containing title, description, specification, schema
*/
export async function generateSpecAndSchemaAndCode(
form: WorkflowForm,
existingSchema?: JSONSchema,
model: string = DEFAULT_MODEL_NAME,
): Promise<{
spec: string;
plan: string;
title: string;
description: string;
resultSchema: JSONSchema;
argumentSchema: JSONSchema;
}> {
let systemPrompt, userContent;
if (!form.plan) {
throw new Error("Plan is required");
}
if (existingSchema && Object.keys(existingSchema).length > 0) {
// When we have an existing schema, focus on generating specification
systemPrompt = SPEC_FROM_SCHEMA_PROMPT;
userContent = hydratePrompt(
llmPrompt(
"spec-and-code-from-schema-user",
`
{{FORM}}
\`\`\`json
{{EXISTING_SCHEMA}}
\`\`\`
Based on this goal and the existing schema, please provide a title, description, any additional schema, detailed specification, and implementation plan.
`,
),
{
FORM: formatForm(form),
EXISTING_SCHEMA: JSON.stringify(existingSchema, null, 2),
},
);
} else {
// When generating from scratch, use the full schema generation prompt
systemPrompt = SCHEMA_FROM_GOAL_PROMPT;
userContent = llmPrompt("schema-from-goal-user", formatForm(form));
}
// Send the request to the LLM using the specified model or default
const response = await new LLMClient().sendRequest({
model: model,
system: systemPrompt.text,
stream: false,
messages: [
{
role: "user",
content: userContent.text,
},
],
metadata: {
context: "workflow",
workflow: "spec-and-schema-gen",
generationId: form.meta.generationId,
systemPrompt: systemPrompt.version,
userPrompt: userContent.version,
space: form.meta.charmManager.getSpaceName(),
},
cache: form.meta.cache,
});
// Extract sections from the response
const title =
parseTagFromResponse(extractTextFromLLMResponse(response), "title") ||
"New Charm";
const description = parseTagFromResponse(
extractTextFromLLMResponse(response),
"description",
);
const spec = parseTagFromResponse(
extractTextFromLLMResponse(response),
"spec",
);
const plan = parseTagFromResponse(
extractTextFromLLMResponse(response),
"plan",
);
// If we have an existing schema, use it; otherwise parse the generated schema
let resultSchema: JSONSchemaMutable;
let argumentSchema: JSONSchemaMutable;
try {
const resultSchemaJson = parseTagFromResponse(
extractTextFromLLMResponse(response),
"result_schema",
);
resultSchema = resultSchemaJson ? JSON.parse(resultSchemaJson) : {};
} catch (error) {
console.warn("Error parsing schema:", error);
// Fallback to an empty schema
resultSchema = {};
}
try {
const argumentSchemaJson = parseTagFromResponse(
extractTextFromLLMResponse(response),
"argument_schema",
);
argumentSchema = argumentSchemaJson ? JSON.parse(argumentSchemaJson) : {};
} catch (error) {
console.warn("Error parsing schema:", error);
// Fallback to an empty schema
argumentSchema = {};
}
// Add title and description to schema
argumentSchema.title = title;
argumentSchema.description = description;
return {
spec,
resultSchema,
title,
description,
argumentSchema,
plan,
};
}