Files

167 lines
5.5 KiB
Rust

//! Nostr kind 31123 skill management for sovereign_browser
//!
//! Skills are PUBLIC Nostr events (kind 31123) that define system prompt
//! templates, LLM parameters, and tool requirements.
//!
//! Port of `agent_skills.c` / `agent_skills.h` from the C project.
//! Uses `nostr_signer` from `rust_core_lib`.
use nostr_signer::traits::NostrSigner;
use std::sync::Arc;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use crate::db;
/// The Nostr kind for skills.
const AGENT_SKILL_KIND: u64 = 31123;
/// db_kv key for the selected skill d-tags.
const AGENT_SKILLS_SELECTED_KEY: &str = "agent.selected_skills";
/// Maximum number of requires_tool tags per skill.
const AGENT_SKILL_MAX_TOOLS: usize = 32;
/// A skill definition.
#[derive(Debug, Clone)]
pub struct AgentSkill {
pub d_tag: String,
pub name: String,
pub description: String,
pub pubkey: String,
pub content: String,
pub requires_tools: Vec<String>,
}
/// Global state for skills.
static G_SKILL_STATE: Lazy<Mutex<SkillState>> = Lazy::new(|| Mutex::new(SkillState::default()));
struct SkillState {
signer: Option<Arc<dyn NostrSigner>>,
pubkey_hex: String,
have_signer: bool,
}
impl Default for SkillState {
fn default() -> Self {
SkillState {
signer: None,
pubkey_hex: String::new(),
have_signer: false,
}
}
}
/// Initialize the skills module.
pub fn agent_skills_init(signer: Option<Arc<dyn NostrSigner>>, pubkey_hex: &str) {
let mut state = G_SKILL_STATE.lock().unwrap();
state.signer = signer;
state.pubkey_hex = pubkey_hex.to_string();
state.have_signer = state.signer.is_some() && !state.pubkey_hex.is_empty();
}
/// Update the signer reference.
pub fn agent_skills_set_signer(signer: Option<Arc<dyn NostrSigner>>, pubkey_hex: &str) {
agent_skills_init(signer, pubkey_hex);
}
/// Fetch kind 31123 skill events from the local SQLite cache.
pub fn agent_skills_fetch() -> Vec<serde_json::Value> {
match db::db_query_events(&[AGENT_SKILL_KIND], None, None, None, None) {
Ok(events) => {
let mut result = Vec::new();
for event in events {
let tags = event["tags"].as_array();
let d_tag = tags.and_then(|t| {
t.iter().find_map(|tag| {
tag.as_array().and_then(|a| {
if a.len() >= 2 && a[0] == "d" {
Some(a[1].as_str().unwrap_or("").to_string())
} else {
None
}
})
})
}).unwrap_or_default();
let name = tags.and_then(|t| {
t.iter().find_map(|tag| {
tag.as_array().and_then(|a| {
if a.len() >= 2 && a[0] == "name" {
Some(a[1].as_str().unwrap_or("").to_string())
} else {
None
}
})
})
}).unwrap_or_default();
let description = tags.and_then(|t| {
t.iter().find_map(|tag| {
tag.as_array().and_then(|a| {
if a.len() >= 2 && a[0] == "description" {
Some(a[1].as_str().unwrap_or("").to_string())
} else {
None
}
})
})
}).unwrap_or_default();
let requires_tools: Vec<String> = tags.map_or(vec![], |t| {
t.iter().filter_map(|tag| {
tag.as_array().and_then(|a| {
if a.len() >= 2 && a[0] == "requires_tool" {
Some(a[1].as_str().unwrap_or("").to_string())
} else {
None
}
})
}).collect()
});
result.push(serde_json::json!({
"d": d_tag,
"name": name,
"description": description,
"requires_tools": requires_tools,
"content": event["content"].as_str().unwrap_or(""),
"pubkey": event["pubkey"].as_str().unwrap_or(""),
}));
}
result
}
Err(_) => Vec::new(),
}
}
/// Get the default Sovereign Browser Skill.
pub fn agent_skills_get_default() -> serde_json::Value {
serde_json::json!({
"d": "",
"name": "Sovereign Browser Skill",
"description": "Default skill for browser automation",
"content": "You are a helpful assistant with access to browser tools.",
"requires_tools": ["browser_navigate", "browser_click", "browser_type", "browser_screenshot"],
"unsaved": true,
"pubkey": "",
})
}
/// Build the combined system prompt from selected skills.
pub fn agent_skills_build_system_prompt(selected_skills: &[String]) -> String {
let skills = agent_skills_fetch();
let mut parts = Vec::new();
for skill in &skills {
let d_tag = skill["d"].as_str().unwrap_or("");
if selected_skills.contains(&d_tag.to_string()) {
if let Some(content) = skill["content"].as_str() {
parts.push(content.to_string());
}
}
}
parts.join("\n\n")
}