UNPKG

c9ai

Version:

Universal AI assistant with vibe-based workflows, hybrid cloud+local AI, and comprehensive tool integration

104 lines (93 loc) 3.93 kB
"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 };