From 7efdb9a13f9c2053eba0372fe0c98113a60a1638 Mon Sep 17 00:00:00 2001 From: Logan King Date: Mon, 30 Jun 2025 01:50:32 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=82=20(assistant=5Fv2):=20add=20cl?= =?UTF-8?q?ap=20flag=20for=20push-to-talk=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assistant_v2/Cargo.lock | 11 +++++++++++ assistant_v2/Cargo.toml | 1 + assistant_v2/src/main.rs | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/assistant_v2/Cargo.lock b/assistant_v2/Cargo.lock index 024fa0f..69b110f 100644 --- a/assistant_v2/Cargo.lock +++ b/assistant_v2/Cargo.lock @@ -121,6 +121,7 @@ dependencies = [ "colored", "cpal", "dotenvy", + "easy_rdev_key", "flume", "futures", "hound", @@ -798,6 +799,16 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "easy_rdev_key" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a235c249bd4457a6e7f965fad371abdb77af6be01eeebbc884558051355d04df" +dependencies = [ + "clap", + "rdev", +] + [[package]] name = "either" version = "1.15.0" diff --git a/assistant_v2/Cargo.toml b/assistant_v2/Cargo.toml index 831c5bb..ac8b6e9 100644 --- a/assistant_v2/Cargo.toml +++ b/assistant_v2/Cargo.toml @@ -23,3 +23,4 @@ clap = { version = "4.4.6", features = ["derive"] } colored = "2.0.4" clipboard = "0.5.0" open = "5.3.1" +easy_rdev_key = "0.1.0" diff --git a/assistant_v2/src/main.rs b/assistant_v2/src/main.rs index 1fe6f99..72274b1 100644 --- a/assistant_v2/src/main.rs +++ b/assistant_v2/src/main.rs @@ -8,6 +8,7 @@ use async_openai::{ Client, }; use clap::Parser; +use easy_rdev_key::PTTKey; use clipboard::{ClipboardContext, ClipboardProvider}; use colored::Colorize; use dotenvy::dotenv; @@ -32,6 +33,17 @@ use uuid::Uuid; #[derive(Parser, Debug)] struct Opt { + /// The push-to-talk key used to activate the microphone. + #[arg(long)] + ptt_key: Option, + + /// The push-to-talk key as a special keycode. + /// Use this if you want to use a key that is not supported by the `PTTKey` enum. + /// You can find out what number to pass for your key by running the `ShowKeyPresses` subcommand. + /// This option conflicts with `--ptt-key`. + #[arg(long, conflicts_with = "ptt_key")] + special_ptt_key: Option, + /// How fast the AI speaks. 1.0 is normal speed. #[arg(long, default_value_t = 1.0)] speech_speed: f32, @@ -158,11 +170,24 @@ async fn main() -> Result<(), Box> { let (audio_tx, audio_rx) = flume::unbounded(); let interrupt_flag = Arc::new(AtomicBool::new(false)); + let ptt_key = match opt.ptt_key { + Some(k) => k.into(), + None => match opt.special_ptt_key { + Some(code) => Key::Unknown(code), + None => { + println!( + "No push to talk key specified. Please pass a key using the --ptt-key argument or the --special-ptt-key argument." + ); + return Ok(()); + } + }, + }; start_ptt_thread( audio_tx.clone(), speak_stream.clone(), opt.duck_ptt, interrupt_flag.clone(), + ptt_key, ); loop { @@ -267,12 +292,12 @@ fn start_ptt_thread( speak_stream: Arc>, duck_ptt: bool, interrupt_flag: Arc, + ptt_key: Key, ) { thread::spawn(move || { let mut recorder = rec::Recorder::new(); let tmp_dir = tempdir().unwrap(); let mut key_pressed = false; - let ptt_key = Key::F9; let mut current_path: Option = None; let mut recording_start = Instant::now(); @@ -515,4 +540,10 @@ mod tests { _ => false, })); } + + #[test] + fn parses_ptt_key_flag() { + let opt = Opt::try_parse_from(["test", "--ptt-key", "f9"]).unwrap(); + assert!(matches!(opt.ptt_key, Some(PTTKey::F9))); + } } From f455506954188732bad0c13738a138eb3f6cdd4f Mon Sep 17 00:00:00 2001 From: Logan King Date: Mon, 30 Jun 2025 02:04:00 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=80=20(v2):=20resolve=20merge=20co?= =?UTF-8?q?nflicts=20and=20allow=20custom=20push-to-talk=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assistant_v2/Cargo.lock | 218 ++++++++++++++- assistant_v2/Cargo.toml | 2 + assistant_v2/FEATURE_PROGRESS.md | 9 +- assistant_v2/src/main.rs | 438 +++++++++++++++++++++++++++++++ 4 files changed, 656 insertions(+), 11 deletions(-) diff --git a/assistant_v2/Cargo.lock b/assistant_v2/Cargo.lock index 69b110f..174dbaf 100644 --- a/assistant_v2/Cargo.lock +++ b/assistant_v2/Cargo.lock @@ -122,6 +122,7 @@ dependencies = [ "cpal", "dotenvy", "easy_rdev_key", + "enigo", "flume", "futures", "hound", @@ -129,6 +130,7 @@ dependencies = [ "rdev", "serde_json", "speakstream", + "sysinfo", "tempfile", "tokio", "tracing", @@ -534,7 +536,7 @@ dependencies = [ "block", "core-foundation 0.9.4", "core-graphics 0.21.0", - "foreign-types", + "foreign-types 0.3.2", "libc", "objc", ] @@ -624,7 +626,7 @@ checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" dependencies = [ "bitflags 1.3.2", "core-foundation 0.7.0", - "foreign-types", + "foreign-types 0.3.2", "libc", ] @@ -636,7 +638,31 @@ checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "foreign-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", "libc", ] @@ -680,7 +706,26 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows", + "windows 0.54.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", ] [[package]] @@ -824,6 +869,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enigo" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802e4b2ae123615659085369b453cba87c5562e46ed8050a909fee18a9bc3157" +dependencies = [ + "core-graphics 0.23.2", + "libc", + "objc", + "pkg-config", + "windows 0.51.1", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -908,7 +966,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -917,6 +996,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1653,6 +1738,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2037,6 +2131,26 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rdev" version = "0.5.3" @@ -2424,7 +2538,7 @@ dependencies = [ "tempfile", "tokio", "tracing", - "windows", + "windows 0.54.0", ] [[package]] @@ -2534,6 +2648,20 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.54.0", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -3044,16 +3172,35 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ - "windows-core", + "windows-core 0.54.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -3115,6 +3262,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3137,6 +3299,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3149,6 +3317,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3161,6 +3335,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3179,6 +3359,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3191,6 +3377,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3203,6 +3395,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3215,6 +3413,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/assistant_v2/Cargo.toml b/assistant_v2/Cargo.toml index ac8b6e9..1668d97 100644 --- a/assistant_v2/Cargo.toml +++ b/assistant_v2/Cargo.toml @@ -24,3 +24,5 @@ colored = "2.0.4" clipboard = "0.5.0" open = "5.3.1" easy_rdev_key = "0.1.0" +sysinfo = "0.32.1" +enigo = "0.1.3" diff --git a/assistant_v2/FEATURE_PROGRESS.md b/assistant_v2/FEATURE_PROGRESS.md index 769d2f9..34aff05 100644 --- a/assistant_v2/FEATURE_PROGRESS.md +++ b/assistant_v2/FEATURE_PROGRESS.md @@ -6,18 +6,19 @@ This document tracks which features from the original assistant have been implem | --- | --- | | Screen brightness control | Done | | System volume adjustment (Windows only) | Pending | -| Media playback commands | Pending | +| Media playback commands | Done | | Launch applications from voice | Pending | | Display log files | Pending | -| Get system info | Pending | +| Get system info | Done | | List and kill processes | Pending | | Run internet speed tests | Pending | | Set the clipboard contents | Done | | Timers with alarm sounds | Pending | -| Change voice or speaking speed | Pending | -| Mute/unmute voice output | Pending | +| Change voice or speaking speed | Done | +| Mute/unmute voice output | Done | | Open OpenAI billing page | Done | | Push-to-talk text-to-speech interface | Done | | Interrupt AI speech with push-to-talk | Done | +| Log function invocation names | Done | Update this table as features are migrated and verified to work in `assistant_v2`. diff --git a/assistant_v2/src/main.rs b/assistant_v2/src/main.rs index 72274b1..1c60784 100644 --- a/assistant_v2/src/main.rs +++ b/assistant_v2/src/main.rs @@ -14,6 +14,7 @@ use colored::Colorize; use dotenvy::dotenv; use futures::StreamExt; use open; +use enigo::{Enigo, KeyboardControllable}; use speakstream::ss::SpeakStream; use std::error::Error; use std::path::PathBuf; @@ -30,6 +31,7 @@ use record::rec; use std::thread; use tempfile::tempdir; use uuid::Uuid; +use sysinfo::{Components, Disks, Networks, System}; #[derive(Parser, Debug)] struct Opt { @@ -57,6 +59,36 @@ struct Opt { duck_ptt: bool, } +fn parse_voice(name: &str) -> Option { + match name.to_lowercase().as_str() { + "alloy" => Some(Voice::Alloy), + "ash" => Some(Voice::Ash), + "coral" => Some(Voice::Coral), + "echo" => Some(Voice::Echo), + "fable" => Some(Voice::Fable), + "onyx" => Some(Voice::Onyx), + "nova" => Some(Voice::Nova), + "sage" => Some(Voice::Sage), + "shimmer" => Some(Voice::Shimmer), + _ => None, + } +} + +fn voice_to_str(voice: &Voice) -> &'static str { + match voice { + Voice::Alloy => "alloy", + Voice::Ash => "ash", + Voice::Coral => "coral", + Voice::Echo => "echo", + Voice::Fable => "fable", + Voice::Onyx => "onyx", + Voice::Nova => "nova", + Voice::Sage => "sage", + Voice::Shimmer => "shimmer", + _ => "unknown", + } +} + #[tokio::main] async fn main() -> Result<(), Box> { let opt = Opt::parse(); @@ -127,6 +159,30 @@ async fn main() -> Result<(), Box> { strict: None, } .into(), + FunctionObject { + name: "media_controls".into(), + description: Some("Plays, pauses or seeks media.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": { + "media_button": { + "type": "string", + "enum": [ + "MediaStop", + "MediaNextTrack", + "MediaPlayPause", + "MediaPrevTrack", + "VolumeUp", + "VolumeDown", + "VolumeMute" + ] + } + }, + "required": ["media_button"] + })), + strict: None, + } + .into(), FunctionObject { name: "set_clipboard".into(), description: Some("Sets the clipboard to the given text.".into()), @@ -151,6 +207,87 @@ async fn main() -> Result<(), Box> { strict: None, } .into(), + FunctionObject { + name: "get_system_info".into(), + description: Some("Returns system information like CPU and memory usage.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into(), + FunctionObject { + name: "set_speech_speed".into(), + description: Some( + "Sets how fast the AI voice speaks. Speed must be between 0.5 and 100.".into(), + ), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {"speed": {"type": "number"}}, + "required": ["speed"], + })), + strict: None, + } + .into(), + FunctionObject { + name: "get_speech_speed".into(), + description: Some("Returns the current AI voice speech speed.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into(), + FunctionObject { + name: "mute_speech".into(), + description: Some("Mutes the AI voice output.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into(), + FunctionObject { + name: "unmute_speech".into(), + description: Some("Unmutes the AI voice output.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into(), + FunctionObject { + name: "set_voice".into(), + description: Some( + "Changes the AI speaking voice. Pass one of: alloy, ash, coral, echo, fable, onyx, nova, sage, shimmer.".into(), + ), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {"voice": {"type": "string"}}, + "required": ["voice"], + })), + strict: None, + } + .into(), + FunctionObject { + name: "get_voice".into(), + description: Some("Returns the name of the current AI voice.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into(), ]) .build()?; @@ -347,6 +484,62 @@ fn start_ptt_thread( }); } +fn get_system_info() -> String { + let mut info = String::new(); + let mut sys = System::new_all(); + sys.refresh_all(); + + info.push_str("=> system:\n"); + + let total_memory = sys.total_memory(); + let used_memory = sys.used_memory(); + let total_swap = sys.total_swap(); + let used_swap = sys.used_swap(); + + info.push_str(&format!("total memory: {} bytes\n", total_memory)); + info.push_str(&format!("used memory : {} bytes\n", used_memory)); + info.push_str(&format!("total swap : {} bytes\n", total_swap)); + info.push_str(&format!("used swap : {} bytes\n", used_swap)); + + let system_name = System::name(); + let kernel_version = System::kernel_version(); + let os_version = System::os_version(); + let host_name = System::host_name(); + + info.push_str(&format!("System name: {:?}\n", system_name)); + info.push_str(&format!("System kernel version: {:?}\n", kernel_version)); + info.push_str(&format!("System OS version: {:?}\n", os_version)); + info.push_str(&format!("System host name: {:?}\n", host_name)); + + let nb_cpus = sys.cpus().len(); + info.push_str(&format!("NB CPUs: {}\n", nb_cpus)); + + info.push_str("=> disks:\n"); + let disks = Disks::new_with_refreshed_list(); + for disk in &disks { + info.push_str(&format!("{:?}\n", disk)); + } + + info.push_str("=> networks:\n"); + let networks = Networks::new_with_refreshed_list(); + for (interface_name, data) in &networks { + info.push_str(&format!( + "{}: {} B (down) / {} B (up)\n", + interface_name, + data.total_received(), + data.total_transmitted(), + )); + } + + info.push_str("=> components:\n"); + let components = Components::new_with_refreshed_list(); + for component in &components { + info.push_str(&format!("{:?}\n", component)); + } + + info +} + async fn handle_requires_action( client: Client, run_object: RunObject, @@ -356,6 +549,7 @@ async fn handle_requires_action( if let Some(required_action) = &run_object.required_action { for tool in &required_action.submit_tool_outputs.tool_calls { + println!("{}{}", "Invoking function: ".purple(), tool.function.name); if tool.function.name == "get_current_temperature" { tool_outputs.push(ToolsOutputs { tool_call_id: Some(tool.id.clone()), @@ -408,6 +602,56 @@ async fn handle_requires_action( }); } + if tool.function.name == "media_controls" { + let button = match serde_json::from_str::(&tool.function.arguments) { + Ok(v) => v["media_button"].as_str().unwrap_or("").to_string(), + Err(_) => String::new(), + }; + + let mut enigo = Enigo::new(); + let msg = match button.as_str() { + "MediaStop" => { + enigo.key_click(enigo::Key::MediaStop); + "MediaStop" + } + "MediaNextTrack" => { + enigo.key_click(enigo::Key::MediaNextTrack); + "MediaNextTrack" + } + "MediaPlayPause" => { + enigo.key_click(enigo::Key::MediaPlayPause); + "MediaPlayPause" + } + "MediaPrevTrack" => { + enigo.key_click(enigo::Key::MediaPrevTrack); + enigo.key_click(enigo::Key::MediaPrevTrack); + "MediaPrevTrack" + } + "VolumeUp" => { + for _ in 0..5 { + enigo.key_click(enigo::Key::VolumeUp); + } + "VolumeUp" + } + "VolumeDown" => { + for _ in 0..5 { + enigo.key_click(enigo::Key::VolumeDown); + } + "VolumeDown" + } + "VolumeMute" => { + enigo.key_click(enigo::Key::VolumeMute); + "VolumeMute" + } + _ => "Unknown button", + }; + + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(msg.into()), + }); + } + if tool.function.name == "open_openai_billing" { let result = open::that("https://platform.openai.com/usage"); let msg = match result { @@ -419,6 +663,81 @@ async fn handle_requires_action( output: Some(msg.into()), }); } + + if tool.function.name == "set_speech_speed" { + let speed = match serde_json::from_str::(&tool.function.arguments) { + Ok(v) => v["speed"].as_f64().unwrap_or(1.0) as f32, + Err(_) => 1.0, + }; + let msg = if (0.5..=100.0).contains(&speed) { + speak_stream.lock().unwrap().set_speech_speed(speed); + format!("Speech speed set to {}", speed) + } else { + "Speed must be between 0.5 and 100.0".to_string() + }; + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(msg.into()), + }); + } + + if tool.function.name == "get_speech_speed" { + let speed = speak_stream.lock().unwrap().get_speech_speed(); + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(format!("{}", speed).into()), + }); + } + + if tool.function.name == "mute_speech" { + speak_stream.lock().unwrap().mute(); + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some("AI voice muted".into()), + }); + } + + if tool.function.name == "unmute_speech" { + speak_stream.lock().unwrap().unmute(); + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some("AI voice unmuted".into()), + }); + } + + if tool.function.name == "set_voice" { + let name = match serde_json::from_str::(&tool.function.arguments) { + Ok(v) => v["voice"].as_str().unwrap_or("").to_string(), + Err(_) => String::new(), + }; + let msg = match parse_voice(&name) { + Some(v) => { + speak_stream.lock().unwrap().set_voice(v); + format!("Voice set to {}", name.to_lowercase()) + } + None => "Invalid voice name".to_string(), + }; + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(msg.into()), + }); + } + + if tool.function.name == "get_system_info" { + let info = get_system_info(); + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(info.into()), + }); + } + + if tool.function.name == "get_voice" { + let name = voice_to_str(&speak_stream.lock().unwrap().get_voice()); + tool_outputs.push(ToolsOutputs { + tool_call_id: Some(tool.id.clone()), + output: Some(format!("{}", name).into()), + }); + } } if let Err(e) = submit_tool_outputs(client, run_object, tool_outputs, speak_stream).await { @@ -541,6 +860,125 @@ mod tests { })); } + #[test] + fn includes_set_speech_speed_function() { + let req = CreateAssistantRequestArgs::default() + .instructions("test") + .model("gpt-4o") + .tools(vec![FunctionObject { + name: "set_speech_speed".into(), + description: Some("Sets how fast the AI voice speaks.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {"speed": {"type": "number"}}, + "required": ["speed"], + })), + strict: None, + } + .into()]) + .build() + .unwrap(); + + let tools = req.tools.unwrap(); + assert!(tools.iter().any(|t| match t { + async_openai::types::AssistantTools::Function(f) => + f.function.name == "set_speech_speed", + _ => false, + })); + } + + #[test] + fn includes_media_controls_function() { + let req = CreateAssistantRequestArgs::default() + .instructions("test") + .model("gpt-4o") + .tools(vec![FunctionObject { + name: "media_controls".into(), + description: Some("Plays, pauses or seeks media.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": { + "media_button": { + "type": "string", + "enum": [ + "MediaStop", + "MediaNextTrack", + "MediaPlayPause", + "MediaPrevTrack", + "VolumeUp", + "VolumeDown", + "VolumeMute" + ] + } + }, + "required": ["media_button"] + })), + strict: None, + } + .into()]) + .build() + .unwrap(); + + let tools = req.tools.unwrap(); + assert!(tools.iter().any(|t| match t { + async_openai::types::AssistantTools::Function(f) => + f.function.name == "media_controls", + _ => false, + })); + } + + #[test] + fn includes_mute_speech_function() { + let req = CreateAssistantRequestArgs::default() + .instructions("test") + .model("gpt-4o") + .tools(vec![FunctionObject { + name: "mute_speech".into(), + description: Some("Mutes the AI voice output.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into()]) + .build() + .unwrap(); + + let tools = req.tools.unwrap(); + assert!(tools.iter().any(|t| match t { + async_openai::types::AssistantTools::Function(f) => f.function.name == "mute_speech", + _ => false, + })); + } + + #[test] + fn includes_get_system_info_function() { + let req = CreateAssistantRequestArgs::default() + .instructions("test") + .model("gpt-4o") + .tools(vec![FunctionObject { + name: "get_system_info".into(), + description: Some("Returns system information like CPU and memory usage.".into()), + parameters: Some(serde_json::json!({ + "type": "object", + "properties": {}, + "required": [], + })), + strict: None, + } + .into()]) + .build() + .unwrap(); + + let tools = req.tools.unwrap(); + assert!(tools.iter().any(|t| match t { + async_openai::types::AssistantTools::Function(f) => f.function.name == "get_system_info", + _ => false, + })); + } + #[test] fn parses_ptt_key_flag() { let opt = Opt::try_parse_from(["test", "--ptt-key", "f9"]).unwrap();