use std::io::{BufRead, IsTerminal};
use crate::{
ApiKey, DEFAULT_DIMENSION, DEFAULT_MAX_TOKENS, EmbedDeviceSetParams, EmbedModelSetParams,
EmbedRequest, LOCAL_MODEL, LocalModel, RemoteModel, Slot, SlotModel,
};
#[derive(Debug, clap::Subcommand)]
pub enum EmbeddingCommand {
Show,
#[command(subcommand)]
Local(LocalCommand),
#[command(subcommand)]
Remote(RemoteCommand),
#[command(subcommand)]
Key(KeyCommand),
}
#[derive(Debug, clap::Subcommand)]
pub enum LocalCommand {
Set {
#[arg(long, default_value_t = DEFAULT_DIMENSION)]
dimension: u32,
#[arg(long, default_value_t = DEFAULT_MAX_TOKENS)]
max_tokens: usize,
},
Clear,
Device {
switch: Switch,
},
}
#[derive(Debug, clap::Subcommand)]
pub enum RemoteCommand {
Set {
#[arg(long)]
endpoint: String,
#[arg(long)]
model: String,
#[arg(long)]
dimension: u32,
},
Clear,
Device {
switch: Switch,
},
}
#[derive(Debug, clap::Subcommand)]
pub enum KeyCommand {
Set,
Clear,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum Switch {
Auto,
On,
Off,
}
impl Switch {
fn enabled(self) -> Option<bool> {
match self {
Switch::Auto => None,
Switch::On => Some(true),
Switch::Off => Some(false),
}
}
}
impl EmbeddingCommand {
pub fn request(self) -> std::io::Result<EmbedRequest> {
let model_set = |slot, model| EmbedRequest::ModelSet(EmbedModelSetParams { slot, model });
let device_set = |slot, switch: Switch| {
EmbedRequest::DeviceSet(EmbedDeviceSetParams {
slot,
enabled: switch.enabled(),
})
};
Ok(match self {
EmbeddingCommand::Show => EmbedRequest::Show,
EmbeddingCommand::Local(LocalCommand::Set {
dimension,
max_tokens,
}) => model_set(
Slot::Local,
Some(SlotModel::Local(LocalModel {
model: LOCAL_MODEL.to_owned(),
dimension,
max_tokens,
})),
),
EmbeddingCommand::Local(LocalCommand::Clear) => model_set(Slot::Local, None),
EmbeddingCommand::Local(LocalCommand::Device { switch }) => {
device_set(Slot::Local, switch)
}
EmbeddingCommand::Remote(RemoteCommand::Set {
endpoint,
model,
dimension,
}) => model_set(
Slot::Remote,
Some(SlotModel::Remote(RemoteModel {
endpoint,
model,
dimension,
})),
),
EmbeddingCommand::Remote(RemoteCommand::Clear) => model_set(Slot::Remote, None),
EmbeddingCommand::Remote(RemoteCommand::Device { switch }) => {
device_set(Slot::Remote, switch)
}
EmbeddingCommand::Key(KeyCommand::Set) => EmbedRequest::KeySet(read_key()?),
EmbeddingCommand::Key(KeyCommand::Clear) => EmbedRequest::KeyClear,
})
}
}
pub fn read_key() -> std::io::Result<ApiKey> {
let key = if std::io::stdin().is_terminal() {
ApiKey::new(rpassword::prompt_password("API key: ")?)
} else {
let mut line = String::new();
std::io::stdin().lock().read_line(&mut line)?;
ApiKey::new(line)
};
if key.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"no key was given",
));
}
Ok(key)
}
#[derive(Debug, clap::Subcommand)]
pub enum ExternalDeviceCommand {
List,
Add {
name: String,
#[arg(long = "app")]
apps: Vec<String>,
#[arg(long)]
description: Option<String>,
},
Retire {
selector: String,
},
Restore {
selector: String,
},
Rotate {
selector: String,
},
Apps {
selector: String,
apps: Vec<String>,
},
}
impl ExternalDeviceCommand {
pub fn request(self, default_app: Option<&str>) -> crate::ExternalDeviceRequest {
use crate::{ExternalDeviceAddParams, ExternalDeviceRequest, ExternalDeviceSetAppsParams};
match self {
ExternalDeviceCommand::List => ExternalDeviceRequest::List,
ExternalDeviceCommand::Add {
name,
mut apps,
description,
} => {
if apps.is_empty()
&& let Some(app) = default_app
{
apps.push(app.to_owned());
}
ExternalDeviceRequest::Add(ExternalDeviceAddParams {
name,
description,
apps,
})
}
ExternalDeviceCommand::Retire { selector } => ExternalDeviceRequest::Retire(selector),
ExternalDeviceCommand::Restore { selector } => ExternalDeviceRequest::Restore(selector),
ExternalDeviceCommand::Rotate { selector } => ExternalDeviceRequest::Rotate(selector),
ExternalDeviceCommand::Apps { selector, apps } => {
ExternalDeviceRequest::SetApps(ExternalDeviceSetAppsParams { selector, apps })
}
}
}
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::*;
#[derive(clap::Parser)]
struct Cli {
#[command(subcommand)]
command: EmbeddingCommand,
}
fn parse(args: &[&str]) -> EmbedRequest {
let mut argv = vec!["x"];
argv.extend_from_slice(args);
Cli::parse_from(argv).command.request().unwrap()
}
#[test]
fn local_set_fills_in_the_defaults() {
let EmbedRequest::ModelSet(p) = parse(&["local", "set", "--dimension", "512"]) else {
panic!("not a model set");
};
assert_eq!(p.slot, Slot::Local);
assert_eq!(
p.model,
Some(SlotModel::Local(LocalModel {
dimension: 512,
..LocalModel::default()
}))
);
}
#[test]
fn remote_set_needs_every_field() {
let argv = [
"x",
"remote",
"set",
"--endpoint",
"https://e",
"--model",
"m",
];
assert!(Cli::try_parse_from(argv).is_err());
}
#[test]
fn device_switches_map_to_options() {
let EmbedRequest::DeviceSet(p) = parse(&["remote", "device", "off"]) else {
panic!("not a device set");
};
assert_eq!((p.slot, p.enabled), (Slot::Remote, Some(false)));
let EmbedRequest::DeviceSet(p) = parse(&["local", "device", "auto"]) else {
panic!("not a device set");
};
assert_eq!((p.slot, p.enabled), (Slot::Local, None));
}
#[test]
fn clear_is_a_model_set_without_a_model() {
let EmbedRequest::ModelSet(p) = parse(&["remote", "clear"]) else {
panic!("not a model set");
};
assert_eq!((p.slot, p.model), (Slot::Remote, None));
}
#[derive(clap::Parser)]
struct XCli {
#[command(subcommand)]
command: ExternalDeviceCommand,
}
#[test]
fn add_defaults_to_the_running_app_and_takes_several() {
let parse = |args: &[&str], app| {
let mut argv = vec!["x"];
argv.extend_from_slice(args);
XCli::parse_from(argv).command.request(app)
};
let crate::ExternalDeviceRequest::Add(p) = parse(&["add", "pendant"], Some("agent")) else {
panic!("not an add")
};
assert_eq!(p.apps, vec!["agent"]);
let crate::ExternalDeviceRequest::Add(p) =
parse(&["add", "hook", "--app", "a", "--app", "b"], Some("agent"))
else {
panic!("not an add")
};
assert_eq!(p.apps, vec!["a", "b"], "named apps replace the default");
}
}