Skip to main content

formal_ai/
engine.rs

1//! Deterministic symbolic engine and Links-Notation knowledge dataset.
2//!
3//! This module hosts the deterministic answer projection primitives used by the
4//! universal solver. Callers should not invoke `FormalAiEngine::answer` to
5//! bypass the solver — it delegates to [`crate::solver::UniversalSolver::solve`]
6//! so every request walks the same 11-step loop documented in `VISION.md`.
7
8pub(crate) use crate::coding::{
9    program_language_by_alias, program_spec, program_template_count, supported_program_languages,
10    supported_program_tasks, ExecutionStatus, ProgramExecution, ProgramSpec, PROGRAM_LANGUAGES,
11    WRITE_PROGRAM_INTENT,
12};
13
14use std::sync::OnceLock;
15
16use serde::{Deserialize, Serialize};
17
18use crate::coding::guidance::{program_explanation_section, program_test_instructions};
19use crate::engine_assistant_name::{
20    assistant_name_answer, chinese_assistant_name_answer, hindi_assistant_name_answer,
21    russian_assistant_name_answer, ASSISTANT_NAME_EXAMPLES,
22};
23pub(crate) use crate::engine_responses::{
24    assistant_free_time_answer, chinese_unknown_answer, farewell_answer, greeting_answer,
25    hindi_unknown_answer, identity_answer, russian_unknown_answer, unknown_answer,
26    unknown_language_fallback_answer, wellbeing_answer,
27};
28use crate::engine_responses::{
29    chinese_assistant_free_time_answer, chinese_courtesy_response_answer, chinese_farewell_answer,
30    chinese_greeting_answer, chinese_identity_answer, chinese_test_status_answer,
31    chinese_wellbeing_answer, courtesy_response_answer, hindi_assistant_free_time_answer,
32    hindi_courtesy_response_answer, hindi_farewell_answer, hindi_greeting_answer,
33    hindi_identity_answer, hindi_test_status_answer, hindi_wellbeing_answer,
34    russian_assistant_free_time_answer, russian_courtesy_response_answer, russian_farewell_answer,
35    russian_greeting_answer, russian_identity_answer, russian_test_status_answer,
36    russian_wellbeing_answer, test_status_answer, ASSISTANT_FREE_TIME_EXAMPLES,
37    COURTESY_RESPONSE_EXAMPLES, GREETING_EXAMPLES, IDENTITY_EXAMPLES, TEST_STATUS_EXAMPLES,
38    UNKNOWN_EXAMPLES,
39};
40use crate::event_log::EventLog;
41use crate::language::Language;
42use crate::links_format::{flatten_lino_value, format_lino_record};
43use crate::seed;
44
45pub const DEFAULT_MODEL: &str = "formal-ai";
46
47// Thinking model + deterministic naturalizer live in `crate::thinking` (issue #488),
48// re-exported so `crate::engine::{...}` / `formal_ai::{...}` paths stay unchanged.
49pub use crate::thinking::{
50    humanize_meta_identifier, naturalize_thinking_step, render_thinking_steps,
51    thinking_language_label, thinking_narrative, ThinkingStep,
52};
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct SymbolicAnswer {
56    pub intent: String,
57    pub answer: String,
58    pub confidence: f32,
59    pub evidence_links: Vec<String>,
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub thinking_steps: Vec<ThinkingStep>,
62    pub links_notation: String,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub execution_recipe: Option<Box<ExecutionRecipe>>,
65}
66
67/// A code artifact whose side effects belong to the requesting agentic client.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ExecutionRecipe {
70    pub language: String,
71    pub source: String,
72    pub path: String,
73    pub commands: Vec<String>,
74}
75
76#[derive(Debug, Default, Clone, Copy)]
77pub struct FormalAiEngine;
78
79impl FormalAiEngine {
80    /// Answer a prompt by running it through the universal solver loop.
81    #[must_use]
82    pub fn answer(&self, prompt: &str) -> SymbolicAnswer {
83        crate::solver::UniversalSolver::default().solve(prompt)
84    }
85}
86
87pub const KNOWLEDGE_SCHEMA_VERSION: &str = "0.2.0";
88
89#[must_use]
90pub fn knowledge_links_notation() -> String {
91    let mut records = vec![
92        format_lino_record(
93            "formal_ai_knowledge",
94            &[
95                ("model", String::from(DEFAULT_MODEL)),
96                ("schema_version", String::from(KNOWLEDGE_SCHEMA_VERSION)),
97                ("dataset_version", String::from(KNOWLEDGE_SCHEMA_VERSION)),
98                (
99                    "policy",
100                    String::from("deterministic symbolic rules; no neural network inference"),
101                ),
102                (
103                    "format",
104                    String::from(
105                        "untyped indented Links Notation via lino-objects-codec format helpers",
106                    ),
107                ),
108                ("rule_count", (1 + 7).to_string()),
109            ],
110        ),
111        format_concept_index_record(),
112        format_type_system_record(),
113        format_doublet_reduction_record(),
114        crate::skill_compiler::natural_language_skill_compiler_record(),
115    ];
116    records.extend(
117        crate::associative_package::default_associative_packages()
118            .into_iter()
119            .map(|package| package.links_notation()),
120    );
121    records.extend([
122        format_lino_record(
123            "rule_greeting",
124            &[
125                ("intent", String::from("greeting")),
126                ("response_link", String::from("response:greeting")),
127                ("answer", String::from(greeting_answer())),
128                ("examples", GREETING_EXAMPLES.join(", ")),
129                ("source", String::from("local symbolic seed set")),
130            ],
131        ),
132        format_lino_record(
133            "rule_courtesy_response",
134            &[
135                ("intent", String::from("courtesy_response")),
136                ("response_link", String::from("response:courtesy_response")),
137                ("answer", String::from(courtesy_response_answer())),
138                ("examples", COURTESY_RESPONSE_EXAMPLES.join(", ")),
139                ("source", String::from("local symbolic seed set")),
140            ],
141        ),
142        format_lino_record(
143            "rule_assistant_free_time",
144            &[
145                ("intent", String::from("assistant_free_time")),
146                (
147                    "response_link",
148                    String::from("response:assistant_free_time"),
149                ),
150                ("answer", String::from(assistant_free_time_answer())),
151                ("examples", ASSISTANT_FREE_TIME_EXAMPLES.join(", ")),
152                ("source", String::from("local symbolic seed set")),
153            ],
154        ),
155        format_lino_record(
156            "rule_identity",
157            &[
158                ("intent", String::from("identity")),
159                ("response_link", String::from("response:identity")),
160                ("answer", String::from(identity_answer())),
161                ("examples", IDENTITY_EXAMPLES.join(", ")),
162                ("source", String::from("local symbolic seed set")),
163            ],
164        ),
165        format_lino_record(
166            "rule_assistant_name",
167            &[
168                ("intent", String::from("assistant_name")),
169                ("response_link", String::from("response:assistant_name")),
170                ("answer", String::from(assistant_name_answer())),
171                ("examples", ASSISTANT_NAME_EXAMPLES.join(", ")),
172                ("source", String::from("local symbolic seed set")),
173            ],
174        ),
175        format_lino_record(
176            "rule_test_status",
177            &[
178                ("intent", String::from("test_status")),
179                ("response_link", String::from("response:test_status")),
180                ("answer", String::from(test_status_answer())),
181                ("examples", TEST_STATUS_EXAMPLES.join(", ")),
182                ("source", String::from("local symbolic seed set")),
183            ],
184        ),
185    ]);
186
187    records.push(format_write_program_rule_record());
188    records.push(format_lino_record(
189        "rule_unknown",
190        &[
191            ("intent", String::from("unknown")),
192            ("response_link", String::from("response:unknown")),
193            ("answer", String::from(unknown_answer())),
194            ("examples", UNKNOWN_EXAMPLES.join(", ")),
195            ("source", String::from("fallback symbolic rule")),
196        ],
197    ));
198
199    records.join("\n\n")
200}
201
202/// Concept index: every named intent is declared once here so consumers can
203/// reference concepts by id instead of duplicating them in every rule.
204/// The literal `intent: <name>` lines are valid Links Notation values and
205/// satisfy the uniqueness invariant from `REQUIREMENTS.md`.
206fn format_concept_index_record() -> String {
207    format_lino_record(
208        "concept_index",
209        &[
210            ("greeting", String::from("intent: greeting")),
211            ("test_status", String::from("intent: test_status")),
212            (
213                "courtesy_response",
214                String::from("intent: courtesy_response"),
215            ),
216            (
217                "assistant_free_time",
218                String::from("intent: assistant_free_time"),
219            ),
220            ("identity", String::from("intent: identity")),
221            ("assistant_name", String::from("intent: assistant_name")),
222            ("write_program", String::from("intent: write_program")),
223            ("translation", String::from("intent: translation")),
224            ("algorithm", String::from("intent: algorithm")),
225            ("meta_explanation", String::from("intent: meta_explanation")),
226            ("unknown", String::from("intent: unknown")),
227        ],
228    )
229}
230
231/// Dynamic type system: every value belongs to a Type → `SubType` → Value
232/// chain so the network can grow new categories without schema migrations.
233fn format_type_system_record() -> String {
234    format_lino_record(
235        "type_system",
236        &[
237            ("Type", String::from("Concept")),
238            ("SubType_intent", String::from("Concept -> Intent")),
239            ("SubType_language", String::from("Concept -> Language")),
240            ("SubType_source", String::from("Concept -> Source")),
241            ("SubType_program", String::from("Concept -> Program")),
242            ("SubType_meaning", String::from("Concept -> Meaning")),
243            ("SubType_trace", String::from("Concept -> Trace")),
244            (
245                "SubType_skill_package",
246                String::from("Concept -> CompiledSkillPackage"),
247            ),
248            (
249                "Value_example",
250                String::from("Type Concept; SubType Intent; Value greeting"),
251            ),
252        ],
253    )
254}
255
256/// Doublet reduction: every record can be projected to {from -> to} pairs.
257/// This declaration is what makes the higher-level Links Notation reducible
258/// to doublet links per `VISION.md`.
259fn format_doublet_reduction_record() -> String {
260    format_lino_record(
261        "doublet_reduction",
262        &[
263            ("doublets", String::from("from -> to pairs")),
264            ("from", String::from("any node id")),
265            ("to", String::from("any node id")),
266            (
267                "invariant",
268                String::from("every record reduces to doublets"),
269            ),
270        ],
271    )
272}
273
274#[must_use]
275pub fn estimate_tokens(text: &str) -> u32 {
276    u32::try_from(text.chars().count()).unwrap_or(u32::MAX)
277}
278
279/// A single node in the network-visualization graph.
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct GraphNode {
282    pub id: String,
283    pub label: String,
284    pub links_notation: String,
285}
286
287/// A doublet-link edge between two nodes.
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct GraphEdge {
290    pub from: String,
291    pub to: String,
292    pub role: String,
293}
294
295/// The graph projection of the engine's knowledge dataset, served from
296/// `/v1/graph`.
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
298pub struct KnowledgeGraph {
299    pub nodes: Vec<GraphNode>,
300    pub edges: Vec<GraphEdge>,
301}
302
303fn known_intent_slugs() -> &'static [String] {
304    static CELL: OnceLock<Vec<String>> = OnceLock::new();
305    CELL.get_or_init(|| {
306        seed::intent_routing()
307            .intents
308            .into_iter()
309            .filter_map(|route| {
310                if route.slug.is_empty() {
311                    None
312                } else {
313                    Some(route.slug)
314                }
315            })
316            .collect()
317    })
318    .as_slice()
319}
320
321fn trace_prefixes() -> &'static [String] {
322    static CELL: OnceLock<Vec<String>> = OnceLock::new();
323    CELL.get_or_init(|| seed::intent_routing().trace_prefixes)
324        .as_slice()
325}
326
327#[must_use]
328pub fn is_known_trace_id(trace: &str) -> bool {
329    if trace_prefixes()
330        .iter()
331        .any(|prefix| trace.starts_with(prefix.as_str()))
332    {
333        return true;
334    }
335    known_intent_slugs()
336        .iter()
337        .any(|slug| trace.eq_ignore_ascii_case(slug) || trace.contains(slug.as_str()))
338}
339
340#[must_use]
341pub fn knowledge_graph() -> KnowledgeGraph {
342    let mut nodes = vec![
343        GraphNode {
344            id: String::from("formal_ai_knowledge"),
345            label: String::from("formal-ai knowledge root"),
346            links_notation: String::from("formal_ai_knowledge"),
347        },
348        GraphNode {
349            id: String::from("rule_greeting"),
350            label: String::from("Greeting rule"),
351            links_notation: format!("rule_greeting answer={}", greeting_answer()),
352        },
353        GraphNode {
354            id: String::from("rule_identity"),
355            label: String::from("Identity rule"),
356            links_notation: format!("rule_identity answer={}", identity_answer()),
357        },
358        GraphNode {
359            id: String::from("rule_assistant_name"),
360            label: String::from("Assistant name rule"),
361            links_notation: format!("rule_assistant_name answer={}", assistant_name_answer()),
362        },
363        GraphNode {
364            id: String::from("rule_courtesy_response"),
365            label: String::from("Courtesy response rule"),
366            links_notation: format!(
367                "rule_courtesy_response answer={}",
368                courtesy_response_answer()
369            ),
370        },
371        GraphNode {
372            id: String::from("rule_assistant_free_time"),
373            label: String::from("Assistant free-time rule"),
374            links_notation: format!(
375                "rule_assistant_free_time answer={}",
376                assistant_free_time_answer()
377            ),
378        },
379        GraphNode {
380            id: String::from("rule_test_status"),
381            label: String::from("Test status rule"),
382            links_notation: format!("rule_test_status answer={}", test_status_answer()),
383        },
384        GraphNode {
385            id: String::from("rule_unknown"),
386            label: String::from("Unknown fallback rule"),
387            links_notation: format!("rule_unknown answer={}", unknown_answer()),
388        },
389    ];
390    let mut edges = vec![
391        GraphEdge {
392            from: String::from("formal_ai_knowledge"),
393            to: String::from("rule_greeting"),
394            role: String::from("contains"),
395        },
396        GraphEdge {
397            from: String::from("formal_ai_knowledge"),
398            to: String::from("rule_identity"),
399            role: String::from("contains"),
400        },
401        GraphEdge {
402            from: String::from("formal_ai_knowledge"),
403            to: String::from("rule_assistant_name"),
404            role: String::from("contains"),
405        },
406        GraphEdge {
407            from: String::from("formal_ai_knowledge"),
408            to: String::from("rule_courtesy_response"),
409            role: String::from("contains"),
410        },
411        GraphEdge {
412            from: String::from("formal_ai_knowledge"),
413            to: String::from("rule_assistant_free_time"),
414            role: String::from("contains"),
415        },
416        GraphEdge {
417            from: String::from("formal_ai_knowledge"),
418            to: String::from("rule_test_status"),
419            role: String::from("contains"),
420        },
421        GraphEdge {
422            from: String::from("formal_ai_knowledge"),
423            to: String::from("rule_unknown"),
424            role: String::from("contains"),
425        },
426        GraphEdge {
427            from: String::from("rule_greeting"),
428            to: String::from("response:greeting"),
429            role: String::from("response_link"),
430        },
431        GraphEdge {
432            from: String::from("rule_identity"),
433            to: String::from("response:identity"),
434            role: String::from("response_link"),
435        },
436        GraphEdge {
437            from: String::from("rule_assistant_name"),
438            to: String::from("response:assistant_name"),
439            role: String::from("response_link"),
440        },
441        GraphEdge {
442            from: String::from("rule_courtesy_response"),
443            to: String::from("response:courtesy_response"),
444            role: String::from("response_link"),
445        },
446        GraphEdge {
447            from: String::from("rule_assistant_free_time"),
448            to: String::from("response:assistant_free_time"),
449            role: String::from("response_link"),
450        },
451        GraphEdge {
452            from: String::from("rule_test_status"),
453            to: String::from("response:test_status"),
454            role: String::from("response_link"),
455        },
456    ];
457    nodes.push(GraphNode {
458        id: String::from("rule_write_program"),
459        label: String::from("Write-program rule"),
460        links_notation: format!(
461            "rule_write_program parameters=language,task languages={} tasks={}",
462            supported_program_languages(),
463            supported_program_tasks()
464        ),
465    });
466    edges.push(GraphEdge {
467        from: String::from("formal_ai_knowledge"),
468        to: String::from("rule_write_program"),
469        role: String::from("contains"),
470    });
471    edges.push(GraphEdge {
472        from: String::from("rule_write_program"),
473        to: String::from("response:write_program"),
474        role: String::from("response_link"),
475    });
476    let (package_nodes, package_edges) =
477        crate::associative_package::default_package_graph_projection();
478    nodes.extend(package_nodes);
479    edges.extend(package_edges);
480    KnowledgeGraph { nodes, edges }
481}
482
483#[must_use]
484pub fn knowledge_graph_dot() -> String {
485    use std::fmt::Write as _;
486    let graph = knowledge_graph();
487    let mut dot = String::from("digraph formal_ai_knowledge {\n");
488    for node in &graph.nodes {
489        let _ = writeln!(dot, "  \"{}\" [label=\"{}\"];", node.id, node.label);
490    }
491    for edge in &graph.edges {
492        let _ = writeln!(
493            dot,
494            "  \"{}\" -> \"{}\" [label=\"{}\"];",
495            edge.from, edge.to, edge.role
496        );
497    }
498    dot.push_str("}\n");
499    dot
500}
501
502#[must_use]
503pub fn stable_id(prefix: &str, text: &str) -> String {
504    crate::web_engine_core::stable_id(prefix, text)
505}
506
507pub(crate) enum SelectedRule {
508    Greeting,
509    Wellbeing,
510    Farewell,
511    TestStatus,
512    CourtesyResponse,
513    AssistantFreeTime,
514    Identity,
515    AssistantName,
516    WriteProgram(ProgramSpec),
517    UnsupportedWriteProgram {
518        task: Option<String>,
519        language: Option<String>,
520    },
521    Unknown,
522}
523
524impl SelectedRule {
525    pub(crate) fn intent(&self) -> String {
526        match self {
527            Self::Greeting => String::from("greeting"),
528            Self::Wellbeing => String::from("wellbeing"),
529            Self::Farewell => String::from("farewell"),
530            Self::TestStatus => String::from("test_status"),
531            Self::CourtesyResponse => String::from("courtesy_response"),
532            Self::AssistantFreeTime => String::from("assistant_free_time"),
533            Self::Identity => String::from("identity"),
534            Self::AssistantName => String::from("assistant_name"),
535            Self::WriteProgram(_) => String::from(WRITE_PROGRAM_INTENT),
536            Self::UnsupportedWriteProgram { .. } => String::from("write_program_unsupported"),
537            Self::Unknown => String::from("unknown"),
538        }
539    }
540
541    pub(crate) fn response_link(&self) -> String {
542        match self {
543            Self::Greeting => String::from("response:greeting"),
544            Self::Wellbeing => String::from("response:wellbeing"),
545            Self::Farewell => String::from("response:farewell"),
546            Self::TestStatus => String::from("response:test_status"),
547            Self::CourtesyResponse => String::from("response:courtesy_response"),
548            Self::AssistantFreeTime => String::from("response:assistant_free_time"),
549            Self::Identity => String::from("response:identity"),
550            Self::AssistantName => String::from("response:assistant_name"),
551            Self::WriteProgram(spec) => spec.response_link(),
552            Self::UnsupportedWriteProgram { .. } => {
553                String::from("response:write_program:unsupported")
554            }
555            Self::Unknown => String::from("response:unknown"),
556        }
557    }
558
559    pub(crate) fn answer(&self) -> String {
560        match self {
561            Self::Greeting => String::from(greeting_answer()),
562            Self::Wellbeing => String::from(wellbeing_answer()),
563            Self::Farewell => String::from(farewell_answer()),
564            Self::TestStatus => String::from(test_status_answer()),
565            Self::CourtesyResponse => String::from(courtesy_response_answer()),
566            Self::AssistantFreeTime => String::from(assistant_free_time_answer()),
567            Self::Identity => String::from(identity_answer()),
568            Self::AssistantName => String::from(assistant_name_answer()),
569            Self::WriteProgram(spec) => write_program_answer(*spec, Language::English, false),
570            Self::UnsupportedWriteProgram { task, language } => unsupported_write_program_answer(
571                task.as_deref(),
572                language.as_deref(),
573                Language::English,
574            ),
575            Self::Unknown => String::from(unknown_answer()),
576        }
577    }
578}
579
580pub(crate) fn language_aware_intent_for(rule: &SelectedRule, _language: Language) -> String {
581    rule.intent()
582}
583
584pub(crate) fn language_aware_answer_for(
585    rule: &SelectedRule,
586    language: Language,
587    prompt: &str,
588    prior_code_response: bool,
589) -> String {
590    match (rule, language) {
591        (SelectedRule::Greeting, Language::Russian) => String::from(russian_greeting_answer()),
592        (SelectedRule::Greeting, Language::Hindi) => String::from(hindi_greeting_answer()),
593        (SelectedRule::Greeting, Language::Chinese) => String::from(chinese_greeting_answer()),
594        (SelectedRule::Wellbeing, Language::Russian) => String::from(russian_wellbeing_answer()),
595        (SelectedRule::Wellbeing, Language::Hindi) => String::from(hindi_wellbeing_answer()),
596        (SelectedRule::Wellbeing, Language::Chinese) => String::from(chinese_wellbeing_answer()),
597        (SelectedRule::Farewell, Language::Russian) => String::from(russian_farewell_answer()),
598        (SelectedRule::Farewell, Language::Hindi) => String::from(hindi_farewell_answer()),
599        (SelectedRule::Farewell, Language::Chinese) => String::from(chinese_farewell_answer()),
600        (SelectedRule::TestStatus, Language::Russian) => String::from(russian_test_status_answer()),
601        (SelectedRule::TestStatus, Language::Hindi) => String::from(hindi_test_status_answer()),
602        (SelectedRule::TestStatus, Language::Chinese) => String::from(chinese_test_status_answer()),
603        (SelectedRule::CourtesyResponse, Language::Russian) => {
604            String::from(russian_courtesy_response_answer())
605        }
606        (SelectedRule::CourtesyResponse, Language::Hindi) => {
607            String::from(hindi_courtesy_response_answer())
608        }
609        (SelectedRule::CourtesyResponse, Language::Chinese) => {
610            String::from(chinese_courtesy_response_answer())
611        }
612        (SelectedRule::AssistantFreeTime, Language::Russian) => {
613            String::from(russian_assistant_free_time_answer())
614        }
615        (SelectedRule::AssistantFreeTime, Language::Hindi) => {
616            String::from(hindi_assistant_free_time_answer())
617        }
618        (SelectedRule::AssistantFreeTime, Language::Chinese) => {
619            String::from(chinese_assistant_free_time_answer())
620        }
621        (SelectedRule::Identity, Language::Russian) => String::from(russian_identity_answer()),
622        (SelectedRule::Identity, Language::Hindi) => String::from(hindi_identity_answer()),
623        (SelectedRule::Identity, Language::Chinese) => String::from(chinese_identity_answer()),
624        (SelectedRule::AssistantName, Language::Russian) => {
625            String::from(russian_assistant_name_answer())
626        }
627        (SelectedRule::AssistantName, Language::Hindi) => {
628            String::from(hindi_assistant_name_answer())
629        }
630        (SelectedRule::AssistantName, Language::Chinese) => {
631            String::from(chinese_assistant_name_answer())
632        }
633        (SelectedRule::WriteProgram(spec), language) => {
634            let answer = write_program_answer(*spec, language, prior_code_response);
635            crate::code_editing::apply_inline_hello_world_output_replacement(prompt, &answer, *spec)
636                .unwrap_or(answer)
637        }
638        (
639            SelectedRule::UnsupportedWriteProgram {
640                task,
641                language: program_language,
642            },
643            language,
644        ) => {
645            unsupported_write_program_answer(task.as_deref(), program_language.as_deref(), language)
646        }
647        (SelectedRule::Unknown, _) => {
648            crate::unknown_opener::language_aware_unknown_answer(prompt, language)
649        }
650        _ => rule.answer(),
651    }
652}
653
654pub(crate) fn response_link_for_intent(rule: &SelectedRule, _intent: &str) -> String {
655    rule.response_link()
656}
657
658/// Match a default hello-world program from the catalog by language alias.
659pub(crate) fn hello_world_program_by_alias(normalized: &str) -> Option<ProgramSpec> {
660    let language = program_language_by_alias(normalized)?;
661    program_spec("hello_world", language.slug)
662}
663
664pub(crate) fn normalize_prompt(prompt: &str) -> String {
665    let canonical: String = prompt
666        .chars()
667        .flat_map(char::to_lowercase)
668        .collect::<String>()
669        .replace("c++", " cpp ")
670        .replace("c#", " csharp ");
671
672    let mut normalized = String::with_capacity(canonical.len());
673    for character in canonical.chars() {
674        if character.is_alphanumeric() || is_script_combining_mark(character) {
675            normalized.push(character);
676        } else {
677            normalized.push(' ');
678        }
679    }
680
681    normalized.split_whitespace().collect::<Vec<_>>().join(" ")
682}
683
684const fn is_script_combining_mark(character: char) -> bool {
685    let codepoint = character as u32;
686    matches!(
687        codepoint,
688        0x0300..=0x036F
689            | 0x0900..=0x094F
690            | 0x0951..=0x0957
691            | 0x0962..=0x0963
692            | 0x0980..=0x09FF
693            | 0x0A00..=0x0A7F
694            | 0x0A80..=0x0AFF
695            | 0x0B00..=0x0B7F
696    )
697}
698
699pub(crate) fn answer_links_notation(
700    prompt: &str,
701    intent: &str,
702    answer: &str,
703    log: &EventLog,
704    trace_id: &str,
705) -> String {
706    let steps = log
707        .events()
708        .iter()
709        .enumerate()
710        .map(|(index, event)| {
711            format!(
712                "step_{index} {} {}",
713                event.kind,
714                flatten_lino_value(&event.payload)
715            )
716        })
717        .collect::<Vec<_>>()
718        .join("; ");
719    let thinking_steps = log
720        .thinking_steps()
721        .iter()
722        .map(|step| {
723            format!(
724                "step_{} {} {} {} {}",
725                step.order,
726                flatten_lino_value(&step.step),
727                flatten_lino_value(&step.level),
728                flatten_lino_value(&step.source_event),
729                flatten_lino_value(&step.detail)
730            )
731        })
732        .collect::<Vec<_>>()
733        .join("; ");
734    format_lino_record(
735        &format!("answer_{}", stable_id("prompt", prompt)),
736        &[
737            ("prompt", String::from(prompt)),
738            ("intent", String::from(intent)),
739            ("answer", String::from(answer)),
740            ("trace", String::from(trace_id)),
741            ("steps", steps),
742            ("thinking_steps", thinking_steps),
743        ],
744    )
745}
746
747fn format_write_program_rule_record() -> String {
748    let sample = program_spec("hello_world", "rust").map_or_else(
749        || String::from("Write-program template catalog is unavailable."),
750        |spec| write_program_answer(spec, Language::English, false),
751    );
752    format_lino_record(
753        "rule_write_program",
754        &[
755            ("intent", String::from(WRITE_PROGRAM_INTENT)),
756            ("parameters", String::from("language, task")),
757            ("languages", supported_program_languages()),
758            ("tasks", supported_program_tasks()),
759            ("template_count", program_template_count().to_string()),
760            ("response_link", String::from("response:write_program")),
761            ("answer", sample),
762            (
763                "examples",
764                String::from(
765                    "Write me hello world program in Rust; Write a Python program that counts to three",
766                ),
767            ),
768            ("source", program_template_sources()),
769        ],
770    )
771}
772
773fn program_template_sources() -> String {
774    let mut sources = Vec::new();
775    for language in PROGRAM_LANGUAGES {
776        if !sources.contains(&language.source) {
777            sources.push(language.source);
778        }
779    }
780    sources.join(", ")
781}
782
783fn write_program_answer(
784    spec: ProgramSpec,
785    language: Language,
786    prior_code_response: bool,
787) -> String {
788    // Issue #324: the natural-language framing around the generated program is
789    // localized to the detected (or preferred) response language so a Russian,
790    // Hindi, or Chinese request no longer receives an all-English reply. The
791    // code itself and the literal shell commands stay in their canonical form
792    // because they are the requested artefact, not prose.
793    //
794    // Issue #330: a code answer must teach a novice — so the program is always
795    // accompanied by a plain-language explanation of *how it works* and
796    // step-by-step instructions for testing it. When the dialog already walked
797    // the user through running code (`prior_code_response`), the verbose setup
798    // steps are omitted and replaced by a short "test it the same way" note so
799    // follow-up edits stay concise.
800    let expected_output = spec.expected_output();
801    format!(
802        "{}\n\n```{}\n{}\n```\n\n{}\n\n{}\n\n{}",
803        write_program_intro(spec.language.name, spec.task.label, language),
804        spec.language.code_fence,
805        spec.template.code,
806        execution_report(&spec.language.execution, &expected_output, language),
807        program_explanation_section(spec, language),
808        program_test_instructions(spec, language, prior_code_response),
809    )
810}
811
812fn write_program_intro(language_name: &str, task_label: &str, language: Language) -> String {
813    match language {
814        Language::Russian => {
815            format!("Вот минимальная программа на языке {language_name} ({task_label}):")
816        }
817        Language::Hindi => {
818            format!("यहाँ {language_name} में एक न्यूनतम प्रोग्राम है ({task_label}):")
819        }
820        Language::Chinese => format!("这是一个最小的 {language_name} 程序({task_label}):"),
821        _ => format!("Here is a minimal {language_name} {task_label} program:"),
822    }
823}
824
825fn unsupported_write_program_answer(
826    task: Option<&str>,
827    language: Option<&str>,
828    response_language: Language,
829) -> String {
830    let task = task.unwrap_or("missing");
831    let language = language.unwrap_or("missing");
832    let languages = supported_program_languages();
833    let tasks = supported_program_tasks();
834    match response_language {
835        Language::Russian => format!(
836            "Я могу выполнить `write_program(language, task)`, но у меня нет шаблона для \
837             языка `{language}` и задачи `{task}`. Поддерживаемые языки: {languages}. \
838             Поддерживаемые задачи: {tasks}."
839        ),
840        Language::Hindi => format!(
841            "मैं `write_program(language, task)` रूट कर सकता हूँ, लेकिन भाषा `{language}` और \
842             कार्य `{task}` के लिए मेरे पास कोई टेम्पलेट नहीं है। समर्थित भाषाएँ: {languages}. \
843             समर्थित कार्य: {tasks}."
844        ),
845        Language::Chinese => format!(
846            "我可以路由 `write_program(language, task)`,但我没有语言 `{language}` 和任务 \
847             `{task}` 的模板。支持的语言:{languages}。支持的任务:{tasks}。"
848        ),
849        _ => format!(
850            "I can route `write_program(language, task)`, but I do not have a template for \
851             language `{language}` and task `{task}`. Supported languages: {languages}. \
852             Supported tasks: {tasks}."
853        ),
854    }
855}
856
857fn execution_report(execution: &ProgramExecution, output: &str, language: Language) -> String {
858    let command_lines = execution_command_lines(execution);
859    let verified = matches!(execution.status, ExecutionStatus::Verified);
860    let status_phrase = execution_status_phrase(execution.status, language);
861    let output_label = execution_output_label(verified, language);
862    let status_line = match language {
863        Language::Russian => format!(
864            "Статус выполнения: {status_phrase} в среде «{}».",
865            execution.environment
866        ),
867        Language::Hindi => format!(
868            "निष्पादन स्थिति: {status_phrase} ({} में)।",
869            execution.environment
870        ),
871        Language::Chinese => {
872            format!("执行状态:{status_phrase}({})。", execution.environment)
873        }
874        _ => format!(
875            "Execution status: {status_phrase} in {}.",
876            execution.environment
877        ),
878    };
879
880    format!(
881        "{status_line}\n{command_lines}\n{output_label}:\n```text\n{output}\n```\n{}",
882        execution.notes
883    )
884}
885
886const fn execution_status_phrase(status: ExecutionStatus, language: Language) -> &'static str {
887    match (status, language) {
888        (ExecutionStatus::Verified, Language::Russian) => "скомпилировано и запущено",
889        (ExecutionStatus::Verified, Language::Hindi) => "संकलित और चलाया गया",
890        (ExecutionStatus::Verified, Language::Chinese) => "已编译并运行",
891        (ExecutionStatus::Unavailable, Language::Russian) => "не скомпилировано и не запущено",
892        (ExecutionStatus::Unavailable, Language::Hindi) => "संकलित या चलाया नहीं गया",
893        (ExecutionStatus::Unavailable, Language::Chinese) => "未编译或运行",
894        (status, _) => status.label(),
895    }
896}
897
898const fn execution_output_label(verified: bool, language: Language) -> &'static str {
899    match (verified, language) {
900        (true, Language::Russian) => "Вывод",
901        (false, Language::Russian) => "Ожидаемый вывод после проверки",
902        (true, Language::Hindi) => "आउटपुट",
903        (false, Language::Hindi) => "सत्यापन के बाद अपेक्षित आउटपुट",
904        (true, Language::Chinese) => "输出",
905        (false, Language::Chinese) => "验证后的预期输出",
906        (true, _) => "Output",
907        (false, _) => "Expected output after verification",
908    }
909}
910
911fn execution_command_lines(execution: &ProgramExecution) -> String {
912    execution.check_command.map_or_else(
913        || format!("Run command: `{}`", execution.run_command),
914        |check_command| {
915            format!(
916                "Check command: `{check_command}`\nRun command: `{}`",
917                execution.run_command
918            )
919        },
920    )
921}