diff --git a/Cargo.lock b/Cargo.lock index 25d3f7e..4b09d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -425,6 +437,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -468,6 +492,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1517,6 +1547,7 @@ dependencies = [ name = "own_assist" version = "0.1.0" dependencies = [ + "dialoguer", "env_logger", "log", "ollama-rs", @@ -1524,6 +1555,7 @@ dependencies = [ "own_mcp", "rmcp", "serde", + "thiserror 2.0.18", "tokio", "url", ] @@ -2271,6 +2303,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -2698,6 +2736,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index be4ae6b..62068fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,6 @@ env_logger = "0.11.10" serde = { version = "1.0.228", features = ["derive"] } url = "2.5.8" own_mcp = { path = "own_mcp" } -own_assist_common = { path = "common" } \ No newline at end of file +own_assist_common = { path = "common" } +thiserror = "2.0.18" +dialoguer = "0.12.0" \ No newline at end of file diff --git a/mcp_server_collection/src/datetime.rs b/mcp_server_collection/src/datetime.rs index 8203de7..5563136 100644 --- a/mcp_server_collection/src/datetime.rs +++ b/mcp_server_collection/src/datetime.rs @@ -32,7 +32,7 @@ impl DateTimeHandler { Local::now().iso_week().week().to_string() } - #[tool(description = "return current weekday as 3-character-string (e.g. Mon, Tue, ...)", annotations(read_only_hint = true))] + #[tool(description = "return current weekday", annotations(read_only_hint = true))] fn get_weekday() -> String { Local::now().weekday().to_string() } diff --git a/own_mcp/src/audio/client.rs b/own_mcp/src/audio/client.rs index ebc45e9..0913b23 100644 --- a/own_mcp/src/audio/client.rs +++ b/own_mcp/src/audio/client.rs @@ -1,4 +1,3 @@ -use std::io::ErrorKind; use std::path::Path; use reqwest::Client; use reqwest::multipart::Form; diff --git a/own_mcp/src/mcp/chat.rs b/own_mcp/src/mcp/chat.rs index d8b40a5..f68efc3 100644 --- a/own_mcp/src/mcp/chat.rs +++ b/own_mcp/src/mcp/chat.rs @@ -7,9 +7,10 @@ use ollama_rs::Ollama; use rmcp::model::{CallToolRequestParams, CallToolResult}; use rmcp::ServiceError; use std::collections::HashMap; +use serde::Deserialize; use thiserror::Error; -#[derive(Debug, Copy, Clone, Default, PartialEq)] +#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq)] pub enum ToolPermission { Allowed, #[default] @@ -186,7 +187,7 @@ impl AgentChat { pub async fn message>( &mut self, user_message: String, - permission_request_callback: fn(mcp_server_name: String, tool_name: String) -> C, + mut permission_request_callback: impl FnMut(String, String) -> C, ) -> Result { let all_tools: Vec = self @@ -213,7 +214,7 @@ impl AgentChat { for tool_call in &response.message.tool_calls { log::debug!("calling tool {}", tool_call.function.name); - let result = self.call_tool(tool_call, permission_request_callback).await?; + let result = self.call_tool(tool_call, &mut permission_request_callback).await?; let contents = match result { Some(result) => result.content @@ -256,7 +257,7 @@ impl AgentChat { async fn call_tool>( &self, tool_call: &ollama_rs::generation::tools::ToolCall, - permission_request_callback: fn(mcp_server_name: String, tool_name: String) -> C, + permission_request_callback: &mut impl FnMut(String, String) -> C, ) -> Result, ChatError> { let arguments_json_object = tool_call .function diff --git a/src/config.rs b/src/config.rs index b4df196..a0d7c44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,8 +6,9 @@ use own_mcp::audio::{AudioClient, AudioClientTrait}; use rmcp::model::Implementation; use serde::Deserialize; use url::Url; -use own_mcp::mcp; +use own_mcp::{mcp, AgentChat}; use own_mcp::mcp::{guaranteed_mcp_server_name, MCPClient}; +use own_mcp::mcp::chat::{ChatError, ToolPermission}; use own_assist_common::config_from_file; #[derive(Debug, Deserialize)] @@ -16,7 +17,8 @@ pub struct Config { #[serde(rename = "audio-server")] audio_server: AudioServerConfig, #[serde(rename = "mcp-servers")] - mcp_servers: Vec + mcp_servers: Vec, + permissions: Option>, } #[derive(Debug, Deserialize)] @@ -75,6 +77,20 @@ impl Config { mcp_clients } + + pub async fn set_tool_permissions(&self, agent_chat: &mut AgentChat) -> Result<(), ChatError> { + if let Some(permissions) = &self.permissions && permissions.len() > 0 { + for permission_config in permissions { + log::info!("set permission for `{}` to {:?}", permission_config.tool_name, permission_config.permission); + agent_chat.set_permission(permission_config.tool_name.clone(), permission_config.permission.clone())?; + } + } + else { + log::debug!("no permissions were provided.") + } + + Ok(()) + } } #[derive(Debug, Deserialize)] @@ -97,4 +113,11 @@ pub struct MCPServerConfig { pub name: Option, pub url: Url, pub authorization: Option, // should probably implement oauth some time // actually, fuck oauth +} + +#[derive(Debug, Deserialize)] +pub struct PermissionConfig { + #[serde(rename = "tool-name")] + pub tool_name: String, + pub permission: ToolPermission } \ No newline at end of file diff --git a/src/human_interface/cli.rs b/src/human_interface/cli.rs new file mode 100644 index 0000000..ffeb380 --- /dev/null +++ b/src/human_interface/cli.rs @@ -0,0 +1,38 @@ +use crate::human_interface::{HumanInterface, HumanInterfaceError}; +use dialoguer::theme::ColorfulTheme; +use dialoguer::{Confirm, Input}; +use own_mcp::mcp::chat::PermissionAnswer; + +pub struct CommandLine; + +impl HumanInterface for CommandLine { + fn agent_message(&self, message: String) { + println!("ai: {}", message); + } + + async fn expect_user_message(&self) -> Result { + let user_message: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("you") + .interact_text() + .map_err(|e| HumanInterfaceError::IoError(e.into()))?; + Ok(user_message) + } + + async fn ask_for_permission(&self, + mcp_server_name: String, + tool_name: String, + ) -> Result { + 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> { + Ok(()) + } +} diff --git a/src/human_interface/mod.rs b/src/human_interface/mod.rs new file mode 100644 index 0000000..6c7118d --- /dev/null +++ b/src/human_interface/mod.rs @@ -0,0 +1,22 @@ +pub mod cli; + +use thiserror::Error; +use own_mcp::mcp::chat::PermissionAnswer; + +#[derive(Debug, Error)] +pub enum HumanInterfaceError { + #[error(transparent)] + IoError(std::io::Error), + #[error("other error in human interface: {0}")] + Other(String) +} + +pub trait HumanInterface { + fn agent_message(&self, message: String); + + fn expect_user_message(&self) -> impl Future>; + + fn ask_for_permission(&self, mcp_server_name: String, tool_name:String) -> impl Future>; + + fn run(&self) -> impl Future>; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index da5a5c8..305ae7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,12 @@ mod config; mod model; +mod human_interface; use own_mcp::audio::{AudioClientTrait}; use own_mcp::mcp::chat::{PermissionAnswer, ToolPermission}; use crate::config::Config; use crate::model::create_model_from_config; - -async fn permission_callback(mcp_server_name: String, tool_name:String) -> PermissionAnswer { - PermissionAnswer::Denied // TODO: implement -} +use crate::human_interface::HumanInterface; #[tokio::main] async fn main() { @@ -19,45 +17,45 @@ async fn main() { std::process::exit(1); }).unwrap(); - let audio_client = config.audio_client(); + /*let audio_client = config.audio_client(); log::debug!("Audio server status {:?}", audio_client.status().await.inspect_err(|e|{ log::error!("audio server error {}", e); std::process::exit(1); - }).unwrap()); + }).unwrap());*/ let ollama = config.ollama_instance(); - - let mcp_clients = config.mcp_clients().await; - 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(); - let 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 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). \ + 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.".to_string(); + let mcp_clients = config.mcp_clients().await; + let mut agent_chat = own_mcp::AgentChat::new(ollama, model_name.clone(), mcp_clients, system_prompt).await.inspect_err( |e| { log::error!("error creating agent: {}", e); std::process::exit(1); } ).unwrap(); + config.set_tool_permissions(&mut agent_chat).await.unwrap(); log::info!("all tools: {:#?}", agent_chat.get_all_tools().collect::>()); - agent_chat.set_permission("datetime:get_weekday".to_string(), ToolPermission::Allowed).unwrap(); - agent_chat.set_permission("datetime:get_utc_datetime".to_string(), ToolPermission::Allowed).unwrap(); - agent_chat.set_permission("datetime:get_local_datetime".to_string(), ToolPermission::Allowed).unwrap(); - agent_chat.set_permission("datetime:get_week".to_string(), ToolPermission::Allowed).unwrap(); + let human_interface = human_interface::cli::CommandLine{}; + human_interface.run().await.unwrap(); - let answer = agent_chat.message("In welcher Kalenderwoche befinden wir uns gerade?".to_string(), permission_callback).await; - - dbg!(answer); + 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"); + human_interface.agent_message(agent_message.content); + } }