queries.js 11.9 KB
/**
 * EvolutionQueries - 进化系统数据查询层
 *
 * 使用方式:
 *   const { evolutionQueries, initQueries } = require('@mas/evolution');
 *   initQueries(knexInstance);
 */
let knex = null;

const RUNS = 'evolution_runs';
const GENOMES = 'skill_genomes';
const EVALUATIONS = 'fitness_evaluations';
const BENCHMARKS = 'evolution_benchmarks';
const LEARNING = 'learning_events';

function _knex(...args) {
  if (!knex) throw new Error('EvolutionQueries: knex 未初始化,请先调用 initQueries(knexInstance)');
  return knex(...args);
}
Object.defineProperty(_knex, 'fn', { get() { return knex.fn; } });

class EvolutionQueries {
  // ══════════════════════════════════════
  // Evolution Runs
  // ══════════════════════════════════════

  async createRun(skillName, config = {}) {
    const [row] = await _knex(RUNS)
      .insert({
        skill_name: skillName,
        config: JSON.stringify(config),
        max_generations: config.maxGenerations || 20,
        population_size: config.populationSize || 8,
      })
      .returning('*');
    return row;
  }

  async getRuns({ page = 1, pageSize = 20, status, skillName } = {}) {
    const base = _knex(RUNS);
    if (status) base.where({ status });
    if (skillName) base.where({ skill_name: skillName });

    const [{ count }] = await base.clone().count();
    const total = parseInt(count);

    const runs = await base.clone()
      .orderBy('created_at', 'desc')
      .limit(pageSize)
      .offset((page - 1) * pageSize);

    return { runs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) };
  }

  async getRun(runId) {
    return _knex(RUNS).where({ id: runId }).first();
  }

  async updateRun(runId, updates) {
    await _knex(RUNS).where({ id: runId }).update(updates);
  }

  // ══════════════════════════════════════
  // Skill Genomes
  // ══════════════════════════════════════

  async insertGenome(genome) {
    const [row] = await _knex(GENOMES)
      .insert({
        run_id: genome.runId,
        skill_name: genome.skillName,
        generation: genome.generation,
        parent_ids: genome.parentIds || [],
        mental_models: JSON.stringify(genome.mentalModels || {}),
        pattern_library: JSON.stringify(genome.patternLibrary || {}),
        judgment: JSON.stringify(genome.judgment || {}),
        tool_fluency: JSON.stringify(genome.toolFluency || {}),
        taste: JSON.stringify(genome.taste || {}),
        scar_tissue: JSON.stringify(genome.scarTissue || {}),
        fitness_scores: JSON.stringify(genome.fitnessScores || {}),
        overall_fitness: genome.overallFitness,
        is_elite: genome.isElite || false,
      })
      .returning('*');
    return row;
  }

  async getGenomesByRun(runId, { generation, aliveOnly = false } = {}) {
    const query = _knex(GENOMES).where({ run_id: runId });
    if (generation !== undefined) query.andWhere({ generation });
    if (aliveOnly) query.andWhere({ is_alive: true });
    return query.orderBy('overall_fitness', 'desc');
  }

  async getGenome(genomeId) {
    return _knex(GENOMES).where({ id: genomeId }).first();
  }

  async getEliteGenomes(runId) {
    return _knex(GENOMES)
      .where({ run_id: runId, is_elite: true })
      .orderBy('overall_fitness', 'desc');
  }

  async getBestGenome(runId) {
    return _knex(GENOMES)
      .where({ run_id: runId })
      .orderBy('overall_fitness', 'desc')
      .first();
  }

  async updateGenomeFitness(genomeId, fitnessScores, overallFitness) {
    await _knex(GENOMES).where({ id: genomeId }).update({
      fitness_scores: JSON.stringify(fitnessScores),
      overall_fitness: overallFitness,
    });
  }

  async markElite(genomeId, isElite = true) {
    await _knex(GENOMES).where({ id: genomeId }).update({ is_elite: isElite });
  }

  async killGenomes(genomeIds) {
    await _knex(GENOMES).whereIn('id', genomeIds).update({ is_alive: false });
  }

  /**
   * 更新基因组的单个维度数据 (F8: 统一 DB 访问层)
   */
  async updateGenomeDimension(genomeId, dimension, data) {
    const serialized = typeof data === 'string' ? data : JSON.stringify(data);
    await _knex(GENOMES).where({ id: genomeId }).update({ [dimension]: serialized });
  }

  // ══════════════════════════════════════
  // Fitness Evaluations
  // ══════════════════════════════════════

  async insertEvaluation(evaluation) {
    const [row] = await _knex(EVALUATIONS)
      .insert({
        genome_id: evaluation.genomeId,
        run_id: evaluation.runId,
        benchmark_id: evaluation.benchmarkId,
        generation: evaluation.generation,
        probe_results: JSON.stringify(evaluation.probeResults || {}),
        dimension_scores: JSON.stringify(evaluation.dimensionScores || {}),
        overall_score: evaluation.overallScore,
        weights: JSON.stringify(evaluation.weights || {}),
        total_tool_calls: evaluation.totalToolCalls,
        redundant_calls: evaluation.redundantCalls,
        execution_time_ms: evaluation.executionTimeMs,
        token_usage: evaluation.tokenUsage,
      })
      .returning('*');
    return row;
  }

  async getEvaluationsByRun(runId, { generation } = {}) {
    const query = _knex(EVALUATIONS).where({ run_id: runId });
    if (generation !== undefined) query.andWhere({ generation });
    return query.orderBy('evaluated_at', 'desc');
  }

  async getEvaluationsByGenome(genomeId) {
    return _knex(EVALUATIONS)
      .where({ genome_id: genomeId })
      .orderBy('evaluated_at', 'desc');
  }

  // ══════════════════════════════════════
  // Benchmarks
  // ══════════════════════════════════════

  async upsertBenchmark(benchmark) {
    const existing = await _knex(BENCHMARKS).where({ id: benchmark.id }).first();
    if (existing) {
      await _knex(BENCHMARKS).where({ id: benchmark.id }).update({
        description: benchmark.description,
        scenario: benchmark.scenario || null,
        evaluation_criteria: benchmark.evaluationCriteria ? JSON.stringify(benchmark.evaluationCriteria) : undefined,
        expected_behaviors: benchmark.expectedBehaviors || undefined,
        difficulty: benchmark.difficulty,
        probes: JSON.stringify(benchmark.probes || []),
        pass_threshold: benchmark.passThreshold,
        is_active: benchmark.isActive !== false,
        updated_at: _knex.fn.now(),
      });
    } else {
      await _knex(BENCHMARKS).insert({
        id: benchmark.id,
        skill_name: benchmark.skillName,
        description: benchmark.description,
        scenario: benchmark.scenario || null,
        evaluation_criteria: benchmark.evaluationCriteria ? JSON.stringify(benchmark.evaluationCriteria) : '{}',
        expected_behaviors: benchmark.expectedBehaviors || [],
        difficulty: benchmark.difficulty || 1,
        probes: JSON.stringify(benchmark.probes || []),
        pass_threshold: benchmark.passThreshold || 0.6,
      });
    }
    return _knex(BENCHMARKS).where({ id: benchmark.id }).first();
  }

  async getBenchmarks(skillName) {
    const query = _knex(BENCHMARKS).where({ is_active: true });
    if (skillName) query.andWhere({ skill_name: skillName });
    return query.orderBy('difficulty');
  }

  // ══════════════════════════════════════
  // Learning Events
  // ══════════════════════════════════════

  async insertLearning(event) {
    const [row] = await _knex(LEARNING)
      .insert({
        genome_id: event.genomeId,
        run_id: event.runId,
        event_type: event.eventType,
        dimension: event.dimension,
        content: JSON.stringify(event.content),
        source: event.source,
      })
      .returning('*');
    return row;
  }

  async getLearningEvents(runId, { dimension, eventType, applied } = {}) {
    const query = _knex(LEARNING).where({ run_id: runId });
    if (dimension) query.andWhere({ dimension });
    if (eventType) query.andWhere({ event_type: eventType });
    if (applied !== undefined) query.andWhere({ applied });
    return query.orderBy('created_at', 'desc');
  }

  async markLearningApplied(eventIds) {
    await _knex(LEARNING).whereIn('id', eventIds).update({ applied: true });
  }

  // ══════════════════════════════════════
  // Dashboard 聚合查询
  // ══════════════════════════════════════

  async getRunStats(runId) {
    const run = await this.getRun(runId);
    if (!run) return null;

    const [genomeCount] = await _knex(GENOMES).where({ run_id: runId }).count();
    const [aliveCount] = await _knex(GENOMES).where({ run_id: runId, is_alive: true }).count();
    const [evalCount] = await _knex(EVALUATIONS).where({ run_id: runId }).count();
    const [learningCount] = await _knex(LEARNING).where({ run_id: runId }).count();

    const best = await this.getBestGenome(runId);

    // 每代最佳适应度趋势
    const fitnessTrend = await _knex(GENOMES)
      .select('generation')
      .max('overall_fitness as best_fitness')
      .avg('overall_fitness as avg_fitness')
      .where({ run_id: runId })
      .whereNotNull('overall_fitness')
      .groupBy('generation')
      .orderBy('generation');

    // 各维度平均分趋势
    const latestGeneration = run.current_generation;
    const currentGenomes = await this.getGenomesByRun(runId, {
      generation: latestGeneration,
      aliveOnly: true,
    });

    const dimensionAvg = {};
    const dimensions = ['mentalModels', 'patternLibrary', 'judgment', 'toolFluency', 'taste', 'scarTissue'];
    for (const dim of dimensions) {
      const scores = currentGenomes
        .map(g => {
          const fs = typeof g.fitness_scores === 'string' ? JSON.parse(g.fitness_scores) : g.fitness_scores;
          return fs?.[dim];
        })
        .filter(s => s !== undefined && s !== null);
      dimensionAvg[dim] = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : null;
    }

    // 学习事件按维度统计
    const learningByDimension = await _knex(LEARNING)
      .select('dimension')
      .count('* as count')
      .where({ run_id: runId })
      .groupBy('dimension');

    return {
      run,
      totalGenomes: parseInt(genomeCount.count),
      aliveGenomes: parseInt(aliveCount.count),
      totalEvaluations: parseInt(evalCount.count),
      totalLearnings: parseInt(learningCount.count),
      bestGenome: best,
      fitnessTrend,
      dimensionAvg,
      learningByDimension,
    };
  }

  async getOverview() {
    const runs = await _knex(RUNS)
      .select('id', 'skill_name', 'status', 'current_generation', 'max_generations', 'best_fitness', 'created_at')
      .orderBy('created_at', 'desc')
      .limit(50);

    const [totalRuns] = await _knex(RUNS).count();
    const [activeRuns] = await _knex(RUNS).where({ status: 'running' }).count();
    const [totalGenomes] = await _knex(GENOMES).count();
    const [totalLearnings] = await _knex(LEARNING).count();

    return {
      runs,
      stats: {
        totalRuns: parseInt(totalRuns.count),
        activeRuns: parseInt(activeRuns.count),
        totalGenomes: parseInt(totalGenomes.count),
        totalLearnings: parseInt(totalLearnings.count),
      },
    };
  }
}

const evolutionQueries = new EvolutionQueries();

function initQueries(knexInstance) {
  knex = knexInstance;
}

module.exports = { evolutionQueries, initQueries };