restructure into multiple crates
add mcp server collection
This commit is contained in:
+8
-17
@@ -1,13 +1,13 @@
|
||||
use own_assist_common::config_loader::ConfigLoadingError;
|
||||
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};
|
||||
use own_mcp::mcp;
|
||||
use own_mcp::mcp::{guaranteed_mcp_server_name, MCPClient};
|
||||
use own_assist_common::config_from_file;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -16,18 +16,9 @@ pub struct Config {
|
||||
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 from_file() -> Result<Self, ConfigLoadingError> {
|
||||
config_from_file("assist.toml")
|
||||
}
|
||||
|
||||
pub fn ollama_config(&self) -> &OllamaConfig {
|
||||
@@ -56,7 +47,7 @@ impl Config {
|
||||
let client = mcp::get_client(
|
||||
mcp_server.url.as_str(),
|
||||
mcp_server.authorization.clone(),
|
||||
Implementation::new("OwnAssist", "0.0.1"),
|
||||
Implementation::new("own_assist", env!("CARGO_PKG_VERSION")),
|
||||
).await.unwrap();
|
||||
|
||||
let server_name = guaranteed_mcp_server_name(mcp_server.name.clone(), &client);
|
||||
@@ -88,5 +79,5 @@ pub struct OllamaModelConfig {
|
||||
pub struct MCPServerConfig {
|
||||
pub name: Option<String>,
|
||||
pub url: Url,
|
||||
pub authorization: Option<String>, // should probably implement oauth some time
|
||||
pub authorization: Option<String>, // should probably implement oauth some time // actually, fuck oauth
|
||||
}
|
||||
+17
-48
@@ -1,23 +1,15 @@
|
||||
pub mod mcp;
|
||||
mod config;
|
||||
mod model;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use ollama_rs::Ollama;
|
||||
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);
|
||||
let config = Config::from_file().inspect_err(|e|{
|
||||
log::error!("error loading assist.toml: {}", e);
|
||||
std::process::exit(1);
|
||||
}).unwrap();
|
||||
|
||||
@@ -28,49 +20,26 @@ async fn main() {
|
||||
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}")
|
||||
log::error!("failed creating ollama model {model_name}: {e}");
|
||||
std::process::exit(1);
|
||||
}).unwrap();
|
||||
|
||||
let mut agent_chat = mcp::chat::AgentChat::new(ollama, model_name.clone(), mcp_clients).await.inspect_err(
|
||||
|
||||
let system_prompt = "Du bist ein Assistent, der per Sprache bedient wird. Du erhälst die Transkription. \
|
||||
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.".to_string();
|
||||
|
||||
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();
|
||||
|
||||
log::debug!("chat: {:#?}", agent_chat);
|
||||
|
||||
|
||||
/*
|
||||
let ollama = Ollama::default();
|
||||
log::info!("all tools: {:#?}", agent_chat.get_all_tools());
|
||||
|
||||
let fetch_client = mcp::get_client(
|
||||
"https://remote.mcpservers.org/fetch/mcp",
|
||||
None::<String>,
|
||||
Implementation::new("OwnAssist", "0.0.1"),
|
||||
).await.unwrap();
|
||||
let answer = agent_chat.message("Welche Tools kannst du benutzten?".to_string()).await;
|
||||
let answer = agent_chat.message("Teste beide Server aus. Nutze bei fetch https://example.com/. Melde mir die Ergebnisse zurück.".to_string()).await;
|
||||
|
||||
let mcp_clients = HashMap::from([
|
||||
("fetch".to_string(), fetch_client),
|
||||
]);
|
||||
|
||||
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![
|
||||
ChatMessage::system("Du bist ein Assistent, der per Sprache bedient wird. Du erhälst die Transkription. \
|
||||
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).".to_string())
|
||||
];
|
||||
|
||||
let user_message = vec![
|
||||
ChatMessage::user("Welche Funktionen stehen dir zur Verfügung?".to_string()),
|
||||
];
|
||||
|
||||
let response = ollama.send_chat_messages_with_history(&mut history, ChatMessageRequest::new("gemma4:e2b".to_string(), user_message).tools(vec![])).await.unwrap();
|
||||
|
||||
log::info!("response: {response:?}");
|
||||
log::debug!("history: {history:?}");*/
|
||||
dbg!(answer);
|
||||
}
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
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::ServiceError;
|
||||
use rmcp::model::{CallToolRequestParams, CallToolResult, };
|
||||
use std::collections::HashMap;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub enum ToolPermission {
|
||||
Allowed,
|
||||
#[default]
|
||||
Ask,
|
||||
Denied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RestrictedTool {
|
||||
permission: ToolPermission, // TODO: USE PERMISSION
|
||||
tool_info: ToolInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MCPServerData {
|
||||
name: String,
|
||||
client: MCPClient,
|
||||
translated_tools: Vec<RestrictedTool>,
|
||||
}
|
||||
|
||||
impl MCPServerData {
|
||||
pub async fn new(client: MCPClient, name: String) -> Result<Self, ServiceError> {
|
||||
let mut tools: Vec<RestrictedTool> = Vec::new();
|
||||
|
||||
for tool_info in crate::mcp::translation::get_server_tool_info(&client, &name).await? {
|
||||
tools.push(RestrictedTool {
|
||||
permission: Default::default(),
|
||||
tool_info,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(MCPServerData {
|
||||
name,
|
||||
client,
|
||||
translated_tools: tools,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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![
|
||||
ChatMessage::system("Du bist ein Assistent, der per Sprache bedient wird. Du erhälst die Transkription. \
|
||||
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.".to_string()
|
||||
)
|
||||
];
|
||||
|
||||
let mut servers = HashMap::new();
|
||||
|
||||
for (name, client) in mcp_clients.into_iter() {
|
||||
let server = MCPServerData::new(client, name).await?;
|
||||
servers.insert(server.name.clone(), server);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
ollama_client,
|
||||
model,
|
||||
mcp_servers: servers,
|
||||
message_history: history,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_tools(&self) -> Vec<ToolInfo> {
|
||||
let mut all_tools: Vec<ToolInfo> = Vec::new();
|
||||
|
||||
for server in self.mcp_servers.values() {
|
||||
for tool in server.translated_tools.iter() {
|
||||
all_tools.push(tool.tool_info.clone());
|
||||
}
|
||||
}
|
||||
|
||||
all_tools
|
||||
}
|
||||
|
||||
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
|
||||
.send_chat_messages_with_history(
|
||||
&mut self.message_history,
|
||||
ChatMessageRequest::new(self.model.clone(), vec![ChatMessage::user(user_message)])
|
||||
.tools(all_tools.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
if response.message.tool_calls.is_empty() {
|
||||
log::trace!("no tool was used for message: {:#?}", response.message);
|
||||
break;
|
||||
}
|
||||
|
||||
for tool_call in &response.message.tool_calls {
|
||||
log::debug!("calling tool {}", tool_call.function.name);
|
||||
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")));
|
||||
}
|
||||
|
||||
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, 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("::")
|
||||
.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());
|
||||
|
||||
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?)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
pub mod chat;
|
||||
mod translation;
|
||||
|
||||
use ollama_rs::generation::tools::{ToolInfo, ToolType};
|
||||
use rmcp::model::InitializeRequestParams;
|
||||
use rmcp::serde_json::{Value};
|
||||
use rmcp::service::{ClientInitializeError, RunningService};
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
|
||||
use rmcp::{
|
||||
RoleClient, ServiceExt,
|
||||
model::{ClientCapabilities, ClientInfo, Implementation},
|
||||
transport::StreamableHttpClientTransport,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type MCPClient = RunningService<RoleClient, InitializeRequestParams>;
|
||||
|
||||
pub(crate) async fn get_client(
|
||||
uri: impl Into<Arc<str>>,
|
||||
authentication: Option<impl Into<String>>,
|
||||
implementation: Implementation,
|
||||
) -> Result<MCPClient, ClientInitializeError> {
|
||||
let mut config = StreamableHttpClientTransportConfig::with_uri(uri);
|
||||
|
||||
if let Some(authentication) = authentication {
|
||||
config = config.auth_header(authentication);
|
||||
}
|
||||
|
||||
let client_info = ClientInfo::new(ClientCapabilities::default(), implementation);
|
||||
|
||||
let transport = StreamableHttpClientTransport::from_config(config);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use ollama_rs::generation::tools::{ToolFunctionInfo, ToolInfo, ToolType};
|
||||
use ollama_rs::re_exports::schemars::_private::serde_json::{Map, Value};
|
||||
use ollama_rs::re_exports::schemars::Schema;
|
||||
use rmcp::ServiceError;
|
||||
use crate::mcp::MCPClient;
|
||||
|
||||
pub(in crate::mcp) async fn get_server_tool_info(
|
||||
client: &MCPClient, server_name: &String,
|
||||
) -> Result<Vec<ToolInfo>, ServiceError> {
|
||||
let tool_info = client
|
||||
.list_tools(Default::default())
|
||||
.await?
|
||||
.tools
|
||||
.iter()
|
||||
.map(|mcp_tool| tool_info_from_mcp_tool(&mcp_tool, server_name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(tool_info)
|
||||
}
|
||||
|
||||
fn tool_info_from_mcp_tool(mcp_tool: &rmcp::model::Tool, server_name: &String) -> ToolInfo {
|
||||
let mut value_map: Map<String, Value> = Map::new();
|
||||
|
||||
for key in mcp_tool.input_schema.keys() {
|
||||
let value = mcp_tool.input_schema.get(key).unwrap().clone();
|
||||
value_map.insert(key.clone(), value);
|
||||
}
|
||||
|
||||
let schema_value = Value::Object(value_map);
|
||||
let schema = Schema::try_from(schema_value).unwrap();
|
||||
|
||||
ToolInfo {
|
||||
tool_type: ToolType::Function,
|
||||
function: ToolFunctionInfo {
|
||||
name: format!("{}::{}", server_name, mcp_tool.name),
|
||||
description: mcp_tool.description.clone().unwrap_or(std::borrow::Cow::from("")).to_string(),
|
||||
parameters: schema,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user