skilllite-evolution 0.1.15

SkillLite Evolution: self-evolving prompts, skills, and memory
Documentation
//! 技能修复:对验证失败的 skill,打包整个目录内容给模型一次性修复

use std::path::Path;

use anyhow::Result;

use crate::gatekeeper_l3_content;
use crate::EvolutionLlm;
use crate::EvolutionMessage;

use super::infer;
use super::parse;
use super::validate::{self, SkillValidation};
use super::MAX_REFINE_ROUNDS;
use super::SKILL_REFINEMENT_PROMPT;

// ─── 工具函数 ────────────────────────────────────────────────────────────────

/// 打包技能目录:目录结构 + 所有文件完整内容
fn build_skill_dir_package(skill_dir: &Path) -> String {
    let mut lines = Vec::new();
    lines.push("## 目录结构".to_string());
    let mut entries: Vec<String> = Vec::new();
    if let Ok(rd) = std::fs::read_dir(skill_dir) {
        for e in rd.filter_map(|e| e.ok()) {
            let name = e.file_name().to_string_lossy().into_owned();
            let path = e.path();
            if path.is_dir() {
                entries.push(format!("{}/", name));
                if let Ok(sub) = std::fs::read_dir(&path) {
                    for se in sub.filter_map(|e| e.ok()) {
                        entries.push(format!("  {}", se.file_name().to_string_lossy()));
                    }
                }
            } else {
                entries.push(name);
            }
        }
    }
    entries.sort();
    lines.push(entries.join("\n"));
    lines.push(String::new());
    lines.push("## 文件内容".to_string());

    let skill_md_path = skill_dir.join("SKILL.md");
    if skill_md_path.is_file() {
        lines.push("### SKILL.md".to_string());
        lines.push(
            skilllite_fs::read_file(&skill_md_path).unwrap_or_else(|_| "(无法读取)".to_string()),
        );
        lines.push(String::new());
    }

    for rel in infer::list_scripts(skill_dir) {
        let full = skill_dir.join(&rel);
        if full.is_file() {
            lines.push(format!("### {}", rel));
            lines.push(skilllite_fs::read_file(&full).unwrap_or_else(|_| "(无法读取)".to_string()));
            lines.push(String::new());
        }
    }
    lines.join("\n")
}

// ─── repair_one_skill(核心:打包目录 → 模型修复 → 应用 → 验证)───────────

