use std::collections::BTreeMap;
use std::fmt::Write as _;
use turnframe_core::ids::{OperationKey, TargetToken, WorkflowKey};
use turnframe_core::operation::{ArgumentSpec, OperationSpec, ValueShape};
use turnframe_core::plan::{ActMutability, TargetPolicy};
use turnframe_core::understanding::{
ActAction, ActId, ActStatus, ActTarget, MessageRef, NotUnderstoodReason, RecordValue,
UnderstoodAct, UnderstoodArgument, UnitId, WordRange,
};
use turnframe_tasks::{TaskCall, TaskId, TaskOutcome};
use crate::check::argument_of;
use crate::input::{PendingAct, Speaker, UnderstandingInput, WorkflowBrief};
use crate::pipeline::{Context, VerifyPolicy};
use crate::progress::{Located, Step};
use crate::render;
use crate::tasks::extract::{Extract, ExtractInput, Extraction, RecordChoice, RecordContext};
use crate::tasks::locate::{AMBIGUOUS, BY_NAME, Candidate, Locate, LocateInput, NEW};
use crate::tasks::verify::{ArgumentVerdict, Overall, Verdict, Verify, VerifyInput};
use crate::values::{Extracted, convert};
use crate::words::Span;
#[derive(Debug, Clone)]
pub(crate) struct Planned<'a> {
pub id: ActId,
pub unit: UnitId,
pub label: &'static str,
pub words: Span,
pub range: WordRange,
pub action: ActAction,
pub workflow: &'a WorkflowBrief,
pub spec: Option<&'a OperationSpec>,
pub continues: Option<Span>,
pub occurrence: Option<(usize, usize)>,
pub pending: Option<&'a PendingAct>,
pub corrects: Vec<&'a PendingAct>,
pub kin: Vec<Span>,
pub also: Vec<OperationKey>,
pub guessed: bool,
pub parent: TaskId,
pub depth: u8,
}
#[derive(Debug, Clone)]
pub(crate) struct Creation {
pub act: ActId,
pub workflow: WorkflowKey,
pub words: Span,
}
#[derive(Debug, Clone)]
pub(crate) enum Chained {
Act(UnderstoodAct),
NotUnderstood {
unit: UnitId,
words: WordRange,
reason: NotUnderstoodReason,
aimed: Option<ActTarget>,
},
Nothing,
}
struct Chain<'c, 'a> {
cx: &'c Context<'a>,
plan: &'c Planned<'a>,
carried: BTreeMap<String, UnderstoodArgument>,
unit: TaskId,
parent: TaskId,
depth: u8,
note: Option<String>,
after: &'static str,
}
impl<'c, 'a> Chain<'c, 'a> {
fn new(cx: &'c Context<'a>, plan: &'c Planned<'a>, note: Option<String>) -> Self {
Self {
cx,
plan,
carried: plan
.pending
.map(|pending| pending.given.clone())
.unwrap_or_default(),
unit: TaskId::new(if plan.id.act == 1 {
plan.unit.to_string()
} else {
plan.id.to_string()
}),
parent: plan.parent.clone(),
depth: plan.depth,
after: if note.is_some() {
".after_cross_check"
} else {
""
},
note,
}
}
}
pub(crate) async fn run(cx: &Context<'_>, plan: &Planned<'_>) -> Chained {
Chain::new(cx, plan, None).run().await
}
pub(crate) async fn run_rerouted(cx: &Context<'_>, plan: &Planned<'_>) -> Chained {
let mut chain = Chain::new(cx, plan, None);
chain.after = ".after_reroute";
chain.run().await
}
#[derive(Debug, Clone)]
pub(crate) enum Revisit {
Value { note: String },
Record { note: String },
Asked,
}
pub(crate) async fn revisit(
cx: &Context<'_>,
plan: &Planned<'_>,
act: &UnderstoodAct,
revisit: &Revisit,
) -> Chained {
let note = match revisit {
Revisit::Value { note } | Revisit::Record { note } => Some(note.clone()),
Revisit::Asked => None,
};
let mut chain = Chain::new(cx, plan, note);
chain.after = ".after_cross_check";
match revisit {
Revisit::Record { .. } => match chain.target().await {
Ok(target) if target == act.target => Chained::Act(act.clone()),
Ok(target) => chain.run_from(target).await,
Err(reason) => chain.not_understood(reason, None),
},
Revisit::Value { .. } => chain.run_from(act.target.clone()).await,
Revisit::Asked => chain.verify_again(act.clone()).await,
}
}
pub(crate) async fn read_values_again(
cx: &Context<'_>,
plan: &Planned<'_>,
act: &UnderstoodAct,
note: String,
after: &'static str,
) -> Chained {
let mut chain = Chain::new(cx, plan, Some(note));
chain.after = after;
chain.run_from(act.target.clone()).await
}
pub(crate) async fn read_again(
cx: &Context<'_>,
plan: &Planned<'_>,
act: &UnderstoodAct,
after: &'static str,
) -> Chained {
let mut chain = Chain::new(cx, plan, None);
chain.after = after;
chain.run_from(act.target.clone()).await
}
pub(crate) fn record_label(turn: &UnderstandingInput, value: &RecordValue) -> String {
match value {
RecordValue::Record { token } => turn
.record(token)
.map_or_else(|| token.to_string(), |(_, record)| record.label.clone()),
RecordValue::SameTurn { act } => format!("the record act {act} creates"),
RecordValue::Named { workflow, named } => {
format!(
"a {workflow} record named {}, not listed",
render::quoted(named)
)
}
}
}
fn created_label(turn: &UnderstandingInput, creation: &Creation) -> String {
let said = turn.message.slice(creation.words).unwrap_or_default();
format!(
"the {} record this message creates, {}",
creation.workflow,
render::quoted(said)
)
}
fn failed(task: &str, code: String) -> NotUnderstoodReason {
NotUnderstoodReason::TaskFailed {
task: task.to_owned(),
code,
}
}
impl<'a> Chain<'_, 'a> {
fn not_understood(&self, reason: NotUnderstoodReason, aimed: Option<ActTarget>) -> Chained {
Chained::NotUnderstood {
unit: self.plan.unit,
words: self.plan.range,
reason,
aimed,
}
}
fn advance<O>(&mut self, id: TaskId, outcome: &TaskOutcome<O>) {
self.depth = outcome.depth();
self.parent = id;
}
async fn run(mut self) -> Chained {
let target = match self.target().await {
Ok(target) => target,
Err(reason) => return self.not_understood(reason, None),
};
self.run_from(target).await
}
async fn verify_again(mut self, mut act: UnderstoodAct) -> Chained {
let mut extracted = Extracted::default();
for (name, argument) in &act.arguments {
if !self.carried.contains_key(name) {
extracted.arguments.insert(name.clone(), argument.clone());
}
}
let verdict = match self.verify(&act.target, &extracted, "verify").await {
Ok(verdict) => verdict,
Err(reason) => return self.not_understood(reason, Some(act.target)),
};
if verdict.confirmed() {
return Chained::Act(act);
}
if verdict.overall == Overall::NotRequested {
return self.not_understood(NotUnderstoodReason::NotRequested, Some(act.target));
}
let at_fault = verdict.at_fault();
for name in &at_fault {
act.arguments.remove(name);
}
if !at_fault.is_empty() {
act.status = ActStatus::NeedsValue {
arguments: at_fault,
reason: None,
};
}
Chained::Act(act)
}
async fn run_from(mut self, target: ActTarget) -> Chained {
self.cx.steps.step(Step::Located {
act: self.plan.id,
record: self.located(&target),
});
let input = self.extract_input(&target);
let mut extraction = None;
let mut extracted = Extracted::default();
if let Some(input) = &input {
match self.extract(input, "extract", None).await {
Ok((output, values)) => {
let only_elsewhere =
values.arguments.is_empty() && !values.elsewhere.is_empty();
if only_elsewhere && self.plan.guessed {
return Chained::Nothing;
}
let (output, values) = self.own_values(input, output, values).await;
if only_elsewhere && values.arguments.is_empty() {
return Chained::Nothing;
}
let (output, values) = self.own_end(input, output, values).await;
let (output, values) = self.records_read_again(input, output, values).await;
extraction = Some(output);
extracted = values;
}
Err(reason) => return self.not_understood(reason, Some(target)),
}
}
self.cx.steps.step(Step::Extracted {
act: self.plan.id,
given: extracted
.arguments
.iter()
.map(|(name, argument)| (name.clone(), self.shown(argument)))
.collect(),
not_given: extracted.not_given.clone(),
});
if let Some(done) = self.corrected(&target) {
for (name, value) in &done.given {
if !extracted.arguments.contains_key(name) {
self.carried
.entry(name.clone())
.or_insert_with(|| value.clone());
}
}
}
let mut target = target;
let mut status = self.missing(&extracted);
if status == ActStatus::Ready && self.verifies() {
match self
.verified(&mut target, input.as_ref(), &mut extraction, &mut extracted)
.await
{
Ok(next) => status = next,
Err(reason) => return self.not_understood(reason, Some(target)),
}
}
let mut arguments = self.carried.clone();
arguments.extend(extracted.arguments.clone());
let mut act = self.act(target, arguments, status);
if act.status == ActStatus::Ready && self.cx.checker.is_active() {
self.checked(&mut act, input.as_ref(), extraction.as_ref())
.await;
}
Chained::Act(act)
}
fn corrected(&self, target: &ActTarget) -> Option<&'a PendingAct> {
let ActTarget::Record { token } = target else {
return None;
};
let on_it: Vec<&'a PendingAct> = self
.plan
.corrects
.iter()
.copied()
.filter(|done| done.record.as_ref() == Some(token))
.collect();
match on_it[..] {
[done] => Some(done),
_ => None,
}
}
async fn own_values(
&mut self,
input: &ExtractInput<'a>,
output: Extraction,
values: Extracted,
) -> (Extraction, Extracted) {
if values.elsewhere.is_empty() {
return (output, values);
}
let mut note = String::new();
for (name, span) in &values.elsewhere {
let said = self.cx.turn.message.slice(*span).unwrap_or_default();
let (from, to) = span.shown();
let _ = writeln!(
note,
"`{name}` points at words {from} to {to}, «{said}», which another part of the \
message holds, asking for something of its own: give this part's value from \
its own words or an earlier message, or not_given."
);
}
match self
.extract(
input,
"extract.after_elsewhere",
Some((&output, note.trim_end())),
)
.await
{
Ok(repaired) => repaired,
Err(_) => (output, values),
}
}
async fn records_read_again(
&mut self,
input: &ExtractInput<'a>,
output: Extraction,
mut values: Extracted,
) -> (Extraction, Extracted) {
let unread: Vec<String> = input
.arguments
.iter()
.filter(|argument| matches!(argument.shape, ValueShape::Record { .. }))
.map(|argument| argument.name.clone())
.filter(|name| {
values.not_given.contains(name)
&& !values.elsewhere.iter().any(|(other, _)| other == name)
&& !self.carried.contains_key(name)
})
.collect();
if unread.is_empty() {
return (output, values);
}
let note = unread
.iter()
.map(|name| {
format!(
"`{name}` was given no value: when words of this part name the record, give \
it; otherwise give not_given again."
)
})
.collect::<Vec<_>>()
.join("\n");
let Ok((again, read)) = self
.extract(input, "extract.after_not_given", Some((&output, ¬e)))
.await
else {
return (output, values);
};
let mut took = false;
for name in &unread {
if let Some(given) = read.arguments.get(name) {
values.arguments.insert(name.clone(), given.clone());
values.not_given.retain(|other| other != name);
took = true;
}
}
(if took { again } else { output }, values)
}
async fn own_end(
&mut self,
input: &ExtractInput<'a>,
output: Extraction,
values: Extracted,
) -> (Extraction, Extracted) {
let last = self.plan.range.last;
let meets_next = self.cx.units.iter().any(|unit| unit.from == last + 1);
let ends_the_part = |name: &String| {
let copied = input.arguments.iter().any(|argument| {
&argument.name == name && argument.shape == ValueShape::Text { written: false }
});
values.arguments.get(name).is_some_and(|argument| {
argument.excerpt.is_some_and(|excerpt| {
excerpt.message == MessageRef::Current && excerpt.words.last == last
})
}) && copied
};
let Some(name) = values
.arguments
.keys()
.find(|name| ends_the_part(name))
.cloned()
else {
return (output, values);
};
if !meets_next {
return (output, values);
}
let span = Span::new(last, last);
let (word, _) = span.shown();
let said = self.cx.turn.message.slice(span).unwrap_or_default();
let note = format!(
"`{name}` ends at word {word}, «{said}», the last word of this part, and the next \
part of the message begins at word {}: if that word joins this part to the next \
one, it is no value's; give the value without it. If it is the value's own word, \
give the same value.",
word + 1
);
match self
.extract(input, "extract.at_part_end", Some((&output, ¬e)))
.await
{
Ok(repaired) => repaired,
Err(_) => (output, values),
}
}
fn located(&self, target: &ActTarget) -> Located {
match target {
ActTarget::Record { token } => Located::Record {
token: token.clone(),
label: self
.cx
.turn
.record(token)
.map_or_else(|| token.to_string(), |(_, record)| record.label.clone()),
},
ActTarget::New { .. } => Located::New,
ActTarget::SameTurn { act } => Located::SameTurn { act: *act },
ActTarget::Card => Located::Card,
ActTarget::NotListed { .. } => Located::NotListed,
ActTarget::Ambiguous { .. } => Located::Ambiguous,
_ => Located::Nothing,
}
}
fn shown(&self, argument: &UnderstoodArgument) -> String {
render::understood(argument, self.cx.turn, |record| self.record_label(record))
}
fn verifies(&self) -> bool {
let mutating = match self.plan.spec {
Some(spec) => spec.mutability == ActMutability::Mutating,
None => true,
};
match self.cx.settings.verify {
VerifyPolicy::Off => false,
VerifyPolicy::All => true,
VerifyPolicy::Mutating => mutating,
}
}
async fn verified(
&mut self,
target: &mut ActTarget,
input: Option<&ExtractInput<'a>>,
extraction: &mut Option<Extraction>,
extracted: &mut Extracted,
) -> Result<ActStatus, NotUnderstoodReason> {
let verdict = self.verify(target, extracted, "verify").await?;
if verdict.confirmed() {
return Ok(ActStatus::Ready);
}
let doubted_words: Vec<(String, Option<WordRange>)> = verdict
.arguments
.iter()
.filter(|(_, judged)| **judged == ArgumentVerdict::TooMuch)
.map(|(name, _)| {
let words = extracted
.arguments
.get(name)
.and_then(|given| given.excerpt);
(name.clone(), words.map(|excerpt| excerpt.words))
})
.collect();
let first_reading = extracted.arguments.clone();
if let (Some(input), Some(previous)) = (input, extraction.as_ref()) {
let earlier = self
.cx
.turn
.transcript
.iter()
.any(|m| m.speaker == Speaker::User);
let feedback = verdict.feedback_given(|name| {
earlier
&& extracted.arguments.get(name).is_some_and(|argument| {
argument
.excerpt
.is_some_and(|excerpt| excerpt.message == MessageRef::Current)
})
});
if let Ok((output, mut values)) = self
.extract(input, "extract.after_verify", Some((previous, &feedback)))
.await
{
let stood = values.elsewhere.iter().map(|(name, _)| name.clone()).chain(
verdict
.arguments
.iter()
.filter(|(name, judged)| {
**judged == ArgumentVerdict::Stated
&& !values.arguments.contains_key(*name)
})
.map(|(name, _)| name.clone()),
);
for name in stood.collect::<Vec<_>>() {
if let Some(first) = extracted.arguments.get(&name) {
values.arguments.insert(name.clone(), first.clone());
values.not_given.retain(|given| *given != name);
}
}
*extraction = Some(output);
*extracted = values;
}
}
let status = self.missing(extracted);
if verdict.overall == Overall::NotRequested
&& self.cx.settings.doubt_votes > 0
&& (extracted.arguments == first_reading || status != ActStatus::Ready)
{
return Err(NotUnderstoodReason::NotRequested);
}
if status != ActStatus::Ready {
return Ok(status);
}
let mut second = self
.verify(target, extracted, "verify.after_repair")
.await?;
for (name, words) in &doubted_words {
let again = extracted
.arguments
.get(name)
.and_then(|given| given.excerpt);
if words.is_some()
&& again.map(|excerpt| excerpt.words) == *words
&& let Some(judged) = second.arguments.get_mut(name)
&& *judged == ArgumentVerdict::TooMuch
{
*judged = ArgumentVerdict::Stated;
}
}
if second.confirmed() {
return Ok(ActStatus::Ready);
}
match second.overall {
Overall::NotRequested => return Err(NotUnderstoodReason::NotRequested),
Overall::WrongRecord => *target = self.retarget(target),
Overall::Confirmed => {}
}
let at_fault = second.at_fault();
for name in &at_fault {
extracted.arguments.remove(name);
}
Ok(if at_fault.is_empty() {
ActStatus::Ready
} else {
ActStatus::NeedsValue {
arguments: at_fault,
reason: None,
}
})
}
async fn checked(
&mut self,
act: &mut UnderstoodAct,
input: Option<&ExtractInput<'a>>,
extraction: Option<&Extraction>,
) {
let Err(rejection) = self.cx.checker.check(act) else {
self.cx.steps.step(Step::Checked {
act: self.plan.id,
refused: None,
});
return;
};
let explanation = rejection.explanation.as_ref().map_or_else(
|| rejection.message_key.clone(),
|text| text.resolve(&self.cx.turn.locale).to_owned(),
);
self.cx.steps.step(Step::Checked {
act: self.plan.id,
refused: Some((
argument_of(&rejection).unwrap_or_else(|| "the act".to_owned()),
explanation.clone(),
)),
});
let Some(argument) = argument_of(&rejection) else {
return;
};
let refused = act
.arguments
.get(&argument)
.map(|given| match &given.value {
turnframe_core::understanding::ArgumentValue::Json(value) => {
render::quoted(&render::value_text(value))
}
turnframe_core::understanding::ArgumentValue::Record(record) => {
render::quoted(&self.record_label(record))
}
});
let reason = refused.as_ref().map_or_else(
|| explanation.clone(),
|refused| format!("{refused}: {explanation}"),
);
let needs = |act: &mut UnderstoodAct| {
act.arguments.remove(&argument);
act.status = ActStatus::NeedsValue {
arguments: vec![argument.clone()],
reason: Some(reason.clone()),
};
};
let (Some(input), Some(previous)) = (input, extraction) else {
return needs(act);
};
let feedback = match &refused {
Some(refused) => format!("{refused} was refused as {argument}: {explanation}"),
None => format!("The value of {argument} was refused: {explanation}"),
};
let Ok((_, values)) = self
.extract(input, "extract.after_check", Some((previous, &feedback)))
.await
else {
return needs(act);
};
let mut repaired = act.clone();
repaired.arguments.extend(values.arguments.clone());
if self.missing(&values) != ActStatus::Ready || self.cx.checker.check(&repaired).is_err() {
return needs(act);
}
if self.verifies() {
let target = repaired.target.clone();
match self.verify(&target, &values, "verify.after_check").await {
Ok(verdict) if verdict.confirmed() => {}
_ => return needs(act),
}
}
*act = repaired;
}
fn missing(&self, extracted: &Extracted) -> ActStatus {
let Some(spec) = self.plan.spec else {
return ActStatus::Ready;
};
let missing: Vec<String> = spec
.arguments
.iter()
.filter(|argument| argument.required && render::model_given(&argument.source))
.filter(|argument| {
!extracted.arguments.contains_key(&argument.name)
&& !self.carried.contains_key(&argument.name)
})
.map(|argument| argument.name.clone())
.collect();
if missing.is_empty() {
ActStatus::Ready
} else {
ActStatus::NeedsValue {
arguments: missing,
reason: None,
}
}
}
fn act(
&self,
target: ActTarget,
arguments: BTreeMap<String, UnderstoodArgument>,
status: ActStatus,
) -> UnderstoodAct {
let mut depends_on = Vec::new();
if let ActTarget::SameTurn { act } = &target {
depends_on.push(*act);
}
for argument in arguments.values() {
if let turnframe_core::understanding::ArgumentValue::Record(RecordValue::SameTurn {
act,
}) = &argument.value
&& !depends_on.contains(act)
{
depends_on.push(*act);
}
}
UnderstoodAct {
id: self.plan.id,
action: self.plan.action.clone(),
target,
arguments,
words: self.plan.range,
depends_on,
status,
}
}
fn candidates(&self) -> Vec<Candidate<'a>> {
let (plan, workflow) = (self.plan, self.plan.workflow);
let mut candidates: Vec<Candidate<'a>> = workflow
.records
.iter()
.filter(|record| plan.spec.is_some_and(|spec| record.offers(&spec.key)))
.map(Candidate::Record)
.collect();
candidates.extend(
self.cx
.creations
.iter()
.filter(|creation| {
let earlier = creation.words.from < plan.words.from
|| (creation.act.unit == plan.id.unit && creation.act.act < plan.id.act);
creation.workflow == workflow.key && earlier
})
.map(|creation| Candidate::SameTurn {
act: creation.act,
words: creation.words,
}),
);
candidates
}
async fn target(&mut self) -> Result<ActTarget, NotUnderstoodReason> {
let workflow = self.plan.workflow.key.clone();
let spec = match &self.plan.action {
ActAction::Start { workflow } => {
return Ok(ActTarget::New {
workflow: workflow.clone(),
});
}
ActAction::Apply { .. } => self.plan.spec,
};
if let Some(pending) = self.plan.pending {
return Ok(pending
.record
.clone()
.map_or(ActTarget::Nothing, |token| ActTarget::Record { token }));
}
let Some(spec) = spec else {
return Ok(ActTarget::Nothing);
};
match spec.target_policy {
TargetPolicy::NewCaseOnly => return Ok(ActTarget::New { workflow }),
TargetPolicy::ActiveInteractionOnly => return Ok(ActTarget::Card),
TargetPolicy::None => return Ok(ActTarget::Nothing),
_ => {}
}
let candidates = self.candidates();
let allow_new = spec.target_policy == TargetPolicy::AllowsNewCase
&& self.plan.workflow.new_case.contains(&spec.key);
let allow_not_listed = matches!(
spec.target_policy,
TargetPolicy::RequiresExistingCase | TargetPolicy::AllowsNewCase
);
match candidates.as_slice() {
[only] if !allow_new => return Ok(candidate_target(*only)),
[] if allow_new => return Ok(ActTarget::New { workflow }),
[] => {
return Ok(ActTarget::NotListed {
workflow,
words: None,
});
}
_ => {}
}
let input = LocateInput {
label: self.plan.label,
words: self.plan.words,
spec,
workflow: &self.plan.workflow.key,
candidates,
allow_new,
allow_not_listed,
note: self.note.clone(),
};
let id = self.unit.child(format!("locate{}", self.after));
let call = TaskCall {
id: &id,
parent: Some(&self.parent),
depth: self.depth.saturating_add(1),
};
let outcome = self
.cx
.engine
.run(self.cx.scope, call, &Locate::new(self.cx.turn), &input)
.await;
self.advance(id.clone(), &outcome);
let tokens = |handles: Vec<&str>| -> Vec<TargetToken> {
handles
.into_iter()
.filter_map(|handle| match input.candidate(handle) {
Some(Candidate::Record(record)) => Some(record.token.clone()),
_ => None,
})
.collect()
};
match outcome {
TaskOutcome::Accepted { output, .. } => Ok(match output.record.as_str() {
NEW => ActTarget::New { workflow },
BY_NAME => ActTarget::NotListed {
workflow,
words: output
.named
.and_then(|span| self.cx.turn.message.range(span).ok()),
},
AMBIGUOUS => ActTarget::Ambiguous {
candidates: tokens(input.handles().iter().map(|(h, _)| h.as_str()).collect()),
},
handle => input
.candidate(handle)
.map_or(ActTarget::Nothing, |candidate| candidate_target(*candidate)),
}),
TaskOutcome::Disagreed { answers, .. } => Ok(ActTarget::Ambiguous {
candidates: tokens(answers.iter().map(|a| a.record.as_str()).collect()),
}),
TaskOutcome::Failed { failure, .. } => Err(failed("locate", failure.code())),
}
}
fn retarget(&self, current: &ActTarget) -> ActTarget {
let records: Vec<TargetToken> = self
.candidates()
.iter()
.filter_map(|candidate| match candidate {
Candidate::Record(record) => Some(record.token.clone()),
Candidate::SameTurn { .. } => None,
})
.collect();
let others = records
.iter()
.any(|token| !matches!(current, ActTarget::Record { token: t } if t == token));
if records.len() > 1 && others {
ActTarget::Ambiguous {
candidates: records,
}
} else {
ActTarget::NotListed {
workflow: self.plan.workflow.key.clone(),
words: None,
}
}
}
fn extract_input(&self, target: &ActTarget) -> Option<ExtractInput<'a>> {
let spec = self.plan.spec?;
let asked: Vec<&'a ArgumentSpec> = spec
.arguments
.iter()
.filter(|argument| render::model_given(&argument.source))
.filter(|argument| {
self.plan
.pending
.is_none_or(|pending| pending.missing.contains(&argument.name))
})
.collect();
if asked.is_empty() {
return None;
}
let record = match target {
ActTarget::Record { token } => self
.cx
.turn
.record(token)
.map_or(RecordContext::Nothing, |(_, record)| {
RecordContext::Existing(record)
}),
ActTarget::New { .. } => RecordContext::New,
ActTarget::SameTurn { .. } => RecordContext::SameTurn,
_ => RecordContext::Nothing,
};
let record_choices = asked
.iter()
.filter_map(|argument| match &argument.shape {
ValueShape::Record { workflow } => {
Some((argument.name.clone(), self.record_choices(workflow)))
}
_ => None,
})
.collect();
Some(ExtractInput {
label: self.plan.label,
words: self.plan.words,
spec,
workflow: self.plan.workflow,
record,
arguments: asked,
record_choices,
continues: self.plan.continues,
others: self.others(),
kin: self.plan.kin.clone(),
also: self.plan.also.clone(),
transcript: self.cx.settings.transcript,
note: self.note.clone(),
occurrence: self.plan.occurrence,
})
}
fn others(&self) -> Vec<Span> {
let within = |outer: Span, span: Span| outer.from <= span.from && span.to <= outer.to;
self.cx
.units
.iter()
.copied()
.filter(|span| {
!within(self.plan.words, *span)
&& !self
.plan
.continues
.is_some_and(|continued| within(continued, *span))
})
.collect()
}
fn record_choices(&self, workflow: &WorkflowKey) -> Vec<RecordChoice> {
let turn = self.cx.turn;
let mut choices: Vec<RecordChoice> = turn
.workflow(workflow)
.map(|brief| brief.records.as_slice())
.unwrap_or_default()
.iter()
.enumerate()
.map(|(position, record)| RecordChoice {
handle: format!("r{}", position + 1),
value: RecordValue::Record {
token: record.token.clone(),
},
label: render::record_line(record, true),
})
.collect();
let created = self
.cx
.creations
.iter()
.filter(|c| &c.workflow == workflow && c.act != self.plan.id);
for (position, creation) in created.enumerate() {
choices.push(RecordChoice {
handle: format!("s{}", position + 1),
value: RecordValue::SameTurn { act: creation.act },
label: created_label(turn, creation),
});
}
choices
}
async fn extract(
&mut self,
input: &ExtractInput<'a>,
name: &str,
feedback: Option<(&Extraction, &str)>,
) -> Result<(Extraction, Extracted), NotUnderstoodReason> {
let id = self.unit.child(format!("{name}{}", self.after));
let task = Extract::new(self.cx.turn);
let call = TaskCall {
id: &id,
parent: Some(&self.parent),
depth: self.depth.saturating_add(1),
};
let outcome = match feedback {
None => self.cx.engine.run(self.cx.scope, call, &task, input).await,
Some((previous, note)) => {
self.cx.steps.step(Step::Repairing {
act: self.plan.id,
because: note.lines().next().unwrap_or_default().to_owned(),
});
self.cx
.engine
.run_with_feedback(self.cx.scope, call, &task, input, previous, note)
.await
}
};
self.advance(id, &outcome);
match outcome {
TaskOutcome::Accepted { output, .. } => {
let values = convert(self.cx.turn, input, &output)
.map_err(|error| failed("extract", error.code.to_owned()))?;
Ok((output, values))
}
TaskOutcome::Disagreed { .. } => Err(NotUnderstoodReason::Unclear),
TaskOutcome::Failed { failure, .. } => Err(failed("extract", failure.code())),
}
}
async fn verify(
&mut self,
target: &ActTarget,
extracted: &Extracted,
name: &str,
) -> Result<Verdict, NotUnderstoodReason> {
let turn = self.cx.turn;
let meaning = match (&self.plan.action, self.plan.spec) {
(_, Some(spec)) => render::operation_line(spec, turn),
(ActAction::Start { workflow }, None) => format!("start a new {workflow} record"),
(ActAction::Apply { operation }, None) => operation.to_string(),
};
let record = match target {
ActTarget::Record { token } => turn.record(token).map_or_else(
|| token.to_string(),
|(_, record)| render::record_line(record, false),
),
ActTarget::New { workflow } => format!("a new {workflow} record"),
ActTarget::SameTurn { .. } => {
format!("the {} record this message creates", self.plan.workflow.key)
}
ActTarget::Card => "the record of the card on screen".to_owned(),
_ => "still to be chosen".to_owned(),
};
let spec = self.plan.spec;
let labels = extracted
.arguments
.keys()
.map(|name| {
let label = spec.map_or_else(
|| name.clone(),
|spec| render::argument_label(spec, name, turn),
);
let deducible =
spec.and_then(|spec| spec.argument_named(name))
.is_some_and(|argument| {
matches!(
argument.source,
turnframe_core::operation::ArgumentSource::Inferred
)
});
let label = if deducible {
format!("{label} (may be deduced)")
} else {
label
};
(name.clone(), label)
})
.collect();
let record_labels = extracted
.arguments
.iter()
.filter_map(|(name, argument)| match &argument.value {
turnframe_core::understanding::ArgumentValue::Record(value) => {
Some((name.clone(), self.record_label(value)))
}
turnframe_core::understanding::ArgumentValue::Json(_) => None,
})
.collect();
let meanings = extracted
.arguments
.keys()
.filter_map(|name| {
let argument = spec?.argument_named(name)?;
matches!(argument.shape, ValueShape::Enum { .. })
.then(|| Some((name.clone(), argument.description.clone()?)))
.flatten()
})
.collect();
let input = VerifyInput {
label: self.plan.label,
words: self.plan.words,
meaning,
record,
arguments: &extracted.arguments,
labels,
record_labels,
meanings,
occurrence: self
.plan
.occurrence
.and_then(|(number, of)| match &self.plan.action {
ActAction::Apply { operation } => Some((operation.to_string(), number, of)),
ActAction::Start { .. } => None,
}),
note: self.note.clone(),
continues: self.plan.continues,
};
let name = format!("{name}{}", self.after);
let (parent, depth) = (self.parent.clone(), self.depth);
let mut output = self.judge(&name, &input, extracted).await?;
let again = self.cx.settings.doubt_votes;
if !output.confirmed() && again > 0 {
let (after, reached) = (self.parent.clone(), self.depth);
let mut confirming = None;
let mut confirmed = 0;
for vote in 1..=again {
(self.parent, self.depth) = (parent.clone(), depth);
let verdict = self
.judge(&format!("{name}.doubt{vote}"), &input, extracted)
.await;
(self.parent, self.depth) = (after.clone(), reached);
let Ok(verdict) = verdict else {
continue;
};
if verdict.confirmed() {
confirmed += 1;
confirming.get_or_insert(verdict);
}
}
if let Some(verdict) = confirming
&& 2 * confirmed > again + 1
{
output = verdict;
}
}
self.cx.steps.step(Step::Verified {
act: self.plan.id,
confirmed: output.confirmed(),
reason: output.reason.clone(),
at_fault: output.at_fault(),
});
Ok(output)
}
async fn judge(
&mut self,
name: &str,
input: &VerifyInput<'_>,
extracted: &Extracted,
) -> Result<Verdict, NotUnderstoodReason> {
let id = self.unit.child(name);
let call = TaskCall {
id: &id,
parent: Some(&self.parent),
depth: self.depth.saturating_add(1),
};
let outcome = self
.cx
.engine
.run(self.cx.scope, call, &Verify::new(self.cx.turn), input)
.await;
self.advance(id, &outcome);
match outcome {
TaskOutcome::Accepted { output, .. } => Ok(self.judged_by_value(output, extracted)),
TaskOutcome::Disagreed { .. } => Err(NotUnderstoodReason::Unclear),
TaskOutcome::Failed { failure, .. } => Err(failed("verify", failure.code())),
}
}
fn record_label(&self, value: &RecordValue) -> String {
let created = match value {
RecordValue::SameTurn { act } => self.cx.creations.iter().find(|c| &c.act == act),
_ => None,
};
created.map_or_else(
|| record_label(self.cx.turn, value),
|creation| created_label(self.cx.turn, creation),
)
}
fn judged_by_value(&self, mut verdict: Verdict, extracted: &Extracted) -> Verdict {
if self.plan.pending.is_some() && verdict.overall == Overall::NotRequested {
verdict.overall = Overall::Confirmed;
}
let Some(spec) = self.plan.spec else {
return verdict;
};
for (name, judged) in &mut verdict.arguments {
let Some(argument) = spec.argument_named(name) else {
continue;
};
let copied = matches!(argument.shape, ValueShape::Text { .. })
|| extracted.arguments.get(name).is_some_and(|given| {
matches!(
given.value,
turnframe_core::understanding::ArgumentValue::Record(
RecordValue::Named { .. }
)
)
});
let deduced = matches!(
argument.source,
turnframe_core::operation::ArgumentSource::Inferred
);
let corrected = self.plan.label == "Correction"
&& extracted
.arguments
.get(name)
.and_then(|given| given.excerpt)
.is_some_and(|excerpt| {
excerpt.message == turnframe_core::understanding::MessageRef::Current
&& self.plan.words.from <= excerpt.words.first
&& excerpt.words.last <= self.plan.words.to
});
let holds_nothing = (!copied && *judged == ArgumentVerdict::TooMuch)
|| (deduced && *judged == ArgumentVerdict::NotStated)
|| (corrected && *judged == ArgumentVerdict::Different);
if holds_nothing {
*judged = ArgumentVerdict::Stated;
}
}
verdict
}
}
fn candidate_target(candidate: Candidate<'_>) -> ActTarget {
match candidate {
Candidate::Record(record) => ActTarget::Record {
token: record.token.clone(),
},
Candidate::SameTurn { act, .. } => ActTarget::SameTurn { act },
}
}