improved error handling

introduced toml config
This commit is contained in:
milan
2026-04-06 13:24:37 +01:00
parent 6bfadfeba7
commit 2dbac8a91a
7 changed files with 352 additions and 92 deletions
+92
View File
@@ -0,0 +1,92 @@
use std::collections::HashMap;
use log::error;
use ollama_rs::headers::{HeaderMap, HeaderValue};
use ollama_rs::Ollama;
use rmcp::model::Implementation;
use serde::Deserialize;
use thiserror::Error;
use url::Url;
use crate::mcp;
use crate::mcp::{guaranteed_mcp_server_name, MCPClient};
#[derive(Debug, Deserialize)]
pub struct Config {
ollama: OllamaConfig,
#[serde(rename = "mcp-servers")]
mcp_servers: Vec<MCPServerConfig>
}
#[derive(Debug, Error)]
pub enum ConfigLoadingError{
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
ParseError(#[from] toml::de::Error),
}
impl Config {
pub async fn from_file() -> Result<Self, ConfigLoadingError> {
let toml_string = tokio::fs::read_to_string("assist.toml").await?;
Ok(toml::from_str(&toml_string)?)
}
pub fn ollama_config(&self) -> &OllamaConfig {
&self.ollama
}
pub fn ollama_instance(&self) -> Ollama {
let ollama_default_url = Url::parse("http://127.0.0.1:11434").unwrap();
let mut ollama_headers = HeaderMap::new();
if let Some(authorization_header) = &self.ollama.authorization {
ollama_headers.append("Authorization", HeaderValue::from_str(&authorization_header).unwrap());
}
let mut ollama = Ollama::from_url(self.ollama.url.clone().unwrap_or(ollama_default_url));
ollama.set_headers(Some(ollama_headers));
ollama
}
pub async fn mcp_clients(&self) -> HashMap<String, MCPClient> {
let mut mcp_clients: HashMap<String, MCPClient> = HashMap::new();
for mcp_server in &self.mcp_servers {
let client = mcp::get_client(
mcp_server.url.as_str(),
mcp_server.authorization.clone(),
Implementation::new("OwnAssist", "0.0.1"),
).await.unwrap();
let server_name = guaranteed_mcp_server_name(mcp_server.name.clone(), &client);
let server_data = client;
mcp_clients.insert(server_name.clone(), server_data);
}
mcp_clients
}
}
#[derive(Debug, Deserialize)]
pub struct OllamaConfig {
pub url: Option<Url>,
pub model: OllamaModelConfig,
pub authorization: Option<String>, // TODO: implement
}
#[derive(Debug, Deserialize)]
pub struct OllamaModelConfig {
pub from_model: String,
pub name: String,
pub context_size: Option<u64>,
pub temperature: Option<f32>,
}
#[derive(Debug, Deserialize)]
pub struct MCPServerConfig {
pub name: Option<String>,
pub url: Url,
pub authorization: Option<String>, // should probably implement oauth some time
}
+36 -65
View File
@@ -1,76 +1,46 @@
pub mod mcp;
mod config;
mod model;
use std::collections::HashMap;
use chrono::Datelike;
use ollama_rs::Ollama;
use ollama_rs::generation::chat::ChatMessage;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use rmcp::model::{Implementation, InitializedNotificationMethod};
/// Get the current local datetime as an iso string
///
/// # Arguments
///
/// returns: Result<String, Box<dyn Error+Sync+Send, Global>>
///
/// # Examples
///
/// ```
///
/// ```
#[ollama_rs::function]
async fn get_current_iso_datetime() -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
use chrono::prelude::*;
let datetime_iso = Local::now().format("%+").to_string();
println!("iso time requested: {datetime_iso}");
Ok(datetime_iso)
}
/// Get the current weekday as a string in english
///
/// # Arguments
///
/// returns: Result<String, Box<dyn Error+Sync+Send, Global>>
#[ollama_rs::function]
async fn get_current_weekday() -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
let weekday = chrono::offset::Local::now().weekday().to_string();
println!("Weekday requested: {weekday}");
Ok(weekday)
}
/// creates a new task to be completed at given timestamp
///
/// # Arguments
///
/// * `name`: a short name of the task
/// * `description`: optional description, keep empty if not needed
/// * `iso_datetime`: must be in ISO 8601, specifies when user should be reminded of task. choose 15:00 if time not specified
///
/// returns: Empty String
///
/// # Examples
///
/// ```
/// create_task("Müll rausbringen".to_string(), None, "2026-11-08T16:23:47.947244200+02:00")
/// ```
#[ollama_rs::function]
async fn create_task(
name: String,
description: Option<String>,
iso_datetime: String,
) -> Result<String, Box<dyn std::error::Error + Sync + Send>> {
println!("lol, {name}, {description:?}, {iso_datetime}");
Ok(String::new())
}
use reqwest::header::{HeaderMap, HeaderValue};
use rmcp::model::{Implementation};
use url::Url;
use crate::config::Config;
use crate::mcp::chat::MCPServerData;
use crate::mcp::guaranteed_mcp_server_name;
use crate::model::create_model_from_config;
#[tokio::main]
async fn main() {
env_logger::init();
let config = Config::from_file().await.inspect_err(|e|{
log::error!("error loading assist.toml {}", e);
std::process::exit(1);
}).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}")
}).unwrap();
let mut agent_chat = mcp::chat::AgentChat::new(ollama, model_name.clone(), mcp_clients).await.inspect_err(
|e| {
log::error!("error creating agent: {}", e);
}
);
log::debug!("chat: {:#?}", agent_chat);
/*
let ollama = Ollama::default();
let fetch_client = mcp::get_client(
@@ -83,9 +53,10 @@ async fn main() {
("fetch".to_string(), fetch_client),
]);
let mut agent_chat = mcp::chat::AgentChat::new(ollama, mcp_clients).await.unwrap();
let mut agent_chat = mcp::chat::AgentChat::new(ollama, "ollama:e2b".to_string(), mcp_clients).await.unwrap();
log::debug!("chat: {:#?}", agent_chat);
agent_chat.message(String::from("Was ist auf der Website https://uno.mboemer.com zu finden?")).await.unwrap();
*/
/*let ollama = Ollama::default();
let mut history = vec![
+74 -21
View File
@@ -1,13 +1,14 @@
use std::collections::HashMap;
use ollama_rs::error::OllamaError;
use crate::mcp::MCPClient;
use ollama_rs::Ollama;
use ollama_rs::error::OllamaError;
use ollama_rs::generation::chat::ChatMessage;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::tools::ToolInfo;
use rmcp::model::{CallToolRequestParams, CallToolResult};
use rmcp::ServiceError;
use serde::__private228::de::content_as_str;
use rmcp::model::{CallToolRequestParams, CallToolResult, };
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Copy, Clone, Default)]
pub enum ToolPermission {
@@ -52,13 +53,27 @@ impl MCPServerData {
#[derive(Debug)]
pub struct AgentChat {
ollama_client: Ollama,
model: String,
mcp_servers: HashMap<String, MCPServerData>,
message_history: Vec<ChatMessage>,
}
#[derive(Debug, Error)]
pub enum ChatError {
#[error(transparent)]
OllamaError(#[from] OllamaError),
#[error(transparent)]
ServiceError(#[from] ServiceError),
#[error("the function name could not be parsed")]
FunctionParseError(String),
#[error("error parsing provided arguments")]
ArgumentParsingError,
}
impl AgentChat {
pub async fn new(
ollama_client: Ollama,
model: String,
mcp_clients: HashMap<String, MCPClient>,
) -> Result<Self, ServiceError> {
let history = vec![
@@ -78,6 +93,7 @@ impl AgentChat {
Ok(Self {
ollama_client,
model,
mcp_servers: servers,
message_history: history,
})
@@ -95,19 +111,17 @@ impl AgentChat {
all_tools
}
pub async fn message(&mut self, user_message: String) -> Result<(), OllamaError> {
pub async fn message(&mut self, user_message: String) -> Result<(), ChatError> {
let all_tools = self.get_all_tools();
log::debug!("all tools: {:#?}", all_tools);
let mut response = self.ollama_client
let mut response = self
.ollama_client
.send_chat_messages_with_history(
&mut self.message_history,
ChatMessageRequest::new(
"gemma4:e2b".to_string(),
vec![ChatMessage::user(user_message)],
)
.tools(all_tools),
ChatMessageRequest::new(self.model.clone(), vec![ChatMessage::user(user_message)])
.tools(all_tools.clone()),
)
.await?;
@@ -119,31 +133,70 @@ 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).await.unwrap(); // FIXME: nuh uh
let contents = result.content.iter().filter_map(|content| content.as_text()).map(|text_content|text_content.text.clone()).collect::<Vec<_>>();
let result = self.call_tool(tool_call).await?;
let contents = result
.content
.iter()
.filter_map(|content| content.as_text())
.map(|text_content| text_content.text.clone())
.collect::<Vec<_>>();
log::debug!("contents: {:#?}", contents);
self.message_history.push(ChatMessage::tool(contents.join("\n")));
self.message_history
.push(ChatMessage::tool(contents.join("\n")));
}
response = self.ollama_client.send_chat_messages_with_history(&mut self.message_history, ChatMessageRequest::new("gemma4:e2b".to_string(), Vec::new())).await?;
response = self
.ollama_client
.send_chat_messages_with_history(
&mut self.message_history,
ChatMessageRequest::new(self.model.clone(), Vec::new()).tools(all_tools.clone()),
)
.await?;
log::debug!("received message: {:#?}", response.message);
}
Ok(())
}
async fn call_tool(&self, tool_call: &ollama_rs::generation::tools::ToolCall) -> Result<CallToolResult, ServiceError> { // TODO: replace ServiceError handling with something more flexible
let arguments_json_object = tool_call.function.arguments.as_object().unwrap(); // FIXME: remove unwrap
async fn call_tool(
&self,
tool_call: &ollama_rs::generation::tools::ToolCall,
) -> Result<CallToolResult, ChatError> {
let arguments_json_object = tool_call
.function
.arguments
.as_object()
.ok_or(ChatError::ArgumentParsingError)
.inspect_err(|_| log::error!("arguments are not of type object"))?;
log::trace!("arguments_json_object: {:?}", arguments_json_object);
let (mcp_name, function_name) = tool_call.function.name.split_once("::").unwrap(); // FIXME: this is not how we handle errors in rust
let (mcp_name, function_name) =
tool_call
.function
.name
.split_once("::")
.ok_or(ChatError::FunctionParseError(
tool_call.function.name.clone(),
))?;
let (mcp_name, function_name) = (mcp_name.to_string(), function_name.to_string());
let request_params = CallToolRequestParams::new(function_name).with_arguments(arguments_json_object.clone());
let request_params =
CallToolRequestParams::new(function_name).with_arguments(arguments_json_object.clone());
log::debug!("calling tool {} with request_params: {:#?}", tool_call.function.name, request_params);
self.mcp_servers.get(&mcp_name).unwrap().client.call_tool(request_params).await
log::debug!(
"calling tool {} with request_params: {:#?}",
tool_call.function.name,
request_params
);
Ok(self
.mcp_servers
.get(&mcp_name)
.unwrap()
.client
.call_tool(request_params)
.await?)
}
}
+24 -1
View File
@@ -13,7 +13,7 @@ use rmcp::{
};
use std::sync::Arc;
type MCPClient = RunningService<RoleClient, InitializeRequestParams>;
pub type MCPClient = RunningService<RoleClient, InitializeRequestParams>;
pub(crate) async fn get_client(
uri: impl Into<Arc<str>>,
@@ -33,4 +33,27 @@ pub(crate) async fn get_client(
client_info.serve(transport).await.inspect_err(|e| {
log::error!("client error: {:?}", e);
})
}
fn generate_random_mcp_server_name() -> String {
use rand::distr::Alphanumeric;
use rand::RngExt;
let mut rng = rand::rng();
(0..5)
.map(|_| rng.sample(Alphanumeric) as char)
.collect()
}
pub fn guaranteed_mcp_server_name(user_specified_name: Option<String>, client: &MCPClient) -> String {
if let Some(user_specified_name) = user_specified_name {
user_specified_name
} else if let Some(peer_info) = client.peer_info() {
peer_info.server_info.name.clone()
} else {
let random_name = generate_random_mcp_server_name();
log::warn!("no name was specified by the user or the client, so a random name had to be generated: `{}` for {:#?}", random_name, client);
random_name
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::config::OllamaModelConfig;
use ollama_rs::Ollama;
use ollama_rs::error::OllamaError;
use ollama_rs::models::ModelOptions;
use ollama_rs::models::create::{CreateModelRequest, CreateModelStatus};
pub async fn create_model_from_config(
ollama: &Ollama,
config: &OllamaModelConfig,
) -> Result<CreateModelStatus, OllamaError> {
let model_options = ModelOptions::default()
.num_ctx(config.context_size.unwrap_or(2048))// 2048 is the ollama default.
.temperature(config.temperature.unwrap_or(0.8)); // 0.8 is the ollama default
ollama
.create_model(
CreateModelRequest::new(config.name.clone())
.from_model(config.from_model.clone())
.parameters(model_options),
)
.await
}