agent_office/services/kb/
domain.rs1use crate::domain::{Node, Properties, PropertyValue, Timestamp};
2use chrono::Utc;
3use serde::{Deserialize, Serialize};
4
5pub type NoteId = LuhmannId;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
11pub struct LuhmannId {
12 pub parts: Vec<LuhmannPart>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub enum LuhmannPart {
17 Number(u32),
18 Letter(char),
19}
20
21impl LuhmannId {
22 pub fn parse(s: &str) -> Option<Self> {
24 let mut parts = Vec::new();
25 let mut chars = s.chars().peekable();
26
27 while let Some(&c) = chars.peek() {
28 if c.is_ascii_digit() {
29 let mut num_str = String::new();
31 while let Some(&ch) = chars.peek() {
32 if ch.is_ascii_digit() {
33 num_str.push(ch);
34 chars.next();
35 } else {
36 break;
37 }
38 }
39 if let Ok(n) = num_str.parse::<u32>() {
40 parts.push(LuhmannPart::Number(n));
41 }
42 } else if c.is_ascii_alphabetic() {
43 parts.push(LuhmannPart::Letter(c.to_ascii_lowercase()));
45 chars.next();
46 } else {
47 chars.next(); }
49 }
50
51 if parts.is_empty() {
52 None
53 } else {
54 Some(Self { parts })
55 }
56 }
57
58 pub fn parent(&self) -> Option<Self> {
60 if self.parts.len() <= 1 {
61 None
62 } else {
63 Some(Self {
64 parts: self.parts[..self.parts.len() - 1].to_vec(),
65 })
66 }
67 }
68
69 pub fn next_sibling(&self) -> Option<Self> {
71 if let Some(last) = self.parts.last() {
72 let mut new_parts = self.parts.clone();
73 match last {
74 LuhmannPart::Number(n) => {
75 new_parts.pop();
76 new_parts.push(LuhmannPart::Number(n + 1));
77 }
78 LuhmannPart::Letter(c) => {
79 if let Some(next_char) = (*c as u8 + 1).try_into().ok() {
80 if next_char <= 'z' {
81 new_parts.pop();
82 new_parts.push(LuhmannPart::Letter(next_char));
83 } else {
84 return None; }
86 }
87 }
88 }
89 Some(Self { parts: new_parts })
90 } else {
91 None
92 }
93 }
94
95 pub fn first_child(&self) -> Self {
97 let mut new_parts = self.parts.clone();
98 match self.parts.last() {
100 Some(LuhmannPart::Number(_)) => {
101 new_parts.push(LuhmannPart::Letter('a'));
102 }
103 Some(LuhmannPart::Letter(_)) | None => {
104 new_parts.push(LuhmannPart::Number(1));
105 }
106 }
107 Self { parts: new_parts }
108 }
109
110 pub fn level(&self) -> usize {
112 self.parts.len()
113 }
114
115 pub fn is_descendant_of(&self, other: &Self) -> bool {
117 if other.parts.len() >= self.parts.len() {
118 return false;
119 }
120 self.parts[..other.parts.len()] == other.parts[..]
121 }
122}
123
124impl std::fmt::Display for LuhmannId {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 for part in &self.parts {
127 match part {
128 LuhmannPart::Number(n) => write!(f, "{}", n)?,
129 LuhmannPart::Letter(c) => write!(f, "{}", c)?,
130 }
131 }
132 Ok(())
133 }
134}
135
136impl std::str::FromStr for LuhmannId {
137 type Err = String;
138
139 fn from_str(s: &str) -> Result<Self, Self::Err> {
140 Self::parse(s).ok_or_else(|| format!("Invalid Luhmann ID: {}", s))
141 }
142}
143
144#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
147#[serde(rename_all = "snake_case")]
148pub enum LinkType {
149 References,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156pub struct Note {
157 pub id: NoteId, pub title: String,
159 pub content: String,
160 pub tags: Vec<String>,
161 pub agent_id: Option<String>, pub created_at: Timestamp,
163 pub updated_at: Timestamp,
164}
165
166impl Note {
167 pub fn new(id: LuhmannId, title: impl Into<String>, content: impl Into<String>) -> Self {
168 let now = Utc::now();
169 Self {
170 id,
171 title: title.into(),
172 content: content.into(),
173 tags: Vec::new(),
174 agent_id: None,
175 created_at: now,
176 updated_at: now,
177 }
178 }
179
180 pub fn to_node(&self) -> Node {
181 let mut props = Properties::new();
182 props.insert(
183 "title".to_string(),
184 PropertyValue::String(self.title.clone()),
185 );
186 props.insert(
187 "content".to_string(),
188 PropertyValue::String(self.content.clone()),
189 );
190 props.insert(
192 "luhmann_id".to_string(),
193 PropertyValue::String(self.id.to_string()),
194 );
195 props.insert(
196 "tags".to_string(),
197 PropertyValue::List(
198 self.tags
199 .iter()
200 .map(|t| PropertyValue::String(t.clone()))
201 .collect(),
202 ),
203 );
204 if let Some(ref agent_id) = self.agent_id {
205 props.insert(
206 "agent_id".to_string(),
207 PropertyValue::String(agent_id.clone()),
208 );
209 }
210
211 let node_id = crate::domain::string_to_node_id(&self.id.to_string());
213 let mut node = Node::new("note", props);
214 node.id = node_id;
215 node.created_at = self.created_at;
216 node.updated_at = self.updated_at;
217 node
218 }
219
220 pub fn from_node(node: &Node) -> Option<Self> {
221 if node.node_type != "note" {
222 return None;
223 }
224
225 let title = node.get_property("title")?.as_str()?.to_string();
226 let content = node.get_property("content")?.as_str()?.to_string();
227
228 let luhmann_id = node
230 .get_property("luhmann_id")
231 .and_then(|v| v.as_str())
232 .and_then(|s| LuhmannId::parse(s))?;
233
234 let tags = node
235 .get_property("tags")
236 .and_then(|v| match v {
237 PropertyValue::List(list) => Some(
238 list.iter()
239 .filter_map(|item| item.as_str().map(String::from))
240 .collect(),
241 ),
242 _ => None,
243 })
244 .unwrap_or_default();
245
246 let agent_id = node
247 .get_property("agent_id")
248 .and_then(|v| v.as_str())
249 .map(String::from);
250
251 Some(Self {
252 id: luhmann_id,
253 title,
254 content,
255 tags,
256 agent_id,
257 created_at: node.created_at,
258 updated_at: node.updated_at,
259 })
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
266pub struct NoteLink {
267 pub from_note_id: NoteId,
268 pub to_note_id: NoteId,
269 pub link_type: LinkType,
270 pub context: Option<String>,
271}
272
273impl NoteLink {
274 pub fn new(
275 from_note_id: NoteId,
276 to_note_id: NoteId,
277 link_type: LinkType,
278 context: Option<String>,
279 ) -> Self {
280 Self {
281 from_note_id,
282 to_note_id,
283 link_type,
284 context,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct NoteCounter {
293 pub next_main_id: u32,
294 pub created_at: Timestamp,
295}
296
297impl NoteCounter {
298 pub fn new() -> Self {
299 Self {
300 next_main_id: 1,
301 created_at: Utc::now(),
302 }
303 }
304
305 pub fn to_node(&self) -> Node {
306 let mut props = Properties::new();
307 props.insert(
308 "next_main_id".to_string(),
309 PropertyValue::Integer(self.next_main_id as i64),
310 );
311
312 let mut node = Node::new("note_counter", props);
313 node.id = crate::domain::string_to_node_id("__kb_counter__");
315 node.created_at = self.created_at;
316 node
317 }
318
319 pub fn from_node(node: &Node) -> Option<Self> {
320 if node.node_type != "note_counter" {
321 return None;
322 }
323
324 let next_main_id = node
325 .get_property("next_main_id")
326 .and_then(|v| match v {
327 PropertyValue::Integer(n) => Some(*n as u32),
328 _ => Some(1),
329 })
330 .unwrap_or(1);
331
332 Some(Self {
333 next_main_id,
334 created_at: node.created_at,
335 })
336 }
337
338 pub fn next_main_topic_id(&mut self) -> LuhmannId {
340 let id = LuhmannId {
341 parts: vec![LuhmannPart::Number(self.next_main_id)],
342 };
343 self.next_main_id += 1;
344 id
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_luhmann_id_parsing() {
354 let id = LuhmannId::parse("1a2b").unwrap();
355 assert_eq!(id.parts.len(), 4);
356 assert!(matches!(id.parts[0], LuhmannPart::Number(1)));
357 assert!(matches!(id.parts[1], LuhmannPart::Letter('a')));
358 assert!(matches!(id.parts[2], LuhmannPart::Number(2)));
359 assert!(matches!(id.parts[3], LuhmannPart::Letter('b')));
360 }
361
362 #[test]
363 fn test_luhmann_id_display() {
364 let id = LuhmannId::parse("1a2").unwrap();
365 assert_eq!(id.to_string(), "1a2");
366 }
367
368 #[test]
369 fn test_luhmann_parent() {
370 let id = LuhmannId::parse("1a2").unwrap();
371 let parent = id.parent().unwrap();
372 assert_eq!(parent.to_string(), "1a");
373 }
374
375 #[test]
376 fn test_luhmann_next_sibling() {
377 let id = LuhmannId::parse("1a").unwrap();
378 let next = id.next_sibling().unwrap();
379 assert_eq!(next.to_string(), "1b");
380
381 let id2 = LuhmannId::parse("1").unwrap();
382 let next2 = id2.next_sibling().unwrap();
383 assert_eq!(next2.to_string(), "2");
384 }
385
386 #[test]
387 fn test_luhmann_first_child() {
388 let id = LuhmannId::parse("1").unwrap();
389 let child = id.first_child();
390 assert_eq!(child.to_string(), "1a");
391
392 let id2 = LuhmannId::parse("1a").unwrap();
393 let child2 = id2.first_child();
394 assert_eq!(child2.to_string(), "1a1");
395 }
396}