apply formatting
This commit is contained in:
@@ -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
|
||||||
@@ -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,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};
|
||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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(
|
||||||
µphone,
|
µphone,
|
||||||
&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
@@ -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;
|
||||||
+51
-29
@@ -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())
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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,
|
|
||||||
&wav_spec,
|
|
||||||
&wav_file_path,
|
|
||||||
)
|
|
||||||
.map_err(|e| HumanInterfaceError::Other(format!("hound error: {e}")))?;
|
.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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
@@ -41,12 +41,16 @@ 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(
|
||||||
|
&self,
|
||||||
mcp_server_name: String,
|
mcp_server_name: String,
|
||||||
tool_name: String,
|
tool_name: String,
|
||||||
) -> Result<PermissionAnswer, HumanInterfaceError> {
|
) -> Result<PermissionAnswer, HumanInterfaceError> {
|
||||||
@@ -65,4 +69,3 @@ impl HumanInterface for CommandLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
+2
-5
@@ -9,16 +9,13 @@ 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
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(match interface {
|
|
||||||
HumanInterface::Cli => tlt!("system_prompt_cli"),
|
HumanInterface::Cli => tlt!("system_prompt_cli"),
|
||||||
HumanInterface::Audio => tlt!("system_prompt_audio"),
|
HumanInterface::Audio => tlt!("system_prompt_audio"),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user