c9ai
Version:
Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration
104 lines (93 loc) • 3.93 kB
JavaScript
"use strict";
const fs = require("node:fs/promises");
const path = require("node:path");
const { getCloudProvider } = require("../providers");
const TEMPLATES_DIR = path.join(__dirname, "templates");
function buildSystemPrompt() {
return (
"You are a senior workflow designer. " +
"Given a user goal, produce a single JSON workflow template only, no prose. " +
"The JSON must match this minimal schema and be executable locally by C9AI:\n" +
`{
"id": "kebab-case-id",
"name": "Title Case Name",
"description": "one line",
"vibe": { "energy": "string", "mood": "string", "duration": "NN-minutes", "tags": ["..."] },
"flow": [
{ "step": "kebab-name", "description": "what to do", "tools": ["web.search","rss.read","fs.write","shell.run"], "vibe": "phase", "estimatedTime": "NN-NNmin" }
],
"adaptations": { "quick-scan": { "description": "...", "modifications": ["..."] } },
"triggers": { "time": ["morning"], "energy": ["focused"], "context": ["quiet-space"] },
"credits": { "estimated": 10, "breakdown": { "web.search": 0, "ai.write": 5 } }
}`
);
}
function buildUserPrompt(name, goal) {
return (
`Goal: ${goal}\n` +
(name ? `Preferred Template Name: ${name}\n` : "") +
"Constraints:\n- Prefer mapped tools: web.search, rss.read, fs.write, fs.read, shell.run, youtube.search.\n" +
"- Steps should be concrete and locally executable.\n" +
"- Output valid JSON only."
);
}
function safeJsonParse(text) {
try { return { ok: true, data: JSON.parse(text) }; } catch (e) {
// try to extract json code block
const m = text.match(/```json\s*([\s\S]*?)```/i) || text.match(/```\s*([\s\S]*?)```/);
if (m) {
try { return { ok: true, data: JSON.parse(m[1]) }; } catch {}
}
// attempt to trim leading/trailing text
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start !== -1 && end !== -1 && end > start) {
try { return { ok: true, data: JSON.parse(text.slice(start, end + 1)) }; } catch {}
}
return { ok: false, error: e.message, raw: text };
}
}
function minimalValidate(t) {
const errors = [];
const req = (cond, msg) => { if (!cond) errors.push(msg); };
req(t && typeof t === 'object', 'template must be an object');
if (!t) return { ok: false, errors };
req(typeof t.id === 'string' && t.id.length > 2, 'id required');
req(typeof t.name === 'string' && t.name.length > 2, 'name required');
req(Array.isArray(t.flow) && t.flow.length > 0, 'flow must be non-empty');
if (Array.isArray(t.flow)) {
t.flow.forEach((s,i)=>{
req(typeof s.step === 'string', `flow[${i}].step required`);
req(Array.isArray(s.tools), `flow[${i}].tools array required`);
});
}
return { ok: errors.length === 0, errors };
}
async function saveTemplateJson(template) {
await fs.mkdir(TEMPLATES_DIR, { recursive: true });
const file = path.join(TEMPLATES_DIR, `${template.id}.json`);
await fs.writeFile(file, JSON.stringify(template, null, 2), 'utf8');
return file;
}
async function generateAndSaveWorkflow({ provider = 'claude', name, goal, id }) {
const cloud = getCloudProvider(provider);
const system = buildSystemPrompt();
const user = buildUserPrompt(name, goal);
const res = await cloud.call({ messages: [
{ role: 'system', content: system },
{ role: 'user', content: user }
], max_tokens: 1200, temperature: 0.4 });
const parsed = safeJsonParse(res.text || '');
if (!parsed.ok) {
throw new Error(`Provider returned non-JSON. Details: ${parsed.error || 'parse failed'}`);
}
const tpl = parsed.data;
if (id && (!tpl.id || tpl.id !== id)) tpl.id = id; // force id if provided
const v = minimalValidate(tpl);
if (!v.ok) {
throw new Error(`Generated workflow invalid: ${v.errors.join('; ')}`);
}
const file = await saveTemplateJson(tpl);
return { file, template: tpl };
}
module.exports = { generateAndSaveWorkflow };