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
+26
View File
@@ -0,0 +1,26 @@
name: publish release
on: [ push ]
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: checkout
uses: actions/checkout@v6
- name: toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: x86_64-pc-windows-gnu
- run: cargo build --release
- name: Release
uses: softprops/action-gh-release@v3
with:
files: target/release/*.exe
+4 -1
View File
@@ -41,7 +41,10 @@ type AudioResult<T> = Result<T, AudioError>;
impl AudioClient { impl AudioClient {
fn get(&self, url: Url) -> reqwest::RequestBuilder { 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()),
)
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod recording;
pub mod client; pub mod client;
pub mod models; pub mod models;
pub mod recording;
pub use client::{AudioClient, AudioClientTrait}; pub use client::{AudioClient, AudioClientTrait};
+2 -2
View File
@@ -20,14 +20,14 @@ pub struct TranscriptionSegment {
pub temperature: f32, pub temperature: f32,
pub avg_logprob: f32, pub avg_logprob: f32,
pub compression_ratio: f32, pub compression_ratio: f32,
pub no_speech_prob: f32 pub no_speech_prob: f32,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct TranscriptionResponse { pub struct TranscriptionResponse {
pub text: String, pub text: String,
pub segments: Vec<TranscriptionSegment>, pub segments: Vec<TranscriptionSegment>,
pub language: String pub language: String,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
+27 -12
View File
@@ -1,12 +1,12 @@
use cpal::traits::{DeviceTrait, StreamTrait}; use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::{BuildStreamError, Device, SampleFormat, SizedSample, Stream, SupportedStreamConfig}; 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 hound::WavSpec;
use log::{debug, error, info, trace}; 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; use thiserror::Error;
fn generic_sample_to_i16(sample: impl SizedSample + Into<f64>, format: SampleFormat) -> i16 { 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_samples = samples.clone();
let thread_recording_stop = recording_should_stop.clone(); let thread_recording_stop = recording_should_stop.clone();
let thread_device_config = device_config.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( start_recording_blocking_with_parameters(
&microphone, &microphone,
&thread_device_config, &thread_device_config,
@@ -144,7 +144,7 @@ pub fn start(
thread_samples, thread_samples,
thread_recording_stop, thread_recording_stop,
) )
); });
let spec = WavSpec { let spec = WavSpec {
channels: device_config.channels(), 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> { pub fn stop_and_take_data(
let handler = handler.lock().map_err(|_|RecordingError::ThreadPoison)?.take().ok_or(RecordingError::RecordingAlreadyStopped)?; 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 samples = handler.samples.clone();
let spec = handler.spec.clone(); let spec = handler.spec.clone();
handler.stop_recording()?; 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( pub fn samples_to_wav(
samples: impl IntoIterator<Item = i16>, samples: impl IntoIterator<Item = i16>,
spec: &WavSpec, spec: &WavSpec,
filepath: impl AsRef<path::Path> filepath: impl AsRef<path::Path>,
) -> Result<Vec<u8>, hound::Error> { ) -> Result<Vec<u8>, hound::Error> {
let wav_bytes = std::io::Cursor::new(Vec::<u8>::new()); 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)?; let mut file_writer = hound::WavWriter::create(&filepath, *spec)?;
trace!("writing samples..."); trace!("writing samples...");
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod mcp;
pub mod audio; 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 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 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 rmcp::ServiceError;
use std::collections::HashMap; use rmcp::model::{CallToolRequestParams, CallToolResult};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Copy, Clone, Default, Deserialize, PartialEq)] #[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". /// * `name`: must be in the format "mcp_server_name:tool_or_resource_name".
/// ///
/// returns: Option<&RestrictedTool> /// returns: Option<&RestrictedTool>
pub fn get_tool( pub fn get_tool(mpc_server_data: &MCPServerData, name: String) -> Option<&RestrictedTool> {
mpc_server_data: &MCPServerData,
name: String,
) -> Option<&RestrictedTool> {
mpc_server_data mpc_server_data
.translated_tools .translated_tools
.iter() .iter()
.find(|tool| tool.tool_info.function.name == name) .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_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 mcp_server_data: &mut MCPServerData = self
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_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; mcp_server_data.translated_tools[tool_index].permission = permission;
Ok(()) Ok(())
} }
@@ -187,12 +195,12 @@ impl AgentChat {
&mut self, &mut self,
user_message: String, user_message: String,
mut permission_request_callback: impl FnMut(String, String) -> C, mut permission_request_callback: impl FnMut(String, String) -> C,
) -> Result<ChatMessage, ChatError> ) -> Result<ChatMessage, ChatError> {
{
let all_tools: Vec<ToolInfo> = self let all_tools: Vec<ToolInfo> = self
.get_all_tools() .get_all_tools()
.filter_map(|restricted_tool|{ .filter_map(|restricted_tool| {
if restricted_tool.permission == ToolPermission::Denied { // filters denied tools to save tokens if restricted_tool.permission == ToolPermission::Denied {
// filters denied tools to save tokens
None None
} else { } else {
Some(restricted_tool.tool_info.clone()) Some(restricted_tool.tool_info.clone())
@@ -219,16 +227,22 @@ impl AgentChat {
for tool_call in &response.message.tool_calls { for tool_call in &response.message.tool_calls {
log::debug!("calling tool {}", tool_call.function.name); 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 // 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:#?}"); 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 { } else {
let contents = match result { let contents = match result {
Some(result) => result.content Some(result) => result
.content
.iter() .iter()
.filter_map(|content| content.as_text()) .filter_map(|content| content.as_text())
.map(|text_content| text_content.text.clone()) .map(|text_content| text_content.text.clone())
@@ -270,7 +284,7 @@ impl AgentChat {
&self, &self,
tool_call: &ollama_rs::generation::tools::ToolCall, tool_call: &ollama_rs::generation::tools::ToolCall,
permission_request_callback: &mut impl FnMut(String, String) -> C, permission_request_callback: &mut impl FnMut(String, String) -> C,
) -> Result<Option<CallToolResult>, ChatError> { ) -> Result<Option<CallToolResult>, ChatError> {
let arguments_json_object = tool_call let arguments_json_object = tool_call
.function .function
.arguments .arguments
@@ -286,10 +300,10 @@ impl AgentChat {
let mcp_server_data = self let mcp_server_data = self
.get_mcp_server_by_name(mcp_server_name.clone()) .get_mcp_server_by_name(mcp_server_name.clone())
.ok_or(ChatError::ServiceNotFoundError(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()) let restricted_tool =
.ok_or(ChatError::ToolNotFoundError( AgentChat::get_tool(&mcp_server_data, full_unparsed_tool_name.clone()).ok_or(
full_unparsed_tool_name.clone(), ChatError::ToolNotFoundError(full_unparsed_tool_name.clone()),
))?; )?;
match restricted_tool.permission { match restricted_tool.permission {
ToolPermission::Allowed => { ToolPermission::Allowed => {
@@ -301,7 +315,8 @@ impl AgentChat {
} }
ToolPermission::Ask => { ToolPermission::Ask => {
log::trace!("ask restricted tool: {:?}", restricted_tool); 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 { match response {
PermissionAnswer::Granted => { PermissionAnswer::Granted => {
log::trace!("granted restricted tool: {:?}", restricted_tool); log::trace!("granted restricted tool: {:?}", restricted_tool);
@@ -323,7 +338,9 @@ impl AgentChat {
request_params 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, MCP_TEST_SERVER_URL,
None::<String>, None::<String>,
Implementation::new("test", env!("CARGO_PKG_VERSION")), 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(); let tools: Vec<RestrictedTool> = chat.get_all_tools().cloned().collect();
assert_eq!(tools.len(), 1); assert_eq!(tools.len(), 1);
assert_eq!(tools[0].permission, ToolPermission::Ask); 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(); let tools: Vec<RestrictedTool> = chat.get_all_tools().cloned().collect();
assert_eq!(tools[0].permission, ToolPermission::Allowed); assert_eq!(tools[0].permission, ToolPermission::Allowed);
+11 -6
View File
@@ -34,24 +34,29 @@ pub async fn get_client(
} }
fn generate_random_mcp_server_name() -> String { fn generate_random_mcp_server_name() -> String {
use rand::distr::Alphanumeric;
use rand::RngExt; use rand::RngExt;
use rand::distr::Alphanumeric;
let mut rng = rand::rng(); let mut rng = rand::rng();
(0..5) (0..5).map(|_| rng.sample(Alphanumeric) as char).collect()
.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 { if let Some(user_specified_name) = user_specified_name {
user_specified_name user_specified_name
} else if let Some(peer_info) = client.peer_info() { } else if let Some(peer_info) = client.peer_info() {
peer_info.server_info.name.clone() peer_info.server_info.name.clone()
} else { } else {
let random_name = generate_random_mcp_server_name(); 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 random_name
} }
} }
+6 -3
View File
@@ -74,10 +74,13 @@ fn tool_info_from_mcp_resource(
description: format!( description: format!(
"type: {} - {}", "type: {} - {}",
mcp_resource mcp_resource
.mime_type.clone() .mime_type
.clone()
.unwrap_or("No type provided".to_string()), .unwrap_or("No type provided".to_string()),
mcp_resource.clone() mcp_resource
.description.clone() .clone()
.description
.clone()
.unwrap_or("No description".to_string()) .unwrap_or("No description".to_string())
), ),
parameters: schema, parameters: schema,
+30 -16
View File
@@ -1,15 +1,15 @@
use own_assist_common::config_loader::ConfigLoadingError;
use std::collections::HashMap;
use ollama_rs::headers::{HeaderMap, HeaderValue};
use ollama_rs::Ollama; use ollama_rs::Ollama;
use ollama_rs::headers::{HeaderMap, HeaderValue};
use own_assist_common::config_from_file;
use own_assist_common::config_loader::ConfigLoadingError;
use own_mcp::audio::{AudioClient, AudioClientTrait}; use own_mcp::audio::{AudioClient, AudioClientTrait};
use own_mcp::mcp::chat::{ChatError, ToolPermission};
use own_mcp::mcp::{MCPClient, guaranteed_mcp_server_name};
use own_mcp::{AgentChat, mcp};
use rmcp::model::Implementation; use rmcp::model::Implementation;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use url::Url; use url::Url;
use own_mcp::{mcp, AgentChat};
use own_mcp::mcp::{guaranteed_mcp_server_name, MCPClient};
use own_mcp::mcp::chat::{ChatError, ToolPermission};
use own_assist_common::config_from_file;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
@@ -35,7 +35,10 @@ impl Config {
let mut ollama_headers = HeaderMap::new(); let mut ollama_headers = HeaderMap::new();
if let Some(authorization_header) = &self.ollama.authorization { if let Some(authorization_header) = &self.ollama.authorization {
ollama_headers.append("Authorization", HeaderValue::from_str(authorization_header).unwrap()); ollama_headers.append(
"Authorization",
HeaderValue::from_str(authorization_header).unwrap(),
);
} }
let ollama_default_url = Url::parse("http://127.0.0.1:11434").unwrap(); let ollama_default_url = Url::parse("http://127.0.0.1:11434").unwrap();
@@ -58,7 +61,9 @@ impl Config {
} }
} }
pub async fn mcp_clients(&self) -> Result<HashMap<String, MCPClient>, rmcp::service::ClientInitializeError> { pub async fn mcp_clients(
&self,
) -> Result<HashMap<String, MCPClient>, rmcp::service::ClientInitializeError> {
let mut mcp_clients: HashMap<String, MCPClient> = HashMap::new(); let mut mcp_clients: HashMap<String, MCPClient> = HashMap::new();
for mcp_server in &self.mcp_servers { for mcp_server in &self.mcp_servers {
@@ -66,7 +71,8 @@ impl Config {
mcp_server.url.as_str(), mcp_server.url.as_str(),
mcp_server.authorization.clone(), mcp_server.authorization.clone(),
Implementation::new("own_assist", env!("CARGO_PKG_VERSION")), Implementation::new("own_assist", env!("CARGO_PKG_VERSION")),
).await?; )
.await?;
let server_name = guaranteed_mcp_server_name(mcp_server.name.clone(), &client); let server_name = guaranteed_mcp_server_name(mcp_server.name.clone(), &client);
let server_data = client; let server_data = client;
@@ -78,13 +84,21 @@ impl Config {
} }
pub async fn set_tool_permissions(&self, agent_chat: &mut AgentChat) -> Result<(), ChatError> { pub async fn set_tool_permissions(&self, agent_chat: &mut AgentChat) -> Result<(), ChatError> {
if let Some(permissions) = &self.permissions && !permissions.is_empty() { if let Some(permissions) = &self.permissions
&& !permissions.is_empty()
{
for permission_config in permissions { for permission_config in permissions {
log::info!("set permission for `{}` to {:?}", permission_config.tool_name, permission_config.permission); log::info!(
agent_chat.set_permission(permission_config.tool_name.clone(), permission_config.permission)?; "set permission for `{}` to {:?}",
permission_config.tool_name,
permission_config.permission
);
agent_chat.set_permission(
permission_config.tool_name.clone(),
permission_config.permission,
)?;
} }
} } else {
else {
log::debug!("no permissions were provided.") log::debug!("no permissions were provided.")
} }
@@ -138,5 +152,5 @@ pub struct MCPServerConfig {
pub struct PermissionConfig { pub struct PermissionConfig {
#[serde(rename = "tool-name")] #[serde(rename = "tool-name")]
pub tool_name: String, pub tool_name: String,
pub permission: ToolPermission pub permission: ToolPermission,
} }
+34 -25
View File
@@ -1,5 +1,6 @@
use crate::translate;
use crate::human_interface::{HumanInterface, HumanInterfaceError}; use crate::human_interface::{HumanInterface, HumanInterfaceError};
use crate::tlt;
use crate::translate;
use cpal::traits::DeviceTrait; use cpal::traits::DeviceTrait;
use own_mcp::audio::models::VoiceRequest; use own_mcp::audio::models::VoiceRequest;
use own_mcp::audio::{AudioClient, AudioClientTrait}; use own_mcp::audio::{AudioClient, AudioClientTrait};
@@ -8,7 +9,6 @@ use std::io;
use std::path::Path; use std::path::Path;
use tokio::fs::create_dir_all; use tokio::fs::create_dir_all;
use tokio::io::AsyncBufReadExt; use tokio::io::AsyncBufReadExt;
use crate::tlt;
const TEMPORARY_AUDIO_PATH: &str = "temporary_audio"; const TEMPORARY_AUDIO_PATH: &str = "temporary_audio";
@@ -31,7 +31,7 @@ impl Audio {
async fn wait_for_input_line(expected_line: &str) -> Result<(), io::Error> { async fn wait_for_input_line(expected_line: &str) -> Result<(), io::Error> {
loop { loop {
let stdin_reader = tokio::io::BufReader::new(tokio::io::stdin()); let stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
log::trace!("wating for '{expected_line}' to continue"); log::trace!("waiting for '{expected_line}' to continue");
if let Some(line) = stdin_reader.lines().next_line().await? { if let Some(line) = stdin_reader.lines().next_line().await? {
log::debug!("Received line: {line:?}"); log::debug!("Received line: {line:?}");
if line == expected_line { if line == expected_line {
@@ -53,8 +53,8 @@ impl Audio {
log::debug!("Received line: {line:?}"); log::debug!("Received line: {line:?}");
match line.as_str() { match line.as_str() {
GRANTED_LINE => { return Ok(PermissionAnswer::Granted) }, GRANTED_LINE => return Ok(PermissionAnswer::Granted),
DENIED_LINE => { return Ok(PermissionAnswer::Denied) }, DENIED_LINE => return Ok(PermissionAnswer::Denied),
_ => {} _ => {}
}; };
} }
@@ -64,10 +64,7 @@ impl Audio {
async fn play_text(&self, text: String) -> Result<(), HumanInterfaceError> { async fn play_text(&self, text: String) -> Result<(), HumanInterfaceError> {
let bytes = self let bytes = self
.client .client
.tts(VoiceRequest { .tts(VoiceRequest { text, config: None })
text,
config: None,
})
.await .await
.map_err(|e| HumanInterfaceError::Other(format!("Failed getting tts: {e}")))?; .map_err(|e| HumanInterfaceError::Other(format!("Failed getting tts: {e}")))?;
@@ -75,9 +72,11 @@ impl Audio {
tokio::fs::write(&wav_file_path, &bytes).await?; tokio::fs::write(&wav_file_path, &bytes).await?;
let handle = rodio::DeviceSinkBuilder::open_default_sink().expect("open default audio stream"); let handle =
rodio::DeviceSinkBuilder::open_default_sink().expect("open default audio stream");
let file = std::fs::File::open(wav_file_path)?; let file = std::fs::File::open(wav_file_path)?;
let player = rodio::play(handle.mixer(), file).map_err(|e| HumanInterfaceError::Other(format!("Failed playing audio: {e}")))?; let player = rodio::play(handle.mixer(), file)
.map_err(|e| HumanInterfaceError::Other(format!("Failed playing audio: {e}")))?;
player.sleep_until_end(); player.sleep_until_end();
@@ -111,14 +110,14 @@ impl HumanInterface for Audio {
let wav_file_path = Path::new(TEMPORARY_AUDIO_PATH).join("recording.wav"); let wav_file_path = Path::new(TEMPORARY_AUDIO_PATH).join("recording.wav");
samples_to_wav( samples_to_wav(samples, &wav_spec, &wav_file_path)
samples, .map_err(|e| HumanInterfaceError::Other(format!("hound error: {e}")))?;
&wav_spec,
&wav_file_path,
)
.map_err(|e| HumanInterfaceError::Other(format!("hound error: {e}")))?;
let transcription = self.client.transcribe(wav_file_path).await.map_err(|e|HumanInterfaceError::Other(format!("failed to transcribe: {e}")))?; let transcription = self
.client
.transcribe(wav_file_path)
.await
.map_err(|e| HumanInterfaceError::Other(format!("failed to transcribe: {e}")))?;
log::info!("transcription: {transcription:?}"); log::info!("transcription: {transcription:?}");
@@ -130,7 +129,12 @@ impl HumanInterface for Audio {
mcp_server_name: String, mcp_server_name: String,
tool_name: String, tool_name: String,
) -> Result<PermissionAnswer, HumanInterfaceError> { ) -> Result<PermissionAnswer, HumanInterfaceError> {
self.play_text(tlt!("tool_allow_question_audio", mcp_server_name, tool_name)).await?; self.play_text(tlt!(
"tool_allow_question_audio",
mcp_server_name,
tool_name
))
.await?;
let decision = Self::wait_for_decision().await?; let decision = Self::wait_for_decision().await?;
log::debug!("decision: {decision:?}"); log::debug!("decision: {decision:?}");
@@ -146,11 +150,13 @@ mod tests {
const AUTH_HEADER: &str = include_str!(".AUTH_HEADER"); const AUTH_HEADER: &str = include_str!(".AUTH_HEADER");
async fn get_test_human_interface() -> Audio { async fn get_test_human_interface() -> Audio {
Audio::new(AudioClient::new( Audio::new(
url::Url::parse("https://audio.mboemer.de" AudioClient::new(url::Url::parse("https://audio.mboemer.de").unwrap())
).unwrap()). .with_authorization(AUTH_HEADER.to_string()),
with_authorization( cpal::Host::default().default_input_device().unwrap(),
AUTH_HEADER.to_string()), cpal::Host::default().default_input_device().unwrap()).await.unwrap() )
.await
.unwrap()
} }
#[tokio::test] #[tokio::test]
@@ -166,6 +172,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_tts_playing() { async fn test_tts_playing() {
let human_interface = get_test_human_interface().await; let human_interface = get_test_human_interface().await;
human_interface.play_text(String::from("Peter ist jetzt in deinem PC.")).await.unwrap(); human_interface
.play_text(String::from("Peter ist jetzt in deinem PC."))
.await
.unwrap();
} }
} }
+13 -10
View File
@@ -1,11 +1,11 @@
use std::time::Duration;
use console::style;
use crate::translate;
use crate::human_interface::{HumanInterface, HumanInterfaceError}; use crate::human_interface::{HumanInterface, HumanInterfaceError};
use crate::tlt;
use crate::translate;
use console::style;
use dialoguer::theme::ColorfulTheme; use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Input}; use dialoguer::{Confirm, Input};
use own_mcp::mcp::chat::PermissionAnswer; use own_mcp::mcp::chat::PermissionAnswer;
use crate::tlt; use std::time::Duration;
pub struct CommandLine { pub struct CommandLine {
progress_bar: Option<indicatif::ProgressBar>, progress_bar: Option<indicatif::ProgressBar>,
@@ -29,7 +29,7 @@ impl HumanInterface for CommandLine {
self.progress_bar = None; self.progress_bar = None;
} }
println!("{}: {}",style( tlt!("assistant")).bold().italic(),message); println!("{}: {}", style(tlt!("assistant")).bold().italic(), message);
Ok(()) Ok(())
} }
@@ -41,14 +41,18 @@ impl HumanInterface for CommandLine {
// waiting for agent response // waiting for agent response
self.progress_bar = Some(Self::spinner()); self.progress_bar = Some(Self::spinner());
self.progress_bar.clone().unwrap().enable_steady_tick(Duration::from_millis(100)); self.progress_bar
.clone()
.unwrap()
.enable_steady_tick(Duration::from_millis(100));
Ok(user_message) Ok(user_message)
} }
async fn ask_for_permission(&self, async fn ask_for_permission(
mcp_server_name: String, &self,
tool_name: String, mcp_server_name: String,
tool_name: String,
) -> Result<PermissionAnswer, HumanInterfaceError> { ) -> Result<PermissionAnswer, HumanInterfaceError> {
if let Some(progress_bar) = &self.progress_bar { if let Some(progress_bar) = &self.progress_bar {
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
@@ -65,4 +69,3 @@ impl HumanInterface for CommandLine {
} }
} }
} }
+12 -5
View File
@@ -1,21 +1,28 @@
pub mod cli;
pub mod audio; pub mod audio;
pub mod cli;
use thiserror::Error;
use own_mcp::mcp::chat::PermissionAnswer; use own_mcp::mcp::chat::PermissionAnswer;
use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum HumanInterfaceError { pub enum HumanInterfaceError {
#[error(transparent)] #[error(transparent)]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("other error in human interface: {0}")] #[error("other error in human interface: {0}")]
Other(String) Other(String),
} }
pub trait HumanInterface { pub trait HumanInterface {
fn agent_message(&mut self, message: String) -> impl Future<Output = Result<(), HumanInterfaceError>>; fn agent_message(
&mut self,
message: String,
) -> impl Future<Output = Result<(), HumanInterfaceError>>;
fn expect_user_message(&mut self) -> impl Future<Output = Result<String, HumanInterfaceError>>; fn expect_user_message(&mut self) -> impl Future<Output = Result<String, HumanInterfaceError>>;
fn ask_for_permission(&self, mcp_server_name: String, tool_name:String) -> impl Future<Output = Result<PermissionAnswer, HumanInterfaceError>>; fn ask_for_permission(
&self,
mcp_server_name: String,
tool_name: String,
) -> impl Future<Output = Result<PermissionAnswer, HumanInterfaceError>>;
} }
+4 -5
View File
@@ -77,11 +77,10 @@ mod tests {
let formatted = format_dynamically("{...} is cool!".to_string(), vec!["Rust".to_string()]); let formatted = format_dynamically("{...} is cool!".to_string(), vec!["Rust".to_string()]);
assert_eq!(formatted, "Rust is cool!"); assert_eq!(formatted, "Rust is cool!");
let formatted = format_dynamically("{...}, {...} and {...} are three consecutive numbers.".to_string(), vec![ let formatted = format_dynamically(
"1".to_string(), "{...}, {...} and {...} are three consecutive numbers.".to_string(),
"2".to_string(), vec!["1".to_string(), "2".to_string(), "3".to_string()],
"3".to_string(), );
]);
assert_eq!(formatted, "1, 2 and 3 are three consecutive numbers."); assert_eq!(formatted, "1, 2 and 3 are three consecutive numbers.");
} }
} }
+3 -7
View File
@@ -10,9 +10,8 @@ use crate::model::create_model_from_config;
use base64::Engine; use base64::Engine;
use clap::Parser; use clap::Parser;
use cpal::traits::HostTrait; use cpal::traits::HostTrait;
use own_mcp::AgentChat;
use std::fmt::Display;
use own_assist_common::exit_msg; use own_assist_common::exit_msg;
use own_mcp::AgentChat;
/// partial mcp client with cli and voice interaction /// partial mcp client with cli and voice interaction
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -76,10 +75,7 @@ async fn main() {
env_logger::init(); env_logger::init();
let config = Config::from_file() let config = Config::from_file()
.inspect_err(|e| { .inspect_err(exit_msg!("error loading assist.toml: {}"))
log::error!("error loading assist.toml: {}", e);
std::process::exit(1);
})
.unwrap(); .unwrap();
let ollama = config.ollama_instance(); let ollama = config.ollama_instance();
@@ -98,7 +94,7 @@ async fn main() {
.inspect_err(exit_msg!("failed creating MCP clients")) .inspect_err(exit_msg!("failed creating MCP clients"))
.unwrap(); .unwrap();
let mut agent_chat = own_mcp::AgentChat::new(ollama, model_name.clone(), mcp_clients) let mut agent_chat = AgentChat::new(ollama, model_name.clone(), mcp_clients)
.await .await
.inspect_err(exit_msg!("failed to create agent")) .inspect_err(exit_msg!("failed to create agent"))
.unwrap(); .unwrap();
+5 -8
View File
@@ -9,19 +9,16 @@ use ollama_rs::models::create::{CreateModelRequest, CreateModelStatus};
pub async fn create_model_from_config( pub async fn create_model_from_config(
ollama: &Ollama, ollama: &Ollama,
config: &OllamaModelConfig, config: &OllamaModelConfig,
interface: &HumanInterface interface: &HumanInterface,
) -> Result<CreateModelStatus, OllamaError> { ) -> Result<CreateModelStatus, OllamaError> {
let model_options = ModelOptions::default() let model_options = ModelOptions::default()
.num_ctx(config.context_size.unwrap_or(2048)) // 2048 is the ollama default. .num_ctx(config.context_size.unwrap_or(2048)) // 2048 is the ollama default.
.temperature(config.temperature.unwrap_or(0.8)); // 0.8 is the ollama default .temperature(config.temperature.unwrap_or(0.8)); // 0.8 is the ollama default
let system_prompt = config let system_prompt = config.system_prompt.clone().unwrap_or(match interface {
.system_prompt HumanInterface::Cli => tlt!("system_prompt_cli"),
.clone() HumanInterface::Audio => tlt!("system_prompt_audio"),
.unwrap_or(match interface { });
HumanInterface::Cli => tlt!("system_prompt_cli"),
HumanInterface::Audio => tlt!("system_prompt_audio"),
});
// print message to warn user of long waiting times // print message to warn user of long waiting times
if let Ok(models) = ollama.list_local_models().await if let Ok(models) = ollama.list_local_models().await