feat(skills): enhance SkillsLoader to support workspace skills directory and update skills loading logic
feat(get-skill): add action parameter for skill retrieval and implement skill listing functionality fix(session): adjust skills prompt formatting for improved clarity
This commit is contained in:
parent
bafa7a606c
commit
c81b1e42c7
@ -605,7 +605,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_threshold() {
|
fn test_threshold() {
|
||||||
let compressor = ContextCompressor::new(mock_provider(), 128_000, test_memory_manager());
|
let compressor = ContextCompressor::new(mock_provider(), 128_000, test_memory_manager());
|
||||||
assert_eq!(compressor.threshold(), 64_000);
|
assert_eq!(compressor.threshold(), 89_600);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@ -487,7 +487,7 @@ impl Session {
|
|||||||
if skills_prompt.trim().is_empty() {
|
if skills_prompt.trim().is_empty() {
|
||||||
base_prompt
|
base_prompt
|
||||||
} else {
|
} else {
|
||||||
format!("{}\n\n## Skills\n\n{}\n\nUse the `get_skill` tool to load a skill's full content when needed.", base_prompt, skills_prompt)
|
format!("{}\n\n{}", base_prompt, skills_prompt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,8 +810,9 @@ impl SessionManager {
|
|||||||
bus: Arc<MessageBus>,
|
bus: Arc<MessageBus>,
|
||||||
memory_manager: Arc<crate::memory::MemoryManager>,
|
memory_manager: Arc<crate::memory::MemoryManager>,
|
||||||
) -> Result<Self, AgentError> {
|
) -> Result<Self, AgentError> {
|
||||||
let skills_loader = SkillsLoader::new();
|
let mut skills_loader = SkillsLoader::new();
|
||||||
skills_loader.load_skills();
|
skills_loader.load_skills();
|
||||||
|
skills_loader.set_workspace_skills_dir(provider_config.workspace_dir.clone());
|
||||||
let skills_loader = Arc::new(skills_loader);
|
let skills_loader = Arc::new(skills_loader);
|
||||||
|
|
||||||
let tools = Arc::new(create_default_tools(skills_loader.clone(), memory_manager.clone()));
|
let tools = Arc::new(create_default_tools(skills_loader.clone(), memory_manager.clone()));
|
||||||
|
|||||||
@ -24,6 +24,7 @@ struct SkillsState {
|
|||||||
loaded_skills: Vec<Skill>,
|
loaded_skills: Vec<Skill>,
|
||||||
last_picobot_mtime: Option<SystemTime>,
|
last_picobot_mtime: Option<SystemTime>,
|
||||||
last_agent_mtime: Option<SystemTime>,
|
last_agent_mtime: Option<SystemTime>,
|
||||||
|
last_workspace_mtime: Option<SystemTime>,
|
||||||
last_load_time: SystemTime,
|
last_load_time: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ impl Default for SkillsState {
|
|||||||
loaded_skills: Vec::new(),
|
loaded_skills: Vec::new(),
|
||||||
last_picobot_mtime: None,
|
last_picobot_mtime: None,
|
||||||
last_agent_mtime: None,
|
last_agent_mtime: None,
|
||||||
|
last_workspace_mtime: None,
|
||||||
last_load_time: SystemTime::now(),
|
last_load_time: SystemTime::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +45,7 @@ impl Default for SkillsState {
|
|||||||
pub struct SkillsLoader {
|
pub struct SkillsLoader {
|
||||||
picobot_skills_dir: PathBuf,
|
picobot_skills_dir: PathBuf,
|
||||||
agent_skills_dir: PathBuf,
|
agent_skills_dir: PathBuf,
|
||||||
|
workspace_skills_dir: Option<PathBuf>,
|
||||||
state: Arc<Mutex<SkillsState>>,
|
state: Arc<Mutex<SkillsState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +56,7 @@ impl SkillsLoader {
|
|||||||
Self {
|
Self {
|
||||||
picobot_skills_dir: home.join(".picobot/skills"),
|
picobot_skills_dir: home.join(".picobot/skills"),
|
||||||
agent_skills_dir: home.join(".agent/skills"),
|
agent_skills_dir: home.join(".agent/skills"),
|
||||||
|
workspace_skills_dir: None,
|
||||||
state: Arc::new(Mutex::new(SkillsState::default())),
|
state: Arc::new(Mutex::new(SkillsState::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,10 +66,16 @@ impl SkillsLoader {
|
|||||||
Self {
|
Self {
|
||||||
picobot_skills_dir: picobot_dir,
|
picobot_skills_dir: picobot_dir,
|
||||||
agent_skills_dir: agent_dir,
|
agent_skills_dir: agent_dir,
|
||||||
|
workspace_skills_dir: None,
|
||||||
state: Arc::new(Mutex::new(SkillsState::default())),
|
state: Arc::new(Mutex::new(SkillsState::default())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the workspace skills directory (./skills under workspace root)
|
||||||
|
pub fn set_workspace_skills_dir(&mut self, workspace_path: PathBuf) {
|
||||||
|
self.workspace_skills_dir = Some(workspace_path.join("skills"));
|
||||||
|
}
|
||||||
|
|
||||||
/// Load all skills from both directories and record modification times
|
/// Load all skills from both directories and record modification times
|
||||||
pub fn load_skills(&self) {
|
pub fn load_skills(&self) {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
@ -104,6 +114,20 @@ impl SkillsLoader {
|
|||||||
state.last_agent_mtime = Self::get_dir_mtime(&self.agent_skills_dir);
|
state.last_agent_mtime = Self::get_dir_mtime(&self.agent_skills_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load from workspace ./skills (if set)
|
||||||
|
if let Some(ref ws_dir) = self.workspace_skills_dir {
|
||||||
|
if ws_dir.exists() {
|
||||||
|
let loaded = self.load_skills_from_dir(ws_dir);
|
||||||
|
tracing::debug!(
|
||||||
|
dir = %ws_dir.display(),
|
||||||
|
count = loaded.len(),
|
||||||
|
"Loaded skills from workspace directory"
|
||||||
|
);
|
||||||
|
state.loaded_skills.extend(loaded);
|
||||||
|
state.last_workspace_mtime = Self::get_dir_mtime(ws_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.last_load_time = SystemTime::now();
|
state.last_load_time = SystemTime::now();
|
||||||
|
|
||||||
if state.loaded_skills.is_empty() {
|
if state.loaded_skills.is_empty() {
|
||||||
@ -130,7 +154,18 @@ impl SkillsLoader {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
picobot_changed || agent_changed
|
let workspace_changed = if let Some(ref ws_dir) = self.workspace_skills_dir {
|
||||||
|
if ws_dir.exists() {
|
||||||
|
let current_mtime = Self::get_dir_mtime(ws_dir);
|
||||||
|
current_mtime != state.last_workspace_mtime
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
picobot_changed || agent_changed || workspace_changed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reload skills if changes are detected
|
/// Reload skills if changes are detected
|
||||||
@ -247,46 +282,53 @@ impl SkillsLoader {
|
|||||||
parts.join("\n\n---\n\n")
|
parts.join("\n\n---\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build full skills prompt combining always skills and summary (checks for changes first)
|
/// Build full skills prompt: directory conventions, always-skill summary, always-skill content
|
||||||
pub fn build_skills_prompt(&self) -> String {
|
pub fn build_skills_prompt(&self) -> String {
|
||||||
self.reload_if_changed();
|
self.reload_if_changed();
|
||||||
let state = self.state.lock().unwrap();
|
let state = self.state.lock().unwrap();
|
||||||
|
|
||||||
let mut prompt = String::new();
|
|
||||||
|
|
||||||
|
if state.loaded_skills.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prompt = String::from("## Skills\n\n");
|
||||||
|
|
||||||
|
// Directory conventions
|
||||||
|
prompt.push_str("### 目录说明\n\n");
|
||||||
|
prompt.push_str("- `~/.agent/skills/` — 外部共享 skill 目录(第三方、系统级 skill)\n");
|
||||||
|
prompt.push_str("- `~/.picobot/skills/` — 安装 skill 的默认目录\n");
|
||||||
|
prompt.push_str("- `./skills/` — 工作目录下的 skill,picobot 自行创建的 skill 存放于此\n\n");
|
||||||
|
prompt.push_str("安装或创建 skill 时请按上述目录规范存放。\n\n");
|
||||||
|
|
||||||
|
// Always skills summary
|
||||||
let always_skills: Vec<_> = state.loaded_skills.iter().filter(|s| s.always).collect();
|
let always_skills: Vec<_> = state.loaded_skills.iter().filter(|s| s.always).collect();
|
||||||
if !always_skills.is_empty() {
|
if !always_skills.is_empty() {
|
||||||
|
prompt.push_str("### 常用技能\n\n");
|
||||||
|
for skill in &always_skills {
|
||||||
|
let path_str = skill.path.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "—".to_string());
|
||||||
|
prompt.push_str(&format!(
|
||||||
|
"- **{}**: {} [路径: `{}`]\n",
|
||||||
|
skill.name, skill.description, path_str
|
||||||
|
));
|
||||||
|
}
|
||||||
|
prompt.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage instructions
|
||||||
|
prompt.push_str("### 使用方法\n\n");
|
||||||
|
prompt.push_str("- 使用 `get_skill` 工具 action=\"list\" 列出所有可用 skill 及其名称、简介、路径\n");
|
||||||
|
prompt.push_str("- 使用 `get_skill` 工具 action=\"get\" 并提供 `skill_name` 获取指定 skill 完整内容\n");
|
||||||
|
|
||||||
|
// Always skills full content
|
||||||
|
if !always_skills.is_empty() {
|
||||||
|
prompt.push_str("\n---\n\n");
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
for skill in always_skills {
|
for skill in &always_skills {
|
||||||
parts.push(format!("## Skill: {}\n\n{}", skill.name, skill.content));
|
parts.push(format!("## Skill: {}\n\n{}", skill.name, skill.content));
|
||||||
}
|
}
|
||||||
prompt.push_str(&parts.join("\n\n---\n\n"));
|
prompt.push_str(&parts.join("\n\n---\n\n"));
|
||||||
prompt.push_str("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_other_skills = state.loaded_skills.iter().any(|s| !s.always);
|
|
||||||
if has_other_skills {
|
|
||||||
prompt.push_str("## Available Skills\n\n");
|
|
||||||
prompt.push_str("Skills teach the agent how to use specific capabilities. Use the `get_skill` tool to load a skill's full content when needed.\n\n");
|
|
||||||
|
|
||||||
let mut lines = vec!["<skills>".to_string()];
|
|
||||||
for skill in &state.loaded_skills {
|
|
||||||
if skill.always {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lines.push(" <skill>".to_string());
|
|
||||||
lines.push(format!(" <name>{}</name>", escape_xml(&skill.name)));
|
|
||||||
lines.push(format!(
|
|
||||||
" <description>{}</description>",
|
|
||||||
escape_xml(&skill.description)
|
|
||||||
));
|
|
||||||
if let Some(path) = &skill.path {
|
|
||||||
lines.push(format!(" <path>{}</path>", escape_xml(&path.to_string_lossy())));
|
|
||||||
}
|
|
||||||
lines.push(" </skill>".to_string());
|
|
||||||
}
|
|
||||||
lines.push("</skills>".to_string());
|
|
||||||
prompt.push_str(&lines.join("\n"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt
|
prompt
|
||||||
|
|||||||
@ -44,12 +44,17 @@ impl Tool for GetSkillTool {
|
|||||||
json!({
|
json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["get", "list"],
|
||||||
|
"description": "操作类型: get 获取指定 skill 完整内容, list 列出所有可用 skill"
|
||||||
|
},
|
||||||
"skill_name": {
|
"skill_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name of the skill to retrieve"
|
"description": "Name of the skill to retrieve,仅在 action 为 get 时必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["skill_name"]
|
"required": []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +63,17 @@ impl Tool for GetSkillTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||||
|
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("get");
|
||||||
|
|
||||||
|
match action {
|
||||||
|
"list" => self.list_skills_full(),
|
||||||
|
_ => self.get_skill_by_name(&args),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetSkillTool {
|
||||||
|
fn get_skill_by_name(&self, args: &serde_json::Value) -> anyhow::Result<ToolResult> {
|
||||||
let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
|
let skill_name = match args.get("skill_name").and_then(|v| v.as_str()) {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
None => {
|
None => {
|
||||||
@ -100,6 +116,33 @@ impl Tool for GetSkillTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_skills_full(&self) -> anyhow::Result<ToolResult> {
|
||||||
|
let skills = self.skills_loader.get_loaded_skills();
|
||||||
|
if skills.is_empty() {
|
||||||
|
return Ok(ToolResult {
|
||||||
|
success: true,
|
||||||
|
output: "当前没有安装任何 skill".to_string(),
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut output = format!("可用 skill (共 {} 个):\n", skills.len());
|
||||||
|
for s in &skills {
|
||||||
|
let always_mark = if s.always { " [常驻]" } else { "" };
|
||||||
|
let path_str = s.path.as_ref()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "—".to_string());
|
||||||
|
output.push_str(&format!(
|
||||||
|
"- {}{}\n 简介: {}\n 路径: {}\n",
|
||||||
|
s.name, always_mark, s.description, path_str
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(ToolResult {
|
||||||
|
success: true,
|
||||||
|
output,
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user