UNPKG

prompt-version-manager

Version:

Centralized prompt management system for Human Behavior AI agents

550 lines (535 loc) 20.9 kB
"use strict"; /** * Core versioning operations for PVM TypeScript. */ 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.VersioningEngine = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const uuid_1 = require("uuid"); const engine_1 = require("../storage/engine"); const objects_1 = require("../storage/objects"); const exceptions_1 = require("../core/exceptions"); class VersioningEngine { storage; repoPath; constructor(repoPath = '.pvm') { this.repoPath = repoPath; this.storage = new engine_1.StorageEngine(repoPath); } /** * Initialize a new PVM repository. */ async init() { try { // Create repository directory await fs.mkdir(this.repoPath, { recursive: true }); // Initialize storage engine directories explicitly await fs.mkdir(path.join(this.repoPath, 'objects'), { recursive: true }); await fs.mkdir(path.join(this.repoPath, 'refs'), { recursive: true }); await fs.mkdir(path.join(this.repoPath, 'refs', 'heads'), { recursive: true }); await fs.mkdir(path.join(this.repoPath, 'refs', 'tags'), { recursive: true }); // Create prompts directory for tracking prompt templates const promptsDir = path.join(this.repoPath, 'prompts'); await fs.mkdir(promptsDir, { recursive: true }); // Create README for the prompts directory const readmePath = path.join(promptsDir, 'README.md'); try { await fs.access(readmePath); } catch { const readmeContent = `# Prompts Directory This directory contains all prompt templates tracked by PVM. ## Structure - Each \`.md\` file is a prompt template - Templates use YAML frontmatter for metadata - Variables are denoted with \`{{variable_name}}\` ## Example Template \`\`\`markdown --- id: example-prompt version: 1.0.0 variables: [name, task] --- You are a helpful assistant. Help {{name}} with {{task}}. \`\`\` `; await fs.writeFile(readmePath, readmeContent); } // Create example template.md const templatePath = path.join(promptsDir, 'template.md'); try { await fs.access(templatePath); } catch { const templateContent = `--- version: 1.0.0 description: Example template demonstrating PVM's simplified API author: PVM tags: [example, demo] --- You are a {{role}} assistant with expertise in {{domain}}. Your task is to {{task}}. Please provide a response that is {{style}} and focuses on practical solutions. `; await fs.writeFile(templatePath, templateContent); } // Create example.ts in the parent directory const exampleTsPath = path.join(path.dirname(this.repoPath), 'example.ts'); try { await fs.access(exampleTsPath); } catch { const exampleTsContent = `/** * Example script demonstrating PVM's simplified API in TypeScript. * This file was auto-generated by 'pvm init'. */ import { prompts, model } from '@hb/prompt-version-control'; async function main() { // Load and render the template with variables const prompt = prompts("template.md", [ "helpful AI", // {{role}} "TypeScript", // {{domain}} "explain interfaces", // {{task}} "beginner-friendly" // {{style}} ]); console.log("Rendered prompt:"); console.log("-".repeat(50)); console.log(prompt); console.log("-".repeat(50)); // Example: Execute with a model (requires API key) // Uncomment the following lines and set your API key to run: // process.env.OPENAI_API_KEY = "your-api-key-here"; // // const response = await model.complete( // "gpt-3.5-turbo", // prompt, // "example-task" // ); // // console.log("\\nModel response:"); // console.log(response.content); // console.log(\`\\nTokens used: \${response.metadata.tokens.total}\`); } // Run the example main().catch(console.error); `; await fs.writeFile(exampleTsPath, exampleTsContent); } // Just set up the initial refs structure like Git await this.storage.storeRef('HEAD', 'refs/heads/main'); console.log(`Initialized empty PVM repository in ${this.repoPath}`); } catch (error) { throw new exceptions_1.StorageError(`Failed to initialize repository: ${error}`); } } /** * Check if the current directory is a PVM repository. */ async isRepo() { try { await fs.access(this.repoPath); await fs.access(path.join(this.repoPath, 'objects')); await fs.access(path.join(this.repoPath, 'refs')); // Also check if HEAD exists (indicating proper initialization) try { await this.storage.loadRef('HEAD'); return true; } catch { return false; } } catch { return false; } } /** * Add a prompt to the staging area. */ async add(content, messages, modelConfig = {}, metadata = {}, tags = []) { if (!(await this.isRepo())) { throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.'); } try { // Create prompt version const promptVersion = { id: (0, uuid_1.v4)(), hash: '', content, messages, modelConfig, metadata, parentId: null, createdAt: new Date(), tags }; // Store prompt version object const promptObj = new objects_1.PromptVersionObject(promptVersion); const promptHash = await this.storage.storeObject(promptObj); // Update the prompt version with the actual hash promptVersion.hash = promptHash; // Also store content as blob for easy access const blobObj = new objects_1.BlobObject(content); await this.storage.storeObject(blobObj); // Add to staging area (stored as individual refs like Python) const tag = tags.length > 0 ? tags[0] : 'unnamed'; await this.addToStaging(promptHash, tag); console.log(`Added prompt version ${promptHash.substring(0, 8)}`); return promptHash; } catch (error) { throw new exceptions_1.StorageError(`Failed to add prompt: ${error}`); } } /** * Commit staged changes. */ async commit(message, author = 'PVM User', metadata = {}) { if (!(await this.isRepo())) { throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.'); } try { // Get staged prompt versions const stagedPrompts = await this.getStagingRefs(); if (stagedPrompts.length === 0) { throw new exceptions_1.InvalidCommitError('No changes staged for commit'); } // Get current HEAD commit let parentHashes = []; try { const headRef = await this.storage.loadRef('HEAD'); if (headRef.startsWith('refs/')) { // HEAD points to a branch ref, resolve it to get the commit hash const currentCommit = await this.storage.loadRef(headRef); parentHashes = [currentCommit]; } else { // HEAD points directly to a commit parentHashes = [headRef]; } } catch (error) { // No previous commits (initial commit) } // Create tree object from staged prompts (Git-like structure) const treeEntries = {}; const stagedPromptsInfo = await this.getStagedPrompts(); for (let i = 0; i < stagedPromptsInfo.length; i++) { const [promptHash, tag] = stagedPromptsInfo[i]; const filename = tag !== 'unnamed' ? `${tag}_${i}.prompt` : `prompt_${i}.prompt`; treeEntries[filename] = promptHash; } const treeObj = new objects_1.TreeObject(treeEntries); const treeHash = await this.storage.storeObject(treeObj); // Create commit object const commit = { hash: '', message, author, timestamp: new Date(), parentHashes, treeHash, promptVersions: stagedPrompts, // Keep for backward compatibility metadata }; const commitObj = new objects_1.CommitObject(commit); const commitHash = await this.storage.storeObject(commitObj); // Update the commit object with the actual hash commit.hash = commitHash; // Update current branch to point to new commit try { const headRef = await this.storage.loadRef('HEAD'); if (headRef.startsWith('refs/')) { // HEAD points to a branch, update that branch await this.storage.storeRef(headRef, commitHash, 'branch'); } else { // HEAD points directly to a commit (detached HEAD) await this.storage.storeRef('HEAD', commitHash, 'head'); } } catch (error) { // First commit, create main branch await this.storage.storeRef('refs/heads/main', commitHash, 'branch'); await this.storage.storeRef('HEAD', 'refs/heads/main'); } // Clear staging area await this.clearStaging(); console.log(`Committed ${stagedPrompts.length} prompt version(s): ${commitHash.substring(0, 8)}`); return commitHash; } catch (error) { if (error instanceof exceptions_1.InvalidCommitError) { throw error; } throw new exceptions_1.StorageError(`Failed to commit: ${error}`); } } /** * Show the difference between commits or working directory. */ async diff(commitHash1, commitHash2) { if (!(await this.isRepo())) { throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.'); } try { let commit1Prompts = []; let commit2Prompts = []; // If no commits specified, compare staging with HEAD if (!commitHash1 && !commitHash2) { // Get what's currently staged (only new additions) const stagingRefs = await this.getStagingRefs(); // Get what's in HEAD try { const headHash = await this.storage.loadRef('HEAD'); const headCommit = await this.storage.loadObject(headHash); headCommit.commit.hash = headHash; // Set hash from storage key commit1Prompts = headCommit.commit.promptVersions; } catch { // No previous commits commit1Prompts = []; } // For staging vs HEAD, only show what's staged as "added" // Everything in HEAD is conceptually still there commit2Prompts = [...commit1Prompts, ...stagingRefs]; } else if (commitHash1 && !commitHash2) { // Compare commit with HEAD const commit1 = await this.storage.loadObject(commitHash1); commit1.commit.hash = commitHash1; // Set hash from storage key commit1Prompts = commit1.commit.promptVersions; try { const headHash = await this.storage.loadRef('HEAD'); const headCommit = await this.storage.loadObject(headHash); headCommit.commit.hash = headHash; // Set hash from storage key commit2Prompts = headCommit.commit.promptVersions; } catch { // No HEAD commit } } else if (commitHash1 && commitHash2) { // Compare two specific commits const commit1 = await this.storage.loadObject(commitHash1); const commit2 = await this.storage.loadObject(commitHash2); commit1.commit.hash = commitHash1; // Set hash from storage key commit2.commit.hash = commitHash2; // Set hash from storage key commit1Prompts = commit1.commit.promptVersions; commit2Prompts = commit2.commit.promptVersions; } // Calculate differences const set1 = new Set(commit1Prompts); const set2 = new Set(commit2Prompts); const added = Array.from(set2).filter(hash => !set1.has(hash)); const removed = Array.from(set1).filter(hash => !set2.has(hash)); const modified = []; // For now, we don't track modifications // Get details for each change const details = []; for (const hash of added) { try { const promptObj = await this.storage.loadObject(hash); details.push({ hash, status: 'added', content: promptObj.promptVersion.content, messages: promptObj.promptVersion.messages, }); } catch (error) { details.push({ hash, status: 'added', }); } } for (const hash of removed) { try { const promptObj = await this.storage.loadObject(hash); details.push({ hash, status: 'removed', content: promptObj.promptVersion.content, messages: promptObj.promptVersion.messages, }); } catch (error) { details.push({ hash, status: 'removed', }); } } return { added, removed, modified, details }; } catch (error) { throw new exceptions_1.StorageError(`Failed to calculate diff: ${error}`); } } /** * Show commit history. */ async log(limit = 10) { if (!(await this.isRepo())) { return []; // Return empty array for non-repos instead of throwing } try { const commits = []; const visited = new Set(); // Start from HEAD - resolve HEAD to actual commit hash let currentHash; try { const headRef = await this.storage.loadRef('HEAD'); if (headRef.startsWith('refs/')) { // HEAD points to a branch ref, resolve it to get the commit hash currentHash = await this.storage.loadRef(headRef); } else { // HEAD points directly to a commit currentHash = headRef; } } catch { return []; // No commits yet } // Traverse commit history while (commits.length < limit && currentHash && !visited.has(currentHash)) { visited.add(currentHash); const commitObj = await this.storage.loadObject(currentHash); // Set the hash from the storage key (Git-like behavior) commitObj.commit.hash = currentHash; commits.push(commitObj.commit); // Move to parent (we only follow first parent for now) if (commitObj.commit.parentHashes.length > 0) { currentHash = commitObj.commit.parentHashes[0]; } else { break; } } return commits; } catch (error) { throw new exceptions_1.StorageError(`Failed to get log: ${error}`); } } /** * Get the current branch name. */ async getCurrentBranch() { // For now, we'll just return 'main' as the default branch // In the future, we could implement proper branch tracking return 'main'; } /** * Add a prompt to the staging area (Python-compatible approach). */ async addToStaging(promptHash, tag) { const stagingRef = `staging/${tag}_${promptHash.substring(0, 8)}`; await this.storage.storeRef(stagingRef, promptHash, 'staging'); } /** * Get all staged prompts (Python-compatible approach). */ async getStagedPrompts() { const refs = await this.storage.listRefs(); const staged = []; for (const ref of refs) { if (ref.name.startsWith('staging/')) { const tag = ref.name.substring(8).split('_')[0]; // Extract tag from ref name staged.push([ref.hash, tag]); } } return staged; } /** * Get staging area references (legacy method for compatibility). */ async getStagingRefs() { const staged = await this.getStagedPrompts(); return staged.map(([hash, _tag]) => hash); } /** * Get repository status. */ async status() { if (!(await this.isRepo())) { throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.'); } try { const currentBranch = await this.getCurrentBranch(); const stagedChanges = (await this.getStagingRefs()).length; let lastCommit; try { lastCommit = await this.storage.loadRef('HEAD'); } catch { // No commits yet } const stats = await this.storage.getStats(); return { currentBranch, stagedChanges, lastCommit: lastCommit?.substring(0, 8), stats, }; } catch (error) { throw new exceptions_1.StorageError(`Failed to get status: ${error}`); } } /** * Clear the staging area (Python-compatible approach). */ async clearStaging() { const refs = await this.storage.listRefs(); for (const ref of refs) { if (ref.name.startsWith('staging/')) { await this.storage.deleteRef(ref.name); } } } /** * Close the versioning engine. */ async close() { await this.storage.close(); } } exports.VersioningEngine = VersioningEngine; //# sourceMappingURL=operations.js.map