| 1 | //! SplitFire's tools: the music theory engine, audio analysis and the separation library. |
| 2 | //! Workspace files and shell, MCP and approval come from `ed-acp`. |
| 3 | |
| 4 | pub mod audio; |
| 5 | pub mod library; |
| 6 | pub mod theory; |
| 7 | |
| 8 | use agent_client_protocol::schema::v1::{ToolCallLocation, ToolKind}; |
| 9 | use anyhow::{Result, anyhow}; |
| 10 | use async_trait::async_trait; |
| 11 | use serde_json::Value; |
| 12 | |
| 13 | pub use ed_acp::{ |
| 14 | DescribeCtx, ToolCtx, ToolOutcome, Toolset, absolutize, function_def, resolve_in, |
| 15 | }; |
| 16 | |
| 17 | /// The music tools as one [`Toolset`]: six `theory_*` tools, `analyze_audio` and |
| 18 | /// `list_separations`. All are read-only and never ask for approval. |
| 19 | #[derive(Debug, Default, Clone, Copy)] |
| 20 | pub struct MusicTools; |
| 21 | |
| 22 | #[async_trait] |
| 23 | impl Toolset for MusicTools { |
| 24 | fn definitions(&self) -> Vec<Value> { |
| 25 | let mut defs = theory::definitions(); |
| 26 | defs.push(audio::definition()); |
| 27 | defs.push(library::definition()); |
| 28 | defs |
| 29 | } |
| 30 | |
| 31 | fn handles(&self, name: &str) -> bool { |
| 32 | name.starts_with("theory_") || name == audio::NAME || name == library::NAME |
| 33 | } |
| 34 | |
| 35 | fn describe( |
| 36 | &self, |
| 37 | name: &str, |
| 38 | args: &Value, |
| 39 | ctx: &DescribeCtx<'_>, |
| 40 | ) -> (String, ToolKind, Vec<ToolCallLocation>) { |
| 41 | if name.starts_with("theory_") { |
| 42 | return (theory::title(name, args), ToolKind::Think, vec![]); |
| 43 | } |
| 44 | if name == library::NAME { |
| 45 | return ("List separated songs".to_string(), ToolKind::Search, vec![]); |
| 46 | } |
| 47 | let path = args.get("path").and_then(Value::as_str); |
| 48 | let loc = path |
| 49 | .map(|p| absolutize(&resolve_in(ctx.cwd, p))) |
| 50 | .map(ToolCallLocation::new) |
| 51 | .into_iter() |
| 52 | .collect(); |
| 53 | ( |
| 54 | format!("Analyze {}", path.unwrap_or("audio file")), |
| 55 | ToolKind::Read, |
| 56 | loc, |
| 57 | ) |
| 58 | } |
| 59 | |
| 60 | async fn execute( |
| 61 | &self, |
| 62 | ctx: &ToolCtx, |
| 63 | _tool_call_id: &str, |
| 64 | name: &str, |
| 65 | args: Value, |
| 66 | ) -> Result<ToolOutcome> { |
| 67 | if name.starts_with("theory_") { |
| 68 | theory::execute(name, args) |
| 69 | } else if name == audio::NAME { |
| 70 | audio::execute(ctx, args).await |
| 71 | } else if name == library::NAME { |
| 72 | library::execute(ctx, args).await |
| 73 | } else { |
| 74 | Err(anyhow!("unknown tool `{name}`")) |
| 75 | } |
| 76 | } |
| 77 | } |