1use std::collections::{btree_map::Entry, BTreeMap};
4
5use serde::{Deserialize, Serialize};
6
7pub const MODEL_LOGITS_OBSERVATION_PATH: &str = "model.logits";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "dtype", content = "values", rename_all = "snake_case")]
21pub enum TensorObservationData {
22 F32(Vec<f32>),
24 I64(Vec<i64>),
26 U64(Vec<u64>),
28 Bool(Vec<bool>),
30}
31
32impl TensorObservationData {
33 pub fn len(&self) -> usize {
35 match self {
36 Self::F32(values) => values.len(),
37 Self::I64(values) => values.len(),
38 Self::U64(values) => values.len(),
39 Self::Bool(values) => values.len(),
40 }
41 }
42
43 pub fn is_empty(&self) -> bool {
45 self.len() == 0
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct TensorObservation {
52 shape: Vec<usize>,
53 data: TensorObservationData,
54}
55
56impl TensorObservation {
57 pub fn new(shape: Vec<usize>, data: TensorObservationData) -> Result<Self, ObservationError> {
59 let elements = shape.iter().try_fold(1usize, |count, dimension| {
60 count
61 .checked_mul(*dimension)
62 .ok_or(ObservationError::ShapeOverflow)
63 })?;
64 if elements != data.len() {
65 return Err(ObservationError::ElementCount {
66 shape,
67 expected: elements,
68 actual: data.len(),
69 });
70 }
71 Ok(Self { shape, data })
72 }
73
74 pub fn shape(&self) -> &[usize] {
76 &self.shape
77 }
78
79 pub const fn data(&self) -> &TensorObservationData {
81 &self.data
82 }
83
84 pub fn into_parts(self) -> (Vec<usize>, TensorObservationData) {
86 (self.shape, self.data)
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
93pub enum ObservationValue {
94 Tensor(TensorObservation),
96 Float(f64),
98 Integer(i64),
100 Unsigned(u64),
102 Boolean(bool),
104 Text(String),
106}
107
108#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
110pub struct ObservationSet {
111 values: BTreeMap<String, ObservationValue>,
112}
113
114impl ObservationSet {
115 pub const fn new() -> Self {
117 Self {
118 values: BTreeMap::new(),
119 }
120 }
121
122 pub fn insert(
124 &mut self,
125 path: impl Into<String>,
126 value: ObservationValue,
127 ) -> Result<(), ObservationError> {
128 let path = path.into();
129 if path.is_empty() {
130 return Err(ObservationError::EmptyPath);
131 }
132 match self.values.entry(path) {
133 Entry::Vacant(entry) => {
134 entry.insert(value);
135 }
136 Entry::Occupied(entry) => {
137 return Err(ObservationError::DuplicatePath(entry.key().clone()));
138 }
139 }
140 Ok(())
141 }
142
143 pub fn get(&self, path: &str) -> Option<&ObservationValue> {
145 self.values.get(path)
146 }
147
148 pub fn iter(&self) -> impl Iterator<Item = (&str, &ObservationValue)> {
150 self.values
151 .iter()
152 .map(|(path, value)| (path.as_str(), value))
153 }
154
155 pub fn len(&self) -> usize {
157 self.values.len()
158 }
159
160 pub fn is_empty(&self) -> bool {
162 self.values.is_empty()
163 }
164
165 pub fn prefixed(self, prefix: &str) -> Result<Self, ObservationError> {
167 if prefix.is_empty() {
168 return Ok(self);
169 }
170 let mut output = Self::new();
171 for (path, value) in self.values {
172 output.insert(format!("{prefix}.{path}"), value)?;
173 }
174 Ok(output)
175 }
176
177 pub fn extend(&mut self, other: Self) -> Result<(), ObservationError> {
179 for (path, value) in other.values {
180 self.insert(path, value)?;
181 }
182 Ok(())
183 }
184}
185
186#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
188#[serde(tag = "match", content = "path", rename_all = "snake_case")]
189pub enum ObservationSelector {
190 Exact(String),
192 Prefix(String),
194}
195
196impl ObservationSelector {
197 pub fn matches(&self, path: &str) -> bool {
199 match self {
200 Self::Exact(expected) => path == expected,
201 Self::Prefix(prefix) => {
202 path == prefix
203 || path
204 .strip_prefix(prefix)
205 .is_some_and(|suffix| suffix.starts_with('.'))
206 }
207 }
208 }
209}
210
211#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
213pub struct ObservationRequest {
214 selectors: Vec<ObservationSelector>,
215}
216
217impl ObservationRequest {
218 pub const fn all() -> Self {
220 Self {
221 selectors: Vec::new(),
222 }
223 }
224
225 pub fn selected(selectors: impl IntoIterator<Item = ObservationSelector>) -> Self {
227 Self {
228 selectors: selectors.into_iter().collect(),
229 }
230 }
231
232 pub fn matches(&self, path: &str) -> bool {
234 self.selectors.is_empty() || self.selectors.iter().any(|selector| selector.matches(path))
235 }
236
237 pub fn selectors(&self) -> &[ObservationSelector] {
239 &self.selectors
240 }
241}
242
243#[derive(Debug)]
245pub struct InspectedOutput<O> {
246 pub output: O,
248 pub observations: ObservationSet,
250}
251
252#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
254pub enum ObservationError {
255 #[error("tensor observation shape element count overflowed")]
257 ShapeOverflow,
258 #[error(
260 "tensor observation shape {shape:?} requires {expected} values, but received {actual}"
261 )]
262 ElementCount {
263 shape: Vec<usize>,
265 expected: usize,
267 actual: usize,
269 },
270 #[error("observation path must not be empty")]
272 EmptyPath,
273 #[error("duplicate observation path {0:?}")]
275 DuplicatePath(String),
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn tensor_shape_and_values_must_agree() {
284 let tensor = TensorObservation::new(
285 vec![2, 2],
286 TensorObservationData::F32(vec![1.0, 2.0, 3.0, 4.0]),
287 )
288 .unwrap();
289 assert_eq!(tensor.shape(), [2, 2]);
290 assert!(matches!(tensor.data(), TensorObservationData::F32(_)));
291 assert!(matches!(
292 TensorObservation::new(vec![2], TensorObservationData::I64(vec![1])),
293 Err(ObservationError::ElementCount { .. })
294 ));
295 }
296
297 #[test]
298 fn selectors_and_sets_are_stable_and_collision_safe() {
299 let request = ObservationRequest::selected([
300 ObservationSelector::Exact(MODEL_LOGITS_OBSERVATION_PATH.into()),
301 ObservationSelector::Prefix("model.layers.2".into()),
302 ]);
303 assert_eq!(MODEL_LOGITS_OBSERVATION_PATH, "model.logits");
304 assert!(request.matches(MODEL_LOGITS_OBSERVATION_PATH));
305 assert!(request.matches("model.layers.2.output"));
306 assert!(!request.matches("model.layers.20.output"));
307
308 let mut set = ObservationSet::new();
309 set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(3))
310 .unwrap();
311 assert_eq!(
312 set.insert(MODEL_LOGITS_OBSERVATION_PATH, ObservationValue::Unsigned(4)),
313 Err(ObservationError::DuplicatePath(
314 MODEL_LOGITS_OBSERVATION_PATH.into()
315 ))
316 );
317 }
318}