add experimental action
publish release / release (push) Failing after 3m38s

apply formatting
This commit is contained in:
2026-05-08 22:30:11 +02:00
parent 51b1123cf9
commit d9ade649d3
17 changed files with 242 additions and 143 deletions
+4 -1
View File
@@ -41,7 +41,10 @@ type AudioResult<T> = Result<T, AudioError>;
impl AudioClient {
fn get(&self, url: Url) -> reqwest::RequestBuilder {
self.client.get(url).header("Authorization", self.authorization.clone().unwrap_or(String::new()))
self.client.get(url).header(
"Authorization",
self.authorization.clone().unwrap_or(String::new()),
)
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
pub mod recording;
pub mod client;
pub mod models;
pub use client::{AudioClient, AudioClientTrait};
pub mod recording;
pub use client::{AudioClient, AudioClientTrait};
+3 -3
View File
@@ -20,14 +20,14 @@ pub struct TranscriptionSegment {
pub temperature: f32,
pub avg_logprob: f32,
pub compression_ratio: f32,
pub no_speech_prob: f32
pub no_speech_prob: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TranscriptionResponse {
pub text: String,
pub segments: Vec<TranscriptionSegment>,
pub language: String
pub language: String,
}
#[derive(Debug, Clone, Serialize)]
@@ -57,4 +57,4 @@ impl Default for SynthesisConfig {
pub struct VoiceRequest {
pub text: String,
pub config: Option<SynthesisConfig>,
}
}
+28 -13
View File
@@ -1,12 +1,12 @@
use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::{BuildStreamError, Device, SampleFormat, SizedSample, Stream, SupportedStreamConfig};
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use std::{path, thread};
use std::thread::JoinHandle;
use std::time::Duration;
use hound::WavSpec;
use log::{debug, error, info, trace};
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use std::{path, thread};
use thiserror::Error;
fn generic_sample_to_i16(sample: impl SizedSample + Into<f64>, format: SampleFormat) -> i16 {
@@ -136,7 +136,7 @@ pub fn start(
let thread_samples = samples.clone();
let thread_recording_stop = recording_should_stop.clone();
let thread_device_config = device_config.clone();
let record_thread_handle = thread::spawn(move ||
let record_thread_handle = thread::spawn(move || {
start_recording_blocking_with_parameters(
&microphone,
&thread_device_config,
@@ -144,7 +144,7 @@ pub fn start(
thread_samples,
thread_recording_stop,
)
);
});
let spec = WavSpec {
channels: device_config.channels(),
@@ -163,25 +163,40 @@ pub fn start(
})))
}
pub fn stop_and_take_data(handler: Arc<Mutex<Option<RecordingHandler>>>) -> Result<(Vec<i16>, WavSpec), RecordingError> {
let handler = handler.lock().map_err(|_|RecordingError::ThreadPoison)?.take().ok_or(RecordingError::RecordingAlreadyStopped)?;
pub fn stop_and_take_data(
handler: Arc<Mutex<Option<RecordingHandler>>>,
) -> Result<(Vec<i16>, WavSpec), RecordingError> {
let handler = handler
.lock()
.map_err(|_| RecordingError::ThreadPoison)?
.take()
.ok_or(RecordingError::RecordingAlreadyStopped)?;
let samples = handler.samples.clone();
let spec = handler.spec.clone();
handler.stop_recording()?;
Ok((samples.lock().map_err(|_|RecordingError::ThreadPoison)?.to_vec(), spec))
Ok((
samples
.lock()
.map_err(|_| RecordingError::ThreadPoison)?
.to_vec(),
spec,
))
}
pub fn samples_to_wav(
samples: impl IntoIterator<Item = i16>,
spec: &WavSpec,
filepath: impl AsRef<path::Path>
filepath: impl AsRef<path::Path>,
) -> Result<Vec<u8>, hound::Error> {
let wav_bytes = std::io::Cursor::new(Vec::<u8>::new());
trace!("creating a wav file writer to {}", filepath.as_ref().display());
trace!(
"creating a wav file writer to {}",
filepath.as_ref().display()
);
let mut file_writer = hound::WavWriter::create(&filepath, *spec)?;
trace!("writing samples...");
@@ -193,4 +208,4 @@ pub fn samples_to_wav(
file_writer.finalize()?;
Ok(wav_bytes.into_inner())
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
pub mod mcp;
pub mod audio;
pub mod mcp;
pub use mcp::chat::AgentChat;
pub use mcp::chat::AgentChat;
+53 -31
View File
@@ -1,13 +1,13 @@
use crate::mcp::MCPClient;
use ollama_rs::error::OllamaError;
use ollama_rs::generation::chat::request::ChatMessageRequest;
use ollama_rs::generation::chat::ChatMessage;
use ollama_rs::generation::tools::ToolInfo;
use ollama_rs::Ollama;
use rmcp::model::{CallToolRequestParams, CallToolResult};
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 std::collections::HashMap;
use rmcp::model::{CallToolRequestParams, CallToolResult};
use serde::Deserialize;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq)]
@@ -164,21 +164,29 @@ impl AgentChat {
/// * `name`: must be in the format "mcp_server_name:tool_or_resource_name".
///
/// returns: Option<&RestrictedTool>
pub fn get_tool(
mpc_server_data: &MCPServerData,
name: String,
) -> Option<&RestrictedTool> {
pub fn get_tool(mpc_server_data: &MCPServerData, name: String) -> Option<&RestrictedTool> {
mpc_server_data
.translated_tools
.iter()
.find(|tool| tool.tool_info.function.name == name)
}
pub fn set_permission(&mut self, full_tool_name: String, permission: ToolPermission) -> Result<(), ChatError> {
pub fn set_permission(
&mut self,
full_tool_name: String,
permission: ToolPermission,
) -> Result<(), ChatError> {
let (mcp_server_name, tool_name) = Self::parse_tool_name(full_tool_name.clone())?;
let mcp_server_data: &mut MCPServerData = self.mcp_servers.get_mut(&mcp_server_name.clone()).ok_or(ChatError::ServiceNotFoundError(mcp_server_name.clone()))?;
let tool_index = mcp_server_data.translated_tools.iter().position(|tool| tool.tool_info.function.name == full_tool_name).ok_or(ChatError::ToolNotFoundError(tool_name))?;
let mcp_server_data: &mut MCPServerData = self
.mcp_servers
.get_mut(&mcp_server_name.clone())
.ok_or(ChatError::ServiceNotFoundError(mcp_server_name.clone()))?;
let tool_index = mcp_server_data
.translated_tools
.iter()
.position(|tool| tool.tool_info.function.name == full_tool_name)
.ok_or(ChatError::ToolNotFoundError(tool_name))?;
mcp_server_data.translated_tools[tool_index].permission = permission;
Ok(())
}
@@ -187,12 +195,12 @@ impl AgentChat {
&mut self,
user_message: String,
mut permission_request_callback: impl FnMut(String, String) -> C,
) -> Result<ChatMessage, ChatError>
{
) -> Result<ChatMessage, ChatError> {
let all_tools: Vec<ToolInfo> = self
.get_all_tools()
.filter_map(|restricted_tool|{
if restricted_tool.permission == ToolPermission::Denied { // filters denied tools to save tokens
.filter_map(|restricted_tool| {
if restricted_tool.permission == ToolPermission::Denied {
// filters denied tools to save tokens
None
} else {
Some(restricted_tool.tool_info.clone())
@@ -219,16 +227,22 @@ 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, &mut permission_request_callback).await?;
let result = self
.call_tool(tool_call, &mut permission_request_callback)
.await?;
// serialize structured content if it exists
if let Some(structured_content) = result.clone().and_then(|result|result.structured_content) {
if let Some(structured_content) =
result.clone().and_then(|result| result.structured_content)
{
log::debug!("structured content: {structured_content:#?}");
self.message_history.push(ChatMessage::tool(structured_content.to_string()));
self.message_history
.push(ChatMessage::tool(structured_content.to_string()));
} else {
let contents = match result {
Some(result) => result.content
Some(result) => result
.content
.iter()
.filter_map(|content| content.as_text())
.map(|text_content| text_content.text.clone())
@@ -270,7 +284,7 @@ impl AgentChat {
&self,
tool_call: &ollama_rs::generation::tools::ToolCall,
permission_request_callback: &mut impl FnMut(String, String) -> C,
) -> Result<Option<CallToolResult>, ChatError> {
) -> Result<Option<CallToolResult>, ChatError> {
let arguments_json_object = tool_call
.function
.arguments
@@ -286,10 +300,10 @@ impl AgentChat {
let mcp_server_data = self
.get_mcp_server_by_name(mcp_server_name.clone())
.ok_or(ChatError::ServiceNotFoundError(mcp_server_name.clone()))?;
let restricted_tool = AgentChat::get_tool(&mcp_server_data, full_unparsed_tool_name.clone())
.ok_or(ChatError::ToolNotFoundError(
full_unparsed_tool_name.clone(),
))?;
let restricted_tool =
AgentChat::get_tool(&mcp_server_data, full_unparsed_tool_name.clone()).ok_or(
ChatError::ToolNotFoundError(full_unparsed_tool_name.clone()),
)?;
match restricted_tool.permission {
ToolPermission::Allowed => {
@@ -301,7 +315,8 @@ impl AgentChat {
}
ToolPermission::Ask => {
log::trace!("ask restricted tool: {:?}", restricted_tool);
let response = permission_request_callback(mcp_server_name, tool_name.clone()).await;
let response =
permission_request_callback(mcp_server_name, tool_name.clone()).await;
match response {
PermissionAnswer::Granted => {
log::trace!("granted restricted tool: {:?}", restricted_tool);
@@ -323,7 +338,9 @@ impl AgentChat {
request_params
);
Ok(Some(mcp_server_data.client.call_tool(request_params).await?))
Ok(Some(
mcp_server_data.client.call_tool(request_params).await?,
))
}
}
@@ -356,16 +373,21 @@ mod tests {
MCP_TEST_SERVER_URL,
None::<String>,
Implementation::new("test", env!("CARGO_PKG_VERSION")),
).await.unwrap(),
)
.await
.unwrap(),
)]);
let mut chat = AgentChat::new(ollama, "lfm2.5-thinking:1.2b".to_string(), mcp_clients).await.unwrap();
let mut chat = AgentChat::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);
assert_eq!(tools[0].permission, ToolPermission::Ask);
chat.set_permission("test:echo".to_string(), ToolPermission::Allowed).unwrap();
chat.set_permission("test:echo".to_string(), ToolPermission::Allowed)
.unwrap();
let tools: Vec<RestrictedTool> = chat.get_all_tools().cloned().collect();
assert_eq!(tools[0].permission, ToolPermission::Allowed);
+12 -7
View File
@@ -34,24 +34,29 @@ pub async fn get_client(
}
fn generate_random_mcp_server_name() -> String {
use rand::distr::Alphanumeric;
use rand::RngExt;
use rand::distr::Alphanumeric;
let mut rng = rand::rng();
(0..5)
.map(|_| rng.sample(Alphanumeric) as char)
.collect()
(0..5).map(|_| rng.sample(Alphanumeric) as char).collect()
}
pub fn guaranteed_mcp_server_name(user_specified_name: Option<String>, client: &MCPClient) -> String {
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);
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
}
}
}
+6 -3
View File
@@ -74,10 +74,13 @@ fn tool_info_from_mcp_resource(
description: format!(
"type: {} - {}",
mcp_resource
.mime_type.clone()
.mime_type
.clone()
.unwrap_or("No type provided".to_string()),
mcp_resource.clone()
.description.clone()
mcp_resource
.clone()
.description
.clone()
.unwrap_or("No description".to_string())
),
parameters: schema,