use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActionSpec {
pub id: &'static str,
pub description: &'static str,
}
#[derive(Debug, Clone)]
pub struct ActionRegistry {
actions: Vec<ActionSpec>,
}
impl ActionRegistry {
pub fn builtin() -> Self {
Self {
actions: vec![
ActionSpec {
id: "open_flashcards",
description: "Navigate to the user's flashcards",
},
ActionSpec {
id: "show_notes",
description: "Show the user's notes",
},
ActionSpec {
id: "open_pdfs",
description: "Navigate to the user's PDF documents",
},
ActionSpec {
id: "go_to_settings",
description: "Open application settings",
},
ActionSpec {
id: "start_revision",
description: "Begin a revision session",
},
ActionSpec {
id: "open_last_document",
description: "Open the last document the user worked on",
},
],
}
}
pub fn new(actions: Vec<ActionSpec>) -> Self {
Self { actions }
}
pub fn actions(&self) -> &[ActionSpec] {
&self.actions
}
pub fn get(&self, id: &str) -> Option<&ActionSpec> {
self.actions.iter().find(|action| action.id == id)
}
pub fn ids_are_unique(&self) -> bool {
let mut seen = std::collections::HashSet::new();
self.actions.iter().all(|action| seen.insert(action.id))
}
pub fn tools_json(&self) -> String {
let tools: Vec<Value> = self
.actions
.iter()
.map(|action| {
json!({
"name": action.id,
"description": action.description,
"parameters": {
"type": "object",
"properties": {}
}
})
})
.collect();
let json = serde_json::to_string(&tools).expect("tool schema serializes");
debug_assert!(json.contains("\"name\":\"open_flashcards\""));
json
}
}
impl Default for ActionRegistry {
fn default() -> Self {
Self::builtin()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_registry_covers_the_plan_actions() {
let registry = ActionRegistry::default();
assert!(registry.ids_are_unique());
for id in [
"open_flashcards",
"show_notes",
"open_pdfs",
"go_to_settings",
"start_revision",
"open_last_document",
] {
assert!(registry.get(id).is_some(), "missing action {id}");
}
}
#[test]
fn tools_json_is_a_valid_needle_schema_array() {
let registry = ActionRegistry::default();
let json = registry.tools_json();
let tools: Vec<Value> = serde_json::from_str(&json).expect("valid JSON array");
assert_eq!(tools.len(), registry.actions().len());
let first = &tools[0];
assert!(first["name"].is_string());
assert!(first["description"].is_string());
assert_eq!(first["parameters"]["type"], "object");
}
#[test]
fn tools_json_puts_name_before_description() {
let registry = ActionRegistry::default();
let json = registry.tools_json();
let first_tool = json.split('{').nth(1).unwrap_or_default();
let name_pos = first_tool.find("\"name\"").expect("name key");
let description_pos = first_tool.find("\"description\"").expect("description key");
assert!(
name_pos < description_pos,
"expected \"name\" before \"description\", got: {first_tool}"
);
}
}