Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
206
crates/gyandoot-skills/src/lib.rs
Normal file
206
crates/gyandoot-skills/src/lib.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
//! # Gyandoot Skills System
|
||||
//!
|
||||
//! Skills are reusable procedural knowledge — loaded, preprocessed, and invoked
|
||||
//! by the agent to perform specific tasks. Supports discovery, bundles, templates, and hubs.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Trust level for a skill.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TrustLevel {
|
||||
Builtin,
|
||||
Trusted,
|
||||
Community,
|
||||
Unverified,
|
||||
}
|
||||
|
||||
/// Metadata for a discovered skill.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SkillMeta {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub category: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub trust: TrustLevel,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// A loaded skill with full content.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Skill {
|
||||
pub meta: SkillMeta,
|
||||
pub content: String,
|
||||
pub templates: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Discovers skills from filesystem locations.
|
||||
pub struct SkillsManager {
|
||||
skill_dirs: Vec<PathBuf>,
|
||||
skills: HashMap<String, Skill>,
|
||||
}
|
||||
|
||||
impl SkillsManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
skill_dirs: Vec::new(),
|
||||
skills: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_skill_dir(mut self, dir: PathBuf) -> Self {
|
||||
self.skill_dirs.push(dir);
|
||||
self
|
||||
}
|
||||
|
||||
/// Scan all registered directories for skills.
|
||||
pub fn scan(&mut self) -> Vec<SkillMeta> {
|
||||
let mut metas = Vec::new();
|
||||
for dir in &self.skill_dirs {
|
||||
if dir.exists() {
|
||||
info!(path = %dir.display(), "Scanning for skills");
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() && path.join("SKILL.md").exists() {
|
||||
let name = path.file_stem()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let meta = SkillMeta {
|
||||
name,
|
||||
description: String::new(), // would parse from SKILL.md
|
||||
category: dir.file_stem().and_then(|n| n.to_str()).map(String::from),
|
||||
tags: Vec::new(),
|
||||
trust: TrustLevel::Builtin,
|
||||
path,
|
||||
};
|
||||
metas.push(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for m in &metas {
|
||||
info!(skill = %m.name, "Discovered skill");
|
||||
}
|
||||
metas
|
||||
}
|
||||
|
||||
/// Load a specific skill by name.
|
||||
pub fn load(&mut self, name: &str) -> Option<&Skill> {
|
||||
if self.skills.contains_key(name) {
|
||||
return self.skills.get(name);
|
||||
}
|
||||
// Search directories for matching skill
|
||||
for dir in &self.skill_dirs {
|
||||
let candidate = dir.join(name).join("SKILL.md");
|
||||
if candidate.exists() {
|
||||
let content = std::fs::read_to_string(&candidate).ok().unwrap_or_default();
|
||||
// Parse frontmatter
|
||||
let (_, body) = if let Some(end) = content.find("---\n") {
|
||||
content.split_at(end + 4)
|
||||
} else {
|
||||
("", &content[..])
|
||||
};
|
||||
let meta = SkillMeta {
|
||||
name: name.to_string(),
|
||||
description: content.lines().next().unwrap_or("").to_string(),
|
||||
category: dir.file_stem().and_then(|n| n.to_str()).map(String::from),
|
||||
tags: Vec::new(),
|
||||
trust: TrustLevel::Trusted,
|
||||
path: candidate,
|
||||
};
|
||||
let skill = Skill {
|
||||
meta,
|
||||
content: body.to_string(),
|
||||
templates: HashMap::new(),
|
||||
};
|
||||
self.skills.insert(name.to_string(), skill);
|
||||
return self.skills.get(name);
|
||||
}
|
||||
}
|
||||
warn!(skill = %name, "Skill not found");
|
||||
None
|
||||
}
|
||||
|
||||
pub fn list_skills(&self) -> Vec<SkillMeta> {
|
||||
self.skills.values().map(|s| s.meta.clone()).collect()
|
||||
}
|
||||
|
||||
/// Build compact skill index for system prompt injection.
|
||||
pub fn inject_skills(&self) -> String {
|
||||
let mut lines = Vec::new();
|
||||
lines.push("Available skills (load with skill_view for full content):".to_string());
|
||||
for skill in self.skills.values() {
|
||||
lines.push(format!(
|
||||
"- {} ({}): {}",
|
||||
skill.meta.name,
|
||||
skill.meta.category.as_deref().unwrap_or("general"),
|
||||
skill.meta.description
|
||||
));
|
||||
}
|
||||
if self.skills.is_empty() {
|
||||
lines.push("(no skills loaded)".to_string());
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SkillsManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes skill content — resolves templates and variables.
|
||||
pub struct Preprocessor;
|
||||
|
||||
impl Preprocessor {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Resolve ${VAR} placeholders in skill content.
|
||||
pub fn process(&self, content: &str, vars: &HashMap<String, String>) -> String {
|
||||
let mut result = content.to_string();
|
||||
for (key, value) in vars {
|
||||
result = result.replace(&format!("${{{}}}", key), value);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_preprocessor_variable_resolution() {
|
||||
let pp = Preprocessor::new();
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("NAME".to_string(), "Maya".to_string());
|
||||
let result = pp.process("Hello ${NAME}!", &vars);
|
||||
assert_eq!(result, "Hello Maya!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skills_manager_initialization() {
|
||||
let mut sm = SkillsManager::new();
|
||||
sm = sm.with_skill_dir(PathBuf::from("/tmp/test-skills"));
|
||||
let metas = sm.scan();
|
||||
// Should be empty since dir doesn't exist
|
||||
assert!(metas.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preprocessor_no_substitution() {
|
||||
let pp = Preprocessor::new();
|
||||
let vars = HashMap::new();
|
||||
let result = pp.process("No variables here", &vars);
|
||||
assert_eq!(result, "No variables here");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user