initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+10
@@ -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/
|
||||||
Generated
+11
@@ -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>
|
||||||
Generated
+8
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+14
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user