Added ability to execute commands in a specified directory.
This commit is contained in:
72
src/chat.rs
72
src/chat.rs
@@ -12,13 +12,15 @@ use irc::client::prelude::{Client, Command, Config as IRCConfig, Message};
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{Level, event, instrument};
|
use tracing::{Level, event, instrument};
|
||||||
|
|
||||||
use crate::{Event, EventManager, LLMHandle, plugin};
|
use crate::{CommandDir, Event, EventManager, LLMHandle, plugin};
|
||||||
|
|
||||||
/// Chat struct that is used to interact with IRC chat.
|
/// Chat struct that is used to interact with IRC chat.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
/// The actual IRC [`irc::client`](client).
|
/// The actual IRC [`irc::client`](client).
|
||||||
client: Client,
|
client: Client,
|
||||||
|
/// Handle to the directory that *may* contain command scripts.
|
||||||
|
command_dir: Option<CommandDir>,
|
||||||
/// Event manager for handling plugin interaction.
|
/// Event manager for handling plugin interaction.
|
||||||
event_manager: Arc<EventManager>,
|
event_manager: Arc<EventManager>,
|
||||||
/// Handle for whichever LLM is being used.
|
/// Handle for whichever LLM is being used.
|
||||||
@@ -52,10 +54,17 @@ impl Chat {
|
|||||||
..IRCConfig::default()
|
..IRCConfig::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let commands_dir = if let Ok(path) = settings.get_string("command-path") {
|
||||||
|
Some(CommandDir::new(path))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
event!(Level::INFO, "IRC connection starting...");
|
event!(Level::INFO, "IRC connection starting...");
|
||||||
|
|
||||||
Ok(Chat {
|
Ok(Chat {
|
||||||
client: Client::from_config(config).await?,
|
client: Client::from_config(config).await?,
|
||||||
|
command_dir: commands_dir,
|
||||||
llm_handle: handle.clone(),
|
llm_handle: handle.clone(),
|
||||||
event_manager: manager,
|
event_manager: manager,
|
||||||
})
|
})
|
||||||
@@ -111,24 +120,63 @@ impl Chat {
|
|||||||
|
|
||||||
// Only handle PRIVMSG for now.
|
// Only handle PRIVMSG for now.
|
||||||
if let Command::PRIVMSG(channel, msg) = &message.command {
|
if let Command::PRIVMSG(channel, msg) = &message.command {
|
||||||
// Just preserve the original behavior for now.
|
// Check it's a command.
|
||||||
if msg.starts_with("!gem") {
|
if let Some((cmd, args)) = command_str(msg) {
|
||||||
let mut llm_response = self.llm_handle.send_request(msg).await?;
|
// Command handling time.
|
||||||
|
|
||||||
event!(Level::INFO, "Asked: {message}");
|
match cmd {
|
||||||
event!(Level::INFO, "Response: {llm_response}");
|
// Just preserve the original behavior for now.
|
||||||
|
"!gem" => {
|
||||||
|
let mut llm_response = self.llm_handle.send_request(msg).await?;
|
||||||
|
|
||||||
// Keep responses to one line for now.
|
event!(Level::INFO, "Asked: {message}");
|
||||||
llm_response.retain(|c| c != '\n' && c != '\r');
|
event!(Level::INFO, "Response: {llm_response}");
|
||||||
|
|
||||||
// TODO: Make this configurable.
|
// Keep responses to one line for now.
|
||||||
llm_response.truncate(500);
|
llm_response.retain(|c| c != '\n' && c != '\r');
|
||||||
|
|
||||||
event!(Level::INFO, "Sending {llm_response} to channel {channel}");
|
// TODO: Make this configurable.
|
||||||
self.client.send_privmsg(channel, llm_response)?;
|
llm_response.truncate(500);
|
||||||
|
|
||||||
|
event!(Level::INFO, "Sending {llm_response} to channel {channel}");
|
||||||
|
self.client.send_privmsg(channel, llm_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
if let Some(cmd_dir) = &self.command_dir {
|
||||||
|
// Strip '!'
|
||||||
|
let cmd_name = &cmd[1..];
|
||||||
|
match cmd_dir.run_command(cmd_name, args).await {
|
||||||
|
Ok(res) => {
|
||||||
|
let output = std::str::from_utf8(&res)?.to_string();
|
||||||
|
self.client.send_privmsg(channel, output)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Log the error but don't crash, and maybe don't even tell the
|
||||||
|
// user unless we're sure it
|
||||||
|
// was meant to be a command?
|
||||||
|
// For now, let's just log it.
|
||||||
|
event!(
|
||||||
|
Level::DEBUG,
|
||||||
|
"Command execution failed or not found: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_str(cmd: &str) -> Option<(&str, &str)> {
|
||||||
|
if !cmd.starts_with('!') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(cmd.split_once(' ').unwrap_or((cmd, "")))
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub mod qna;
|
|||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
pub use chat::Chat;
|
pub use chat::Chat;
|
||||||
|
pub use command::CommandDir;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
pub use event_manager::EventManager;
|
pub use event_manager::EventManager;
|
||||||
pub use qna::LLMHandle;
|
pub use qna::LLMHandle;
|
||||||
|
|||||||
39
src/setup.rs
39
src/setup.rs
@@ -3,11 +3,14 @@
|
|||||||
//! Both command line, and configuration file options are handled here.
|
//! Both command line, and configuration file options are handled here.
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::{Result, eyre::WrapErr};
|
use color_eyre::{
|
||||||
|
Result,
|
||||||
|
eyre::{OptionExt, WrapErr},
|
||||||
|
};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use directories::ProjectDirs;
|
use directories::{BaseDirs, ProjectDirs};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing::{info, instrument};
|
use tracing::{Level, event, info, instrument};
|
||||||
|
|
||||||
// TODO: use [clap(long, short, help_heading = Some(section))]
|
// TODO: use [clap(long, short, help_heading = Some(section))]
|
||||||
/// Struct of potential arguments.
|
/// Struct of potential arguments.
|
||||||
@@ -28,7 +31,7 @@ pub struct Args {
|
|||||||
|
|
||||||
/// Root directory for file based command structure.
|
/// Root directory for file based command structure.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub command_dir: Option<String>,
|
pub command_path: Option<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
/// Instructions to the model on how to behave.
|
/// Instructions to the model on how to behave.
|
||||||
@@ -92,6 +95,27 @@ pub async fn init() -> Result<Setup> {
|
|||||||
Ok(Setup { config: settings })
|
Ok(Setup { config: settings })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a path, expanding `~` to the home directory.
|
||||||
|
///
|
||||||
|
/// If the path does not start with `~`, it is returned as is.
|
||||||
|
pub fn resolve_path(path_str: &str) -> Result<PathBuf> {
|
||||||
|
event!(Level::WARN, "resolve_path called with {path_str}");
|
||||||
|
if let Some(stripped) = path_str.strip_prefix("~") {
|
||||||
|
let base_dirs = BaseDirs::new().ok_or_eyre("Unable to expand '~'.")?;
|
||||||
|
event!(
|
||||||
|
Level::DEBUG,
|
||||||
|
"home_dir() decided on {}",
|
||||||
|
base_dirs.home_dir().display()
|
||||||
|
);
|
||||||
|
let relative = stripped
|
||||||
|
.strip_prefix(std::path::MAIN_SEPARATOR_STR)
|
||||||
|
.unwrap_or(stripped);
|
||||||
|
return Ok(base_dirs.home_dir().join(relative));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PathBuf::from(path_str))
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a configuration object from arguments.
|
/// Create a configuration object from arguments.
|
||||||
///
|
///
|
||||||
/// This is exposed for testing purposes.
|
/// This is exposed for testing purposes.
|
||||||
@@ -117,7 +141,12 @@ pub fn make_config(args: Args) -> Result<Config> {
|
|||||||
.set_override_option("api-key", args.api_key.clone())?
|
.set_override_option("api-key", args.api_key.clone())?
|
||||||
.set_override_option("base-url", args.base_url.clone())?
|
.set_override_option("base-url", args.base_url.clone())?
|
||||||
.set_override_option("chroot-dir", args.chroot_dir.clone())?
|
.set_override_option("chroot-dir", args.chroot_dir.clone())?
|
||||||
.set_override_option("command-path", args.command_dir.clone())?
|
.set_override_option(
|
||||||
|
"command-path",
|
||||||
|
// A path expansion is a panic situation so just unwrap() is fine.
|
||||||
|
args.command_path
|
||||||
|
.map(|p| resolve_path(&p).unwrap().to_string_lossy().to_string()),
|
||||||
|
)?
|
||||||
.set_override_option("model", args.model.clone())?
|
.set_override_option("model", args.model.clone())?
|
||||||
.set_override_option("nick-password", args.nick_password.clone())?
|
.set_override_option("nick-password", args.nick_password.clone())?
|
||||||
.set_override_option("instruct", args.instruct.clone())?
|
.set_override_option("instruct", args.instruct.clone())?
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ port = 6667
|
|||||||
base_url: None, /* Should fail if required and not in file/env? No, base-url is optional
|
base_url: None, /* Should fail if required and not in file/env? No, base-url is optional
|
||||||
* in args */
|
* in args */
|
||||||
chroot_dir: None,
|
chroot_dir: None,
|
||||||
command_dir: None,
|
command_path: None,
|
||||||
instruct: None,
|
instruct: None,
|
||||||
model: None, // Should fallback to file
|
model: None, // Should fallback to file
|
||||||
channels: None,
|
channels: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user