UNPKG

prompt-version-manager

Version:

Centralized prompt management system for Human Behavior AI agents

608 lines (579 loc) 20 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DashboardServer = void 0; const express_1 = __importDefault(require("express")); const ws_1 = require("ws"); const versioning_1 = require("../core/versioning"); const manager_1 = require("../chains/manager"); const tracker_1 = require("../chains/tracker"); const path = __importStar(require("path")); const fs = __importStar(require("fs/promises")); const marked_1 = require("marked"); class DashboardServer { app; wss; versioning; chainManager; chainTracker; port; constructor(repoPath, port = 3000) { this.app = (0, express_1.default)(); this.port = port; this.versioning = new versioning_1.VersioningOperations(repoPath); this.chainManager = new manager_1.ChainManager(repoPath); this.chainTracker = new tracker_1.ChainTracker(repoPath); this.setupRoutes(); } setupRoutes() { // Configure marked for better rendering marked_1.marked.setOptions({ breaks: true, gfm: true, headerIds: true, headerPrefix: '', }); // Serve static files this.app.use(express_1.default.static(path.join(__dirname))); this.app.use('/docs', express_1.default.static(path.join(__dirname, '../../../docs'))); // Documentation routes this.setupDocsRoutes(); // API endpoints this.app.get('/api/dashboard', async (req, res) => { try { const data = await this.getDashboardData(); res.json(data); } catch (error) { res.status(500).json({ error: 'Failed to fetch dashboard data' }); } }); this.app.get('/api/commits', async (req, res) => { try { const commits = await this.versioning.log({ limit: 50 }); res.json(commits); } catch (error) { res.status(500).json({ error: 'Failed to fetch commits' }); } }); this.app.get('/api/commits/:hash/diff', async (req, res) => { try { const diff = await this.getCommitDiff(req.params.hash); res.json(diff); } catch (error) { res.status(500).json({ error: 'Failed to fetch commit diff' }); } }); this.app.get('/api/chains', async (req, res) => { try { const chains = await this.chainManager.listChains(); res.json(chains); } catch (error) { res.status(500).json({ error: 'Failed to fetch chains' }); } }); this.app.get('/api/chains/:id', async (req, res) => { try { const chain = await this.chainManager.getChain(req.params.id); const nodes = await this.chainManager.getChainNodes(req.params.id); res.json({ chain, nodes }); } catch (error) { res.status(500).json({ error: 'Failed to fetch chain details' }); } }); this.app.get('/api/metrics', async (req, res) => { try { const metrics = await this.getMetrics(); res.json(metrics); } catch (error) { res.status(500).json({ error: 'Failed to fetch metrics' }); } }); // Serve the dashboard HTML this.app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'web-dashboard.html')); }); } setupDocsRoutes() { const docFiles = { 'home': { file: 'dashboard-introduction.md', title: 'PVM Documentation', nav: 'Home' }, 'quick-start': { file: 'quick-start-guide.md', title: 'Quick Start Guide', nav: 'Quick Start' }, 'faq': { file: 'faq.md', title: 'Frequently Asked Questions', nav: 'FAQ' }, 'glossary': { file: 'glossary.md', title: 'Glossary', nav: 'Glossary' } }; const docsCSS = ` <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background: #fafafa; } .container { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .nav { background: #2c3e50; color: white; padding: 20px; border-radius: 8px 8px 0 0; margin: -40px -40px 40px -40px; position: relative; } .nav h1 { margin: 0; color: white; display: inline-block; } .nav-links { margin-top: 15px; } .nav-links a { color: #ecf0f1; text-decoration: none; margin-right: 20px; padding: 5px 10px; border-radius: 4px; transition: background 0.2s; } .nav-links a:hover { background: rgba(255,255,255,0.1); } .nav-links a.active { background: #3498db; } .nav-toggle { position: absolute; right: 20px; top: 20px; background: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 14px; } .nav-toggle:hover { background: #2980b9; } h1, h2, h3 { color: #2c3e50; } h1 { border-bottom: 3px solid #3498db; padding-bottom: 10px; } h2 { border-bottom: 1px solid #ecf0f1; padding-bottom: 5px; margin-top: 30px; } code { background: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-family: 'Monaco', 'Consolas', monospace; font-size: 0.9em; } pre { background: #2c3e50; color: #ecf0f1; padding: 20px; border-radius: 6px; overflow-x: auto; font-family: 'Monaco', 'Consolas', monospace; position: relative; } pre code { background: none; color: inherit; padding: 0; } blockquote { border-left: 4px solid #3498db; padding-left: 20px; margin-left: 0; color: #666; font-style: italic; } table { border-collapse: collapse; width: 100%; margin: 20px 0; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } th { background: #f8f9fa; font-weight: 600; } .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ecf0f1; color: #666; text-align: center; } @media (max-width: 768px) { body { padding: 10px; } .container { padding: 20px; } .nav { margin: -20px -20px 20px -20px; padding: 15px; } .nav-links a { display: block; margin: 5px 0; } } </style> `; const generateNav = (currentPage) => { return Object.keys(docFiles).map(key => { const doc = docFiles[key]; const activeClass = key === currentPage ? ' active' : ''; return `<a href="/docs/${key}"${activeClass}>${doc.nav}</a>`; }).join(''); }; const renderMarkdown = async (filePath, pageKey) => { try { const docsDir = path.join(__dirname, '../../../docs'); const markdown = await fs.readFile(path.join(docsDir, filePath), 'utf8'); const html = (0, marked_1.marked)(markdown); const docInfo = docFiles[pageKey]; return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${docInfo.title} - PVM</title> ${docsCSS} </head> <body> <div class="container"> <div class="nav"> <h1>PVM Documentation</h1> <a href="/" class="nav-toggle">← Back to Dashboard</a> <div class="nav-links"> ${generateNav(pageKey)} </div> </div> <div class="content"> ${html} </div> <div class="footer"> <p>PVM (Prompt Version Management) - Making AI prompt development better</p> <p>Generated on ${new Date().toLocaleString()}</p> </div> </div> <script> // Smooth scrolling for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth' }); } }); }); // Add copy buttons to code blocks document.querySelectorAll('pre code').forEach(block => { const button = document.createElement('button'); button.innerHTML = 'Copy'; button.style.cssText = 'position:absolute;top:10px;right:10px;background:#3498db;color:white;border:none;padding:5px 10px;border-radius:3px;cursor:pointer;font-size:12px;'; button.onclick = () => { navigator.clipboard.writeText(block.textContent); button.innerHTML = 'Copied!'; setTimeout(() => button.innerHTML = 'Copy', 2000); }; block.parentElement.style.position = 'relative'; block.parentElement.appendChild(button); }); </script> </body> </html>`; } catch (error) { return ` <!DOCTYPE html> <html> <head><title>Error - PVM Docs</title></head> <body> <h1>Error</h1> <p>Could not load documentation: ${error.message}</p> <a href="/">Back to Dashboard</a> </body> </html>`; } }; // Documentation routes this.app.get('/docs', (req, res) => { res.redirect('/docs/home'); }); this.app.get('/docs/home', async (req, res) => { const html = await renderMarkdown('dashboard-introduction.md', 'home'); res.send(html); }); Object.keys(docFiles).forEach(key => { if (key !== 'home') { this.app.get(`/docs/${key}`, async (req, res) => { const html = await renderMarkdown(docFiles[key].file, key); res.send(html); }); } }); // API endpoint for raw markdown this.app.get('/api/docs/:file', async (req, res) => { const filename = req.params.file; try { const docsDir = path.join(__dirname, '../../../docs'); const content = await fs.readFile(path.join(docsDir, filename), 'utf8'); res.type('text/markdown').send(content); } catch (error) { res.status(404).json({ error: 'File not found' }); } }); } async getDashboardData() { const [commits, chains, metrics] = await Promise.all([ this.versioning.log({ limit: 20 }), this.chainManager.listChains(), this.getMetrics() ]); // Get chain flow for the latest chain let chainFlow = { nodes: [], edges: [] }; if (chains.length > 0) { const latestChain = chains[chains.length - 1]; chainFlow = await this.getChainFlow(latestChain.id); } return { commits, chains, metrics, chainFlow }; } async getMetrics() { // Read analytics data if available const analyticsPath = process.env.ANALYTICS_EXPORT_PATH || './analytics-reports'; let totalRuns = 0; let successfulRuns = 0; let totalCost = 0; let totalTokens = 0; const costHistory = { labels: [], data: [] }; const providerTokens = {}; try { const files = await fs.readdir(analyticsPath); const analyticsFiles = files.filter(f => f.startsWith('market-research-analytics-')); for (const file of analyticsFiles.slice(-10)) { // Last 10 runs const content = await fs.readFile(path.join(analyticsPath, file), 'utf-8'); const data = JSON.parse(content); totalRuns++; if (data.pipelineMetrics?.successfulSteps === data.pipelineMetrics?.totalSteps) { successfulRuns++; } const runCost = data.pipelineMetrics?.totalCost || 0; totalCost += runCost; totalTokens += data.pipelineMetrics?.totalTokens?.total || 0; costHistory.labels.push(`Run ${totalRuns}`); costHistory.data.push(runCost); // Aggregate provider tokens if (data.pipelineMetrics?.providerBreakdown) { for (const [provider, stats] of Object.entries(data.pipelineMetrics.providerBreakdown)) { providerTokens[provider] = (providerTokens[provider] || 0) + stats.tokens.total; } } } } catch (error) { console.error('Failed to read analytics data:', error); } // Calculate provider distribution const totalProviderTokens = Object.values(providerTokens).reduce((a, b) => a + b, 0); const providerDistribution = { labels: Object.keys(providerTokens), data: Object.values(providerTokens).map(t => Math.round((t / totalProviderTokens) * 100)) }; return { totalRuns, successRate: totalRuns > 0 ? (successfulRuns / totalRuns) * 100 : 0, avgCost: totalRuns > 0 ? totalCost / totalRuns : 0, totalTokens, costHistory, tokenUsage: { labels: Object.keys(providerTokens), data: Object.values(providerTokens) }, providerDistribution }; } async getChainFlow(chainId) { const nodes = await this.chainManager.getChainNodes(chainId); const flowNodes = []; const flowEdges = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; flowNodes.push({ id: node.id, name: node.name || `Step ${i + 1}`, status: node.status, provider: node.provider || 'unknown', model: node.model || 'unknown', tokens: node.metrics?.tokens.total || 0, duration: node.metrics?.latency || 0 }); if (i > 0) { flowEdges.push({ from: nodes[i - 1].id, to: node.id, tokens: node.metrics?.tokens.input || 0 }); } } return { nodes: flowNodes, edges: flowEdges }; } async getCommitDiff(hash) { const commit = await this.versioning.show(hash); if (!commit.parent) { return { type: 'initial', content: 'Initial commit - no parent to compare' }; } try { const currentPrompt = await this.versioning.storage.getObject(commit.prompt); const parentCommit = await this.versioning.storage.getObject(commit.parent); const parentPrompt = await this.versioning.storage.getObject(parentCommit.prompt); return { type: 'diff', current: currentPrompt, parent: parentPrompt, changes: this.calculateChanges(parentPrompt, currentPrompt) }; } catch (error) { return { type: 'error', content: 'Failed to generate diff' }; } } calculateChanges(oldObj, newObj) { // Simple change detection const changes = { added: [], removed: [], modified: [] }; const oldStr = JSON.stringify(oldObj, null, 2); const newStr = JSON.stringify(newObj, null, 2); if (oldStr !== newStr) { changes.modified.push('Prompt content changed'); } return changes; } async start() { await this.versioning.init(); const server = this.app.listen(this.port, () => { console.log(`Dashboard server running at http://localhost:${this.port}`); }); // Setup WebSocket for real-time updates this.wss = new ws_1.WebSocketServer({ server }); this.wss.on('connection', (ws) => { console.log('Dashboard client connected'); // Send initial data this.getDashboardData().then(data => { ws.send(JSON.stringify({ type: 'initial', data })); }); // Setup periodic updates const interval = setInterval(async () => { try { const data = await this.getDashboardData(); ws.send(JSON.stringify({ type: 'update', data })); } catch (error) { console.error('Failed to send update:', error); } }, 5000); ws.on('close', () => { console.log('Dashboard client disconnected'); clearInterval(interval); }); }); } } exports.DashboardServer = DashboardServer; // CLI entry point if (require.main === module) { const server = new DashboardServer(process.env.PVM_REPO_PATH || './pvm-data', parseInt(process.env.DASHBOARD_PORT || '3000')); server.start().catch(console.error); } //# sourceMappingURL=dashboard-server.js.map