167 lines
5.5 KiB
Rust
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")
|
|
}
|