agent_office/services/kb/
domain.rs1use crate::domain::{Node, NodeId, Properties, PropertyValue, Timestamp};
2use chrono::Utc;
3use serde::{Deserialize, Serialize};
4
5pub type NoteId = NodeId;
6pub type TagId = NodeId;
7pub type AgentId = String;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
12pub struct LuhmannId {
13 pub parts: Vec<LuhmannPart>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub enum LuhmannPart {
18 Number(u32),
19 Letter(char),
20}
21
22impl LuhmannId {
23 pub fn parse(s: &str) -> Option<Self> {
25 let mut parts = Vec::new();
26 let mut chars = s.chars().peekable();
27
28 while let Some(&c) = chars.peek() {
29 if c.is_ascii_digit() {
30 let mut num_str = String::new();
32 while let Some(&ch) = chars.peek() {
33 if ch.is_ascii_digit() {
34 num_str.push(ch);
35 chars.next();
36 } else {
37 break;
38 }
39 }
40 if let Ok(n) = num_str.parse::<u32>() {
41 parts.push(LuhmannPart::Number(n));
42 }
43 } else if c.is_ascii_alphabetic() {
44 parts.push(LuhmannPart::Letter(c.to_ascii_lowercase()));
46 chars.next();
47 } else {
48 chars.next(); }
50 }
51
52 if parts.is_empty() {
53 None
54 } else {
55 Some(Self { parts })
56 }
57 }
58
59 pub fn parent(&self) -> Option<Self> {
61 if self.parts.len() <= 1 {
62 None
63 } else {
64 Some(Self {
65 parts: self.parts[..self.parts.len() - 1].to_vec(),
66 })
67 }
68 }
69
70 pub fn next_sibling(&self) -> Option<Self> {
72 if let Some(last) = self.parts.last() {
73 let mut new_parts = self.parts.clone();
74 match last {
75 LuhmannPart::Number(n) => {
76 new_parts.pop();
77 new_parts.push(LuhmannPart::Number(n + 1));
78 }
79 LuhmannPart::Letter(c) => {
80 if let Some(next_char) = (*c as u8 + 1).try_into().ok() {
81 if next_char <= 'z' {
82 new_parts.pop();
83 new_parts.push(LuhmannPart::Letter(next_char));
84 } else {
85 return None; }
87 }
88 }
89 }
90 Some(Self { parts: new_parts })
91 } else {
92 None
93 }
94 }
95
96 pub fn first_child(&self) -> Self {
98 let mut new_parts = self.parts.clone();
99 match self.parts.last() {
101 Some(LuhmannPart::Number(_)) => {
102 new_parts.push(LuhmannPart::Letter('a'));
103 }
104 Some(LuhmannPart::Letter(_)) | None => {
105 new_parts.push(LuhmannPart::Number(1));
106 }
107 }
108 Self { parts: new_parts }
109 }
110
111 pub fn insert_between(&self, next: &Self) -> Option<Self> {
113 if self.parent() != next.parent() {
115 return None;
116 }
117
118 let mut new_parts = self.parts.clone();
120 match self.parts.last() {
121 Some(LuhmannPart::Number(_)) => {
122 new_parts.push(LuhmannPart::Letter('a'));
123 }
124 Some(LuhmannPart::Letter(_)) => {
125 new_parts.push(LuhmannPart::Number(1));
126 }
127 None => {}
128 }
129 Some(Self { parts: new_parts })
130 }
131
132 pub fn level(&self) -> usize {
134 self.parts.len()
135 }
136
137 pub fn is_descendant_of(&self, other: &Self) -> bool {
139 if other.parts.len() >= self.parts.len() {
140 return false;
141 }
142 self.parts[..other.parts.len()] == other.parts[..]
143 }
144}
145
146impl std::fmt::Display for LuhmannId {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 for part in &self.parts {
149 match part {
150 LuhmannPart::Number(n) => write!(f, "{}", n)?,
151 LuhmannPart::Letter(c) => write!(f, "{}", c)?,
152 }
153 }
154 Ok(())
155 }
156}
157
158impl std::str::FromStr for LuhmannId {
159 type Err = String;
160
161 fn from_str(s: &str) -> Result<Self, Self::Err> {
162 Self::parse(s).ok_or_else(|| format!("Invalid Luhmann ID: {}", s))
163 }
164}
165
166#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
169#[serde(rename_all = "snake_case")]
170pub enum LinkType {
171 References,
173}
174
175impl LinkType {
176 pub fn as_str(&self) -> &'static str {
177 "references"
178 }
179
180 pub fn from_str(s: &str) -> Option<Self> {
181 match s {
182 "references" => Some(LinkType::References),
183 _ => None,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct Note {
191 pub id: NoteId,
192 pub luhmann_id: Option<LuhmannId>, pub title: String,
194 pub content: String,
195 pub created_by: AgentId,
196 pub tags: Vec<String>,
197 pub created_at: Timestamp,
198 pub updated_at: Timestamp,
199}
200
201impl Note {
202 pub fn new(created_by: AgentId, title: impl Into<String>, content: impl Into<String>) -> Self {
203 let now = Utc::now();
204 Self {
205 id: NoteId::new_v4(),
206 luhmann_id: None,
207 title: title.into(),
208 content: content.into(),
209 created_by,
210 tags: Vec::new(),
211 created_at: now,
212 updated_at: now,
213 }
214 }
215
216 pub fn with_luhmann_id(mut self, luhmann_id: LuhmannId) -> Self {
217 self.luhmann_id = Some(luhmann_id);
218 self
219 }
220
221 pub fn to_node(&self) -> Node {
222 let mut props = Properties::new();
223 props.insert(
224 "title".to_string(),
225 PropertyValue::String(self.title.clone()),
226 );
227 props.insert(
228 "content".to_string(),
229 PropertyValue::String(self.content.clone()),
230 );
231 props.insert(
232 "created_by".to_string(),
233 PropertyValue::String(self.created_by.to_string()),
234 );
235 if let Some(ref lid) = self.luhmann_id {
236 props.insert(
237 "luhmann_id".to_string(),
238 PropertyValue::String(lid.to_string()),
239 );
240 }
241 props.insert(
242 "tags".to_string(),
243 PropertyValue::List(
244 self.tags
245 .iter()
246 .map(|t| PropertyValue::String(t.clone()))
247 .collect(),
248 ),
249 );
250
251 let mut node = Node::new("note", props);
252 node.id = self.id;
253 node.created_at = self.created_at;
254 node.updated_at = self.updated_at;
255 node
256 }
257
258 pub fn from_node(node: &Node) -> Option<Self> {
259 if node.node_type != "note" {
260 return None;
261 }
262
263 let title = node.get_property("title")?.as_str()?.to_string();
264 let content = node.get_property("content")?.as_str()?.to_string();
265
266 let created_by = node
267 .get_property("created_by")
268 .and_then(|v| v.as_str())
269 .map(|s| s.to_string())?;
270
271 let luhmann_id = node
272 .get_property("luhmann_id")
273 .and_then(|v| v.as_str())
274 .and_then(|s| LuhmannId::parse(s));
275
276 let tags = node
277 .get_property("tags")
278 .and_then(|v| match v {
279 PropertyValue::List(list) => Some(
280 list.iter()
281 .filter_map(|item| item.as_str().map(String::from))
282 .collect(),
283 ),
284 _ => None,
285 })
286 .unwrap_or_default();
287
288 Some(Self {
289 id: node.id,
290 luhmann_id,
291 title,
292 content,
293 created_by,
294 tags,
295 created_at: node.created_at,
296 updated_at: node.updated_at,
297 })
298 }
299
300 pub fn add_tag(&mut self, tag: impl Into<String>) {
301 let tag = tag.into();
302 if !self.tags.contains(&tag) {
303 self.tags.push(tag);
304 }
305 self.updated_at = Utc::now();
306 }
307
308 pub fn remove_tag(&mut self, tag: &str) {
309 self.tags.retain(|t| t != tag);
310 self.updated_at = Utc::now();
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
316pub struct NoteLink {
317 pub from_note_id: NoteId,
318 pub to_note_id: NoteId,
319 pub link_type: LinkType,
320 pub context: Option<String>,
321}
322
323impl NoteLink {
324 pub fn new(
325 from_note_id: NoteId,
326 to_note_id: NoteId,
327 link_type: LinkType,
328 context: Option<String>,
329 ) -> Self {
330 Self {
331 from_note_id,
332 to_note_id,
333 link_type,
334 context,
335 }
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct KnowledgeBase {
342 pub agent_id: AgentId,
343 pub name: String,
344 pub description: Option<String>,
345 pub created_at: Timestamp,
346 pub next_main_id: u32, }
348
349impl KnowledgeBase {
350 pub fn new(agent_id: AgentId, name: impl Into<String>) -> Self {
351 Self {
352 agent_id,
353 name: name.into(),
354 description: None,
355 created_at: Utc::now(),
356 next_main_id: 1,
357 }
358 }
359
360 pub fn to_node(&self) -> Node {
361 let mut props = Properties::new();
362 props.insert("name".to_string(), PropertyValue::String(self.name.clone()));
363 if let Some(desc) = &self.description {
364 props.insert(
365 "description".to_string(),
366 PropertyValue::String(desc.clone()),
367 );
368 }
369 props.insert(
370 "next_main_id".to_string(),
371 PropertyValue::Integer(self.next_main_id as i64),
372 );
373
374 let mut node = Node::new("knowledge_base", props);
375 node.id = crate::domain::string_to_node_id(&self.agent_id);
377 node.created_at = self.created_at;
378 node
379 }
380
381 pub fn from_node(node: &Node) -> Option<Self> {
382 if node.node_type != "knowledge_base" {
383 return None;
384 }
385
386 let name = node.get_property("name")?.as_str()?.to_string();
387 let description = node
388 .get_property("description")
389 .and_then(|v| v.as_str())
390 .map(String::from);
391 let next_main_id = node
392 .get_property("next_main_id")
393 .and_then(|v| match v {
394 PropertyValue::Integer(n) => Some(*n as u32),
395 _ => Some(1),
396 })
397 .unwrap_or(1);
398
399 let agent_id = node
401 .get_property("agent_id")
402 .and_then(|v| v.as_str())
403 .map(String::from)
404 .unwrap_or_else(|| node.id.to_string());
405
406 Some(Self {
407 agent_id,
408 name,
409 description,
410 created_at: node.created_at,
411 next_main_id,
412 })
413 }
414
415 pub fn next_main_topic_id(&mut self) -> LuhmannId {
417 let id = LuhmannId {
418 parts: vec![LuhmannPart::Number(self.next_main_id)],
419 };
420 self.next_main_id += 1;
421 id
422 }
423}
424
425#[derive(Debug, Clone)]
427pub struct GraphPath {
428 pub start_note_id: NoteId,
429 pub end_note_id: NoteId,
430 pub path: Vec<(NoteId, LinkType)>,
431 pub distance: usize,
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_luhmann_id_parsing() {
440 let id = LuhmannId::parse("1a2b").unwrap();
441 assert_eq!(id.parts.len(), 4);
442 assert!(matches!(id.parts[0], LuhmannPart::Number(1)));
443 assert!(matches!(id.parts[1], LuhmannPart::Letter('a')));
444 assert!(matches!(id.parts[2], LuhmannPart::Number(2)));
445 assert!(matches!(id.parts[3], LuhmannPart::Letter('b')));
446 }
447
448 #[test]
449 fn test_luhmann_id_display() {
450 let id = LuhmannId::parse("1a2").unwrap();
451 assert_eq!(id.to_string(), "1a2");
452 }
453
454 #[test]
455 fn test_luhmann_parent() {
456 let id = LuhmannId::parse("1a2").unwrap();
457 let parent = id.parent().unwrap();
458 assert_eq!(parent.to_string(), "1a");
459 }
460
461 #[test]
462 fn test_luhmann_next_sibling() {
463 let id = LuhmannId::parse("1a").unwrap();
464 let next = id.next_sibling().unwrap();
465 assert_eq!(next.to_string(), "1b");
466
467 let id2 = LuhmannId::parse("1").unwrap();
468 let next2 = id2.next_sibling().unwrap();
469 assert_eq!(next2.to_string(), "2");
470 }
471
472 #[test]
473 fn test_luhmann_first_child() {
474 let id = LuhmannId::parse("1").unwrap();
475 let child = id.first_child();
476 assert_eq!(child.to_string(), "1a");
477
478 let id2 = LuhmannId::parse("1a").unwrap();
479 let child2 = id2.first_child();
480 assert_eq!(child2.to_string(), "1a1");
481 }
482}