/// 修复单个技能:打包整个目录给模型,模型返回修复,应用后验证,最多 MAX_REFINE_ROUNDS 轮
pub async fn repair_one_skill<L: EvolutionLlm>(
    llm: &L,
    model: &str,
    skill_dir: &Path,
    _skill_name: &str,
    entry_point: &str,
    test_input: &str,
    on_msg: Option<&(dyn Fn(&str) + Send + Sync)>,
) -> Result<(bool, String)> {
    let script_path = skill_dir.join(entry_point);
    let skill_md_path = skill_dir.join("SKILL.md");
    let mut current_test_input = test_input.to_string();

    for round in 1..=MAX_REFINE_ROUNDS {
        // 每轮先安装/更新依赖:无 package.json/requirements.txt 时从 SKILL.md compatibility 推断(大模型可能上轮已补全)
        let env_path = super::env_helper::ensure_skill_deps_and_env(skill_dir);

        let (exec_ok, exec_trace) = infer::test_skill_invoke(
            skill_dir,
            entry_point,
            &current_test_input,
            env_path.as_deref(),
        )?;
        let doc_error = validate::check_skill_md_completeness(skill_dir, llm, model).await;
        if exec_ok && doc_error.is_none() {
            return Ok((true, String::new()));
        }
        if let Some(f) = on_msg {
            f(&format!("第 {}/{} 轮修复…", round, MAX_REFINE_ROUNDS));
        }

        let error_trace = if !exec_ok {
            exec_trace
        } else {
            format!(
                "脚本执行通过,但 {}",
                doc_error.unwrap_or_else(|| "文档校验未通过".to_string())
            )
        };

        let package = build_skill_dir_package(skill_dir);
        let prompt = SKILL_REFINEMENT_PROMPT
            .replace("{{skill_dir_package}}", &package)
            .replace("{{tested_script}}", entry_point)
            .replace("{{current_test_input}}", &current_test_input)
            .replace("{{error_trace}}", &error_trace);

        let messages = vec![EvolutionMessage::user(&prompt)];
        let (parsed, raw_dbg) = llm_repair_call(llm, model, &messages, &error_trace).await?;
        let Some(parsed) = parsed else {
            tracing::warn!(
                "Skill repair round {}: model returned no fix. Raw: {}",
                round,
                raw_dbg
            );
            if let Some(f) = on_msg {
                f(&format!("第 {} 轮模型未给出有效修复,继续重试…", round));
            }
            continue;
        };

        if let Some(f) = on_msg {
            f(&parsed.fix_summary);
            if let Some(ref r) = parsed.user_reply {
                f(r);
            }
        }

        if let Some(ref md) = parsed.fix_skill_md {
            if gatekeeper_l3_content(md).is_ok()
                && validate::check_skill_md_completeness_heuristic(md).is_none()
            {
                skilllite_fs::write_file(&skill_md_path, md)?;
            } else if gatekeeper_l3_content(md).is_ok() {
                tracing::warn!(
                    "Repair returned fix_skill_md but still incomplete (missing Usage/Examples), skip applying"
                );
            }
        }
        if let Some(ref script) = parsed.fixed_script {
            if gatekeeper_l3_content(script).is_ok() {
                skilllite_fs::write_file(&script_path, script)?;
                #[cfg(unix)]
                {
                    use std::os::unix::fs::PermissionsExt;
                    let _ = std::fs::set_permissions(
                        &script_path,
                        std::fs::Permissions::from_mode(0o755),
                    );
                }
            }
        }
        if let Some(ref ti) = parsed.fix_test_input {
            current_test_input = ti.clone();
        }
    }

    let env_path = super::env_helper::ensure_skill_deps_and_env(skill_dir);
    let (ok, final_trace) = infer::test_skill_invoke(
        skill_dir,
        entry_point,
        &current_test_input,
        env_path.as_deref(),
    )?;
    let doc_error = validate::check_skill_md_completeness(skill_dir, llm, model).await;
    if ok && doc_error.is_none() {
        return Ok((true, String::new()));
    }
    let fail_reason = if !ok {
        final_trace.lines().take(20).collect::<Vec<_>>().join("\n")
    } else {
        doc_error.unwrap_or_default()
    };
    Ok((
        false,
        format!("{} 轮修复后仍失败\n{}", MAX_REFINE_ROUNDS, fail_reason),
    ))
}

