converge_knowledge/core/
entry.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct KnowledgeEntry {
11 pub id: Uuid,
13
14 pub title: String,
16
17 pub content: String,
19
20 pub category: Option<String>,
22
23 pub tags: Vec<String>,
25
26 pub source: Option<String>,
28
29 pub metadata: Metadata,
31
32 pub created_at: DateTime<Utc>,
34
35 pub updated_at: DateTime<Utc>,
37
38 pub access_count: u64,
40
41 pub learned_relevance: f32,
43
44 pub related_entries: Vec<Uuid>,
46}
47
48impl KnowledgeEntry {
49 pub fn new(title: impl Into<String>, content: impl Into<String>) -> Self {
51 let now = Utc::now();
52 Self {
53 id: Uuid::new_v4(),
54 title: title.into(),
55 content: content.into(),
56 category: None,
57 tags: Vec::new(),
58 source: None,
59 metadata: Metadata::new(),
60 created_at: now,
61 updated_at: now,
62 access_count: 0,
63 learned_relevance: 1.0,
64 related_entries: Vec::new(),
65 }
66 }
67
68 pub fn with_category(mut self, category: impl Into<String>) -> Self {
70 self.category = Some(category.into());
71 self
72 }
73
74 pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
76 self.tags = tags.into_iter().map(Into::into).collect();
77 self
78 }
79
80 pub fn with_source(mut self, source: impl Into<String>) -> Self {
82 self.source = Some(source.into());
83 self
84 }
85
86 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88 self.metadata.insert(key, value);
89 self
90 }
91
92 pub fn with_related(mut self, related_id: Uuid) -> Self {
94 self.related_entries.push(related_id);
95 self
96 }
97
98 pub fn embedding_text(&self) -> String {
100 let mut parts = vec![self.title.clone(), self.content.clone()];
101
102 if let Some(category) = &self.category {
103 parts.push(category.clone());
104 }
105
106 if !self.tags.is_empty() {
107 parts.push(self.tags.join(" "));
108 }
109
110 parts.join(" ")
111 }
112
113 pub fn record_access(&mut self, relevance_boost: f32) {
115 self.access_count += 1;
116 self.learned_relevance = (self.learned_relevance + relevance_boost) / 2.0;
117 self.updated_at = Utc::now();
118 }
119}
120
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct Metadata {
124 data: HashMap<String, String>,
125}
126
127impl Metadata {
128 pub fn new() -> Self {
130 Self::default()
131 }
132
133 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
135 self.data.insert(key.into(), value.into());
136 }
137
138 pub fn get(&self, key: &str) -> Option<&str> {
140 self.data.get(key).map(String::as_str)
141 }
142
143 pub fn remove(&mut self, key: &str) -> Option<String> {
145 self.data.remove(key)
146 }
147
148 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
150 self.data.iter().map(|(k, v)| (k.as_str(), v.as_str()))
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.data.is_empty()
156 }
157
158 pub fn len(&self) -> usize {
160 self.data.len()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_entry_creation() {
170 let entry = KnowledgeEntry::new("Test Title", "Test content")
171 .with_category("Testing")
172 .with_tags(["rust", "testing"])
173 .with_source("https://example.com")
174 .with_metadata("author", "test");
175
176 assert_eq!(entry.title, "Test Title");
177 assert_eq!(entry.content, "Test content");
178 assert_eq!(entry.category, Some("Testing".to_string()));
179 assert_eq!(entry.tags, vec!["rust", "testing"]);
180 assert_eq!(entry.source, Some("https://example.com".to_string()));
181 assert_eq!(entry.metadata.get("author"), Some("test"));
182 }
183
184 #[test]
185 fn test_embedding_text() {
186 let entry = KnowledgeEntry::new("Rust Guide", "A guide to Rust programming")
187 .with_category("Programming")
188 .with_tags(["rust", "guide"]);
189
190 let text = entry.embedding_text();
191 assert!(text.contains("Rust Guide"));
192 assert!(text.contains("A guide to Rust programming"));
193 assert!(text.contains("Programming"));
194 assert!(text.contains("rust guide"));
195 }
196
197 #[test]
198 fn test_access_recording() {
199 let mut entry = KnowledgeEntry::new("Test", "Content");
200 let initial_relevance = entry.learned_relevance;
201
202 entry.record_access(1.5);
203
204 assert_eq!(entry.access_count, 1);
205 assert!((entry.learned_relevance - (initial_relevance + 1.5) / 2.0).abs() < f32::EPSILON);
206 }
207}