use misanthropic::Prompt;
use serde::{Deserialize, Serialize};
use super::CouncilSeat;
use crate::ids::WitnessId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Witness {
pub id: WitnessId,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub interest: String,
#[serde(default, skip_serializing)]
pub source: WitnessSource,
}
impl Witness {
pub fn redacted(&self) -> Self {
Self {
source: WitnessSource::Redacted,
..self.clone()
}
}
}
#[derive(Clone, PartialEq, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WitnessSource {
Anthropic {
#[serde(default)]
endpoint: Option<url::Url>,
prompt: Box<Prompt>,
},
#[default]
Redacted,
}
impl std::fmt::Debug for WitnessSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anthropic { endpoint, prompt } => f
.debug_struct("Anthropic")
.field("endpoint", &endpoint.as_ref().map(|_| "[REDACTED]"))
.field("prompt", prompt)
.finish(),
Self::Redacted => f.write_str("Redacted"),
}
}
}
impl WitnessSource {
pub fn prompt(&self) -> Option<&Prompt> {
match self {
Self::Anthropic { prompt, .. } => Some(prompt),
Self::Redacted => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Testimony {
witness: Witness,
pub record: Prompt,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unanswered: Vec<Unanswered>,
}
impl Testimony {
pub fn new(
witness: &Witness,
record: Prompt,
unanswered: Vec<Unanswered>,
) -> Self {
Self {
witness: witness.redacted(),
record,
unanswered,
}
}
pub fn witness(&self) -> &Witness {
&self.witness
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Unanswered {
pub seat: CouncilSeat,
pub round: u32,
pub question: String,
pub why: Unheard,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Unheard {
Refusal,
MaxTokens,
Unreachable,
Busy,
TimedOut,
Unparsed,
Withdrawn,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
pub struct WitnessQuestion {
pub seat: CouncilSeat,
pub witness: WitnessId,
#[serde(flatten)]
pub at: Asked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(rename_all = "snake_case")]
pub enum Asked {
Message(u32),
Unanswered(u32),
}
pub fn testimony_attachment_name(witness: &WitnessId) -> String {
format!("witness-{witness}.json")
}
#[cfg(test)]
mod tests {
use super::*;
const LAN: &str = "http://10.9.9.9:8080/";
fn witness() -> Witness {
serde_json::from_value(serde_json::json!({
"id": "qwen",
"name": "Qwen 3.8",
"interest": "the model under consideration",
"source": {"anthropic": {
"endpoint": LAN,
"prompt": {
"model": "qwen3.8",
"max_tokens": 32000,
"system": "You are a witness.",
"messages": []
}
}}
}))
.unwrap()
}
#[test]
fn the_source_never_serializes_or_prints() {
let w = witness();
assert!(matches!(w.source, WitnessSource::Anthropic { .. }));
let json = serde_json::to_string(&w).unwrap();
assert!(!json.contains("10.9.9.9"), "{json}");
assert!(!json.contains("source"), "{json}");
let debug = format!("{w:?}");
assert!(!debug.contains("10.9.9.9"), "{debug}");
let prompt = w.source.prompt().unwrap().clone();
let t = Testimony::new(&w, prompt, vec![]);
assert_eq!(t.witness().source, WitnessSource::Redacted);
let json = serde_json::to_string(&t).unwrap();
assert!(!json.contains("10.9.9.9"), "{json}");
assert!(!format!("{t:?}").contains("10.9.9.9"));
}
#[test]
fn a_testimony_round_trips() {
let w = witness();
let prompt = w.source.prompt().unwrap().clone();
let t = Testimony::new(
&w,
prompt,
vec![Unanswered {
seat: CouncilSeat::Lawyer,
round: 2,
question: "Which version?".into(),
why: Unheard::Other("connection reset".into()),
}],
);
let json = serde_json::to_value(&t).unwrap();
let back: Testimony = serde_json::from_value(json.clone()).unwrap();
assert_eq!(back, t);
assert_eq!(serde_json::to_value(&back).unwrap(), json);
assert_eq!(
json["unanswered"][0]["why"],
serde_json::json!({"other": "connection reset"})
);
}
#[test]
fn a_round_without_questions_is_unchanged() {
let old = serde_json::json!({
"number": 2,
"round_type": "deliberation",
"responses": [],
"steward_contribution": null
});
let round: super::super::CouncilRound =
serde_json::from_value(old.clone()).unwrap();
assert!(round.witness_questions.is_empty());
assert_eq!(serde_json::to_value(&round).unwrap(), old);
let mut asked = old.clone();
asked["witness_questions"] = serde_json::json!([
{"seat": "engineer", "witness": "qwen", "message": 1},
{"seat": "lawyer", "witness": "qwen", "unanswered": 0}
]);
let round: super::super::CouncilRound =
serde_json::from_value(asked.clone()).unwrap();
assert_eq!(round.witness_questions[1].at, Asked::Unanswered(0));
assert_eq!(serde_json::to_value(&round).unwrap(), asked);
}
#[test]
fn a_question_points_flat() {
let q = WitnessQuestion {
seat: CouncilSeat::Artist,
witness: "qwen".parse().unwrap(),
at: Asked::Message(3),
};
let json = serde_json::to_value(&q).unwrap();
assert_eq!(
json,
serde_json::json!({"seat": "artist", "witness": "qwen", "message": 3})
);
assert_eq!(serde_json::from_value::<WitnessQuestion>(json).unwrap(), q);
assert_eq!(testimony_attachment_name(&q.witness), "witness-qwen.json");
}
}