Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CRITICAL-1/2: json_utils 花括号匹配改为括号平衡算法 - 处理字符串字面量中的花括号和转义引号 - 新增 5 个测试(平衡匹配、字符串内花括号、转义引号、extract_string_array) HIGH-4: EvolutionMiddleware 只取第一个事件(remove(0)),不丢弃后续 HIGH-5: EvolutionMiddleware 先 read() 判空再 write(),减少锁竞争 HIGH-7: from_experience_store 使用传入 store 的 viking 实例(不再忽略参数) - ExperienceStore 新增 viking() getter MEDIUM-9: skill_generator + workflow_composer JSON 数组解析去重 - 新增 json_utils::extract_string_array() 共享函数 MEDIUM-14: EvolutionMiddleware 注入文本去除多余缩进空格 测试: zclaw-growth 133 tests, zclaw-runtime 87 tests, workspace 0 failures
165 lines
5.5 KiB
Rust
165 lines
5.5 KiB
Rust
//! 技能生成器
|
||
//! 将聚合的经验模式通过 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<String>,
|
||
pub tools: Vec<String>,
|
||
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<SkillCandidate> {
|
||
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\"}");
|
||
}
|
||
}
|