1use std::collections::BTreeMap;
9use std::sync::OnceLock;
10
11use lino_objects_codec::format::escape_reference;
12
13use crate::engine::{
14 normalize_prompt, program_language_by_alias, program_spec, stable_id, SelectedRule,
15 WRITE_PROGRAM_INTENT,
16};
17use crate::event_log::EventLog;
18use crate::link_store::{LinkStore, LinkStoreError};
19use crate::links_format::format_lino_record;
20use crate::memory::MemoryEvent;
21use crate::method_registry::MethodRegistry;
22use crate::probability::ProbabilityStore;
23use crate::seed;
24use crate::solver::{ConversationTurn, UniversalSolver};
25use crate::translation::{FormalizationAnchorKind, FormalizationCandidate, FormalizationRole};
26use crate::{concepts, cue_lexicon};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum IntentKind {
30 Task,
31 Question,
32 Requirement,
33 Statement,
34 Courtesy,
35 Unknown,
36}
37
38impl IntentKind {
39 #[must_use]
40 pub const fn slug(self) -> &'static str {
41 match self {
42 Self::Task => "task",
43 Self::Question => "question",
44 Self::Requirement => "requirement",
45 Self::Statement => "statement",
46 Self::Courtesy => "courtesy",
47 Self::Unknown => "unknown",
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct IntentFormalization {
54 pub impulse_id: String,
55 pub source_text: String,
56 pub normalized_text: String,
57 pub language: String,
58 pub kind: IntentKind,
59 pub knowns: Vec<String>,
60 pub relevants: Vec<String>,
61 pub parameters: BTreeMap<String, String>,
62 pub route: Option<String>,
63 pub response_link: Option<String>,
64}
65
66impl IntentFormalization {
67 #[must_use]
80 pub fn to_links_notation(&self) -> String {
81 let mut pairs = vec![
82 ("impulse_id", self.impulse_id.clone()),
83 ("source_text", self.source_text.clone()),
84 ("normalized_text", self.normalized_text.clone()),
85 ("language", self.language.clone()),
86 ("kind", self.kind.slug().to_owned()),
87 ];
88 if let Some(route) = &self.route {
89 pairs.push(("route", route.clone()));
90 }
91 if let Some(response_link) = &self.response_link {
92 pairs.push(("response_link", response_link.clone()));
93 }
94 for (name, value) in &self.parameters {
95 pairs.push(("parameter", format!("{name}={value}")));
99 }
100 for known in &self.knowns {
101 pairs.push(("known", known.clone()));
102 }
103 for relevant in &self.relevants {
104 pairs.push(("relevant", relevant.clone()));
105 }
106
107 let mut out = format_lino_record(
111 &format!(
112 "intent_formalization {}",
113 escape_reference(&self.impulse_id)
114 ),
115 &pairs,
116 );
117 out.push('\n');
118 out
119 }
120
121 #[must_use]
122 pub fn has_relevant_handler(&self, name: &str) -> bool {
123 self.relevants
124 .iter()
125 .any(|relevant| relevant == &format!("handler:{name}"))
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct IntentFormalizationCacheEntry {
131 pub formalization: IntentFormalization,
132 pub cache_hit: bool,
133}
134
135#[derive(Debug, Default, Clone)]
136pub struct IntentFormalizationCache {
137 records: BTreeMap<String, IntentFormalization>,
138}
139
140impl IntentFormalizationCache {
141 #[must_use]
142 pub const fn new() -> Self {
143 Self {
144 records: BTreeMap::new(),
145 }
146 }
147
148 #[must_use]
149 pub fn len(&self) -> usize {
150 self.records.len()
151 }
152
153 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 self.records.is_empty()
156 }
157
158 #[must_use]
159 pub fn get(&self, prompt: &str) -> Option<&IntentFormalization> {
160 let id = impulse_id_for(prompt);
161 self.records.get(&id)
162 }
163
164 pub fn formalize_or_insert(
165 &mut self,
166 prompt: &str,
167 language: &str,
168 candidate: Option<&FormalizationCandidate>,
169 ) -> IntentFormalizationCacheEntry {
170 let id = impulse_id_for(prompt);
171 if let Some(formalization) = self.records.get(&id) {
172 return IntentFormalizationCacheEntry {
173 formalization: formalization.clone(),
174 cache_hit: true,
175 };
176 }
177
178 let formalization = formalize_intent(prompt, language, candidate);
179 self.records.insert(id, formalization.clone());
180 IntentFormalizationCacheEntry {
181 formalization,
182 cache_hit: false,
183 }
184 }
185
186 pub fn append_to_link_store<S: LinkStore>(
187 &self,
188 store: &mut S,
189 ) -> Result<usize, LinkStoreError> {
190 let mut inserted = 0;
191 for formalization in self.records.values() {
192 store.append_memory_event(MemoryEvent {
193 id: formalization.impulse_id.clone(),
194 kind: Some(String::from("intent_formalization")),
195 content: Some(formalization.to_links_notation()),
196 evidence: vec![format!("intent_formalization:{}", formalization.impulse_id)],
197 ..MemoryEvent::default()
198 })?;
199 inserted += 1;
200 }
201 Ok(inserted)
202 }
203}
204
205impl UniversalSolver {
206 #[must_use]
207 pub fn solve_with_intent_cache(
208 &self,
209 prompt: &str,
210 intent_cache: &mut IntentFormalizationCache,
211 ) -> crate::engine::SymbolicAnswer {
212 self.solve_with_history_probability_store_and_intent_cache(
213 prompt,
214 &[],
215 &ProbabilityStore::new(),
216 intent_cache,
217 )
218 }
219}
220
221#[must_use]
222pub fn impulse_id_for(prompt: &str) -> String {
223 stable_id("impulse", &normalize_prompt(prompt))
224}
225
226#[must_use]
227pub fn formalize_intent(
228 prompt: &str,
229 language: &str,
230 candidate: Option<&FormalizationCandidate>,
231) -> IntentFormalization {
232 let normalized = normalize_prompt(prompt);
233 let route = route_for_prompt(&normalized);
234 let parameters = write_program_parameters(&normalized).unwrap_or_default();
235 let mut knowns = vec![
236 format!("impulse:{}", impulse_id_for(prompt)),
237 format!("language:{language}"),
238 ];
239 let mut relevants = Vec::new();
240
241 if let Some(candidate) = candidate {
242 append_candidate_knowns(candidate, &mut knowns, &mut relevants);
243 }
244 for (name, value) in ¶meters {
245 push_unique(&mut knowns, format!("parameter:{name}:{value}"));
246 }
247 append_prompt_relevants(prompt, &normalized, &mut relevants);
248
249 let route_slug = route
250 .as_ref()
251 .map(|matched| matched.slug.clone())
252 .or_else(|| route_from_relevants(&relevants));
253 if let Some(route_slug) = &route_slug {
254 push_unique(&mut relevants, format!("route:{route_slug}"));
255 if MethodRegistry::from_dispatch()
256 .method_for_route(route_slug)
257 .is_some()
258 {
259 push_unique(&mut relevants, format!("handler:{route_slug}"));
260 }
261 }
262
263 IntentFormalization {
264 impulse_id: impulse_id_for(prompt),
265 source_text: prompt.to_owned(),
266 normalized_text: normalized.clone(),
267 language: language.to_owned(),
268 kind: infer_kind(prompt, &normalized, route_slug.as_deref(), candidate),
269 knowns,
270 relevants,
271 parameters,
272 response_link: route.map(|matched| matched.response_link),
273 route: route_slug,
274 }
275}
276
277pub(crate) fn record_intent_formalization(
278 log: &mut EventLog,
279 entry: &IntentFormalizationCacheEntry,
280) {
281 let formalization = &entry.formalization;
282 let cache_state = if entry.cache_hit { "hit" } else { "miss" };
283 log.append(
284 "intent_formalization_cache",
285 format!("{cache_state} {}", formalization.impulse_id),
286 );
287 if entry.cache_hit {
288 log.append(
289 "cache_hit",
290 format!("intent_formalization:{}", formalization.impulse_id),
291 );
292 } else {
293 log.append("intent_formalization", formalization.to_links_notation());
294 }
295 log.append(
296 "intent_formalization:kind",
297 formalization.kind.slug().to_owned(),
298 );
299 if let Some(route) = &formalization.route {
300 log.append("intent_formalization:route", route.clone());
301 }
302 for relevant in &formalization.relevants {
303 log.append("intent_formalization:relevant", relevant.clone());
304 }
305}
306
307#[must_use]
308pub(crate) fn select_rule_for_intent(intent: &IntentFormalization) -> SelectedRule {
309 match intent.route.as_deref() {
310 Some("greeting") => SelectedRule::Greeting,
311 Some("wellbeing") => SelectedRule::Wellbeing,
312 Some("farewell") => SelectedRule::Farewell,
313 Some("test_status") => SelectedRule::TestStatus,
314 Some("courtesy_response") => SelectedRule::CourtesyResponse,
315 Some("assistant_free_time") => SelectedRule::AssistantFreeTime,
316 Some("assistant_name") => SelectedRule::AssistantName,
317 Some("identity") => SelectedRule::Identity,
318 Some(WRITE_PROGRAM_INTENT) => write_program_rule_for_intent(intent),
319 _ => SelectedRule::Unknown,
320 }
321}
322
323#[derive(Debug, Clone)]
324struct MatchedRoute {
325 slug: String,
326 response_link: String,
327}
328
329fn route_for_prompt(normalized: &str) -> Option<MatchedRoute> {
330 if write_program_parameters(normalized).is_some() {
331 return Some(MatchedRoute {
332 slug: String::from(WRITE_PROGRAM_INTENT),
333 response_link: String::from("response:write_program"),
334 });
335 }
336 seed::intent_routing()
337 .intents
338 .into_iter()
339 .find(|route| matches_route(normalized, route))
340 .map(|route| MatchedRoute {
341 slug: route.slug,
342 response_link: route.response_link,
343 })
344}
345
346fn matches_route(normalized: &str, route: &seed::IntentRoute) -> bool {
347 route.keywords.iter().any(|keyword| normalized == keyword)
348 || route.phrases.iter().any(|phrase| normalized == phrase)
349 || route
350 .tokens
351 .iter()
352 .any(|token| contains_token(normalized, token))
353 || route.combos.iter().any(|combo| {
354 !combo.is_empty() && combo.iter().all(|token| contains_token(normalized, token))
355 })
356}
357
358fn contains_token(normalized: &str, expected: &str) -> bool {
359 if crate::coding::contains_cjk(expected) {
363 return normalized.contains(expected);
364 }
365 normalized.split_whitespace().any(|token| token == expected)
366}
367
368fn write_program_rule_for_intent(intent: &IntentFormalization) -> SelectedRule {
369 let task = intent.parameters.get("task").cloned();
370 let language = intent.parameters.get("language").cloned();
371 if let (Some(task_slug), Some(language_slug)) = (task.as_deref(), language.as_deref()) {
372 if let Some(spec) = program_spec(task_slug, language_slug) {
373 return SelectedRule::WriteProgram(spec);
374 }
375 }
376 SelectedRule::UnsupportedWriteProgram { task, language }
377}
378
379pub(crate) struct WriteProgramRecovery {
382 pub rule: SelectedRule,
386 pub trace: Option<String>,
389 pub plan: Option<String>,
393}
394
395pub(crate) struct ProgramCoreferenceRewrite {
398 pub rule: SelectedRule,
399 pub trace: String,
400}
401
402#[must_use]
410pub(crate) fn rewrite_bare_program_coreference_rule(
411 rule: &SelectedRule,
412 follow_up: &str,
413 history: &[ConversationTurn],
414) -> Option<ProgramCoreferenceRewrite> {
415 if matches!(
416 rule,
417 SelectedRule::WriteProgram(_) | SelectedRule::UnsupportedWriteProgram { .. }
418 ) {
419 return None;
420 }
421
422 let normalized = normalize_prompt(follow_up);
423 if !crate::program_coreference::looks_like_bare_program_artifact_follow_up(&normalized) {
424 return None;
425 }
426
427 let context = active_program_context(history)?;
428 let trace = format!(
429 "referent=active_program_artifact task={} language={}",
430 context.task, context.language
431 );
432 Some(ProgramCoreferenceRewrite {
433 rule: SelectedRule::UnsupportedWriteProgram {
434 task: Some(context.task),
435 language: Some(context.language),
436 },
437 trace,
438 })
439}
440
441pub(crate) struct ActiveProgramContext {
442 pub(crate) task: String,
443 pub(crate) language: String,
444}
445
446pub(crate) fn active_program_context(history: &[ConversationTurn]) -> Option<ActiveProgramContext> {
447 let mut task = None;
448 let mut language = None;
449 for turn in history.iter().rev() {
450 let normalized = normalize_prompt(&turn.content);
451 let Some(parameters) = write_program_parameters(&normalized) else {
452 continue;
453 };
454 if task.is_none() {
455 task = parameters.get("task").cloned();
456 }
457 if language.is_none() {
458 language = parameters.get("language").cloned();
459 }
460 if task.is_some() && language.is_some() {
461 break;
462 }
463 }
464 Some(ActiveProgramContext {
465 task: task?,
466 language: language?,
467 })
468}
469
470#[must_use]
484pub(crate) fn recover_write_program_rule(
485 rule: SelectedRule,
486 follow_up: &str,
487 history: &[ConversationTurn],
488) -> WriteProgramRecovery {
489 let SelectedRule::UnsupportedWriteProgram { task, language } = &rule else {
490 return WriteProgramRecovery {
491 rule,
492 trace: None,
493 plan: None,
494 };
495 };
496
497 let mut recovered_task = task.clone();
498 let mut recovered_language = language.clone();
499
500 if recovered_task.is_none() || recovered_language.is_none() {
501 for turn in history.iter().rev() {
502 let normalized = normalize_prompt(&turn.content);
503 let Some(parameters) = write_program_parameters(&normalized) else {
504 continue;
505 };
506 if recovered_task.is_none() {
507 recovered_task = parameters.get("task").cloned();
508 }
509 if recovered_language.is_none() {
510 recovered_language = parameters.get("language").cloned();
511 }
512 if recovered_task.is_some() && recovered_language.is_some() {
513 break;
514 }
515 }
516 }
517
518 let normalized_follow_up = normalize_prompt(follow_up);
523 let modifiers = detected_program_modifiers(&normalized_follow_up);
524 let mut plan = None;
525 if !modifiers.is_empty() {
526 if let Some(base) = recovered_task.as_deref() {
527 let lowered = crate::program_plan::lower(base, &modifiers);
528 if lowered.was_modified() {
529 plan = Some(lowered.links_notation());
530 }
531 recovered_task = Some(lowered.resolved_task);
532 }
533 }
534
535 if let (Some(task_slug), Some(language_slug)) =
536 (recovered_task.as_deref(), recovered_language.as_deref())
537 {
538 if let Some(spec) = program_spec(task_slug, language_slug) {
539 let trace = format!("write_program task={task_slug} language={language_slug}");
540 return WriteProgramRecovery {
541 rule: SelectedRule::WriteProgram(spec),
542 trace: Some(trace),
543 plan,
544 };
545 }
546 }
547
548 WriteProgramRecovery {
549 rule: SelectedRule::UnsupportedWriteProgram {
550 task: recovered_task,
551 language: recovered_language,
552 },
553 trace: None,
554 plan,
555 }
556}
557
558fn operation_vocabulary() -> &'static seed::OperationVocabulary {
559 static VOCABULARY: OnceLock<seed::OperationVocabulary> = OnceLock::new();
560 VOCABULARY.get_or_init(seed::operation_vocabulary)
561}
562
563pub(crate) fn detected_program_modifiers(normalized: &str) -> Vec<String> {
571 let program_modifiers = crate::program_plan::modifier_slugs();
572 operation_vocabulary()
573 .detect(normalized)
574 .into_iter()
575 .filter(|slug| program_modifiers.contains(slug.as_str()))
576 .collect()
577}
578
579fn write_program_parameters(normalized: &str) -> Option<BTreeMap<String, String>> {
580 let task = crate::coding::program_task_by_alias(normalized);
581 let language = requested_program_language(normalized);
582 let lexicon = crate::seed::lexicon();
589 let mentions_program_request =
590 lexicon.mentions_role(crate::seed::ROLE_PROGRAM_REQUEST, normalized);
591 let asks_for_program = lexicon.mentions_role(crate::seed::ROLE_PROGRAM_KIND, normalized)
592 && mentions_program_request;
593 let asks_for_known_language_program = language
594 .as_deref()
595 .is_some_and(|language| mentions_program_request && known_write_program_language(language));
596 if task.is_none() && !asks_for_program && !asks_for_known_language_program {
597 return None;
598 }
599 let mut parameters = BTreeMap::new();
600 if let Some(task) = task {
601 let modifiers = detected_program_modifiers(normalized);
605 let task_slug = crate::program_plan::resolve_task(task.slug, &modifiers);
606 parameters.insert(String::from("task"), task_slug);
607 }
608 if let Some(language) = language {
609 parameters.insert(String::from("language"), language);
610 }
611 Some(parameters)
612}
613
614fn known_write_program_language(language: &str) -> bool {
615 crate::coding::program_language_by_slug(language).is_some()
616 || crate::knowledge::CodingOracle::knows_language(language)
617}
618
619fn requested_program_language(normalized: &str) -> Option<String> {
620 if let Some(language) = program_language_by_alias(normalized) {
621 return Some(String::from(language.slug));
622 }
623 let lexicon = crate::seed::lexicon();
635 let preposition_surfaces = lexicon.words_for_role_in_languages(
636 crate::seed::ROLE_IMPLEMENTATION_LANGUAGE_PREPOSITION,
637 &["en", "ru"],
638 );
639 let language_noun_surfaces = lexicon.words_for_role_in_languages(
640 crate::seed::ROLE_IMPLEMENTATION_LANGUAGE_NOUN,
641 &["en", "ru"],
642 );
643 let tokens = normalized.split_whitespace().collect::<Vec<_>>();
644 for (index, token) in tokens.iter().enumerate() {
645 if !preposition_surfaces
646 .iter()
647 .any(|surface| surface.as_str() == *token)
648 {
649 continue;
650 }
651 let Some(next) = tokens.get(index + 1) else {
652 continue;
653 };
654 if language_noun_surfaces
655 .iter()
656 .any(|surface| surface.as_str() == *next)
657 {
658 if let Some(after_language_word) = tokens.get(index + 2) {
659 return Some((*after_language_word).to_owned());
660 }
661 continue;
662 }
663 return Some((*next).to_owned());
664 }
665 None
666}
667
668fn append_candidate_knowns(
669 candidate: &FormalizationCandidate,
670 knowns: &mut Vec<String>,
671 relevants: &mut Vec<String>,
672) {
673 for slot in &candidate.slots {
674 push_unique(
675 knowns,
676 slot_known_link(slot.role, slot.anchor.kind, &slot.anchor.id),
677 );
678 if slot.role == FormalizationRole::Predicate && slot.anchor.id == "wikidata:P5972" {
679 push_unique(relevants, String::from("handler:translation"));
680 push_unique(relevants, String::from("route:translation"));
681 }
682 }
683 for term in &candidate.unresolved_terms {
684 push_unique(knowns, format!("formalization_unresolved:{term}"));
685 }
686}
687
688fn slot_known_link(role: FormalizationRole, kind: FormalizationAnchorKind, id: &str) -> String {
689 match (role, kind) {
690 (FormalizationRole::Subject, FormalizationAnchorKind::WikidataItem) => {
691 format!("formalization:subject_q:{id}")
692 }
693 (FormalizationRole::Predicate, FormalizationAnchorKind::WikidataProperty) => {
694 format!("formalization:predicate_p:{id}")
695 }
696 (FormalizationRole::Object, FormalizationAnchorKind::WikidataItem) => {
697 format!("formalization:object_q:{id}")
698 }
699 (_, FormalizationAnchorKind::WikidataItem) => {
700 format!("formalization:item_q:{id}")
701 }
702 (_, FormalizationAnchorKind::WikidataProperty) => {
703 format!("formalization:property_p:{id}")
704 }
705 (
706 _,
707 FormalizationAnchorKind::WikipediaArticle | FormalizationAnchorKind::WiktionaryEntry,
708 ) => {
709 format!("formalization:fallback:{id}")
710 }
711 (_, FormalizationAnchorKind::RawText) => format!("formalization:raw:{id}"),
712 }
713}
714
715fn append_prompt_relevants(prompt: &str, normalized: &str, relevants: &mut Vec<String>) {
716 let lower_prompt = prompt.to_ascii_lowercase();
717 let operation_view = seed::operation_vocabulary().canonicalized_prompt(normalized);
718 let handlers = [
719 (
720 "handler:execution_failure",
721 cue_lexicon::matches("execution_failure_prompt", &lower_prompt)
722 || cue_lexicon::matches("execution_failure_normalized", normalized),
723 ),
724 ("handler:arithmetic", looks_arithmetic(prompt, normalized)),
725 (
726 "handler:web_search",
727 cue_lexicon::matches("web_search", normalized)
728 || looks_like_latest_news_search(normalized)
729 || looks_like_records_information_search(normalized),
730 ),
731 (
732 "handler:procedural_how_to",
733 cue_lexicon::matches("procedural_how_to", normalized)
734 || crate::solver_handler_how::looks_like_procedural_how_to(normalized),
735 ),
736 (
737 "handler:proof_request",
738 cue_lexicon::matches("proof_request", normalized),
739 ),
740 (
741 "handler:write_script",
742 cue_lexicon::matches("write_script", normalized),
743 ),
744 (
745 "handler:write_program",
746 write_program_parameters(normalized).is_some(),
747 ),
748 (
749 "handler:program_synthesis",
750 looks_like_program_synthesis(&operation_view),
751 ),
752 (
753 "handler:text_manipulation",
754 looks_like_text_manipulation(&operation_view),
755 ),
756 (
757 "handler:software_project",
758 cue_lexicon::matches("software_project", normalized),
759 ),
760 (
761 "handler:meta_explanation",
762 seed::lexicon().mentions_role_raw(seed::ROLE_ASSISTANT_MECHANISM_INQUIRY, normalized),
763 ),
764 (
765 "handler:concept_lookup",
766 cue_lexicon::matches("concept_lookup", normalized)
767 || looks_like_single_concept_lookup(prompt),
768 ),
769 ("handler:calendar_create_event", {
770 let lex = seed::lexicon();
771 let has_day_ref = lex
772 .words_for_role(seed::ROLE_CALENDAR_DAY_REFERENCE)
773 .iter()
774 .any(|w| contains_term_for_relevants(normalized, w));
775 let has_digit = normalized.chars().any(|c| c.is_ascii_digit());
776 let has_date_signal = has_day_ref || has_digit;
777 let has_schedule = lex
778 .words_for_role(seed::ROLE_CALENDAR_SCHEDULE_ACTION)
779 .iter()
780 .any(|w| contains_term_for_relevants(normalized, w))
781 || lex
782 .words_for_role(seed::ROLE_CALENDAR_EVENT)
783 .iter()
784 .any(|w| contains_term_for_relevants(normalized, w));
785 let fallback_cue = cue_lexicon::matches("calendar_fallback_verbs", normalized)
792 || (cue_lexicon::matches("calendar_ru_date_marker", normalized)
793 && (normalized.contains("в ") || normalized.contains(':')))
794 || (has_digit && cue_lexicon::matches("calendar_digit_actions", normalized));
795 has_date_signal && (has_schedule || fallback_cue)
796 }),
797 ];
798 for (handler, matches) in handlers {
799 if matches {
800 push_unique(relevants, String::from(handler));
801 }
802 }
803}
804
805fn looks_like_single_concept_lookup(prompt: &str) -> bool {
806 concepts::extract_concept_query(prompt).is_some_and(|query| {
807 !query
808 .term
809 .chars()
810 .any(|character| matches!(character, ',' | ';' | ',' | '、'))
811 })
812}
813
814fn looks_arithmetic(prompt: &str, normalized: &str) -> bool {
815 let raw = prompt.to_ascii_lowercase();
820 raw.chars().any(|c| c.is_ascii_digit())
821 && cue_lexicon::cues("arithmetic_operators")
822 .iter()
823 .any(|operator| raw.contains(operator) || normalized.contains(operator))
824}
825
826fn looks_like_latest_news_search(normalized: &str) -> bool {
827 let padded = format!(" {normalized} ");
828 let lexicon = seed::lexicon();
829 lexicon.mentions_role_raw(seed::ROLE_WEB_SEARCH_NEWS_SUBJECT, &padded)
830 && lexicon.mentions_role_raw(seed::ROLE_WEB_SEARCH_NEWS_RECENCY, &padded)
831}
832
833fn looks_like_records_information_search(normalized: &str) -> bool {
838 let padded = format!(" {normalized} ");
839 let lexicon = seed::lexicon();
840 lexicon.mentions_role_raw(seed::ROLE_WEB_SEARCH_RECORDS_SUBJECT, &padded)
841 && lexicon.mentions_role_raw(seed::ROLE_WEB_SEARCH_TOPIC_MARKER, &padded)
842}
843
844fn looks_like_program_synthesis(normalized: &str) -> bool {
845 let lexicon = crate::seed::lexicon();
850 let similar_elements = lexicon
851 .meaning("signal_similar_elements")
852 .is_some_and(|signal| signal.evidenced_in(normalized));
853 lexicon.mentions_role(crate::seed::ROLE_PROGRAM_SYNTHESIS_SUBJECT, normalized)
854 && (lexicon.mentions_role(crate::seed::ROLE_PROGRAM_SYNTHESIS_DOMAIN, normalized)
855 || similar_elements)
856 && lexicon.mentions_role(crate::seed::ROLE_PROGRAM_SYNTHESIS_ACTION, normalized)
857}
858
859fn looks_like_text_manipulation(normalized: &str) -> bool {
860 cue_lexicon::matches("text_manipulation", normalized)
863}
864
865fn has_any_token(normalized: &str, tokens: &[&str]) -> bool {
866 tokens.iter().any(|token| contains_token(normalized, token))
867}
868
869fn contains_term_for_relevants(haystack: &str, needle: &str) -> bool {
872 if needle.is_empty() {
873 return false;
874 }
875 haystack.contains(needle)
876}
877
878fn route_from_relevants(relevants: &[String]) -> Option<String> {
879 let registry = MethodRegistry::from_dispatch();
880 relevants.iter().find_map(|relevant| {
881 let slug = relevant
882 .strip_prefix("route:")
883 .or_else(|| relevant.strip_prefix("handler:"))
884 .filter(|slug| registry.method_for_route(slug).is_some())?;
885 Some(slug.to_owned())
886 })
887}
888
889fn infer_kind(
890 prompt: &str,
891 normalized: &str,
892 route: Option<&str>,
893 candidate: Option<&FormalizationCandidate>,
894) -> IntentKind {
895 match route {
896 Some("greeting" | "wellbeing" | "farewell" | "courtesy_response") => IntentKind::Courtesy,
897 Some("assistant_name" | "identity") => IntentKind::Question,
898 Some(
899 "translation"
900 | "algorithm"
901 | "write_program"
902 | "text_manipulation"
903 | "software_project_plan"
904 | "software_project_implementation",
905 ) => IntentKind::Task,
906 _ if contains_question_mark(prompt) || starts_with_question_word(normalized) => {
907 IntentKind::Question
908 }
909 _ if has_any_token(normalized, &["must", "should", "require", "requires"]) => {
910 IntentKind::Requirement
911 }
912 _ if has_any_token(
913 normalized,
914 &[
915 "translate",
916 "write",
917 "calculate",
918 "search",
919 "find",
920 "prove",
921 "define",
922 ],
923 ) =>
924 {
925 IntentKind::Task
926 }
927 _ if candidate.is_some_and(|candidate| !candidate.slots.is_empty()) => {
928 IntentKind::Statement
929 }
930 _ => IntentKind::Unknown,
931 }
932}
933
934fn starts_with_question_word(normalized: &str) -> bool {
935 crate::seed::lexicon()
942 .words_for_role_in_languages(crate::seed::ROLE_INTERROGATIVE_OPENER, &["en", "ru"])
943 .iter()
944 .any(|word| {
945 normalized
946 .strip_prefix(word.as_str())
947 .is_some_and(|rest| rest.starts_with(' '))
948 })
949}
950
951fn contains_question_mark(prompt: &str) -> bool {
952 prompt.contains('?') || prompt.contains('?')
953}
954
955fn push_unique(values: &mut Vec<String>, value: String) {
956 if !values.contains(&value) {
957 values.push(value);
958 }
959}