systemprompt_models/brain/
statement.rs1use 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#[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
57pub 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
69fn 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
93fn 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]
171pub 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}