use async_trait::async_trait;
use serde_json::Value;
use thiserror::Error;
use nexo_tool_meta::admin::persona::{
PersonaLocales, PersonaSaveLocalizedRequest, PersonaSaveLocalizedResponse,
};
use crate::agent::admin_rpc::dispatcher::{AdminRpcError, AdminRpcResult};
#[derive(Debug, Error)]
pub enum PersonaStoreError {
#[error("invalid locale: {0}")]
InvalidLocale(String),
#[error("agent {0:?} not found")]
NotFound(String),
#[error("io: {0}")]
Io(String),
}
#[async_trait]
pub trait PersonaSnapshotReader: Send + Sync + std::fmt::Debug {
async fn read_locales(&self, agent_id: &str) -> Option<PersonaLocales>;
}
#[async_trait]
pub trait PersonaStore: Send + Sync + std::fmt::Debug {
async fn save_localized(
&self,
req: PersonaSaveLocalizedRequest,
) -> Result<PersonaSaveLocalizedResponse, PersonaStoreError>;
}
pub async fn save_localized(store: &dyn PersonaStore, params: Value) -> AdminRpcResult {
let req: PersonaSaveLocalizedRequest = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return AdminRpcResult::err(AdminRpcError::InvalidParams(e.to_string())),
};
match store.save_localized(req).await {
Ok(resp) => AdminRpcResult::ok(serde_json::to_value(resp).unwrap_or(Value::Null)),
Err(PersonaStoreError::InvalidLocale(msg)) => {
AdminRpcResult::err(AdminRpcError::InvalidParams(msg))
}
Err(PersonaStoreError::NotFound(id)) => {
AdminRpcResult::err(AdminRpcError::Internal(format!("agent {id:?} not found")))
}
Err(PersonaStoreError::Io(msg)) => {
AdminRpcResult::err(AdminRpcError::Internal(format!("io error: {msg}")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nexo_tool_meta::admin::persona::PersonaLocales;
#[derive(Debug)]
struct StubStore;
#[async_trait]
impl PersonaStore for StubStore {
async fn save_localized(
&self,
req: PersonaSaveLocalizedRequest,
) -> Result<PersonaSaveLocalizedResponse, PersonaStoreError> {
assert_eq!(req.agent_id, "cody");
Ok(PersonaSaveLocalizedResponse {
written_paths: vec!["/tmp/cody/IDENTITY.es.md".into()],
persona_locales: PersonaLocales {
available: vec!["es".into(), "en".into()],
snapshots: vec![],
},
})
}
}
#[derive(Debug)]
struct ErrStore;
#[async_trait]
impl PersonaStore for ErrStore {
async fn save_localized(
&self,
_req: PersonaSaveLocalizedRequest,
) -> Result<PersonaSaveLocalizedResponse, PersonaStoreError> {
Err(PersonaStoreError::InvalidLocale("klingon".into()))
}
}
#[tokio::test]
async fn save_localized_ok_emits_response() {
let params = serde_json::json!({
"agent_id": "cody",
"locale": "es",
"system_prompt": "p",
"identity": "i",
"soul": "s",
"user": "u",
"agents": "a",
});
let res = save_localized(&StubStore, params).await;
let v = res.result.expect("ok");
assert_eq!(v["persona_locales"]["available"][0], "es");
}
#[tokio::test]
async fn save_localized_invalid_locale_maps_to_invalid_params() {
let params = serde_json::json!({
"agent_id": "x",
"locale": "klingon",
"system_prompt": "",
"identity": "",
"soul": "",
"user": "",
"agents": "",
});
let res = save_localized(&ErrStore, params).await;
let err = res.error.unwrap();
assert!(matches!(err, AdminRpcError::InvalidParams(_)));
}
}