gepa-spo
Version:
Genetic-Pareto prompt optimizer to evolve system prompts from a few rollouts with modular support and intelligent crossover
283 lines (282 loc) • 10.1 kB
JavaScript
/**
* Utility functions for handling modular candidates
*/
/**
* Check if a candidate is modular (has modules array)
*/
export function isModular(candidate) {
return candidate.modules !== undefined && candidate.modules.length > 0;
}
/**
* Check if a candidate is single-system (has system string)
*/
export function isSingleSystem(candidate) {
return candidate.system !== undefined;
}
/**
* Get the number of modules in a candidate
*/
export function getModuleCount(candidate) {
if (isModular(candidate)) {
return candidate.modules.length;
}
return 1; // Single system counts as 1 module
}
/**
* Get a specific module by index
*/
export function getModule(candidate, index) {
if (isModular(candidate)) {
return candidate.modules[index] || null;
}
if (isSingleSystem(candidate) && index === 0) {
return { id: 'system', prompt: candidate.system };
}
return null;
}
/**
* Set a specific module by index
*/
export function setModule(candidate, index, module) {
if (isModular(candidate)) {
const newModules = [...candidate.modules];
newModules[index] = module;
return { ...candidate, modules: newModules };
}
if (isSingleSystem(candidate) && index === 0) {
return { ...candidate, system: module.prompt };
}
throw new Error(`Cannot set module at index ${index} for candidate type`);
}
/**
* Concatenate modules into a single system prompt for backward compatibility
*/
export function concatenateModules(candidate) {
if (isSingleSystem(candidate)) {
return candidate.system;
}
if (isModular(candidate)) {
return candidate.modules.map(m => m.prompt).join('\n\n');
}
throw new Error('Candidate must have either system or modules');
}
/**
* Serialize a candidate to a string representation for storage
*/
export function serializeCandidate(candidate) {
if (isSingleSystem(candidate)) {
return candidate.system;
}
if (isModular(candidate)) {
return JSON.stringify({ modules: candidate.modules });
}
throw new Error('Candidate must have either system or modules');
}
/**
* Deserialize a candidate from a string representation
*/
export function deserializeCandidate(serialized) {
try {
const parsed = JSON.parse(serialized);
if (parsed.modules && Array.isArray(parsed.modules)) {
return { modules: parsed.modules };
}
}
catch {
// Not JSON, treat as single system
}
return { system: serialized };
}
/**
* Create a deep copy of a candidate
*/
export function cloneCandidate(candidate) {
if (isSingleSystem(candidate)) {
return { system: candidate.system };
}
if (isModular(candidate)) {
return { modules: candidate.modules.map(m => ({ ...m })) };
}
throw new Error('Candidate must have either system or modules');
}
/**
* Validate that a candidate has the expected structure
*/
export function validateCandidate(candidate) {
if (!isSingleSystem(candidate) && !isModular(candidate)) {
throw new Error('Candidate must have either system or modules');
}
if (isModular(candidate)) {
if (candidate.modules.length === 0) {
throw new Error('Modular candidate must have at least one module');
}
for (const module of candidate.modules) {
if (!module.id || !module.prompt) {
throw new Error('Each module must have id and prompt');
}
}
}
}
/**
* Merge two parent candidates to create a child candidate
* Uses module-level ancestry and scores to determine which modules to take from each parent
*/
export function mergeCandidates(parentA, parentB, lineageA, lineageB, scoreA, scoreB) {
// Check for incompatible structures first
if ((isSingleSystem(parentA) && isModular(parentB)) || (isModular(parentA) && isSingleSystem(parentB))) {
throw new Error('Cannot merge candidates with incompatible structures');
}
// Ensure both parents have the same module structure
const moduleCountA = getModuleCount(parentA);
const moduleCountB = getModuleCount(parentB);
if (moduleCountA !== moduleCountB) {
throw new Error('Cannot merge candidates with different module counts');
}
const moduleCount = moduleCountA;
// For single-system candidates, treat as 1 module
if (moduleCount === 1) {
// Simple case: take the better scoring parent's system
const betterParent = scoreA >= scoreB ? parentA : parentB;
return cloneCandidate(betterParent);
}
// For modular candidates, merge module by module
if (isModular(parentA) && isModular(parentB)) {
const mergedModules = [];
for (let moduleIndex = 0; moduleIndex < moduleCount; moduleIndex++) {
const changedInA = lineageA.includes(moduleIndex);
const changedInB = lineageB.includes(moduleIndex);
let selectedModule;
if (changedInA && !changedInB) {
// Only parent A changed this module - take from A
selectedModule = parentA.modules[moduleIndex];
}
else if (changedInB && !changedInA) {
// Only parent B changed this module - take from B
selectedModule = parentB.modules[moduleIndex];
}
else if (changedInA && changedInB) {
// Both parents changed this module - take from the better scoring parent
const betterParent = scoreA >= scoreB ? parentA : parentB;
selectedModule = betterParent.modules[moduleIndex];
}
else {
// Neither parent changed this module - take from parent A (default)
selectedModule = parentA.modules[moduleIndex];
}
mergedModules.push({ ...selectedModule });
}
return { modules: mergedModules };
}
throw new Error('Cannot merge candidates with incompatible structures');
}
/**
* Check if two candidates are direct ancestors/descendants
*/
export function areDirectRelatives(candidateIndexA, candidateIndexB, lineage) {
// Check if A is a direct ancestor of B
let current = candidateIndexB;
while (current !== undefined) {
const entry = lineage.find(l => l.candidateIndex === current);
if (!entry)
break;
if (entry.parentIndex === candidateIndexA)
return true;
current = entry.parentIndex;
}
// Check if B is a direct ancestor of A
current = candidateIndexA;
while (current !== undefined) {
const entry = lineage.find(l => l.candidateIndex === current);
if (!entry)
break;
if (entry.parentIndex === candidateIndexB)
return true;
current = entry.parentIndex;
}
return false;
}
/**
* Check if a merge triplet (parentA, parentB, ancestor) has been tried before
*/
export function hasBeenTriedBefore(parentAIndex, parentBIndex, ancestorIndex, triedTriplets) {
// Check both orderings since merge is symmetric
return triedTriplets.some(([a, b, anc]) => (a === parentAIndex && b === parentBIndex && anc === ancestorIndex) ||
(a === parentBIndex && b === parentAIndex && anc === ancestorIndex));
}
/**
* Find a shared ancestor between two candidates
*/
export function findSharedAncestor(candidateIndexA, candidateIndexB, lineage) {
// Get all ancestors of A (including A itself)
const ancestorsA = new Set();
let current = candidateIndexA;
while (current !== undefined) {
ancestorsA.add(current);
const entry = lineage.find(l => l.candidateIndex === current);
if (!entry)
break;
if (entry.parentIndex !== undefined) {
current = entry.parentIndex;
}
else {
break;
}
}
// Check if any ancestor of B is also an ancestor of A
current = candidateIndexB;
while (current !== undefined) {
if (ancestorsA.has(current)) {
return current;
}
const entry = lineage.find(l => l.candidateIndex === current);
if (!entry)
break;
if (entry.parentIndex !== undefined) {
current = entry.parentIndex;
}
else {
break;
}
}
return null;
}
/**
* Check if a merged candidate introduces module-level novelty
*/
export function hasModuleNovelty(mergedCandidate, parentA, parentB, lineageA, lineageB) {
const moduleCount = getModuleCount(mergedCandidate);
// If merged candidate is identical to one of the parents, no novelty
if (JSON.stringify(mergedCandidate) === JSON.stringify(parentA) ||
JSON.stringify(mergedCandidate) === JSON.stringify(parentB)) {
return false;
}
for (let moduleIndex = 0; moduleIndex < moduleCount; moduleIndex++) {
const moduleA = getModule(parentA, moduleIndex);
const moduleB = getModule(parentB, moduleIndex);
const moduleMerged = getModule(mergedCandidate, moduleIndex);
if (!moduleA || !moduleB || !moduleMerged)
continue;
// Check if this module is different from both parents
const changedInA = lineageA.includes(moduleIndex);
const changedInB = lineageB.includes(moduleIndex);
if (changedInA && changedInB) {
// Both parents changed this module - check if merged is different from both
if (moduleMerged.prompt !== moduleA.prompt && moduleMerged.prompt !== moduleB.prompt) {
return true;
}
}
else if (changedInA && !changedInB) {
// Only A changed - check if merged is different from B
if (moduleMerged.prompt !== moduleB.prompt) {
return true;
}
}
else if (changedInB && !changedInA) {
// Only B changed - check if merged is different from A
if (moduleMerged.prompt !== moduleA.prompt) {
return true;
}
}
}
return false;
}