diff --git a/Cargo.lock b/Cargo.lock index 4b09d95..df63387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -1053,6 +1093,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1547,8 +1600,12 @@ dependencies = [ name = "own_assist" version = "0.1.0" dependencies = [ + "base64", + "clap", + "console", "dialoguer", "env_logger", + "indicatif", "log", "ollama-rs", "own_assist_common", @@ -2748,6 +2805,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 62068fe..aa0a5d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,8 @@ url = "2.5.8" own_mcp = { path = "own_mcp" } own_assist_common = { path = "common" } thiserror = "2.0.18" -dialoguer = "0.12.0" \ No newline at end of file +dialoguer = "0.12.0" +indicatif = "0.18.4" +console = "0.16.3" +clap = { version = "4.6.0", features = ["derive"] } +base64 = "0.22.1" \ No newline at end of file diff --git a/src/human_interface/cli.rs b/src/human_interface/cli.rs index ffeb380..1eb71b9 100644 --- a/src/human_interface/cli.rs +++ b/src/human_interface/cli.rs @@ -1,20 +1,47 @@ +use std::time::Duration; +use console::style; +use crate::translate; use crate::human_interface::{HumanInterface, HumanInterfaceError}; use dialoguer::theme::ColorfulTheme; use dialoguer::{Confirm, Input}; use own_mcp::mcp::chat::PermissionAnswer; +use crate::tlt; -pub struct CommandLine; +pub struct CommandLine { + progress_bar: Option, +} -impl HumanInterface for CommandLine { - fn agent_message(&self, message: String) { - println!("ai: {}", message); +impl CommandLine { + pub fn new() -> CommandLine { + CommandLine { progress_bar: None } } - async fn expect_user_message(&self) -> Result { + fn spinner() -> indicatif::ProgressBar { + indicatif::ProgressBar::new_spinner() + } +} + +impl HumanInterface for CommandLine { + fn agent_message(&mut self, message: String) { + // stopped waiting for message + if let Some(progress_bar) = &self.progress_bar { + progress_bar.finish_and_clear(); + self.progress_bar = None; + } + + println!("{}: {}",style( tlt!("assistant")).bold().italic(),message); + } + + async fn expect_user_message(&mut self) -> Result { let user_message: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("you") + .with_prompt(tlt!("you")) .interact_text() .map_err(|e| HumanInterfaceError::IoError(e.into()))?; + + // waiting for agent response + self.progress_bar = Some(Self::spinner()); + self.progress_bar.clone().unwrap().enable_steady_tick(Duration::from_millis(100)); + Ok(user_message) } @@ -22,17 +49,23 @@ impl HumanInterface for CommandLine { mcp_server_name: String, tool_name: String, ) -> Result { + if let Some(progress_bar) = &self.progress_bar { + progress_bar.finish_and_clear(); + } + let confirmation = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(format!("Allow usage of {mcp_server_name}:{tool_name}")) .interact() .map_err(|e| HumanInterfaceError::IoError(e.into()))?; + match confirmation { true => Ok(PermissionAnswer::Granted), false => Ok(PermissionAnswer::Denied), } } - async fn run(&self) -> Result<(), HumanInterfaceError> { + async fn run(&mut self) -> Result<(), HumanInterfaceError> { Ok(()) } } + diff --git a/src/human_interface/mod.rs b/src/human_interface/mod.rs index 6c7118d..3674314 100644 --- a/src/human_interface/mod.rs +++ b/src/human_interface/mod.rs @@ -12,11 +12,11 @@ pub enum HumanInterfaceError { } pub trait HumanInterface { - fn agent_message(&self, message: String); + fn agent_message(&mut self, message: String); - fn expect_user_message(&self) -> impl Future>; + fn expect_user_message(&mut self) -> impl Future>; fn ask_for_permission(&self, mcp_server_name: String, tool_name:String) -> impl Future>; - fn run(&self) -> impl Future>; + fn run(&mut self) -> impl Future>; } \ No newline at end of file diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs index e3a3dd4..08ef250 100644 --- a/src/i18n/mod.rs +++ b/src/i18n/mod.rs @@ -47,7 +47,7 @@ fn format_dynamically(mut template: String, arguments: Vec>) - } pub fn translate(template_name: String, arguments: Vec>) -> String { - let locale: Locale = LOCALE_SELECTION.read().unwrap().clone(); + let locale: Locale = *LOCALE_SELECTION.read().unwrap(); let languages = load_languages(); let translation = languages .get(&locale) diff --git a/src/i18n/translations/de.json b/src/i18n/translations/de.json index 6d3e0f8..7df9fdf 100644 --- a/src/i18n/translations/de.json +++ b/src/i18n/translations/de.json @@ -1,5 +1,9 @@ { "translations": { - "system_prompt": "Du bist ein Assistent, der per Sprache bedient wird. Du erhälst die Transkription und dein Output wird per Sprache ausgegeben und sollte dementsprechend auch kurz sein und kein Markdown enthalten. Wichtiger als deine Antworten sind deine Aktionen. Nutze bitte die tools, falls du sie brauchst um Informationen zu bekommen (z.B. über das aktuelle Datum oder den aktuellen Wochentag). Du bist in einem Agent Loop und kannst mehrere Tools hintereinander nutzen. Falls der Nutzer das Nutzen eines Tools ablehnt, sag ihm bescheid, dass du es brauchst." + "system_prompt": "Du bist ein Assistent, der per Sprache bedient wird. Du erhälst die Transkription und dein Output wird per Sprache ausgegeben und sollte dementsprechend auch kurz sein und kein Markdown enthalten. Wichtiger als deine Antworten sind deine Aktionen. Nutze bitte die tools, falls du sie brauchst um Informationen zu bekommen (z.B. über das aktuelle Datum oder den aktuellen Wochentag). Du bist in einem Agent Loop und kannst mehrere Tools hintereinander nutzen. Falls der Nutzer das Nutzen eines Tools ablehnt, sag ihm bescheid, dass du es brauchst.", + "you": "Du", + "assistant": "Assistent", + "username": "Nutzername", + "password": "Passwort" } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7d5df37..63a6066 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,56 @@ mod config; -mod model; mod human_interface; mod i18n; +mod model; - -use crate::i18n::translate; -use own_mcp::audio::{AudioClientTrait}; use crate::config::Config; -use crate::model::create_model_from_config; use crate::human_interface::HumanInterface; +use crate::i18n::translate; +use crate::model::create_model_from_config; +use clap::Parser; +use std::fmt::Display; +use base64::Engine; + +/// partial mcp client with cli and voice interaction +#[derive(Parser, Debug)] +#[command(version, long_about = None)] +struct Args { + /// generate an encoded basic auth header + #[clap(long, short, action)] + basic_auth: bool, +} + +fn basic_auth_tool() { + let username: String = dialoguer::Input::new() + .with_prompt(tlt!("username")) + .interact_text() + .unwrap(); + + let password: String = dialoguer::Password::new() + .with_prompt(tlt!("password")) + .interact() + .unwrap(); + + let encoded= base64::prelude::BASE64_STANDARD.encode(format!("{}:{}", username, password)); + println!("Basic {}", encoded); +} #[tokio::main] async fn main() { + let args = Args::parse(); + if args.basic_auth { + basic_auth_tool(); + return; + } + env_logger::init(); - let config = Config::from_file().inspect_err(|e|{ - log::error!("error loading assist.toml: {}", e); - std::process::exit(1); - }).unwrap(); + let config = Config::from_file() + .inspect_err(|e| { + log::error!("error loading assist.toml: {}", e); + std::process::exit(1); + }) + .unwrap(); /*let audio_client = config.audio_client(); log::debug!("Audio server status {:?}", audio_client.status().await.inspect_err(|e|{ @@ -28,34 +61,60 @@ async fn main() { let ollama = config.ollama_instance(); let model_name = &config.ollama_config().model.name; - create_model_from_config(&ollama, &config.ollama_config().model).await.inspect_err(|e|{ - log::error!("failed creating ollama model {model_name}: {e}"); - std::process::exit(1); - }).unwrap(); + create_model_from_config(&ollama, &config.ollama_config().model) + .await + .inspect_err(exit_msg!(format!( + "failed creating ollama model {model_name}" + ))) + .unwrap(); - let mcp_clients = config.mcp_clients().await.inspect_err(|e|{ - log::error!("failed creating MCP clients: {e}"); - std::process::exit(1); - }).unwrap(); + let mcp_clients = config + .mcp_clients() + .await + .inspect_err(exit_msg!("failed creating MCP clients")) + .unwrap(); - let mut agent_chat = own_mcp::AgentChat::new(ollama, model_name.clone(), mcp_clients).await.inspect_err( - |e| { - log::error!("error creating agent: {}", e); - std::process::exit(1); - } - ).unwrap(); + let mut agent_chat = own_mcp::AgentChat::new(ollama, model_name.clone(), mcp_clients) + .await + .inspect_err(exit_msg!("failed to create agent")) + .unwrap(); config.set_tool_permissions(&mut agent_chat).await.unwrap(); - log::info!("all tools: {:#?}", agent_chat.get_all_tools().collect::>()); + log::info!( + "all tools: {:#?}", + agent_chat.get_all_tools().collect::>() + ); - let human_interface = human_interface::cli::CommandLine{}; + let mut human_interface = human_interface::cli::CommandLine::new(); human_interface.run().await.unwrap(); loop { let user_message = human_interface.expect_user_message().await.unwrap(); - let agent_message = agent_chat.message(user_message, async |mcp_server_name, tool_name|{ - human_interface.ask_for_permission(mcp_server_name, tool_name).await.expect("io should not fail") - }).await.expect("failed communicating with agent"); + let immutable_interface = &human_interface; + let agent_message = agent_chat + .message(user_message, async |mcp_server_name, tool_name| { + immutable_interface + .ask_for_permission(mcp_server_name, tool_name) + .await + .inspect_err(exit_msg!("Human interface error")) + .unwrap() + }) + .await + .inspect_err(exit_msg!("failed communicating with agent")) + .unwrap(); human_interface.agent_message(agent_message.content); } } + +fn exit_with_error_message(error: impl Display, message: impl Display) { + log::error!("{message}: {error}"); + println!("{}", message); // makes sure that message is printed even if logging is deactivated + std::process::exit(1); +} + +#[macro_export] +macro_rules! exit_msg { + ($message:expr) => { + |e| exit_with_error_message(e, $message) + }; +}