refactoring
partially implement tts for human interface
This commit is contained in:
@@ -2,3 +2,4 @@
|
|||||||
*/target
|
*/target
|
||||||
assist.toml
|
assist.toml
|
||||||
mcp_server_collection/server.toml
|
mcp_server_collection/server.toml
|
||||||
|
temporary_audio
|
||||||
@@ -6,14 +6,13 @@ permissions = [
|
|||||||
|
|
||||||
[ollama]
|
[ollama]
|
||||||
url = "..." # optional
|
url = "..." # optional
|
||||||
model = {
|
authorization="..." # optional
|
||||||
from_model = "..."
|
[ollama.model]
|
||||||
name = "..." name of model to be created
|
from-model = "..."
|
||||||
|
name = "..." # name of model to be created
|
||||||
system_prompt = "..." # optional
|
system_prompt = "..." # optional
|
||||||
context_size = 2048 # optional
|
context_size = 2048 # optional
|
||||||
temperature = 0.8 # optional
|
temperature = 0.8 # optional
|
||||||
}
|
|
||||||
authorization="..." # optional
|
|
||||||
|
|
||||||
[mcp-servers]
|
[mcp-servers]
|
||||||
name="..." # optional, otherwise provided server name or random server name is chosen
|
name="..." # optional, otherwise provided server name or random server name is chosen
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::path::Path;
|
use crate::audio::models::{AudioServerStatus, TranscriptionResponse, VoiceRequest};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use reqwest::multipart::Form;
|
use reqwest::multipart::Form;
|
||||||
|
use std::path::Path;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use crate::audio::models::{AudioServerStatus, TranscriptionResponse, VoiceRequest};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub struct AudioClient {
|
pub struct AudioClient {
|
||||||
@@ -19,7 +19,10 @@ pub trait AudioClientTrait {
|
|||||||
|
|
||||||
fn status(&self) -> impl Future<Output = Result<AudioServerStatus, AudioError>>;
|
fn status(&self) -> impl Future<Output = Result<AudioServerStatus, AudioError>>;
|
||||||
|
|
||||||
fn transcribe(&self, audio_file_path: impl AsRef<Path>) -> impl Future<Output=AudioResult<TranscriptionResponse>>;
|
fn transcribe(
|
||||||
|
&self,
|
||||||
|
audio_file_path: impl AsRef<Path>,
|
||||||
|
) -> impl Future<Output = AudioResult<TranscriptionResponse>>;
|
||||||
|
|
||||||
fn tts(&self, voice_request: VoiceRequest) -> impl Future<Output = AudioResult<bytes::Bytes>>;
|
fn tts(&self, voice_request: VoiceRequest) -> impl Future<Output = AudioResult<bytes::Bytes>>;
|
||||||
}
|
}
|
||||||
@@ -36,13 +39,27 @@ pub enum AudioError {
|
|||||||
|
|
||||||
type AudioResult<T> = Result<T, AudioError>;
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AudioClientTrait for AudioClient {
|
impl AudioClientTrait for AudioClient {
|
||||||
fn new(base_url: Url) -> Self {
|
fn new(base_url: Url) -> Self {
|
||||||
AudioClient { client: Client::new(), authorization: None, base_url }
|
AudioClient {
|
||||||
|
client: Client::new(),
|
||||||
|
authorization: None,
|
||||||
|
base_url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_client(base_url: Url, client: Client) -> Self {
|
fn from_client(base_url: Url, client: Client) -> Self {
|
||||||
AudioClient { client, authorization: None, base_url }
|
AudioClient {
|
||||||
|
client,
|
||||||
|
authorization: None,
|
||||||
|
base_url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_authorization(mut self, authorization: String) -> Self {
|
fn with_authorization(mut self, authorization: String) -> Self {
|
||||||
@@ -51,19 +68,35 @@ impl AudioClientTrait for AudioClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn status(&self) -> AudioResult<AudioServerStatus> {
|
async fn status(&self) -> AudioResult<AudioServerStatus> {
|
||||||
let response = self.client.get(self.base_url.clone()).send().await?.error_for_status()?;
|
let response = self
|
||||||
|
.get(self.base_url.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
Ok(response.json().await?)
|
Ok(response.json().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn transcribe(&self, audio_file_path: impl AsRef<Path>) -> AudioResult<TranscriptionResponse> {
|
async fn transcribe(
|
||||||
|
&self,
|
||||||
|
audio_file_path: impl AsRef<Path>,
|
||||||
|
) -> AudioResult<TranscriptionResponse> {
|
||||||
let form = Form::new().file("audio_file", audio_file_path).await?;
|
let form = Form::new().file("audio_file", audio_file_path).await?;
|
||||||
|
|
||||||
let response = self.client.get(self.base_url.join("transcribe")?).multipart(form).send().await?;
|
let response = self
|
||||||
|
.get(self.base_url.join("transcribe")?)
|
||||||
|
.multipart(form)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
Ok(response.json().await?)
|
Ok(response.json().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tts(&self, voice_request: VoiceRequest) -> AudioResult<bytes::Bytes> {
|
async fn tts(&self, voice_request: VoiceRequest) -> AudioResult<bytes::Bytes> {
|
||||||
let response = self.client.get(self.base_url.join("tts")?).json(&voice_request).send().await?.error_for_status()?;
|
let response = self
|
||||||
|
.get(self.base_url.join("tts")?)
|
||||||
|
.json(&voice_request)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
Ok(response.bytes().await?)
|
Ok(response.bytes().await?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ pub struct OllamaConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct OllamaModelConfig {
|
pub struct OllamaModelConfig {
|
||||||
|
#[serde(rename = "from-model")]
|
||||||
pub from_model: String,
|
pub from_model: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub system_prompt: Option<String>,
|
pub system_prompt: Option<String>,
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use crate::human_interface::{HumanInterface, HumanInterfaceError};
|
||||||
|
use own_mcp::audio::models::VoiceRequest;
|
||||||
|
use own_mcp::audio::{AudioClient, AudioClientTrait};
|
||||||
|
use own_mcp::mcp::chat::PermissionAnswer;
|
||||||
|
use std::path::Path;
|
||||||
|
use tokio::fs::create_dir_all;
|
||||||
|
|
||||||
|
const TEMPORARY_AUDIO_PATH: &str = "temporary_audio";
|
||||||
|
|
||||||
|
struct Audio {
|
||||||
|
client: AudioClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Audio {
|
||||||
|
async fn new(client: AudioClient) -> Result<Self, HumanInterfaceError> {
|
||||||
|
create_dir_all(TEMPORARY_AUDIO_PATH)
|
||||||
|
.await
|
||||||
|
.map_err(HumanInterfaceError::IoError)?;
|
||||||
|
Ok(Self { client })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HumanInterface for Audio {
|
||||||
|
async fn agent_message(&mut self, message: String) -> Result<(), HumanInterfaceError> {
|
||||||
|
let bytes = self
|
||||||
|
.client
|
||||||
|
.tts(VoiceRequest {
|
||||||
|
text: message,
|
||||||
|
config: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| HumanInterfaceError::Other(format!("Failed getting tts: {e}")))?;
|
||||||
|
|
||||||
|
tokio::fs::write(Path::new(TEMPORARY_AUDIO_PATH).join("tts.wav"), &bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|e| HumanInterfaceError::IoError(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expect_user_message(&mut self) -> Result<String, HumanInterfaceError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ask_for_permission(
|
||||||
|
&self,
|
||||||
|
mcp_server_name: String,
|
||||||
|
tool_name: String,
|
||||||
|
) -> Result<PermissionAnswer, HumanInterfaceError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_tts_fetching() {
|
||||||
|
let mut human_interface = Audio::new(
|
||||||
|
AudioClient::new(
|
||||||
|
url::Url::parse("https://audio.mboemer.de"
|
||||||
|
).unwrap()).
|
||||||
|
with_authorization(
|
||||||
|
"Basic bWlsYW46a3lLRVIjOVJAaDlAXldBI3pVXkAhb0pRIXViczNNV0g3U1NzQDVUenppJVFmciY5WEpxJXB1YmN6YiEkdmY1Z2FQd0N2aEd3".to_string())
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
human_interface
|
||||||
|
.agent_message(String::from("Peter ist jetzt in deinem PC."))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ impl CommandLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HumanInterface for CommandLine {
|
impl HumanInterface for CommandLine {
|
||||||
fn agent_message(&mut self, message: String) {
|
async fn agent_message(&mut self, message: String) -> Result<(), HumanInterfaceError> {
|
||||||
// stopped waiting for message
|
// stopped waiting for message
|
||||||
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();
|
||||||
@@ -30,6 +30,7 @@ impl HumanInterface for CommandLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!("{}: {}",style( tlt!("assistant")).bold().italic(),message);
|
println!("{}: {}",style( tlt!("assistant")).bold().italic(),message);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn expect_user_message(&mut self) -> Result<String, HumanInterfaceError> {
|
async fn expect_user_message(&mut self) -> Result<String, HumanInterfaceError> {
|
||||||
@@ -63,9 +64,5 @@ impl HumanInterface for CommandLine {
|
|||||||
false => Ok(PermissionAnswer::Denied),
|
false => Ok(PermissionAnswer::Denied),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&mut self) -> Result<(), HumanInterfaceError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
pub mod audio;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use own_mcp::mcp::chat::PermissionAnswer;
|
use own_mcp::mcp::chat::PermissionAnswer;
|
||||||
@@ -12,11 +13,9 @@ pub enum HumanInterfaceError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait HumanInterface {
|
pub trait HumanInterface {
|
||||||
fn agent_message(&mut self, message: String);
|
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>>;
|
||||||
|
|
||||||
fn run(&mut self) -> impl Future<Output = Result<(), HumanInterfaceError>>;
|
|
||||||
}
|
}
|
||||||
+12
-5
@@ -7,9 +7,9 @@ use crate::config::Config;
|
|||||||
use crate::human_interface::HumanInterface;
|
use crate::human_interface::HumanInterface;
|
||||||
use crate::i18n::translate;
|
use crate::i18n::translate;
|
||||||
use crate::model::create_model_from_config;
|
use crate::model::create_model_from_config;
|
||||||
|
use base64::Engine;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use base64::Engine;
|
|
||||||
|
|
||||||
/// partial mcp client with cli and voice interaction
|
/// partial mcp client with cli and voice interaction
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -86,7 +86,6 @@ async fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut human_interface = human_interface::cli::CommandLine::new();
|
let mut human_interface = human_interface::cli::CommandLine::new();
|
||||||
human_interface.run().await.unwrap();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let user_message = human_interface.expect_user_message().await.unwrap();
|
let user_message = human_interface.expect_user_message().await.unwrap();
|
||||||
@@ -96,13 +95,21 @@ async fn main() {
|
|||||||
immutable_interface
|
immutable_interface
|
||||||
.ask_for_permission(mcp_server_name, tool_name)
|
.ask_for_permission(mcp_server_name, tool_name)
|
||||||
.await
|
.await
|
||||||
.inspect_err(exit_msg!("Human interface error"))
|
.inspect_err(exit_msg!(
|
||||||
|
"Human interface error while asking for permission"
|
||||||
|
))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.inspect_err(exit_msg!("failed communicating with agent"))
|
.inspect_err(exit_msg!("Failed communicating with agent."))
|
||||||
|
.unwrap();
|
||||||
|
human_interface
|
||||||
|
.agent_message(agent_message.content)
|
||||||
|
.await
|
||||||
|
.inspect_err(exit_msg!(
|
||||||
|
"Human interface error while trying to display message"
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
human_interface.agent_message(agent_message.content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-3
@@ -1,10 +1,10 @@
|
|||||||
use crate::translate;
|
|
||||||
use crate::config::OllamaModelConfig;
|
use crate::config::OllamaModelConfig;
|
||||||
|
use crate::tlt;
|
||||||
|
use crate::translate;
|
||||||
use ollama_rs::Ollama;
|
use ollama_rs::Ollama;
|
||||||
use ollama_rs::error::OllamaError;
|
use ollama_rs::error::OllamaError;
|
||||||
use ollama_rs::models::ModelOptions;
|
use ollama_rs::models::ModelOptions;
|
||||||
use ollama_rs::models::create::{CreateModelRequest, CreateModelStatus};
|
use ollama_rs::models::create::{CreateModelRequest, CreateModelStatus};
|
||||||
use crate::tlt;
|
|
||||||
|
|
||||||
pub async fn create_model_from_config(
|
pub async fn create_model_from_config(
|
||||||
ollama: &Ollama,
|
ollama: &Ollama,
|
||||||
@@ -14,7 +14,22 @@ pub async fn create_model_from_config(
|
|||||||
.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.system_prompt.clone().unwrap_or(tlt!("system_prompt"));
|
let system_prompt = config
|
||||||
|
.system_prompt
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(tlt!("system_prompt"));
|
||||||
|
|
||||||
|
// print message to warn user of long waiting times
|
||||||
|
if let Ok(models) = ollama.list_local_models().await
|
||||||
|
&& !models.iter().any(|m| m.name == config.from_model)
|
||||||
|
{
|
||||||
|
let message = format!(
|
||||||
|
"Model `{}` is not installed and will be downloaded first.",
|
||||||
|
config.from_model
|
||||||
|
);
|
||||||
|
log::info!("{}", message);
|
||||||
|
println!("{}", console::style(message).yellow().bold());
|
||||||
|
}
|
||||||
|
|
||||||
ollama
|
ollama
|
||||||
.create_model(
|
.create_model(
|
||||||
|
|||||||
Reference in New Issue
Block a user