//! 技能生成器 //! 将聚合的经验模式通过 LLM 转化为 SKILL.md 内容 //! 提供 prompt 构建和 JSON 结果解析 use crate::pattern_aggregator::AggregatedPattern; use zclaw_types::Result; /// 技能候选项 #[derive(Debug, Clone)] pub struct SkillCandidate { pub name: String, pub description: String, pub triggers: Vec, pub tools: Vec, pub body_markdown: String, pub source_pattern: String, pub confidence: f32, /// 技能版本号,用于后续迭代追踪 pub version: u32, } /// LLM 驱动的技能生成 prompt const SKILL_GENERATION_PROMPT: &str = r#" 你是一个技能设计专家。根据以下用户反复出现的问题和解决步骤,生成一个可复用的技能定义。 问题模式:{pain_pattern} 解决步骤:{steps} 使用的工具:{tools} 行业背景:{industry} 请生成以下 JSON: ```json { "name": "技能名称(简短中文)", "description": "技能描述(一段话)", "triggers": ["触发词1", "触发词2", "触发词3"], "tools": ["tool1", "tool2"], "body_markdown": "技能的 Markdown 正文,包含步骤说明", "confidence": 0.85 } ``` "#; /// 技能生成器 /// 负责 prompt 构建和 LLM 返回的 JSON 解析 pub struct SkillGenerator; impl SkillGenerator { pub fn new() -> Self { Self } /// 从聚合模式构建 LLM prompt pub fn build_prompt(pattern: &AggregatedPattern) -> String { SKILL_GENERATION_PROMPT .replace("{pain_pattern}", &pattern.pain_pattern) .replace("{steps}", &pattern.common_steps.join(" → ")) .replace("{tools}", &pattern.tools_used.join(", ")) .replace("{industry}", pattern.industry_context.as_deref().unwrap_or("通用")) } /// 解析 LLM 返回的 JSON 为 SkillCandidate pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result { let json_str = crate::json_utils::extract_json_block(json_str); let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", e)) })?; Ok(SkillCandidate { name: raw["name"] .as_str() .unwrap_or("未命名技能") .to_string(), description: raw["description"].as_str().unwrap_or("").to_string(), triggers: crate::json_utils::extract_string_array(&raw, "triggers"), tools: crate::json_utils::extract_string_array(&raw, "tools"), body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(), source_pattern: pattern.pain_pattern.clone(), confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32, version: raw["version"].as_u64().unwrap_or(1) as u32, }) } } impl Default for SkillGenerator { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::experience_store::Experience; fn make_pattern() -> AggregatedPattern { let exp = Experience::new( "agent-1", "报表生成", "researcher", vec!["查询数据库".into(), "格式化输出".into()], "success", ); AggregatedPattern { pain_pattern: "报表生成".to_string(), experiences: vec![exp], common_steps: vec!["查询数据库".into(), "格式化输出".into()], total_reuse: 5, tools_used: vec!["researcher".into()], industry_context: Some("healthcare".into()), } } #[test] fn test_build_prompt() { let pattern = make_pattern(); let prompt = SkillGenerator::build_prompt(&pattern); assert!(prompt.contains("报表生成")); assert!(prompt.contains("查询数据库")); assert!(prompt.contains("researcher")); assert!(prompt.contains("healthcare")); } #[test] fn test_parse_response_valid_json() { let pattern = make_pattern(); let json = r##"{"name":"每日报表","description":"生成每日报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 每日报表\n步骤1","confidence":0.9}"##; let candidate = SkillGenerator::parse_response(json, &pattern).unwrap(); assert_eq!(candidate.name, "每日报表"); assert_eq!(candidate.triggers.len(), 2); assert_eq!(candidate.confidence, 0.9); assert_eq!(candidate.source_pattern, "报表生成"); } #[test] fn test_parse_response_json_block() { let pattern = make_pattern(); let text = r#"```json {"name":"技能A","description":"desc","triggers":["a"],"tools":[],"body_markdown":"body","confidence":0.8} ```"#; let candidate = SkillGenerator::parse_response(text, &pattern).unwrap(); assert_eq!(candidate.name, "技能A"); } #[test] fn test_parse_response_invalid_json() { let pattern = make_pattern(); let result = SkillGenerator::parse_response("not json at all", &pattern); assert!(result.is_err()); } #[test] fn test_extract_json_block_with_markdown() { let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone."; assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}"); } #[test] fn test_extract_json_block_bare() { let text = "{\"key\": \"value\"}"; assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}"); } }