add minimal mistral support

This commit is contained in:
2026-07-30 20:39:31 +02:00
parent 1ec8a12f18
commit 199d4080cb
9 changed files with 390 additions and 59 deletions
+10 -8
View File
@@ -1,20 +1,22 @@
[package]
name = "own_mcp"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
license = "GPL-3.0"
[dependencies]
ollama-rs = {version = "0.3.4", features = ["macros", "headers"]}
ollama-rs = {version = "0.3.6", features = ["macros", "headers"]}
reqwest = { version = "0.13.2", features = ["stream", "multipart", "form"] }
tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros"] }
rmcp = {version="2.2.0", features = ["transport-streamable-http-client-reqwest", "reqwest", "client", "auth", "transport-child-process"]}
rmcp = {version="3.0.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"] }
thiserror = "2.0.17"
env_logger = "0.11.11"
serde = { version = "1.0.229", features = ["derive"] }
thiserror = "2.0.19"
url = "2.5.8"
rand = "0.10.0"
rand = "0.10.2"
cpal = "0.18.1"
hound = "3.5.1"
bytes = "1.11.1"
bytes = "1.11.1"
async-trait = "0.1.91"
chrono = "0.4.45"
+16 -22
View File
@@ -1,9 +1,7 @@
use crate::mcp::MCPClient;
use ollama_rs::Ollama;
use ollama_rs::error::OllamaError;
use crate::mcp::llmclient::{LLMClient, LLMError};
use ollama_rs::generation::chat::ChatMessage;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::completion::request::GenerationRequest;
use ollama_rs::generation::tools::{ToolCall, ToolInfo};
use rmcp::ServiceError;
use rmcp::model::{CallToolRequestParams, CallToolResult};
@@ -77,7 +75,7 @@ impl MCPServerData {
#[derive(Debug)]
pub struct AgentChat {
ollama_client: Ollama,
llm_client: Box<dyn LLMClient>,
model: String,
mcp_servers: HashMap<String, MCPServerData>,
message_history: Vec<ChatMessage>,
@@ -86,7 +84,7 @@ pub struct AgentChat {
#[derive(Debug, Error)]
pub enum ChatError {
#[error(transparent)]
OllamaError(#[from] OllamaError),
LLMError(#[from] LLMError),
#[error(transparent)]
ServiceError(#[from] ServiceError),
#[error("the function name could not be parsed")]
@@ -113,7 +111,7 @@ pub struct ToolUsage {
impl AgentChat {
pub async fn new(
ollama_client: Ollama,
llm_client: Box<dyn LLMClient>,
model: String,
mcp_clients: HashMap<String, MCPClient>,
) -> Result<Self, ServiceError> {
@@ -126,17 +124,8 @@ impl AgentChat {
servers.insert(server.name.clone(), server);
}
// lets the ollama server load the model
let ollama_client_clone = ollama_client.clone();
let model_clone = model.clone();
tokio::spawn(async move {
let _ = ollama_client_clone
.generate(GenerationRequest::new(model_clone, ""))
.await;
});
Ok(Self {
ollama_client,
llm_client,
model,
mcp_servers: servers,
message_history: history,
@@ -232,7 +221,7 @@ impl AgentChat {
log::debug!("all tools: {all_tools:#?}");
let mut response = self
.ollama_client
.llm_client
.send_chat_messages_with_history(
&mut self.message_history,
ChatMessageRequest::new(self.model.clone(), vec![ChatMessage::user(user_message)])
@@ -288,7 +277,7 @@ impl AgentChat {
}
response = self
.ollama_client
.llm_client
.send_chat_messages_with_history(
&mut self.message_history,
ChatMessageRequest::new(self.model.clone(), Vec::new())
@@ -375,7 +364,7 @@ impl AgentChat {
pub fn without_history(self) -> Self {
Self {
ollama_client: self.ollama_client,
llm_client: self.llm_client,
model: self.model,
mcp_servers: self.mcp_servers,
message_history: vec![],
@@ -392,6 +381,7 @@ mod tests {
use super::*;
use crate::mcp::chat::RestrictedTool;
use crate::mcp::get_client;
use ollama_rs::Ollama;
use rmcp::model::Implementation;
use std::collections::HashMap;
@@ -421,9 +411,13 @@ mod tests {
.unwrap(),
)]);
let mut chat = AgentChat::new(ollama, "lfm2.5-thinking:1.2b".to_string(), mcp_clients)
.await
.unwrap();
let mut chat = AgentChat::new(
Box::new(ollama),
"lfm2.5-thinking:1.2b".to_string(),
mcp_clients,
)
.await
.unwrap();
let tools: Vec<RestrictedTool> = chat.get_all_tools().cloned().collect();
assert_eq!(tools.len(), 1);
+216
View File
@@ -0,0 +1,216 @@
use crate::mcp::llmclient::{LLMClient, LLMError};
use async_trait::async_trait;
use ollama_rs::generation::chat::ChatMessage as OllamaChatMessage;
use ollama_rs::generation::chat::request::ChatMessageRequest as OllamaChatMessageRequest;
use ollama_rs::generation::chat::{ChatMessageResponse, MessageRole};
use ollama_rs::generation::tools::{ToolCall, ToolInfo};
use ollama_rs::history::ChatHistory;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct Mistral {
api_key: String,
system_prompt: Option<String>,
}
impl Mistral {
pub fn new(api_key: String) -> Mistral {
Mistral {
api_key,
system_prompt: None,
}
}
pub fn with_system_prompt(mut self, system_prompt: Option<String>) -> Mistral {
self.system_prompt = system_prompt;
self
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Message {
content: String,
role: MessageRole,
#[serde(
skip_serializing_if = "Vec::is_empty",
default = "Vec::new",
deserialize_with = "parse_tool_calls"
)]
tool_calls: Vec<ToolCall>,
}
fn parse_tool_calls<'de, D>(d: D) -> Result<Vec<ToolCall>, D::Error>
where
D: serde::Deserializer<'de>,
{
Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or(Vec::new()))
}
impl From<OllamaChatMessage> for Message {
fn from(message: OllamaChatMessage) -> Message {
Message {
content: message.content,
role: message.role,
tool_calls: message.tool_calls,
}
}
}
impl From<Message> for OllamaChatMessage {
fn from(message: Message) -> OllamaChatMessage {
OllamaChatMessage {
role: message.role,
content: message.content,
tool_calls: message.tool_calls,
images: None,
thinking: None,
}
}
}
#[derive(Debug, Serialize)]
struct ChatCompletionRequest {
messages: Vec<Message>,
model: String,
tools: Vec<ToolInfo>,
}
impl From<OllamaChatMessageRequest> for ChatCompletionRequest {
fn from(request: OllamaChatMessageRequest) -> ChatCompletionRequest {
ChatCompletionRequest {
messages: request.messages.into_iter().map(|m| m.into()).collect(),
model: request.model_name,
tools: request.tools,
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct ChatCompletionChoice {
message: Message,
}
#[derive(Debug, Clone, Deserialize)]
struct ChatCompletionResponse {
choices: Vec<ChatCompletionChoice>,
model: String,
#[serde(rename = "created")]
created_unix_seconds: i64,
}
impl TryFrom<ChatCompletionResponse> for ChatMessageResponse {
type Error = LLMError;
fn try_from(value: ChatCompletionResponse) -> Result<ChatMessageResponse, LLMError> {
let created = chrono::DateTime::from_timestamp(value.created_unix_seconds, 0)
.ok_or(LLMError::Other("failed converting timestamp".to_string()))?;
Ok(ChatMessageResponse {
model: value.model,
created_at: created.to_rfc3339(),
message: value
.choices
.first()
.ok_or(LLMError::Other("answer does not exist".to_string()))?
.message
.clone()
.into(),
logprobs: None,
done: false,
final_data: None,
})
}
}
#[async_trait]
impl LLMClient for Mistral {
async fn send_chat_messages_with_history(
&self,
history: &mut Vec<OllamaChatMessage>,
mut request: ollama_rs::generation::chat::request::ChatMessageRequest,
) -> Result<ChatMessageResponse, LLMError> {
if let Some(first_message) = history.first() // add system prompt as first message
&& first_message.role == MessageRole::System
&& let Some(system_prompt) = self.system_prompt.clone()
{
history.insert(
0,
OllamaChatMessage::new(MessageRole::System, system_prompt),
);
}
history.append(&mut request.messages);
request.messages = history.messages().to_vec();
let client = reqwest::Client::new();
let chat_completion_request: ChatCompletionRequest = request.into();
let response = client
.post("https://api.mistral.ai/v1/chat/completions")
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&chat_completion_request)
.send()
.await
.map_err(|e| LLMError::NetworkError(e.to_string()))?;
let response = response
.error_for_status()
.map_err(|e| LLMError::Other(e.to_string()))?;
let completion_response: ChatCompletionResponse = response
.json()
.await
.map_err(|e| LLMError::Other(e.to_string()))?;
let chat_message_response: ChatMessageResponse = completion_response.try_into()?;
Ok(chat_message_response)
}
async fn preload_model(&self, _model: &str) {
// do nothing
}
}
#[cfg(test)]
mod tests {
use super::*;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::chat::{ChatMessage, MessageRole};
use rmcp::schemars;
#[derive(schemars::JsonSchema)]
struct BeepToolParameters {
location: String,
}
#[tokio::test]
async fn sending_message() {
let client = Mistral::new(include_str!(".MISTRAL_API_KEY").to_string());
let mut history = vec![];
dbg!(
client
.send_chat_messages_with_history(
&mut history,
ChatMessageRequest::new(
"mistral-medium-3-5".to_string(),
vec![ChatMessage::new(
MessageRole::User,
"Mach ein Geräusch in der Küche".to_string()
)],
)
.tools(vec![ToolInfo {
tool_type: ollama_rs::generation::tools::ToolType::Function,
function: ollama_rs::generation::tools::ToolFunctionInfo {
name: "make_sound".to_string(),
description: "alerts the user with a beep".to_string(),
parameters: schemars::schema_for!(BeepToolParameters),
}
}]),
)
.await
.unwrap()
);
}
}
+27
View File
@@ -0,0 +1,27 @@
pub mod mistral;
pub mod ollama;
use async_trait::async_trait;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::chat::{ChatMessage, ChatMessageResponse};
use std::fmt::Debug;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LLMError {
#[error("network error: {0}")]
NetworkError(String),
#[error("llm error: {0}")]
Other(String),
}
#[async_trait]
pub trait LLMClient: Debug + Send + Sync {
async fn send_chat_messages_with_history(
&self,
history: &mut Vec<ChatMessage>,
request: ChatMessageRequest,
) -> Result<ChatMessageResponse, LLMError>;
async fn preload_model(&self, model: &str);
}
+38
View File
@@ -0,0 +1,38 @@
use crate::mcp::llmclient::{LLMClient, LLMError};
use async_trait::async_trait;
use ollama_rs::Ollama;
use ollama_rs::error::OllamaError;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::chat::{ChatMessage, ChatMessageResponse};
use ollama_rs::generation::completion::request::GenerationRequest;
impl From<OllamaError> for LLMError {
fn from(err: OllamaError) -> LLMError {
match err {
OllamaError::ToolCallError(e) => LLMError::Other(e.to_string()),
OllamaError::JsonError(e) => LLMError::Other(e.to_string()),
OllamaError::ReqwestError(e) => LLMError::NetworkError(e.to_string()),
OllamaError::InternalError(e) => LLMError::Other(e.message),
OllamaError::Other(e) => LLMError::Other(e.to_string()),
}
}
}
#[async_trait]
impl LLMClient for Ollama {
async fn send_chat_messages_with_history(
&self,
history: &mut Vec<ChatMessage>,
request: ChatMessageRequest,
) -> Result<ChatMessageResponse, LLMError> {
Ok(self
.send_chat_messages_with_history(history, request)
.await?)
}
async fn preload_model(&self, model: &str) {
let _ = self
.generate(GenerationRequest::new(model.to_string(), ""))
.await;
}
}
+7 -2
View File
@@ -1,6 +1,9 @@
pub mod chat;
mod llmclient;
mod translation;
pub use llmclient::mistral::Mistral;
use rmcp::model::InitializeRequestParams;
use rmcp::service::{ClientInitializeError, RunningService};
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
@@ -48,8 +51,10 @@ pub fn guaranteed_mcp_server_name(
) -> 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 if let Some(peer_info) = client.peer_info()
&& let Some(server_info) = &peer_info.server_info
{
server_info.name.clone()
} else {
let random_name = generate_random_mcp_server_name();
log::warn!(