1use serde::Serialize;
4use serde_json::Value;
5use typesafe::{Choice, Noul, Question, Questions, Score};
6
7pub type Entry = (String, Question);
9
10#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Turn {
17 pub who: Option<String>,
19 pub said: String,
21}
22
23const WHO_KEYS: [&str; 4] = ["who", "role", "speaker", "from"];
25const SAID_KEYS: [&str; 4] = ["said", "text", "content", "message"];
27
28pub fn turns_of(state: &Value) -> Option<Vec<Turn>> {
34 let items = state.as_array().filter(|a| !a.is_empty())?;
35 let mut turns = Vec::with_capacity(items.len());
36 for item in items {
37 let object = item.as_object()?;
38 let pick = |keys: [&str; 4]| {
39 keys.into_iter()
40 .find_map(|k| object.get(k).and_then(Value::as_str))
41 };
42 let said = pick(SAID_KEYS)?;
43 turns.push(Turn {
44 who: pick(WHO_KEYS).filter(|w| !w.is_empty()).map(str::to_owned),
45 said: said.to_owned(),
46 });
47 }
48 Some(turns)
49}
50
51pub fn turns_to_json(turns: &[Turn]) -> Value {
53 Value::Array(
54 turns
55 .iter()
56 .map(|t| {
57 let mut map = serde_json::Map::new();
58 if let Some(who) = &t.who {
59 map.insert("who".to_owned(), Value::String(who.clone()));
60 }
61 map.insert("said".to_owned(), Value::String(t.said.clone()));
62 Value::Object(map)
63 })
64 .collect(),
65 )
66}
67
68pub fn turn_text(turn: &Turn) -> String {
70 match &turn.who {
71 Some(who) => format!("{who}: {}", turn.said),
72 None => turn.said.clone(),
73 }
74}
75
76#[derive(Debug, Default, Clone)]
78pub struct Session {
79 pub state: Value,
81 pub questions: Vec<Entry>,
83 pub model: Option<String>,
85}
86
87impl Session {
88 pub fn new() -> Self {
89 Self {
90 state: Value::String(String::new()),
91 questions: Vec::new(),
92 model: None,
93 }
94 }
95
96 pub fn state_is_empty(&self) -> bool {
97 is_empty_value(&self.state)
98 }
99
100 pub fn state_preview(&self) -> String {
102 if let Some(turns) = self.turns()
103 && let Some(last) = turns.last()
104 {
105 let plural = if turns.len() == 1 { "" } else { "s" };
106 return format!("{} turn{plural} · {}", turns.len(), turn_text(last));
107 }
108 match &self.state {
109 Value::String(s) => s.clone(),
110 other => other.to_string(),
111 }
112 }
113
114 pub fn turns(&self) -> Option<Vec<Turn>> {
116 turns_of(&self.state)
117 }
118
119 pub fn add_turn(&mut self, turn: Turn) -> Result<Vec<Turn>, String> {
126 let Some(mut turns) = self.turns().or_else(|| self.seed_turns()) else {
127 return Err(
128 "The state is JSON that is not a conversation, so there is no thread to add to."
129 .to_owned(),
130 );
131 };
132 turns.push(turn);
133 self.state = turns_to_json(&turns);
134 Ok(turns)
135 }
136
137 pub fn drop_turn(&mut self) -> Option<Turn> {
139 let mut turns = self.turns()?;
140 let last = turns.pop()?;
141 self.state = if turns.is_empty() {
142 Value::String(String::new())
143 } else {
144 turns_to_json(&turns)
145 };
146 Some(last)
147 }
148
149 fn seed_turns(&self) -> Option<Vec<Turn>> {
151 if self.state_is_empty() {
152 return Some(Vec::new());
153 }
154 match &self.state {
155 Value::String(s) => Some(vec![Turn {
156 who: None,
157 said: s.clone(),
158 }]),
159 _ => None,
160 }
161 }
162
163 pub fn insert(&mut self, name: String, question: Question) -> bool {
165 if let Some(slot) = self.questions.iter_mut().find(|(n, _)| *n == name) {
166 slot.1 = question;
167 true
168 } else {
169 self.questions.push((name, question));
170 false
171 }
172 }
173
174 pub fn remove(&mut self, name: &str) -> bool {
175 let before = self.questions.len();
176 self.questions.retain(|(n, _)| n != name);
177 self.questions.len() != before
178 }
179
180 pub fn to_questions(&self) -> Questions {
181 self.questions.iter().cloned().collect()
182 }
183
184 pub fn request_json(&self, model: &str) -> String {
189 self.body_json(model, true)
190 }
191
192 pub fn request_json_compact(&self, model: &str) -> String {
195 self.body_json(model, false)
196 }
197
198 fn body_json(&self, model: &str, pretty: bool) -> String {
199 #[derive(Serialize)]
200 struct Body<'a> {
201 state: &'a Value,
202 model: &'a str,
203 questions: &'a Questions,
204 }
205 let questions = self.to_questions();
206 let body = Body {
207 state: &self.state,
208 model,
209 questions: &questions,
210 };
211 if pretty {
212 serde_json::to_string_pretty(&body)
213 } else {
214 serde_json::to_string(&body)
215 }
216 .unwrap_or_else(|e| format!("<unencodable: {e}>"))
217 }
218}
219
220pub fn is_empty_value(v: &Value) -> bool {
225 match v {
226 Value::Null => true,
227 Value::String(s) => s.trim().is_empty(),
228 Value::Array(a) => a.is_empty(),
229 Value::Object(o) => o.is_empty(),
230 _ => false,
231 }
232}
233
234pub fn parse_noul(args: &str) -> Result<Entry, String> {
236 let (name, rest) = split_name(args, ":noul is_urgent The message conveys urgency")?;
237 let mut parts = rest.split('|').map(str::trim);
238 let instructions = parts.next().unwrap_or("");
239 if instructions.is_empty() {
240 return Err(
241 "A noul needs instructions: :noul is_urgent The message conveys urgency".into(),
242 );
243 }
244 let mut q = Noul::new(value(instructions));
245 for part in parts {
246 let (tag, text) = part
247 .split_once(':')
248 .map(|(t, v)| (t.trim().to_ascii_lowercase(), v.trim()))
249 .ok_or_else(|| format!("Expected `yes: …` or `no: …`, got {part:?}."))?;
250 match tag.as_str() {
251 "yes" | "true" => q = q.when_true(value(text)),
252 "no" | "false" => q = q.when_false(value(text)),
253 _ => return Err(format!("Unknown criterion {tag:?}; use `yes:` or `no:`.")),
254 }
255 }
256 Ok((name, q.into()))
257}
258
259pub fn parse_choice(args: &str) -> Result<Entry, String> {
261 let (name, rest) = split_name(
262 args,
263 ":choice department Which team handles this | billing=Payments | technical=Bugs",
264 )?;
265 let mut parts = rest.split('|').map(str::trim);
266 let instructions = parts.next().unwrap_or("");
267 if instructions.is_empty() {
268 return Err("A choice needs instructions before the first `|`.".into());
269 }
270 let mut q = Choice::new(value(instructions));
271 let mut count = 0;
272 for part in parts.filter(|p| !p.is_empty()) {
273 q = match part.split_once('=') {
274 Some((label, desc)) => q.option(label.trim(), value(desc.trim())),
275 None => q.label(part),
276 };
277 count += 1;
278 }
279 if count < 2 {
280 return Err(
281 "A choice needs at least two options: … | billing=Payments | technical=Bugs".into(),
282 );
283 }
284 Ok((name, q.into()))
285}
286
287pub fn parse_score(args: &str) -> Result<Entry, String> {
289 let (name, rest) = split_name(
290 args,
291 ":score frustration How frustrated they are | Calm | Annoyed | Furious",
292 )?;
293 let mut parts = rest.split('|').map(str::trim);
294 let instructions = parts.next().unwrap_or("");
295 if instructions.is_empty() {
296 return Err("A score needs instructions before the first `|`.".into());
297 }
298 let levels: Vec<Value> = parts.filter(|p| !p.is_empty()).map(value).collect();
299 if levels.len() < 2 {
300 return Err(
301 "A score needs at least two ordered levels: … | Calm | Annoyed | Furious".into(),
302 );
303 }
304 Ok((name, Score::new(value(instructions), levels).into()))
305}
306
307pub fn parse_raw(args: &str) -> Result<Entry, String> {
309 let (name, rest) = split_name(
310 args,
311 r#":raw tone {"type": "noul", "instructions": "Polite?"}"#,
312 )?;
313 let v: Value = serde_json::from_str(&rest).map_err(|e| format!("Not valid JSON: {e}"))?;
314 Ok((name, Question::Raw(v)))
315}
316
317pub fn parse_turn(args: &str) -> Result<Turn, String> {
322 let text = args.trim();
323 let example = ":turn customer: The payout failed again";
324 if text.is_empty() {
325 return Err(format!("A turn needs something said. Try: {example}"));
326 }
327 let (head, rest) = match text.split_once(char::is_whitespace) {
328 Some((head, rest)) => (head, rest.trim()),
329 None => (text, ""),
330 };
331 let Some(who) = head.strip_suffix(':').filter(|w| !w.is_empty()) else {
332 return Ok(Turn {
333 who: None,
334 said: text.to_owned(),
335 });
336 };
337 if rest.is_empty() {
338 return Err(format!("Nothing said after {head:?}. Try: {example}"));
339 }
340 Ok(Turn {
341 who: Some(who.to_owned()),
342 said: rest.to_owned(),
343 })
344}
345
346fn split_name(args: &str, example: &str) -> Result<(String, String), String> {
347 let args = args.trim();
348 let (name, rest) = args
349 .split_once(char::is_whitespace)
350 .ok_or_else(|| format!("Missing name or body. Try: {example}"))?;
351 if name.is_empty() {
352 return Err(format!("Missing a question name. Try: {example}"));
353 }
354 Ok((name.to_owned(), rest.trim().to_owned()))
355}
356
357pub fn value(text: &str) -> Value {
360 let t = text.trim();
361 if (t.starts_with('{') || t.starts_with('['))
362 && let Ok(v) = serde_json::from_str::<Value>(t)
363 {
364 return v;
365 }
366 Value::String(t.to_owned())
367}
368
369pub fn from_body(text: &str) -> Result<Session, String> {
372 let body: Value = serde_json::from_str(text).map_err(|e| format!("Not valid JSON: {e}"))?;
373 let obj = body.as_object().ok_or("Expected a JSON object.")?;
374 let questions = obj
375 .get("questions")
376 .and_then(Value::as_object)
377 .ok_or("Expected a `questions` object.")?;
378 Ok(Session {
379 state: obj.get("state").cloned().unwrap_or(Value::Null),
380 model: obj.get("model").and_then(Value::as_str).map(str::to_owned),
381 questions: questions
382 .iter()
383 .map(|(name, q)| (name.clone(), question_from_json(q)))
384 .collect(),
385 })
386}
387
388pub fn question_from_json(v: &Value) -> Question {
390 let instructions = v.get("instructions").cloned();
391 let criteria = v.get("criteria");
392 match v.get("type").and_then(Value::as_str) {
393 Some("noul") => {
394 let mut q = match instructions {
395 Some(i) => Noul::new(i),
396 None => Noul::default(),
397 };
398 if let Some(yes) = criteria.and_then(|c| c.get("true")) {
399 q = q.when_true(yes.clone());
400 }
401 if let Some(no) = criteria.and_then(|c| c.get("false")) {
402 q = q.when_false(no.clone());
403 }
404 q.into()
405 }
406 Some("choice") => {
407 let mut q = Choice::new(instructions.unwrap_or(Value::Null));
408 if let Some(map) = criteria.and_then(Value::as_object) {
409 for (label, desc) in map {
410 q = match desc {
411 Value::Null => q.label(label.clone()),
412 d => q.option(label.clone(), d.clone()),
413 };
414 }
415 }
416 q.into()
417 }
418 Some("score") => {
419 let levels = criteria
420 .and_then(Value::as_array)
421 .cloned()
422 .unwrap_or_default();
423 Score::new(instructions.unwrap_or(Value::Null), levels).into()
424 }
425 _ => Question::Raw(v.clone()),
426 }
427}