Skip to main content

systemprompt_models/brain/
statement.rs

1//! Statement rendering and the identity and statement hashes.
2//!
3//! A template substitutes `{name}` with a field (or `{subject}` with the
4//! subject id) and includes an optional group `{name? text}` only when
5//! `name` is present and non-empty; placeholders inside the group render
6//! as usual. A required placeholder with no value is an error.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::collections::BTreeMap;
12
13use serde_json::{Map, Value};
14use sha2::{Digest, Sha256};
15use systemprompt_identifiers::FactKindId;
16
17use super::kind_decl::SUBJECT_FIELD;
18use super::subject::SubjectKey;
19
20/// Why a template could not be parsed or rendered.
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum StatementError {
23    #[error("statement template has an unbalanced brace at byte {0}")]
24    Unbalanced(usize),
25    #[error("statement placeholder {0:?} is not a field name")]
26    BadPlaceholder(String),
27    #[error("statement field {0:?} has no value")]
28    MissingField(String),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32enum Segment {
33    Text(String),
34    Field(String),
35    Optional { field: String, body: Vec<Self> },
36}
37
38pub fn placeholders(template: &str) -> Result<Vec<String>, StatementError> {
39    let mut names = Vec::new();
40    collect(&parse(template, 0, false)?.0, &mut names);
41    Ok(names)
42}
43
44fn collect(segments: &[Segment], names: &mut Vec<String>) {
45    for segment in segments {
46        match segment {
47            Segment::Text(_) => {},
48            Segment::Field(name) => names.push(name.clone()),
49            Segment::Optional { field, body } => {
50                names.push(field.clone());
51                collect(body, names);
52            },
53        }
54    }
55}
56
57// JSON: a fact's fields are the kind's declared field map.
58pub fn render_statement(
59    template: &str,
60    subject: &SubjectKey,
61    fields: &Map<String, Value>,
62) -> Result<String, StatementError> {
63    let (segments, _) = parse(template, 0, false)?;
64    let mut out = String::new();
65    render_into(&segments, subject, fields, &mut out)?;
66    Ok(out)
67}
68
69// JSON: a fact's fields are the kind's declared field map.
70fn render_into(
71    segments: &[Segment],
72    subject: &SubjectKey,
73    fields: &Map<String, Value>,
74    out: &mut String,
75) -> Result<(), StatementError> {
76    for segment in segments {
77        match segment {
78            Segment::Text(text) => out.push_str(text),
79            Segment::Field(name) => out.push_str(
80                &lookup(name, subject, fields)
81                    .ok_or_else(|| StatementError::MissingField(name.clone()))?,
82            ),
83            Segment::Optional { field, body } => {
84                if lookup(field, subject, fields).is_some() {
85                    render_into(body, subject, fields, out)?;
86                }
87            },
88        }
89    }
90    Ok(())
91}
92
93// JSON: a fact's fields are the kind's declared field map.
94fn lookup(name: &str, subject: &SubjectKey, fields: &Map<String, Value>) -> Option<String> {
95    if name == SUBJECT_FIELD {
96        return Some(subject.id.clone());
97    }
98    match fields.get(name)? {
99        Value::Null => None,
100        Value::String(text) if text.trim().is_empty() => None,
101        Value::String(text) => Some(text.clone()),
102        other => Some(other.to_string()),
103    }
104}
105
106fn parse(
107    template: &str,
108    start: usize,
109    nested: bool,
110) -> Result<(Vec<Segment>, usize), StatementError> {
111    let bytes = template.as_bytes();
112    let mut segments = Vec::new();
113    let mut text = String::new();
114    let mut i = start;
115    while i < bytes.len() {
116        match bytes[i] {
117            b'{' => {
118                if !text.is_empty() {
119                    segments.push(Segment::Text(std::mem::take(&mut text)));
120                }
121                let (segment, next) = parse_placeholder(template, i)?;
122                segments.push(segment);
123                i = next;
124            },
125            b'}' if nested => {
126                if !text.is_empty() {
127                    segments.push(Segment::Text(text));
128                }
129                return Ok((segments, i + 1));
130            },
131            b'}' => return Err(StatementError::Unbalanced(i)),
132            _ => {
133                let ch = template[i..]
134                    .chars()
135                    .next()
136                    .ok_or(StatementError::Unbalanced(i))?;
137                text.push(ch);
138                i += ch.len_utf8();
139            },
140        }
141    }
142    if nested {
143        return Err(StatementError::Unbalanced(start));
144    }
145    if !text.is_empty() {
146        segments.push(Segment::Text(text));
147    }
148    Ok((segments, i))
149}
150
151fn parse_placeholder(template: &str, open: usize) -> Result<(Segment, usize), StatementError> {
152    let rest = &template[open + 1..];
153    let name_len = rest
154        .find(|c: char| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'))
155        .ok_or(StatementError::Unbalanced(open))?;
156    let name = rest[..name_len].to_owned();
157    let after = open + 1 + name_len;
158    match template.as_bytes().get(after) {
159        Some(b'}') if !name.is_empty() => Ok((Segment::Field(name), after + 1)),
160        Some(b'?') if !name.is_empty() => {
161            let (body, next) = parse(template, after + 1, true)?;
162            Ok((Segment::Optional { field: name, body }, next))
163        },
164        _ => Err(StatementError::BadPlaceholder(
165            template[open..=after].to_owned(),
166        )),
167    }
168}
169
170#[must_use]
171// JSON: a fact's fields are the kind's declared field map.
172pub fn identity_hash(
173    kind: &FactKindId,
174    subject: &SubjectKey,
175    identity: &[String],
176    fields: &Map<String, Value>,
177) -> String {
178    let mut parts: BTreeMap<&str, String> = BTreeMap::new();
179    for name in identity {
180        let value = if name == SUBJECT_FIELD {
181            subject.to_string()
182        } else {
183            fields
184                .get(name)
185                .map_or_else(String::new, |value| match value {
186                    Value::String(text) => text.clone(),
187                    Value::Null => String::new(),
188                    other => other.to_string(),
189                })
190        };
191        parts.insert(name, normalise(&value));
192    }
193    let mut hasher = Sha256::new();
194    hasher.update(kind.as_str().as_bytes());
195    for (name, value) in parts {
196        hasher.update([0u8]);
197        hasher.update(name.as_bytes());
198        hasher.update([0u8]);
199        hasher.update(value.as_bytes());
200    }
201    hex::encode(hasher.finalize())
202}
203
204#[must_use]
205pub fn statement_hash(statement: &str) -> String {
206    hex::encode(Sha256::digest(normalise(statement).as_bytes()))
207}
208
209fn normalise(value: &str) -> String {
210    value
211        .split_whitespace()
212        .collect::<Vec<_>>()
213        .join(" ")
214        .to_lowercase()
215}