1use std::collections::BTreeMap;
8use std::fmt::Write as _;
9
10use serde::{Deserialize, Serialize};
11use serde_json::{Value, json};
12use turnframe_core::ids::OperationKey;
13use turnframe_core::operation::{ArgumentSpec, DateExpr, OperationSpec, ValueShape};
14use turnframe_core::understanding::RecordValue;
15use turnframe_provider::request::Message;
16use turnframe_tasks::{ModelTask, StructuralError, TaskKind};
17
18use crate::input::{RecordBrief, UnderstandingInput, WorkflowBrief};
19use crate::render;
20use crate::schema::{any_of, date_expression, index, object, one_of, variant};
21use crate::values;
22use crate::words::Span;
23
24pub const CURRENT: &str = "current";
26
27pub const BY_NAME: &str = "by_name";
29
30const BUILT_IN: &str = include_str!("../../prompts/understand/extract.md");
31
32#[derive(Debug, Clone, Copy)]
34pub struct Extract<'a> {
35 turn: &'a UnderstandingInput,
36}
37
38impl<'a> Extract<'a> {
39 #[must_use]
41 pub const fn new(turn: &'a UnderstandingInput) -> Self {
42 Self { turn }
43 }
44
45 #[must_use]
47 pub const fn turn(&self) -> &'a UnderstandingInput {
48 self.turn
49 }
50}
51
52#[derive(Debug, Clone, Copy)]
54pub enum RecordContext<'a> {
55 Existing(&'a RecordBrief),
57 New,
59 SameTurn,
61 Nothing,
63}
64
65#[derive(Debug, Clone)]
67pub struct RecordChoice {
68 pub handle: String,
70 pub value: RecordValue,
72 pub label: String,
74}
75
76#[derive(Debug, Clone)]
78pub struct ExtractInput<'a> {
79 pub label: &'static str,
81 pub words: Span,
83 pub spec: &'a OperationSpec,
85 pub workflow: &'a WorkflowBrief,
87 pub record: RecordContext<'a>,
89 pub arguments: Vec<&'a ArgumentSpec>,
91 pub record_choices: BTreeMap<String, Vec<RecordChoice>>,
93 pub continues: Option<Span>,
95 pub others: Vec<Span>,
97 pub kin: Vec<Span>,
100 pub also: Vec<OperationKey>,
102 pub transcript: usize,
104 pub note: Option<String>,
106 pub occurrence: Option<(usize, usize)>,
108}
109
110impl ExtractInput<'_> {
111 #[must_use]
113 pub fn messages(&self, turn: &UnderstandingInput) -> Vec<String> {
114 let mut names = vec![CURRENT.to_owned()];
115 names.extend(render::shown_messages(turn, self.transcript));
116 names
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
122pub struct Extraction {
123 pub arguments: BTreeMap<String, Given>,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(tag = "kind", rename_all = "snake_case")]
130#[allow(missing_docs)]
131pub enum Given {
132 NotGiven,
134 Words {
137 text: String,
138 message: String,
139 from: usize,
140 to: usize,
141 },
142 Written {
144 message: String,
145 from: usize,
146 to: usize,
147 text: String,
148 },
149 Value {
151 message: String,
152 from: usize,
153 to: usize,
154 value: Value,
155 },
156 Date {
158 message: String,
159 from: usize,
160 to: usize,
161 date: DateExpr,
162 },
163 Money {
165 message: String,
166 from: usize,
167 to: usize,
168 amount: String,
169 currency: String,
170 },
171 Record {
173 message: String,
174 from: usize,
175 to: usize,
176 record: String,
177 #[serde(default)]
178 name: String,
179 },
180}
181
182impl Given {
183 #[must_use]
185 pub fn pointer(&self) -> Option<(&str, Span)> {
186 match self {
187 Self::NotGiven => None,
188 Self::Words {
189 message, from, to, ..
190 }
191 | Self::Written {
192 message, from, to, ..
193 }
194 | Self::Value {
195 message, from, to, ..
196 }
197 | Self::Date {
198 message, from, to, ..
199 }
200 | Self::Money {
201 message, from, to, ..
202 }
203 | Self::Record {
204 message, from, to, ..
205 } => Some((message, Span::from_shown(*from, *to))),
206 }
207 }
208}
209
210fn given_kind(shape: &ValueShape) -> &'static str {
212 match shape {
213 ValueShape::Text { written: false } => "words",
214 ValueShape::Text { written: true } => "written",
215 ValueShape::Date { .. } => "date",
216 ValueShape::Money => "money",
217 ValueShape::Record { .. } => "record",
218 _ => "value",
219 }
220}
221
222fn argument_schema(
223 argument: &ArgumentSpec,
224 spec: &OperationSpec,
225 messages: &[String],
226 records: Option<&Vec<RecordChoice>>,
227) -> Value {
228 let pointer = || {
229 vec![
230 ("message", one_of(messages.iter().cloned())),
231 ("from", index()),
232 ("to", index()),
233 ]
234 };
235 let mut fields = pointer();
236 let kind = given_kind(&argument.shape);
237 match &argument.shape {
238 ValueShape::Text { written: false } => fields.insert(
239 0,
240 (
241 "text",
242 json!({
243 "type": "string",
244 "description": "The value's own words, copied exactly as the user wrote them, without the punctuation that ends their sentence."
245 }),
246 ),
247 ),
248 ValueShape::Text { written: true } => fields.push(("text", json!({ "type": "string" }))),
249 ValueShape::Enum { values } => fields.push(("value", one_of(values.iter().cloned()))),
250 ValueShape::Integer => fields.push(("value", json!({ "type": "integer" }))),
251 ValueShape::Number => fields.push(("value", json!({ "type": "number" }))),
252 ValueShape::Bool => fields.push(("value", json!({ "type": "boolean" }))),
253 ValueShape::Date { .. } => fields.push(("date", date_expression())),
254 ValueShape::Money => {
255 fields.push(("amount", json!({ "type": "string" })));
256 fields.push(("currency", json!({ "type": "string" })));
257 }
258 ValueShape::Record { .. } => {
259 let mut handles: Vec<String> = records
260 .map(|choices| choices.iter().map(|c| c.handle.clone()).collect())
261 .unwrap_or_default();
262 handles.push(BY_NAME.to_owned());
263 fields.insert(
266 0,
267 (
268 "name",
269 json!({
270 "type": "string",
271 "description": "The words naming the record, copied exactly as the user wrote them, without the punctuation that ends their sentence."
272 }),
273 ),
274 );
275 fields.push(("record", one_of(handles)));
276 }
277 _ => fields.push(("value", structured(spec, &argument.name))),
278 }
279 any_of(vec![variant("not_given", vec![]), variant(kind, fields)])
280}
281
282fn structured(spec: &OperationSpec, name: &str) -> Value {
284 let root = spec.arguments_schema.as_value();
285 let property = root
286 .pointer(&format!("/properties/{name}"))
287 .cloned()
288 .unwrap_or_else(|| json!({}));
289 let defs = root.get("$defs").cloned();
290 inline(property, defs.as_ref(), 0)
291}
292
293fn inline(value: Value, defs: Option<&Value>, depth: usize) -> Value {
294 match value {
295 Value::Object(map) if depth < 16 => {
296 if let Some(Value::String(reference)) = map.get("$ref") {
297 let name = reference.rsplit('/').next().unwrap_or_default();
298 let target = defs.and_then(|d| d.get(name)).cloned().unwrap_or(json!({}));
299 return inline(target, defs, depth + 1);
300 }
301 Value::Object(
302 map.into_iter()
303 .map(|(key, child)| (key, inline(child, defs, depth + 1)))
304 .collect(),
305 )
306 }
307 Value::Array(items) => Value::Array(
308 items
309 .into_iter()
310 .map(|item| inline(item, defs, depth + 1))
311 .collect(),
312 ),
313 other => other,
314 }
315}
316
317impl<'a> ModelTask for Extract<'a> {
318 type Input = ExtractInput<'a>;
319 type Output = Extraction;
320
321 fn kind(&self) -> TaskKind {
322 TaskKind::Extract
323 }
324
325 fn prompt_name(&self) -> &str {
326 "understand.extract"
327 }
328
329 fn instructions(&self) -> &str {
330 BUILT_IN
331 }
332
333 fn schema(&self, input: &ExtractInput<'a>) -> Value {
334 let messages = input.messages(self.turn);
335 let properties = input
336 .arguments
337 .iter()
338 .map(|argument| {
339 (
340 argument.name.as_str(),
341 argument_schema(
342 argument,
343 input.spec,
344 &messages,
345 input.record_choices.get(&argument.name),
346 ),
347 )
348 })
349 .collect();
350 object(vec![("arguments", object(properties))])
351 }
352
353 fn render(&self, input: &ExtractInput<'a>) -> Vec<Message> {
354 let turn = self.turn;
355 let words = &turn.message;
356 let record = match input.record {
357 RecordContext::Existing(record) => {
358 Some(format!("Record: {}", render::record_line(record, false)))
359 }
360 RecordContext::New => Some(format!("Record: a new {} record", input.workflow.key)),
361 RecordContext::SameTurn => Some(format!(
362 "Record: the {} record this message creates",
363 input.workflow.key
364 )),
365 RecordContext::Nothing => None,
366 };
367 let mut choices = String::new();
368 for (name, listed) in &input.record_choices {
369 if !choices.is_empty() {
370 choices.push_str("\n\n");
371 }
372 if listed.is_empty() {
373 let _ = write!(
374 choices,
375 "No record is listed for {name}: one the user names is {BY_NAME}, with its name."
376 );
377 continue;
378 }
379 let _ = write!(choices, "Records {name} may name:");
380 for choice in listed {
381 let _ = write!(choices, "\n- {}: {}", choice.handle, choice.label);
382 }
383 let _ = write!(
384 choices,
385 "\n- {BY_NAME}: a record the user names that is not listed"
386 );
387 }
388 let asked: Vec<&str> = input.arguments.iter().map(|a| a.name.as_str()).collect();
389 let offered = input
390 .spec
391 .arguments
392 .iter()
393 .filter(|argument| render::model_given(&argument.source))
394 .count();
395 let only = (asked.len() < offered).then(|| format!("Give only: {}.", asked.join(", ")));
396 vec![Message::user(render::sections([
397 Some(format!(
398 "Operation: {}",
399 render::operation_line(input.spec, self.turn)
400 )),
401 input
402 .spec
403 .guidance
404 .as_ref()
405 .map(|g| format!("Guidance: {g}")),
406 render::arguments(input.spec, turn),
407 only,
408 render::examples(input.spec),
409 render::glossary(input.workflow),
410 record,
411 (!choices.is_empty()).then_some(choices),
412 Some(format!("Today: {}", turn.today.format("%A %-d %B %Y"))),
413 render::transcript(turn, input.transcript),
414 Some(render::titled_message(
415 &format!("Message ({CURRENT})"),
416 words,
417 )),
418 Some(render::unit(input.label, words, input.words)),
419 input
420 .continues
421 .map(|span| render::unit("It continues", words, span)),
422 (!input.also.is_empty()).then(|| {
423 let keys: Vec<&str> = input.also.iter().map(OperationKey::as_str).collect();
424 format!(
425 "This request also asks for {}: the words of its values are that \
426 operation's.",
427 keys.join(", ")
428 )
429 }),
430 (!input.others.is_empty()).then(|| {
431 let spans: Vec<String> = input
432 .others
433 .iter()
434 .map(|span| {
435 let (from, to) = span.shown();
436 format!("words {from} to {to}")
437 })
438 .collect();
439 format!(
440 "Other parts of the message, each read on its own, give none of this \
441 part's values: {}.",
442 spans.join(", ")
443 )
444 }),
445 input.occurrence.map(|(number, of)| {
446 format!(
447 "This request asks for {} {of} times: give only the values of occurrence \
448 {number} of {of}, counting in the order the message says them.",
449 input.spec.key
450 )
451 }),
452 input
453 .note
454 .as_ref()
455 .map(|note| format!("A check of the whole message found: {note}")),
456 ]))]
457 }
458
459 fn check(&self, input: &ExtractInput<'a>, output: &Extraction) -> Result<(), StructuralError> {
460 values::convert(self.turn, input, output).map(|_| ())
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn a_note_from_the_whole_turn_check_is_shown() {
470 let turn = UnderstandingInput::new("name Lisbon", "en-GB", chrono::NaiveDate::MIN);
471 let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
472 let workflow = WorkflowBrief::new("trip");
473 let input = ExtractInput {
474 label: "Request",
475 words: Span::new(0, 1),
476 spec: &spec,
477 workflow: &workflow,
478 record: RecordContext::Nothing,
479 arguments: Vec::new(),
480 record_choices: BTreeMap::new(),
481 continues: None,
482 others: Vec::new(),
483 kin: Vec::new(),
484 also: Vec::new(),
485 transcript: 0,
486 note: Some("the value of value is in «rent»".to_owned()),
487 occurrence: None,
488 };
489 let rendered = format!("{:?}", Extract::new(&turn).render(&input));
490 assert!(
491 rendered
492 .contains("A check of the whole message found: the value of value is in «rent»"),
493 "{rendered}"
494 );
495 }
496
497 #[test]
498 fn the_words_other_parts_hold_are_named() {
499 let turn =
500 UnderstandingInput::new("name Lisbon, fly tomorrow", "en-GB", chrono::NaiveDate::MIN);
501 let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
502 let workflow = WorkflowBrief::new("trip");
503 let input = ExtractInput {
504 label: "Request",
505 words: Span::new(0, 1),
506 spec: &spec,
507 workflow: &workflow,
508 record: RecordContext::Nothing,
509 arguments: Vec::new(),
510 record_choices: BTreeMap::new(),
511 continues: None,
512 others: vec![Span::new(2, 3)],
513 kin: Vec::new(),
514 also: Vec::new(),
515 transcript: 0,
516 note: None,
517 occurrence: None,
518 };
519 let rendered = format!("{:?}", Extract::new(&turn).render(&input));
520 assert!(
521 rendered.contains(
522 "Other parts of the message, each read on its own, give none of this part's \
523 values: words 3 to 4."
524 ),
525 "{rendered}"
526 );
527 }
528
529 #[test]
530 fn the_other_operations_of_the_request_are_named() {
531 let turn = UnderstandingInput::new(
532 "name Lisbon and fly tomorrow",
533 "en-GB",
534 chrono::NaiveDate::MIN,
535 );
536 let spec = OperationSpec::new("trip.set_name").summary("Name the trip.");
537 let workflow = WorkflowBrief::new("trip");
538 let input = ExtractInput {
539 label: "Request",
540 words: Span::new(0, 4),
541 spec: &spec,
542 workflow: &workflow,
543 record: RecordContext::Nothing,
544 arguments: Vec::new(),
545 record_choices: BTreeMap::new(),
546 continues: None,
547 others: Vec::new(),
548 kin: Vec::new(),
549 also: vec!["trip.set_travel_date".into()],
550 transcript: 0,
551 note: None,
552 occurrence: None,
553 };
554 let rendered = format!("{:?}", Extract::new(&turn).render(&input));
555 assert!(
556 rendered.contains(
557 "This request also asks for trip.set_travel_date: the words of its values are \
558 that operation's."
559 ),
560 "{rendered}"
561 );
562 }
563
564 #[test]
565 fn a_given_value_deserializes_by_its_kind() {
566 let given: Given = serde_json::from_value(json!({
567 "kind": "date", "message": "current", "from": 4, "to": 4,
568 "date": {"kind": "relative", "unit": "day", "amount": 1}
569 }))
570 .unwrap();
571 assert_eq!(
572 given.pointer(),
573 Some(("current", Span::new(3, 3))),
574 "a model counts words from 1"
575 );
576 let absent: Given = serde_json::from_value(json!({"kind": "not_given"})).unwrap();
577 assert_eq!(absent, Given::NotGiven);
578 }
579}