#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Persona {
name: String,
framing: Option<String>,
focus: Option<String>,
}
impl Persona {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
framing: None,
focus: None,
}
}
#[must_use]
pub fn with_framing(mut self, framing: impl Into<String>) -> Self {
self.framing = Some(framing.into());
self
}
#[must_use]
pub fn with_focus(mut self, focus: impl Into<String>) -> Self {
self.focus = Some(focus.into());
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn framing(&self) -> Option<&str> {
self.framing.as_deref()
}
pub fn focus(&self) -> Option<&str> {
self.focus.as_deref()
}
pub fn default_panel() -> Vec<Persona> {
vec![Persona::new("Devil's Advocate")
.with_framing("Assume the proposal is wrong; find how.")
.with_focus("logical gaps and unsupported assumptions")]
}
pub fn archetypes() -> &'static [Persona] {
static ARCHETYPES: std::sync::OnceLock<Vec<Persona>> = std::sync::OnceLock::new();
ARCHETYPES.get_or_init(|| {
vec![
Persona::new("Devil's Advocate")
.with_framing("Assume the proposal is wrong; find how.")
.with_focus("logical gaps and unsupported assumptions"),
Persona::new("Methodologist")
.with_framing("Scrutinize the rigor of every claim.")
.with_focus("proof gaps and methodological soundness"),
Persona::new("Red Team")
.with_framing("Find how this fails in practice.")
.with_focus("failure modes and adversarial conditions"),
Persona::new("Domain Expert")
.with_framing("Evaluate against the domain state of the art.")
.with_focus("technical accuracy and novelty"),
Persona::new("Editor")
.with_framing("Improve clarity and structure.")
.with_focus("readability and missing context"),
]
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Panel {
Default,
Duo,
Panel,
}
impl Panel {
pub fn personas(&self) -> Vec<Persona> {
let archetypes = Persona::archetypes();
match self {
Panel::Default => vec![archetypes[0].clone()],
Panel::Duo => vec![archetypes[0].clone(), archetypes[1].clone()],
Panel::Panel => archetypes.to_vec(),
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"default" => Some(Panel::Default),
"duo" => Some(Panel::Duo),
"panel" => Some(Panel::Panel),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Panel::Default => "default",
Panel::Duo => "duo",
Panel::Panel => "panel",
}
}
}
#[cfg(feature = "backend-http")]
pub fn resolve_panel(
name: &str,
credentials: &crate::backend::credentials::Credentials,
) -> Result<Vec<Persona>, ProserpinaError> {
if let Some(panel) = credentials.panels().get(name) {
return Ok(panel.personas.iter().map(|s| s.to_persona()).collect());
}
if let Some(preset) = Panel::from_name(name) {
return Ok(preset.personas());
}
let mut available: Vec<String> = vec!["default", "duo", "panel"]
.into_iter()
.map(String::from)
.collect();
for n in credentials.panels().keys() {
available.push(n.clone());
}
Err(ProserpinaError::unknown_panel(name, available))
}
#[cfg(feature = "backend-http")]
use crate::error::ProserpinaError;