feat(acp): replace raw config and secret methods (#9000)
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
@@ -74,7 +74,6 @@ mod dispatch;
|
||||
mod extensions;
|
||||
mod providers;
|
||||
mod resources;
|
||||
mod secrets;
|
||||
mod sessions;
|
||||
mod sources;
|
||||
mod tools;
|
||||
|
||||
@@ -1,36 +1,173 @@
|
||||
use super::*;
|
||||
|
||||
impl GooseAcpAgent {
|
||||
pub(super) async fn on_read_config(
|
||||
pub(super) async fn on_preferences_read(
|
||||
&self,
|
||||
req: ReadConfigRequest,
|
||||
) -> Result<ReadConfigResponse, sacp::Error> {
|
||||
req: PreferencesReadRequest,
|
||||
) -> Result<PreferencesReadResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
let response = match config.get_param::<serde_json::Value>(&req.key) {
|
||||
Ok(value) => ReadConfigResponse { value },
|
||||
Err(crate::config::ConfigError::NotFound(_)) => ReadConfigResponse {
|
||||
value: serde_json::Value::Null,
|
||||
},
|
||||
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
let keys = if req.keys.is_empty() {
|
||||
PREFERENCE_DEFS.iter().map(|def| def.key).collect()
|
||||
} else {
|
||||
req.keys
|
||||
};
|
||||
Ok(response)
|
||||
let mut values = Vec::with_capacity(keys.len());
|
||||
|
||||
for key in keys {
|
||||
let def = preference_def(key)?;
|
||||
let value = match config.get_param::<serde_json::Value>(def.config_key) {
|
||||
Ok(value) => value,
|
||||
Err(crate::config::ConfigError::NotFound(_)) => serde_json::Value::Null,
|
||||
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
};
|
||||
values.push(PreferenceValue { key, value });
|
||||
}
|
||||
|
||||
Ok(PreferencesReadResponse { values })
|
||||
}
|
||||
|
||||
pub(super) async fn on_upsert_config(
|
||||
pub(super) async fn on_preferences_save(
|
||||
&self,
|
||||
req: UpsertConfigRequest,
|
||||
req: PreferencesSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.set_param(&req.key, &req.value).internal_err()?;
|
||||
let mut updates = Vec::with_capacity(req.values.len());
|
||||
|
||||
for preference in &req.values {
|
||||
let def = preference_def(preference.key)?;
|
||||
(def.validate)(&preference.value)?;
|
||||
updates.push((def.config_key.to_string(), preference.value.clone()));
|
||||
}
|
||||
|
||||
config.set_param_values(&updates).internal_err()?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_remove_config(
|
||||
pub(super) async fn on_preferences_remove(
|
||||
&self,
|
||||
req: RemoveConfigRequest,
|
||||
req: PreferencesRemoveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.delete(&req.key).internal_err()?;
|
||||
for key in req.keys {
|
||||
let def = preference_def(key)?;
|
||||
config.delete(def.config_key).internal_err()?;
|
||||
}
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_defaults_read(
|
||||
&self,
|
||||
_req: DefaultsReadRequest,
|
||||
) -> Result<DefaultsReadResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
Ok(DefaultsReadResponse {
|
||||
provider_id: optional_config_string(&config, "GOOSE_PROVIDER")?,
|
||||
model_id: optional_config_string(&config, "GOOSE_MODEL")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PreferenceDef {
|
||||
key: PreferenceKey,
|
||||
config_key: &'static str,
|
||||
validate: fn(&serde_json::Value) -> Result<(), sacp::Error>,
|
||||
}
|
||||
|
||||
const PREFERENCE_DEFS: &[PreferenceDef] = &[
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::AutoCompactThreshold,
|
||||
config_key: "GOOSE_AUTO_COMPACT_THRESHOLD",
|
||||
validate: validate_auto_compact_threshold,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceAutoSubmitPhrases,
|
||||
config_key: "VOICE_AUTO_SUBMIT_PHRASES",
|
||||
validate: validate_voice_auto_submit_phrases,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceDictationProvider,
|
||||
config_key: "VOICE_DICTATION_PROVIDER",
|
||||
validate: validate_voice_dictation_provider,
|
||||
},
|
||||
PreferenceDef {
|
||||
key: PreferenceKey::VoiceDictationPreferredMic,
|
||||
config_key: "VOICE_DICTATION_PREFERRED_MIC",
|
||||
validate: validate_voice_dictation_preferred_mic,
|
||||
},
|
||||
];
|
||||
|
||||
fn preference_def(key: PreferenceKey) -> Result<&'static PreferenceDef, sacp::Error> {
|
||||
PREFERENCE_DEFS
|
||||
.iter()
|
||||
.find(|def| def.key == key)
|
||||
.ok_or_else(|| {
|
||||
sacp::Error::internal_error().data(format!("Missing preference definition for {key:?}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_auto_compact_threshold(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_f64() else {
|
||||
return Err(sacp::Error::invalid_params().data("autoCompactThreshold must be a number"));
|
||||
};
|
||||
if !value.is_finite() || value <= 0.0 || value > 1.0 {
|
||||
return Err(sacp::Error::invalid_params()
|
||||
.data("autoCompactThreshold must be greater than 0 and at most 1"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_auto_submit_phrases(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
if !value.is_string() {
|
||||
return Err(sacp::Error::invalid_params().data("voiceAutoSubmitPhrases must be a string"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_dictation_provider(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_str() else {
|
||||
return Err(sacp::Error::invalid_params().data("voiceDictationProvider must be a string"));
|
||||
};
|
||||
if !is_supported_voice_dictation_provider(value) {
|
||||
return Err(sacp::Error::invalid_params().data("voiceDictationProvider is not supported"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_voice_dictation_preferred_mic(value: &serde_json::Value) -> Result<(), sacp::Error> {
|
||||
let Some(value) = value.as_str() else {
|
||||
return Err(
|
||||
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be a string")
|
||||
);
|
||||
};
|
||||
if value.is_empty() {
|
||||
return Err(
|
||||
sacp::Error::invalid_params().data("voiceDictationPreferredMic must be non-empty")
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_supported_voice_dictation_provider(value: &str) -> bool {
|
||||
matches!(value, "openai" | "groq" | "elevenlabs" | "__disabled__") || {
|
||||
#[cfg(feature = "local-inference")]
|
||||
{
|
||||
value == "local"
|
||||
}
|
||||
#[cfg(not(feature = "local-inference"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_config_string(config: &Config, key: &str) -> Result<Option<String>, sacp::Error> {
|
||||
match config.get_param::<String>(key) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(crate::config::ConfigError::NotFound(_)) => Ok(None),
|
||||
Err(e) => Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,52 +200,36 @@ impl GooseAcpAgent {
|
||||
self.on_delete_provider_config(req).await
|
||||
}
|
||||
|
||||
#[custom_method(ReadConfigRequest)]
|
||||
async fn dispatch_read_config(
|
||||
#[custom_method(PreferencesReadRequest)]
|
||||
async fn dispatch_preferences_read(
|
||||
&self,
|
||||
req: ReadConfigRequest,
|
||||
) -> Result<ReadConfigResponse, sacp::Error> {
|
||||
self.on_read_config(req).await
|
||||
req: PreferencesReadRequest,
|
||||
) -> Result<PreferencesReadResponse, sacp::Error> {
|
||||
self.on_preferences_read(req).await
|
||||
}
|
||||
|
||||
#[custom_method(UpsertConfigRequest)]
|
||||
async fn dispatch_upsert_config(
|
||||
#[custom_method(PreferencesSaveRequest)]
|
||||
async fn dispatch_preferences_save(
|
||||
&self,
|
||||
req: UpsertConfigRequest,
|
||||
req: PreferencesSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_upsert_config(req).await
|
||||
self.on_preferences_save(req).await
|
||||
}
|
||||
|
||||
#[custom_method(RemoveConfigRequest)]
|
||||
async fn dispatch_remove_config(
|
||||
#[custom_method(PreferencesRemoveRequest)]
|
||||
async fn dispatch_preferences_remove(
|
||||
&self,
|
||||
req: RemoveConfigRequest,
|
||||
req: PreferencesRemoveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_remove_config(req).await
|
||||
self.on_preferences_remove(req).await
|
||||
}
|
||||
|
||||
#[custom_method(CheckSecretRequest)]
|
||||
async fn dispatch_check_secret(
|
||||
#[custom_method(DefaultsReadRequest)]
|
||||
async fn dispatch_defaults_read(
|
||||
&self,
|
||||
req: CheckSecretRequest,
|
||||
) -> Result<CheckSecretResponse, sacp::Error> {
|
||||
self.on_check_secret(req).await
|
||||
}
|
||||
|
||||
#[custom_method(UpsertSecretRequest)]
|
||||
async fn dispatch_upsert_secret(
|
||||
&self,
|
||||
req: UpsertSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_upsert_secret(req).await
|
||||
}
|
||||
|
||||
#[custom_method(RemoveSecretRequest)]
|
||||
async fn dispatch_remove_secret(
|
||||
&self,
|
||||
req: RemoveSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_remove_secret(req).await
|
||||
req: DefaultsReadRequest,
|
||||
) -> Result<DefaultsReadResponse, sacp::Error> {
|
||||
self.on_defaults_read(req).await
|
||||
}
|
||||
|
||||
#[custom_method(ExportSessionRequest)]
|
||||
@@ -360,6 +344,22 @@ impl GooseAcpAgent {
|
||||
self.on_dictation_config(_req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationSecretSaveRequest)]
|
||||
async fn dispatch_dictation_secret_save(
|
||||
&self,
|
||||
req: DictationSecretSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_dictation_secret_save(req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationSecretDeleteRequest)]
|
||||
async fn dispatch_dictation_secret_delete(
|
||||
&self,
|
||||
req: DictationSecretDeleteRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.on_dictation_secret_delete(req).await
|
||||
}
|
||||
|
||||
#[custom_method(DictationModelsListRequest)]
|
||||
async fn dispatch_dictation_models_list(
|
||||
&self,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
#[cfg(feature = "local-inference")]
|
||||
use crate::dictation::providers::transcribe_local;
|
||||
use crate::dictation::providers::{
|
||||
all_providers, is_configured, transcribe_with_provider, DictationProvider,
|
||||
all_providers, get_provider_def, is_configured, transcribe_with_provider, DictationProvider,
|
||||
};
|
||||
#[cfg(feature = "local-inference")]
|
||||
use crate::dictation::whisper;
|
||||
@@ -125,6 +125,30 @@ impl GooseAcpAgent {
|
||||
Ok(DictationConfigResponse { providers })
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_secret_save(
|
||||
&self,
|
||||
req: DictationSecretSaveRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let provider = parse_dictation_provider(&req.provider)?;
|
||||
let key = dictation_secret_config_key(provider)?;
|
||||
let config = self.config()?;
|
||||
config.set_secret(key, &req.value).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_secret_delete(
|
||||
&self,
|
||||
req: DictationSecretDeleteRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let provider = parse_dictation_provider(&req.provider)?;
|
||||
let key = dictation_secret_config_key(provider)?;
|
||||
let config = self.config()?;
|
||||
config.delete_secret(key).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_dictation_models_list(
|
||||
&self,
|
||||
_req: DictationModelsListRequest,
|
||||
@@ -321,6 +345,29 @@ impl GooseAcpAgent {
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_dictation_provider(provider: &str) -> Result<DictationProvider, sacp::Error> {
|
||||
serde_json::from_value(serde_json::Value::String(provider.to_string()))
|
||||
.map_err(|_| sacp::Error::invalid_params().data(format!("Unknown provider: {provider}")))
|
||||
}
|
||||
|
||||
fn dictation_secret_config_key(provider: DictationProvider) -> Result<&'static str, sacp::Error> {
|
||||
let def = get_provider_def(provider);
|
||||
if def.uses_provider_config {
|
||||
return Err(sacp::Error::invalid_params().data(
|
||||
"Dictation provider uses the main provider configuration. Configure its credentials in provider settings instead.",
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "local-inference")]
|
||||
if provider == DictationProvider::Local {
|
||||
return Err(sacp::Error::invalid_params()
|
||||
.data("Dictation provider does not use an API key or secret."));
|
||||
}
|
||||
|
||||
Ok(def.config_key)
|
||||
}
|
||||
|
||||
fn dictation_model_config_key(provider: DictationProvider) -> Option<String> {
|
||||
match provider {
|
||||
DictationProvider::OpenAI => Some(OPENAI_TRANSCRIPTION_MODEL_CONFIG_KEY.to_string()),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
impl GooseAcpAgent {
|
||||
pub(super) async fn on_check_secret(
|
||||
&self,
|
||||
req: CheckSecretRequest,
|
||||
) -> Result<CheckSecretResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
let exists = config.get_secret::<serde_json::Value>(&req.key).is_ok();
|
||||
Ok(CheckSecretResponse { exists })
|
||||
}
|
||||
|
||||
pub(super) async fn on_upsert_secret(
|
||||
&self,
|
||||
req: UpsertSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.set_secret(&req.key, &req.value).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
pub(super) async fn on_remove_secret(
|
||||
&self,
|
||||
req: RemoveSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.config()?;
|
||||
config.delete_secret(&req.key).internal_err()?;
|
||||
Config::global().invalidate_secrets_cache();
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
}
|
||||
@@ -719,6 +719,20 @@ impl Config {
|
||||
self.save_values(&values)
|
||||
}
|
||||
|
||||
/// Set multiple configuration values in the config file with one read and one write.
|
||||
pub fn set_param_values(&self, updates: &[(String, Value)]) -> Result<(), ConfigError> {
|
||||
if updates.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _guard = self.guard.lock().unwrap();
|
||||
let mut values = self.load_write_config()?;
|
||||
for (key, value) in updates {
|
||||
values.insert(serde_yaml::to_value(key)?, serde_yaml::to_value(value)?);
|
||||
}
|
||||
self.save_values(&values)
|
||||
}
|
||||
|
||||
/// Delete a configuration value in the config file.
|
||||
///
|
||||
/// This will immediately write the value to the config file. The value
|
||||
|
||||
Reference in New Issue
Block a user