prompt-version-manager
Version:
Centralized prompt management system for Human Behavior AI agents
608 lines (579 loc) • 20 kB
JavaScript
"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;
}
(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