1use std::collections::{BTreeMap, VecDeque};
5use std::sync::{Mutex, PoisonError};
6
7use async_trait::async_trait;
8use turnframe_core::ids::{OperationKey, OptionId, TargetToken, WorkflowKey};
9use turnframe_core::plan::AnswerBasis;
10use turnframe_core::understanding::{
11 ActAction, ActId, ActStatus, ActTarget, ArgumentValue, CardAnswer, ConstraintKind, Dispute,
12 Excerpt, FoundBy, MessageRef, NotUnderstood, NotUnderstoodReason, QuestionTopic, RecordValue,
13 Superseded, TurnConstraint, Understanding, UnderstoodAct, UnderstoodArgument,
14 UnderstoodQuestion, Unit, UnitId, UnitKind, Unreadable, WordRange,
15};
16use turnframe_tasks::TaskScope;
17use turnframe_understand::{ActChecker, StepSink, TurnUnderstander, UnderstandingInput, Words};
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21#[error("«{quote}» does not occur in «{text}»")]
22pub struct QuoteNotFound {
23 pub quote: String,
25 pub text: String,
27}
28
29#[derive(Debug, Clone)]
41pub struct UnderstandingBuilder {
42 words: Words,
43 understanding: Understanding,
44 error: Option<QuoteNotFound>,
45}
46
47impl UnderstandingBuilder {
48 #[must_use]
50 pub fn of(text: impl AsRef<str>) -> Self {
51 Self {
52 words: Words::split(text.as_ref()),
53 understanding: Understanding::default(),
54 error: None,
55 }
56 }
57
58 #[must_use]
60 pub fn text(&self) -> &str {
61 self.words.text()
62 }
63
64 fn range(&mut self, quote: &str) -> Option<WordRange> {
65 let found = self.locate(quote);
66 if found.is_none() && self.error.is_none() {
67 self.error = Some(QuoteNotFound {
68 quote: quote.to_owned(),
69 text: self.words.text().to_owned(),
70 });
71 }
72 found
73 }
74
75 fn locate(&self, quote: &str) -> Option<WordRange> {
76 let start = self.words.text().find(quote)?;
77 let end = start + quote.len();
78 let words = self.words.words();
79 let first = words.iter().position(|w| w.end > start)?;
80 let last = words.iter().rposition(|w| w.start < end)?;
81 Some(WordRange {
82 first,
83 last,
84 start: words[first].start,
85 end: words[last].end,
86 })
87 }
88
89 fn unit(&mut self, kind: UnitKind, words: WordRange, workflow: Option<WorkflowKey>) -> UnitId {
90 let id = UnitId(u16::try_from(self.understanding.units.len() + 1).unwrap_or(u16::MAX));
91 self.understanding.units.push(Unit {
92 id,
93 kind,
94 words,
95 workflow,
96 found_by: FoundBy::Segment,
97 });
98 id
99 }
100
101 fn arguments(
102 &self,
103 arguments: &serde_json::Value,
104 words: WordRange,
105 ) -> BTreeMap<String, UnderstoodArgument> {
106 let Some(object) = arguments.as_object() else {
107 return BTreeMap::new();
108 };
109 object
110 .iter()
111 .map(|(name, value)| {
112 let located = value.as_str().and_then(|text| self.locate(text));
113 let argument = UnderstoodArgument {
114 value: ArgumentValue::Json(value.clone()),
115 excerpt: Some(Excerpt {
116 message: MessageRef::Current,
117 words: located.unwrap_or(words),
118 }),
119 };
120 (name.clone(), argument)
121 })
122 .collect()
123 }
124
125 fn act(
126 mut self,
127 action: ActAction,
128 target: ActTarget,
129 arguments: &serde_json::Value,
130 quote: &str,
131 ) -> Self {
132 let Some(words) = self.range(quote) else {
133 return self;
134 };
135 let workflow = match &action {
136 ActAction::Start { workflow } => Some(workflow.clone()),
137 ActAction::Apply { operation } => operation
138 .as_str()
139 .split_once('.')
140 .map(|(workflow, _)| WorkflowKey::from(workflow)),
141 };
142 let unit = self.unit(UnitKind::Request, words, workflow);
143 let mut depends_on = Vec::new();
144 if let ActTarget::SameTurn { act } = &target {
145 depends_on.push(*act);
146 }
147 let arguments = self.arguments(arguments, words);
148 self.understanding.acts.push(UnderstoodAct {
149 id: ActId::new(unit, 1),
150 action,
151 target,
152 arguments,
153 words,
154 depends_on,
155 status: ActStatus::Ready,
156 });
157 self
158 }
159
160 #[must_use]
162 pub fn apply(
163 self,
164 operation: impl Into<OperationKey>,
165 target: impl Into<TargetToken>,
166 arguments: serde_json::Value,
167 quote: &str,
168 ) -> Self {
169 let target = ActTarget::Record {
170 token: target.into(),
171 };
172 self.apply_to(operation, target, arguments, quote)
173 }
174
175 #[must_use]
177 pub fn apply_to(
178 self,
179 operation: impl Into<OperationKey>,
180 target: ActTarget,
181 arguments: serde_json::Value,
182 quote: &str,
183 ) -> Self {
184 let action = ActAction::Apply {
185 operation: operation.into(),
186 };
187 self.act(action, target, &arguments, quote)
188 }
189
190 #[must_use]
193 pub fn apply_to_unlisted(
194 mut self,
195 operation: impl Into<OperationKey>,
196 workflow: impl Into<WorkflowKey>,
197 named: &str,
198 arguments: serde_json::Value,
199 quote: &str,
200 ) -> Self {
201 let words = self.range(named);
202 let target = ActTarget::NotListed {
203 workflow: workflow.into(),
204 words,
205 };
206 self.apply_to(operation, target, arguments, quote)
207 }
208
209 #[must_use]
211 pub fn open(
212 self,
213 operation: impl Into<OperationKey>,
214 workflow: impl Into<WorkflowKey>,
215 arguments: serde_json::Value,
216 quote: &str,
217 ) -> Self {
218 let target = ActTarget::New {
219 workflow: workflow.into(),
220 };
221 self.apply_to(operation, target, arguments, quote)
222 }
223
224 #[must_use]
226 pub fn start(self, workflow: impl Into<WorkflowKey>, quote: &str) -> Self {
227 let workflow = workflow.into();
228 let target = ActTarget::New {
229 workflow: workflow.clone(),
230 };
231 self.act(
232 ActAction::Start { workflow },
233 target,
234 &serde_json::Value::Null,
235 quote,
236 )
237 }
238
239 #[must_use]
242 pub fn with_record(mut self, name: &str, record: RecordValue) -> Self {
243 if let Some(act) = self.understanding.acts.last_mut() {
244 if let RecordValue::SameTurn { act: earlier } = &record
245 && !act.depends_on.contains(earlier)
246 {
247 act.depends_on.push(*earlier);
248 }
249 act.arguments.insert(
250 name.to_owned(),
251 UnderstoodArgument {
252 value: ArgumentValue::Record(record),
253 excerpt: None,
254 },
255 );
256 }
257 self
258 }
259
260 #[must_use]
262 pub fn last_act(&self) -> Option<ActId> {
263 self.understanding.acts.last().map(|act| act.id)
264 }
265
266 #[must_use]
268 pub fn with_status(mut self, status: ActStatus) -> Self {
269 if let Some(act) = self.understanding.acts.last_mut() {
270 act.status = status;
271 }
272 self
273 }
274
275 #[must_use]
277 pub fn needing(self, arguments: &[&str]) -> Self {
278 self.with_status(ActStatus::NeedsValue {
279 arguments: arguments.iter().map(|name| (*name).to_owned()).collect(),
280 reason: None,
281 })
282 }
283
284 #[must_use]
286 pub fn superseded_by_next(mut self) -> Self {
287 if let Some(act) = self.understanding.acts.pop() {
288 let by = UnitId(act.id.unit.0 + 1);
289 self.understanding.superseded.push(Superseded {
290 act: act.id,
291 action: act.action,
292 by,
293 });
294 }
295 self
296 }
297
298 #[must_use]
300 pub fn ask(self, quote: &str) -> Self {
301 self.ask_about(AnswerBasis::CurrentCommittedState, None, &[], quote)
302 }
303
304 #[must_use]
307 pub fn ask_about(
308 mut self,
309 basis: AnswerBasis,
310 record: Option<TargetToken>,
311 subjects: &[&str],
312 quote: &str,
313 ) -> Self {
314 let Some(words) = self.range(quote) else {
315 return self;
316 };
317 let unit = self.unit(UnitKind::Question, words, None);
318 self.understanding.questions.push(UnderstoodQuestion {
319 unit,
320 words,
321 workflow: None,
322 record,
323 subjects: subjects.iter().map(|s| (*s).to_owned()).collect(),
324 basis,
325 topic: match basis {
326 AnswerBasis::GeneralDomainKnowledge => QuestionTopic::Knowledge,
327 _ => QuestionTopic::default(),
328 },
329 continues_previous: false,
330 });
331 self
332 }
333
334 #[must_use]
336 pub fn following_up(mut self) -> Self {
337 if let Some(question) = self.understanding.questions.last_mut() {
338 question.continues_previous = true;
339 }
340 self
341 }
342
343 #[must_use]
345 pub fn about(mut self, topic: QuestionTopic) -> Self {
346 if let Some(question) = self.understanding.questions.last_mut() {
347 question.topic = topic;
348 }
349 self
350 }
351
352 #[must_use]
354 pub fn constrain(mut self, kind: ConstraintKind, quote: &str) -> Self {
355 let Some(words) = self.range(quote) else {
356 return self;
357 };
358 let unit = self.unit(UnitKind::Constraint, words, None);
359 self.understanding
360 .constraints
361 .push(TurnConstraint { unit, kind, words });
362 self
363 }
364
365 #[must_use]
367 pub fn answer_card(mut self, option: impl Into<OptionId>, quote: &str) -> Self {
368 let Some(words) = self.range(quote) else {
369 return self;
370 };
371 let unit = self.unit(UnitKind::CardAnswer, words, None);
372 self.understanding.card_answer = Some(CardAnswer {
373 unit,
374 option: option.into(),
375 words,
376 });
377 self
378 }
379
380 #[must_use]
382 pub fn dispute(mut self, receipt: Option<&str>, quote: &str) -> Self {
383 let Some(words) = self.range(quote) else {
384 return self;
385 };
386 let unit = self.unit(UnitKind::Dispute, words, None);
387 self.understanding.disputes.push(Dispute {
388 unit,
389 words,
390 receipt: receipt.map(str::to_owned),
391 });
392 self
393 }
394
395 #[must_use]
397 pub fn not_understood(mut self, reason: NotUnderstoodReason, quote: &str) -> Self {
398 let Some(words) = self.range(quote) else {
399 return self;
400 };
401 let unit = self.unit(UnitKind::Request, words, None);
402 self.understanding.not_understood.push(NotUnderstood {
403 unit,
404 words,
405 reason,
406 });
407 self
408 }
409
410 #[must_use]
412 pub fn chitchat(mut self, quote: &str) -> Self {
413 if let Some(words) = self.range(quote) {
414 self.unit(UnitKind::Chitchat, words, None);
415 }
416 self
417 }
418
419 pub fn build(self) -> Result<Understanding, QuoteNotFound> {
425 match self.error {
426 Some(error) => Err(error),
427 None => Ok(self.understanding),
428 }
429 }
430}
431
432#[derive(Debug, Default)]
438pub struct ScriptedUnderstanding {
439 queue: Mutex<VecDeque<Understanding>>,
440 seen: Mutex<Vec<UnderstandingInput>>,
441}
442
443impl ScriptedUnderstanding {
444 #[must_use]
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 #[must_use]
452 pub fn then(self, understanding: Understanding) -> Self {
453 self.push(understanding);
454 self
455 }
456
457 pub fn push(&self, understanding: Understanding) {
459 self.queue
460 .lock()
461 .unwrap_or_else(PoisonError::into_inner)
462 .push_back(understanding);
463 }
464
465 #[must_use]
467 pub fn seen(&self) -> Vec<UnderstandingInput> {
468 self.seen
469 .lock()
470 .unwrap_or_else(PoisonError::into_inner)
471 .clone()
472 }
473
474 #[must_use]
476 pub fn remaining(&self) -> usize {
477 self.queue
478 .lock()
479 .unwrap_or_else(PoisonError::into_inner)
480 .len()
481 }
482}
483
484#[async_trait]
485impl TurnUnderstander for ScriptedUnderstanding {
486 async fn understand(
487 &self,
488 _scope: &TaskScope,
489 turn: &UnderstandingInput,
490 _steps: &dyn StepSink,
491 _checker: &dyn ActChecker,
492 ) -> Understanding {
493 self.seen
494 .lock()
495 .unwrap_or_else(PoisonError::into_inner)
496 .push(turn.clone());
497 self.queue
498 .lock()
499 .unwrap_or_else(PoisonError::into_inner)
500 .pop_front()
501 .unwrap_or_else(|| {
502 Understanding::unreadable(Unreadable::Segmentation {
503 code: "unscripted".to_owned(),
504 })
505 })
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn quotes_become_word_ranges_and_values_point_at_their_words() {
515 let understanding = UnderstandingBuilder::of("please set the name to Lisbon")
516 .apply(
517 "trip.set_name",
518 "t_1",
519 serde_json::json!({"value": "Lisbon"}),
520 "set the name to Lisbon",
521 )
522 .build()
523 .unwrap();
524 let act = &understanding.acts[0];
525 assert_eq!((act.words.first, act.words.last), (1, 5));
526 let excerpt = act.arguments["value"].excerpt.unwrap();
527 assert_eq!((excerpt.words.first, excerpt.words.last), (5, 5));
528 assert_eq!(understanding.units[0].workflow, Some("trip".into()));
529 }
530
531 #[test]
532 fn a_quote_that_is_not_in_the_text_fails_the_build() {
533 let error = UnderstandingBuilder::of("hello")
534 .ask("what is the total?")
535 .build()
536 .unwrap_err();
537 assert_eq!(error.quote, "what is the total?");
538 }
539}