initial commit

This commit is contained in:
milan
2026-04-03 22:09:40 +02:00
commit 6bfadfeba7
11 changed files with 3372 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/OwnAssist.iml" filepath="$PROJECT_DIR$/.idea/OwnAssist.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
Generated
+2991
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "OwnAssist"
version = "0.1.0"
edition = "2024"
[dependencies]
ollama-rs = {version = "0.3.4", features = ["macros"]}
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros"] }
chrono = "0.4.44"
rmcp = {version="1.3.0", features = ["transport-streamable-http-client-reqwest", "reqwest", "client", "auth", "transport-child-process"]}
log = {version = "0.4.29"}
env_logger = "0.11.10"
serde = { version = "1.0.228", features = ["derive"] }
+105
View File
@@ -0,0 +1,105 @@
pub mod mcp;
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())
}
#[tokio::main]
async fn main() {
env_logger::init();
let ollama = Ollama::default();
let fetch_client = mcp::get_client(
"https://remote.mcpservers.org/fetch/mcp",
None::<String>,
Implementation::new("OwnAssist", "0.0.1"),
).await.unwrap();
let mcp_clients = HashMap::from([
("fetch".to_string(), fetch_client),
]);
let mut agent_chat = mcp::chat::AgentChat::new(ollama, 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:?}");*/
}
+149
View File
@@ -0,0 +1,149 @@
use std::collections::HashMap;
use ollama_rs::error::OllamaError;
use crate::mcp::MCPClient;
use ollama_rs::Ollama;
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;
#[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,
mcp_servers: HashMap<String, MCPServerData>,
message_history: Vec<ChatMessage>,
}
impl AgentChat {
pub async fn new(
ollama_client: Ollama,
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,
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<(), OllamaError> {
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(
"gemma4:e2b".to_string(),
vec![ChatMessage::user(user_message)],
)
.tools(all_tools),
)
.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.unwrap(); // FIXME: nuh uh
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("gemma4:e2b".to_string(), Vec::new())).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
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) = (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);
self.mcp_servers.get(&mcp_name).unwrap().client.call_tool(request_params).await
}
}
+36
View File
@@ -0,0 +1,36 @@
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;
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);
})
}
+41
View File
@@ -0,0 +1,41 @@
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,
},
}
}