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 insert_between(&self, next: &Self) -> Option<Self> {
112 if self.parent() != next.parent() {
114 return None;
115 }
116
117 let mut new_parts = self.parts.clone();
119 match self.parts.last() {
120 Some(LuhmannPart::Number(_)) => {
121 new_parts.push(LuhmannPart::Letter('a'));
122 }
123 Some(LuhmannPart::Letter(_)) => {
124 new_parts.push(LuhmannPart::Number(1));
125 }
126 None => {}
127 }
128 Some(Self { parts: new_parts })
129 }
130
131 pub fn level(&self) -> usize {
133 self.parts.len()
134 }
135
136 pub fn is_descendant_of(&self, other: &Self) -> bool {
138 if other.parts.len() >= self.parts.len() {
139 return false;
140 }
141 self.parts[..other.parts.len()] == other.parts[..]
142 }
143}
144
145impl std::fmt::Display for LuhmannId {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 for part in &self.parts {
148 match part {
149 LuhmannPart::Number(n) => write!(f, "{}", n)?,
150 LuhmannPart::Letter(c) => write!(f, "{}", c)?,
151 }
152 }
153 Ok(())
154 }
155}
156
157impl std::str::FromStr for LuhmannId {
158 type Err = String;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 Self::parse(s).ok_or_else(|| format!("Invalid Luhmann ID: {}", s))
162 }
163}
164
165#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
168#[serde(rename_all = "snake_case")]
169pub enum LinkType {
170 References,
172}
173
174impl LinkType {
175 pub fn as_str(&self) -> &'static str {
176 "references"
177 }
178
179 pub fn from_str(s: &str) -> Option<Self> {
180 match s {
181 "references" => Some(LinkType::References),
182 _ => None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct Note {
191 pub id: NoteId, pub title: String,
193 pub content: String,
194 pub tags: Vec<String>,
195 pub agent_id: Option<String>, pub created_at: Timestamp,
197 pub updated_at: Timestamp,
198}
199
200impl Note {
201 pub fn new(id: LuhmannId, title: impl Into<String>, content: impl Into<String>) -> Self {
202 let now = Utc::now();
203 Self {
204 id,
205 title: title.into(),
206 content: content.into(),
207 tags: Vec::new(),
208 agent_id: None,
209 created_at: now,
210 updated_at: now,
211 }
212 }
213
214 pub fn new_with_agent(
215 id: LuhmannId,
216 title: impl Into<String>,
217 content: impl Into<String>,
218 agent_id: impl Into<String>,
219 ) -> Self {
220 let mut note = Self::new(id, title, content);
221 note.agent_id = Some(agent_id.into());
222 note
223 }
224
225 pub fn to_node(&self) -> Node {
226 let mut props = Properties::new();
227 props.insert(
228 "title".to_string(),
229 PropertyValue::String(self.title.clone()),
230 );
231 props.insert(
232 "content".to_string(),
233 PropertyValue::String(self.content.clone()),
234 );
235 props.insert(
237 "luhmann_id".to_string(),
238 PropertyValue::String(self.id.to_string()),
239 );
240 props.insert(
241 "tags".to_string(),
242 PropertyValue::List(
243 self.tags
244 .iter()
245 .map(|t| PropertyValue::String(t.clone()))
246 .collect(),
247 ),
248 );
249 if let Some(ref agent_id) = self.agent_id {
250 props.insert(
251 "agent_id".to_string(),
252 PropertyValue::String(agent_id.clone()),
253 );
254 }
255
256 let node_id = crate::domain::string_to_node_id(&self.id.to_string());
258 let mut node = Node::new("note", props);
259 node.id = node_id;
260 node.created_at = self.created_at;
261 node.updated_at = self.updated_at;
262 node
263 }
264
265 pub fn from_node(node: &Node) -> Option<Self> {
266 if node.node_type != "note" {
267 return None;
268 }
269
270 let title = node.get_property("title")?.as_str()?.to_string();
271 let content = node.get_property("content")?.as_str()?.to_string();
272
273 let luhmann_id = node
275 .get_property("luhmann_id")
276 .and_then(|v| v.as_str())
277 .and_then(|s| LuhmannId::parse(s))?;
278
279 let tags = node
280 .get_property("tags")
281 .and_then(|v| match v {
282 PropertyValue::List(list) => Some(
283 list.iter()
284 .filter_map(|item| item.as_str().map(String::from))
285 .collect(),
286 ),
287 _ => None,
288 })
289 .unwrap_or_default();
290
291 let agent_id = node
292 .get_property("agent_id")
293 .and_then(|v| v.as_str())
294 .map(String::from);
295
296 Some(Self {
297 id: luhmann_id,
298 title,
299 content,
300 tags,
301 agent_id,
302 created_at: node.created_at,
303 updated_at: node.updated_at,
304 })
305 }
306
307 pub fn add_tag(&mut self, tag: impl Into<String>) {
308 let tag = tag.into();
309 if !self.tags.contains(&tag) {
310 self.tags.push(tag);
311 }
312 self.updated_at = Utc::now();
313 }
314
315 pub fn remove_tag(&mut self, tag: &str) {
316 self.tags.retain(|t| t != tag);
317 self.updated_at = Utc::now();
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
324pub struct NoteLink {
325 pub from_note_id: NoteId,
326 pub to_note_id: NoteId,
327 pub link_type: LinkType,
328 pub context: Option<String>,
329}
330
331impl NoteLink {
332 pub fn new(
333 from_note_id: NoteId,
334 to_note_id: NoteId,
335 link_type: LinkType,
336 context: Option<String>,
337 ) -> Self {
338 Self {
339 from_note_id,
340 to_note_id,
341 link_type,
342 context,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct NoteCounter {
351 pub next_main_id: u32,
352 pub created_at: Timestamp,
353}
354
355impl NoteCounter {
356 pub fn new() -> Self {
357 Self {
358 next_main_id: 1,
359 created_at: Utc::now(),
360 }
361 }
362
363 pub fn to_node(&self) -> Node {
364 let mut props = Properties::new();
365 props.insert(
366 "next_main_id".to_string(),
367 PropertyValue::Integer(self.next_main_id as i64),
368 );
369
370 let mut node = Node::new("note_counter", props);
371 node.id = crate::domain::string_to_node_id("__kb_counter__");
373 node.created_at = self.created_at;
374 node
375 }
376
377 pub fn from_node(node: &Node) -> Option<Self> {
378 if node.node_type != "note_counter" {
379 return None;
380 }
381
382 let next_main_id = node
383 .get_property("next_main_id")
384 .and_then(|v| match v {
385 PropertyValue::Integer(n) => Some(*n as u32),
386 _ => Some(1),
387 })
388 .unwrap_or(1);
389
390 Some(Self {
391 next_main_id,
392 created_at: node.created_at,
393 })
394 }
395
396 pub fn next_main_topic_id(&mut self) -> LuhmannId {
398 let id = LuhmannId {
399 parts: vec![LuhmannPart::Number(self.next_main_id)],
400 };
401 self.next_main_id += 1;
402 id
403 }
404}
405
406#[derive(Debug, Clone)]
408pub struct GraphPath {
409 pub start_note_id: NoteId,
410 pub end_note_id: NoteId,
411 pub path: Vec<(NoteId, LinkType)>,
412 pub distance: usize,
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_luhmann_id_parsing() {
421 let id = LuhmannId::parse("1a2b").unwrap();
422 assert_eq!(id.parts.len(), 4);
423 assert!(matches!(id.parts[0], LuhmannPart::Number(1)));
424 assert!(matches!(id.parts[1], LuhmannPart::Letter('a')));
425 assert!(matches!(id.parts[2], LuhmannPart::Number(2)));
426 assert!(matches!(id.parts[3], LuhmannPart::Letter('b')));
427 }
428
429 #[test]
430 fn test_luhmann_id_display() {
431 let id = LuhmannId::parse("1a2").unwrap();
432 assert_eq!(id.to_string(), "1a2");
433 }
434
435 #[test]
436 fn test_luhmann_parent() {
437 let id = LuhmannId::parse("1a2").unwrap();
438 let parent = id.parent().unwrap();
439 assert_eq!(parent.to_string(), "1a");
440 }
441
442 #[test]
443 fn test_luhmann_next_sibling() {
444 let id = LuhmannId::parse("1a").unwrap();
445 let next = id.next_sibling().unwrap();
446 assert_eq!(next.to_string(), "1b");
447
448 let id2 = LuhmannId::parse("1").unwrap();
449 let next2 = id2.next_sibling().unwrap();
450 assert_eq!(next2.to_string(), "2");
451 }
452
453 #[test]
454 fn test_luhmann_first_child() {
455 let id = LuhmannId::parse("1").unwrap();
456 let child = id.first_child();
457 assert_eq!(child.to_string(), "1a");
458
459 let id2 = LuhmannId::parse("1a").unwrap();
460 let child2 = id2.first_child();
461 assert_eq!(child2.to_string(), "1a1");
462 }
463}