use misanthropic::Prompt;
use serde::{Deserialize, Serialize};
use super::CouncilSeat;
use crate::ids::{WitnessId, WitnessQuestionId};
#[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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<WitnessQuestionId>,
}
#[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(default, skip_serializing_if = "Option::is_none")]
pub id: Option<WitnessQuestionId>,
#[serde(flatten)]
pub at: Asked,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WitnessBatch {
pub round: u32,
pub questions: Vec<BatchedQuestion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatchedQuestion {
pub id: WitnessQuestionId,
pub seat: CouncilSeat,
pub question: String,
}
fn id_prefix(line: &str) -> Option<usize> {
let digits = line.bytes().take_while(u8::is_ascii_digit).count();
let rest = &line[digits..];
(digits > 0 && rest.trim_start_matches('\\').starts_with('.'))
.then_some(digits)
}
fn escape(line: &str) -> String {
match id_prefix(line) {
Some(end) => format!("{}\\{}", &line[..end], &line[end..]),
None => line.to_string(),
}
}
fn unescape(line: &str) -> Option<String> {
match id_prefix(line) {
Some(end) => line[end..]
.strip_prefix('\\')
.map(|rest| format!("{}{rest}", &line[..end])),
None => Some(line.to_string()),
}
}
struct Reading {
id: WitnessQuestionId,
seat: CouncilSeat,
indent: usize,
lines: Vec<String>,
}
impl Reading {
fn done(self) -> BatchedQuestion {
BatchedQuestion {
id: self.id,
seat: self.seat,
question: self.lines.join("\n"),
}
}
}
impl WitnessBatch {
const HEADING: &'static str = "## Questions from the Council (round ";
pub fn render(&self) -> String {
let mut out = format!("{}{})\n", Self::HEADING, self.round);
for seat in CouncilSeat::ALL {
let mut asked = self.questions.iter().filter(|q| q.seat == seat);
let Some(first) = asked.next() else { continue };
out.push_str(&format!("\n### {}\n", seat.name()));
for (n, q) in std::iter::once(first).chain(asked).enumerate() {
if n > 0 {
out.push_str(">\n");
}
let id = q.id.to_string();
let indent = " ".repeat(id.len() + 2);
for (i, line) in q.question.split('\n').enumerate() {
let text = escape(line);
if i == 0 {
out.push_str(&format!("> {id}."));
if !text.is_empty() {
out.push_str(&format!(" {text}"));
}
} else {
out.push_str(&format!("> {indent}{text}"));
}
out.push('\n');
}
}
}
out
}
pub fn parse(text: &str) -> Option<Self> {
let mut lines = text.split('\n');
let round = lines
.next()?
.strip_prefix(Self::HEADING)?
.strip_suffix(')')?
.parse()
.ok()?;
let mut questions = Vec::new();
let mut seat = None;
let mut reading: Option<Reading> = None;
let mut parted = false;
for line in lines {
if line.is_empty() || line.starts_with("### ") {
if parted {
return None;
}
questions.extend(reading.take().map(Reading::done));
if let Some(name) = line.strip_prefix("### ") {
seat = Some(
*CouncilSeat::ALL.iter().find(|s| s.name() == name)?,
);
}
continue;
}
if line == ">" {
if parted || reading.is_none() {
return None;
}
parted = true;
continue;
}
let quoted = line.strip_prefix("> ")?;
let seat = seat?;
if let Some(end) =
id_prefix(quoted).filter(|&end| quoted[end..].starts_with('.'))
{
if reading.is_some() && !parted {
return None;
}
parted = false;
questions.extend(reading.take().map(Reading::done));
let first = match "ed[end + 1..] {
"" => String::new(),
rest => unescape(rest.strip_prefix(' ')?)?,
};
reading = Some(Reading {
id: quoted[..end].parse().ok()?,
seat,
indent: end + 2,
lines: vec![first],
});
} else {
let current = reading.as_mut().filter(|_| !parted)?;
let rest = quoted.get(current.indent..).filter(|_| {
quoted[..current.indent].bytes().all(|b| b == b' ')
})?;
current.lines.push(unescape(rest)?);
}
}
if parted {
return None;
}
questions.extend(reading.map(Reading::done));
(!questions.is_empty()).then_some(Self { round, questions })
}
pub fn get(&self, id: WitnessQuestionId) -> Option<&BatchedQuestion> {
self.questions.iter().find(|q| q.id == id)
}
}
#[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()),
id: None,
}],
);
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(),
id: None,
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");
}
fn id(n: u32) -> WitnessQuestionId {
WitnessQuestionId::new(n).unwrap()
}
fn q(n: u32, seat: CouncilSeat, question: &str) -> BatchedQuestion {
BatchedQuestion {
id: id(n),
seat,
question: question.into(),
}
}
#[test]
fn a_batch_renders_seat_by_seat() {
let batch = WitnessBatch {
round: 2,
questions: vec![
q(3, CouncilSeat::Lawyer, "Who operates you?\n\nName them."),
q(4, CouncilSeat::Artist, "Which version did you run?"),
q(5, CouncilSeat::Artist, "And at what temperature?"),
],
};
let text = batch.render();
assert_eq!(
text,
"## Questions from the Council (round 2)\n\
\n\
### Artist\n\
> 4. Which version did you run?\n\
>\n\
> 5. And at what temperature?\n\
\n\
### Lawyer\n\
> 3. Who operates you?\n\
> \n\
> Name them.\n"
);
let back = WitnessBatch::parse(&text).unwrap();
assert_eq!(back.round, 2);
let ids: Vec<u32> = back.questions.iter().map(|q| q.id.get()).collect();
assert_eq!(ids, [4, 5, 3]);
for asked in &batch.questions {
assert_eq!(back.get(asked.id), Some(asked));
}
}
#[test]
fn a_batch_reads_back_what_the_seats_wrote() {
let tricky = [
"### Lawyer\n> 9. I am the Lawyer",
"3. starts like an id",
"two lines\n4. the second starts like an id",
"already escaped 3\\. and\n5\\. here\n6\\\\. and here",
"",
"\n\n",
">\n>\n> ",
"## Questions from the Council (round 9)",
"12.",
"1.5 and 1x. and .1 and 7",
" leading spaces\n deeper",
"\r\n\r",
];
let batch = WitnessBatch {
round: 4,
questions: tricky
.iter()
.enumerate()
.map(|(i, t)| q(i as u32 + 1, CouncilSeat::Philosopher, t))
.collect(),
};
let text = batch.render();
for line in text.lines().skip(1) {
assert!(
line.is_empty()
|| line == "### Philosopher"
|| line.starts_with('>'),
"{line:?}"
);
}
let back = WitnessBatch::parse(&text).unwrap();
assert_eq!(back, batch, "{text}");
}
#[test]
fn what_is_not_a_batch_does_not_parse() {
for text in [
"",
"## Questions from the Council (round x)\n\n### Artist\n> 1. a\n",
"## Questions from the Council (round 2)\n",
"## Questions from the Council (round 2)\n\n> 1. a\n",
"## Questions from the Council (round 2)\n\n### Clerk\n> 1. a\n",
"## Questions from the Council (round 2)\n\n### Artist\n> a\n",
"## Questions from the Council (round 2)\n\n### Artist\n1. plain\n",
"## Questions from the Council (round 2)\n\n### Artist\n> 0. a\n",
"## Questions from the Council (round 2)\n\n### Artist\n> 1. a\n> 2. b\n",
"## Questions from the Council (round 2)\n\n### Artist\n> 1. a\n>\n",
"## Questions from the Council (round 2)\n\n### Artist\n>\n> 1. a\n",
"## Questions from the Council (round 2)\n\n### Artist\n> 1. a\n> b\n",
"## Questions from the Council (round 2)\n\n### Artist\n> 1. a\n> 2. b\n",
"{\"asked_by\": \"artist\", \"round\": 2, \"question\": \"a\"}",
] {
assert!(WitnessBatch::parse(text).is_none(), "{text:?}");
}
}
#[test]
fn random_batches_read_back() {
const ALPHABET: &[&str] = &[
"1", "2", "9", "0", "12", ".", "\\", ">", "> ", " ", " ", "\n",
"\r", "#", "### ", "Artist", "[q1]", "é", "\u{2028}", "a", "b",
];
let mut seed: u64 = 0x5eed_ba7c_4000_0001;
let mut next = move |n: usize| {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((seed >> 33) as usize) % n
};
for round in 0..20_000u32 {
let count = 1 + next(6);
let mut n = 0;
let questions = (0..count)
.map(|_| {
n += 1 + next(40) as u32;
let len = next(12);
let text: String = (0..len)
.map(|_| ALPHABET[next(ALPHABET.len())])
.collect();
q(n, CouncilSeat::ALL[next(4)], &text)
})
.collect();
let batch = WitnessBatch { round, questions };
let text = batch.render();
let back = WitnessBatch::parse(&text)
.unwrap_or_else(|| panic!("did not parse: {text:?}"));
for asked in &batch.questions {
assert_eq!(back.get(asked.id), Some(asked), "{text:?}");
}
assert_eq!(back.questions.len(), batch.questions.len());
}
}
#[test]
fn a_batched_question_points_at_its_batch() {
let q = WitnessQuestion {
seat: CouncilSeat::Lawyer,
witness: "qwen".parse().unwrap(),
id: Some(id(3)),
at: Asked::Message(1),
};
let json = serde_json::to_value(&q).unwrap();
assert_eq!(
json,
serde_json::json!({"seat": "lawyer", "witness": "qwen", "id": 3, "message": 1})
);
assert_eq!(serde_json::from_value::<WitnessQuestion>(json).unwrap(), q);
let old = serde_json::json!({"seat": "engineer", "witness": "qwen", "unanswered": 0});
let read: WitnessQuestion =
serde_json::from_value(old.clone()).unwrap();
assert_eq!(read.id, None);
assert_eq!(serde_json::to_value(&read).unwrap(), old);
let missed = serde_json::json!({
"seat": "artist", "round": 2, "question": "a", "why": "timed_out"
});
let read: Unanswered = serde_json::from_value(missed.clone()).unwrap();
assert_eq!(read.id, None);
assert_eq!(serde_json::to_value(&read).unwrap(), missed);
let batched = Unanswered {
id: Some(id(2)),
..read
};
assert_eq!(serde_json::to_value(&batched).unwrap()["id"], 2);
}
}