systemprompt_provider_contracts/documents/
projection.rs1use std::collections::BTreeMap;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use systemprompt_identifiers::{ContentHash, DocumentNodeId};
13
14use super::format::DocumentFormat;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum NodeKind {
19 Body,
20 Heading { level: u8 },
21 Paragraph,
22 ListItem,
23 Table,
24 Row,
25 Cell,
26 Sheet,
27 Range,
28 Slide,
29 Shape,
30 Image,
31 Html,
32 Markdown,
33 Text,
34 Pending,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
38pub struct ProjectionNode {
39 pub id: DocumentNodeId,
40 pub kind: NodeKind,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub text: Option<String>,
43 pub hash8: String,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub children: Vec<Self>,
46 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
47 pub attrs: BTreeMap<String, String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
56pub struct Projection {
57 pub format: DocumentFormat,
58 pub root: Vec<ProjectionNode>,
59 pub content_sha256: ContentHash,
60}
61
62#[must_use]
63pub fn hash8(text: &str) -> String {
64 let normalised = text.split_whitespace().collect::<Vec<_>>().join(" ");
65 let digest = Sha256::digest(normalised.as_bytes());
66 hex::encode(&digest[..4])
67}
68
69#[must_use]
70pub fn content_hash(bytes: &[u8]) -> ContentHash {
71 ContentHash::new(hex::encode(Sha256::digest(bytes)))
72}
73
74impl ProjectionNode {
75 #[must_use]
76 pub fn hashline(&self) -> String {
77 format!(
78 "{}:{}| {}",
79 self.id.as_str(),
80 self.hash8,
81 self.text.as_deref().unwrap_or_default()
82 )
83 }
84}
85
86impl Projection {
87 #[must_use]
88 pub fn find(&self, id: &DocumentNodeId) -> Option<&ProjectionNode> {
89 self.nodes().find(|node| &node.id == id)
90 }
91
92 #[must_use]
93 pub fn lines(&self, range: Option<(&DocumentNodeId, &DocumentNodeId)>) -> Vec<String> {
94 let nodes = self.nodes();
95 match range {
96 None => nodes.map(ProjectionNode::hashline).collect(),
97 Some((start, end)) => {
98 let mut lines = Vec::new();
99 let mut inside = false;
100 for node in nodes {
101 inside |= &node.id == start;
102 if inside {
103 lines.push(node.hashline());
104 if &node.id == end {
105 break;
106 }
107 }
108 }
109 lines
110 },
111 }
112 }
113
114 pub fn nodes(&self) -> impl Iterator<Item = &ProjectionNode> {
115 let mut stack: Vec<&ProjectionNode> = self.root.iter().rev().collect();
116 std::iter::from_fn(move || {
117 let node = stack.pop()?;
118 stack.extend(node.children.iter().rev());
119 Some(node)
120 })
121 }
122}