@ryanfw/prompt-orchestration-pipeline 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/api/validators/json.js +3 -1
- package/src/core/validation.js +3 -1
- package/src/providers/base.js +3 -3
- package/src/task-analysis/enrichers/schema-deducer.js +7 -1
- package/src/ui/endpoints/task-creation-endpoint.js +26 -0
- package/src/ui/endpoints/task-save-endpoint.js +19 -1
- package/src/ui/lib/mention-parser.js +24 -0
- package/src/ui/lib/schema-loader.js +69 -0
- package/src/ui/lib/task-reviewer.js +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server.js",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
46
46
|
"@radix-ui/themes": "^3.2.1",
|
|
47
47
|
"ajv": "^8.17.1",
|
|
48
|
+
"ajv-formats": "^3.0.1",
|
|
48
49
|
"better-sqlite3": "^11.7.0",
|
|
49
50
|
"chokidar": "^3.5.3",
|
|
50
51
|
"commander": "^14.0.2",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
2
3
|
|
|
3
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
4
|
+
const ajv = new Ajv({ allErrors: true, strict: false, strictFormats: false });
|
|
5
|
+
addFormats(ajv);
|
|
4
6
|
|
|
5
7
|
export const validateWithSchema = (schema, data) => {
|
|
6
8
|
let parsedData = data;
|
package/src/core/validation.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
2
3
|
import { getConfig } from "./config.js";
|
|
3
4
|
|
|
4
|
-
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
const ajv = new Ajv({ allErrors: true, strictFormats: false });
|
|
6
|
+
addFormats(ajv);
|
|
5
7
|
|
|
6
8
|
// JSON schema for seed file structure - uses config for validation rules
|
|
7
9
|
function getSeedSchema() {
|
package/src/providers/base.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function sleep(ms) {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Strip markdown code fences from text unconditionally.
|
|
38
|
-
* Handles ```json, ```
|
|
38
|
+
* Handles any language identifier (```json, ```javascript, etc.) or plain ```.
|
|
39
39
|
* @param {string} text - The text to strip fences from
|
|
40
40
|
* @returns {string} The cleaned text, or original if not a string
|
|
41
41
|
*/
|
|
@@ -43,8 +43,8 @@ export function stripMarkdownFences(text) {
|
|
|
43
43
|
if (typeof text !== "string") return text;
|
|
44
44
|
const trimmed = text.trim();
|
|
45
45
|
if (trimmed.startsWith("```")) {
|
|
46
|
-
// Remove opening fence
|
|
47
|
-
let cleaned = trimmed.replace(/^```
|
|
46
|
+
// Remove opening fence with any language identifier
|
|
47
|
+
let cleaned = trimmed.replace(/^```[a-zA-Z]*\s*\n?/, "");
|
|
48
48
|
// Remove closing fence
|
|
49
49
|
cleaned = cleaned.replace(/\n?```\s*$/, "");
|
|
50
50
|
return cleaned.trim();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { chat } from "../../llm/index.js";
|
|
2
2
|
import Ajv from "ajv";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
3
4
|
|
|
4
|
-
const ajv = new Ajv();
|
|
5
|
+
const ajv = new Ajv({ strictFormats: false });
|
|
6
|
+
addFormats(ajv);
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Deduce JSON schema for an artifact using LLM with structured output.
|
|
@@ -52,6 +54,10 @@ export async function deduceArtifactSchema(taskCode, artifact) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
// Validate the generated example against the generated schema
|
|
57
|
+
// Remove any existing schema with the same $id to avoid "schema already exists" error
|
|
58
|
+
if (schema.$id && ajv.getSchema(schema.$id)) {
|
|
59
|
+
ajv.removeSchema(schema.$id);
|
|
60
|
+
}
|
|
55
61
|
const validate = ajv.compile(schema);
|
|
56
62
|
if (!validate(example)) {
|
|
57
63
|
throw new Error(
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { streamSSE } from "../lib/sse.js";
|
|
3
3
|
import { createHighLevelLLM } from "../../llm/index.js";
|
|
4
|
+
import { parseMentions } from "../lib/mention-parser.js";
|
|
5
|
+
import {
|
|
6
|
+
loadSchemaContext,
|
|
7
|
+
buildSchemaPromptSection,
|
|
8
|
+
} from "../lib/schema-loader.js";
|
|
4
9
|
|
|
5
10
|
export async function handleTaskPlan(req, res) {
|
|
6
11
|
console.log("[task-creation-endpoint] Request received");
|
|
@@ -36,10 +41,31 @@ export async function handleTaskPlan(req, res) {
|
|
|
36
41
|
guidelines.length
|
|
37
42
|
);
|
|
38
43
|
|
|
44
|
+
// Parse @mentions and load schema contexts for enrichment
|
|
45
|
+
const mentionedFiles = parseMentions(messages);
|
|
46
|
+
const schemaContexts = [];
|
|
47
|
+
// Load schema contexts sequentially to avoid unbounded concurrent file I/O
|
|
48
|
+
for (const fileName of mentionedFiles) {
|
|
49
|
+
// eslint-disable-next-line no-await-in-loop
|
|
50
|
+
const context = await loadSchemaContext(pipelineSlug, fileName);
|
|
51
|
+
if (context) {
|
|
52
|
+
schemaContexts.push(context);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const schemaEnrichment = buildSchemaPromptSection(schemaContexts);
|
|
56
|
+
|
|
57
|
+
if (schemaEnrichment) {
|
|
58
|
+
console.log(
|
|
59
|
+
"[task-creation-endpoint] Schema enrichment added for:",
|
|
60
|
+
mentionedFiles
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
// Build LLM messages array
|
|
40
65
|
const systemPrompt = `You are a pipeline task assistant. Help users create task definitions following these guidelines:
|
|
41
66
|
|
|
42
67
|
${guidelines}
|
|
68
|
+
${schemaEnrichment ? `\n${schemaEnrichment}\n` : ""}
|
|
43
69
|
|
|
44
70
|
Provide complete, working code. Use markdown code blocks.
|
|
45
71
|
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { getConfig } from "../../core/config.js";
|
|
4
4
|
import { sendJson } from "../utils/http-utils.js";
|
|
5
|
+
import { reviewAndCorrectTask } from "../lib/task-reviewer.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Handle task creation requests
|
|
@@ -52,7 +53,24 @@ export async function handleTaskSave(req, res) {
|
|
|
52
53
|
if (!taskFilePath.startsWith(tasksDir)) {
|
|
53
54
|
return sendJson(res, 400, { error: "Invalid filename" });
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
// Self-correct code before saving
|
|
58
|
+
let finalCode = code;
|
|
59
|
+
try {
|
|
60
|
+
const guidelinesPath = path.join(
|
|
61
|
+
rootDir,
|
|
62
|
+
"docs/pipeline-task-guidelines.md"
|
|
63
|
+
);
|
|
64
|
+
const guidelines = await fs.readFile(guidelinesPath, "utf8");
|
|
65
|
+
finalCode = await reviewAndCorrectTask(code, guidelines);
|
|
66
|
+
} catch (reviewError) {
|
|
67
|
+
console.warn(
|
|
68
|
+
"Task review failed, using original code:",
|
|
69
|
+
reviewError.message
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await fs.writeFile(taskFilePath, finalCode, "utf8");
|
|
56
74
|
|
|
57
75
|
// Update index.js to export new task
|
|
58
76
|
const indexPath = taskRegistryPath;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse @[display](id) mentions from chat messages.
|
|
3
|
+
* Used to extract referenced artifact files for schema enrichment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MENTION_REGEX = /@\[([^\]]+)\]\(([^)]+)\)/g;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract unique filenames from @mentions in messages.
|
|
10
|
+
* @param {Array<{ role: string, content: string }>} messages
|
|
11
|
+
* @returns {string[]} Array of unique filenames
|
|
12
|
+
*/
|
|
13
|
+
export function parseMentions(messages) {
|
|
14
|
+
const filenames = new Set();
|
|
15
|
+
|
|
16
|
+
for (const msg of messages) {
|
|
17
|
+
if (!msg.content) continue;
|
|
18
|
+
for (const match of msg.content.matchAll(MENTION_REGEX)) {
|
|
19
|
+
filenames.add(match[2]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [...filenames];
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema loader utility for task creation prompt enrichment.
|
|
3
|
+
* Loads JSON Schema, sample data, and metadata for referenced artifact files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getPipelineConfig } from "../../core/config.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load schema context for a referenced artifact file.
|
|
12
|
+
* @param {string} pipelineSlug - Pipeline identifier
|
|
13
|
+
* @param {string} fileName - Artifact filename (e.g., "analysis-output.json")
|
|
14
|
+
* @returns {Promise<{ fileName: string, schema: object, sample: object, meta?: object } | null>}
|
|
15
|
+
*/
|
|
16
|
+
export async function loadSchemaContext(pipelineSlug, fileName) {
|
|
17
|
+
try {
|
|
18
|
+
const pipelineConfig = getPipelineConfig(pipelineSlug);
|
|
19
|
+
const pipelineDir = path.dirname(pipelineConfig.pipelineJsonPath);
|
|
20
|
+
const baseName = path.parse(fileName).name;
|
|
21
|
+
const schemasDir = path.join(pipelineDir, "schemas");
|
|
22
|
+
|
|
23
|
+
const schemaPath = path.join(schemasDir, `${baseName}.schema.json`);
|
|
24
|
+
const samplePath = path.join(schemasDir, `${baseName}.sample.json`);
|
|
25
|
+
const metaPath = path.join(schemasDir, `${baseName}.meta.json`);
|
|
26
|
+
|
|
27
|
+
// Schema is required - return null if missing
|
|
28
|
+
const schemaContent = await fs.readFile(schemaPath, "utf8");
|
|
29
|
+
const schema = JSON.parse(schemaContent);
|
|
30
|
+
|
|
31
|
+
// Sample is required - return null if missing
|
|
32
|
+
const sampleContent = await fs.readFile(samplePath, "utf8");
|
|
33
|
+
const sample = JSON.parse(sampleContent);
|
|
34
|
+
|
|
35
|
+
// Meta is optional
|
|
36
|
+
let meta;
|
|
37
|
+
try {
|
|
38
|
+
const metaContent = await fs.readFile(metaPath, "utf8");
|
|
39
|
+
meta = JSON.parse(metaContent);
|
|
40
|
+
} catch {
|
|
41
|
+
// Meta file missing or invalid - that's fine
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { fileName, schema, sample, meta };
|
|
45
|
+
} catch {
|
|
46
|
+
// Any error (pipeline not found, file missing, JSON parse error) -> return null
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build markdown prompt section from schema contexts.
|
|
53
|
+
* @param {Array<{ fileName: string, schema: object, sample: object, meta?: object }>} contexts
|
|
54
|
+
* @returns {string} Markdown formatted section for system prompt
|
|
55
|
+
*/
|
|
56
|
+
export function buildSchemaPromptSection(contexts) {
|
|
57
|
+
if (!contexts || contexts.length === 0) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sections = contexts.map((ctx) => {
|
|
62
|
+
let section = `### @${ctx.fileName}\n\n`;
|
|
63
|
+
section += `**JSON Schema:**\n\n\`\`\`json\n${JSON.stringify(ctx.schema, null, 2)}\n\`\`\`\n\n`;
|
|
64
|
+
section += `**Sample Data:**\n\n\`\`\`json\n${JSON.stringify(ctx.sample, null, 2)}\n\`\`\``;
|
|
65
|
+
return section;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return `## Referenced Files\n\n${sections.join("\n\n")}`;
|
|
69
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHighLevelLLM } from "../../llm/index.js";
|
|
2
|
+
import { stripMarkdownFences } from "../../providers/base.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Review and correct task code using LLM
|
|
6
|
+
* @param {string} code - The task code to review
|
|
7
|
+
* @param {string} guidelines - Pipeline task guidelines
|
|
8
|
+
* @returns {Promise<string>} - Returns the original code if the LLM responds with
|
|
9
|
+
* NO_CHANGES_NEEDED; otherwise returns the LLM's corrected code output (after
|
|
10
|
+
* markdown fence stripping), which may be empty or invalid if the LLM response
|
|
11
|
+
* or formatting is unexpected.
|
|
12
|
+
*/
|
|
13
|
+
export async function reviewAndCorrectTask(code, guidelines) {
|
|
14
|
+
const llm = createHighLevelLLM();
|
|
15
|
+
|
|
16
|
+
const prompt = `Review this pipeline task code for:
|
|
17
|
+
1. JavaScript syntax errors
|
|
18
|
+
2. Logic flaws or bugs
|
|
19
|
+
3. Violations of the pipeline task guidelines below
|
|
20
|
+
4. Missing error handling for io/llm operations
|
|
21
|
+
|
|
22
|
+
If the code is correct, respond with exactly: NO_CHANGES_NEEDED
|
|
23
|
+
|
|
24
|
+
If corrections are needed, respond with only the corrected code (no explanation).
|
|
25
|
+
|
|
26
|
+
## Guidelines
|
|
27
|
+
|
|
28
|
+
${guidelines}
|
|
29
|
+
|
|
30
|
+
## Code to Review
|
|
31
|
+
|
|
32
|
+
\`\`\`javascript
|
|
33
|
+
${code}
|
|
34
|
+
\`\`\``;
|
|
35
|
+
|
|
36
|
+
const messages = [{ role: "user", content: prompt }];
|
|
37
|
+
|
|
38
|
+
const response = await llm.chat({ messages, responseFormat: "text" });
|
|
39
|
+
const content = response.content || "";
|
|
40
|
+
const trimmedContent = content.trim();
|
|
41
|
+
|
|
42
|
+
// If the LLM returned no usable content, keep the original code
|
|
43
|
+
if (!trimmedContent) {
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (trimmedContent.includes("NO_CHANGES_NEEDED")) {
|
|
48
|
+
return code;
|
|
49
|
+
}
|
|
50
|
+
return stripMarkdownFences(content);
|
|
51
|
+
}
|