/// 调用模型获取修复方案(含 unfixable 重试 + JSON 解析重试)
/// 返回 (Option<修复方案>, 最后一次模型原始输出)
async fn llm_repair_call<L: EvolutionLlm>(
    llm: &L,
    model: &str,
    messages: &[EvolutionMessage],
    error_trace: &str,
) -> Result<(Option<parse::RefinedSkill>, String)> {
    let content = llm.complete(messages, model, 0.3).await?.trim().to_string();
    let mut last_raw = content.clone();

    match parse::parse_refinement_response(&content) {
        Ok(Some(r)) => return Ok((Some(r), last_raw)),
        Ok(None) => {
            tracing::info!("Repair: model returned no fix or unfixable, will retry");
        }
        Err(e) => {
            tracing::info!("Repair JSON parse failed: {}", e);
            let retry_msg = format!(
                "你的输出无法解析为 JSON。错误: {}。请重新输出合法 JSON,包含 fix_summary 和至少一个修复字段(fixed_script / fix_test_input / fix_skill_md)。所有键名用双引号,换行用 \\n 转义。",
                e
            );
            let mut msgs = messages.to_vec();
            msgs.push(EvolutionMessage::user(&retry_msg));
            let content2 = llm.complete(&msgs, model, 0.3).await?.trim().to_string();
            last_raw = content2.clone();
            if let Ok(Some(r)) = parse::parse_refinement_response(&content2) {
                return Ok((Some(r), last_raw));
            }
        }
    }

    let err_brief: String = error_trace.lines().take(10).collect::<Vec<_>>().join("\n");
    let mut msgs = messages.to_vec();
    for attempt in 1..=3 {
        let force_msg = format!(
            "【第{attempt}次强制要求】这个技能必须被修复,没有例外。\n\n错误:\n{err_brief}\n\n你必须输出一个合法的 JSON 对象,至少包含以下字段之一:\n- \"fix_test_input\": \"{{\\\"base\\\": 2, \\\"exponent\\\": 10}}\"  (注意:值是字符串,内容是 JSON)\n- \"fixed_script\": \"#!/usr/bin/env python3\\n...\"  (完整脚本内容)\n- \"fix_skill_md\": \"---\\nname: ...\\n---\\n...\"  (完整 SKILL.md)\n\n同时必须包含 \"fix_summary\": \"说明修了什么\"。\n\n示例输出:\n```json\n{{\"fix_summary\": \"补充测试输入\", \"fix_test_input\": \"{{\\\"base\\\": 2, \\\"exponent\\\": 10}}\", \"fixed_script\": null, \"fix_skill_md\": null}}\n```\n\n不要返回 unfixable。不要解释,直接输出 JSON。",
        );
        msgs.push(EvolutionMessage::user(&force_msg));
        let content_r = llm.complete(&msgs, model, 0.3).await?.trim().to_string();
        last_raw = content_r.clone();
        tracing::info!("Force retry {attempt}: raw len={}", content_r.len());
        if let Ok(Some(r)) = parse::parse_refinement_response(&content_r) {
            return Ok((Some(r), last_raw));
        }
    }
    Ok((None, last_raw))
}

// ─── repair_skills ───────────────────────────────────────────────────────────

/// 修复技能:先验证,再对失败的逐个打包修复
pub async fn repair_skills<L: EvolutionLlm>(
    skills_root: &Path,
    llm: &L,
    model: &str,
) -> Result<Vec<(String, bool)>> {
    let validated = validate::validate_skills(skills_root, llm, model, None).await?;
    let failed: Vec<&SkillValidation> = validated.iter().filter(|v| !v.passed).collect();

    if failed.is_empty() {
        return Ok(validated
            .into_iter()
            .map(|v| (v.skill_name, true))
            .collect());
    }

    eprintln!("\n🔧 修复 {} 个失败的技能...", failed.len());
    let mut results: Vec<(String, bool)> = Vec::new();
    for v in &validated {
        if v.passed {
            results.push((v.skill_name.clone(), true));
            continue;
        }
        let (ep, ti) = match (&v.entry_point, &v.test_input) {
            (Some(ep), Some(ti)) => (ep.as_str(), ti.as_str()),
            _ => {
                eprintln!("  ⏭️ {} (推理失败,跳过)", v.skill_name);
                results.push((v.skill_name.clone(), false));
                continue;
            }
        };
        let idx = results.iter().filter(|(_, ok)| !ok).count() + 1;
        eprintln!("🔧 [{}/{}] {} ...", idx, failed.len(), v.skill_name);
        let on_msg = |msg: &str| eprintln!("  💬 {}", msg);
        let (ok, reason) = repair_one_skill(
            llm,
            model,
            &v.skill_dir,
            &v.skill_name,
            ep,
            ti,
            Some(&on_msg),
        )
        .await
        .unwrap_or_else(|e| (false, format!("{}", e)));

        if ok {
            eprintln!("  ✅ {}", v.skill_name);
        } else {
            eprintln!("  ❌ {}\n{}", v.skill_name, reason);
        }
        results.push((v.skill_name.clone(), ok));
    }
    Ok(results)
}