Skip to main content

converge_knowledge/core/
entry.rs

1//! Knowledge entry types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8/// A single entry in the knowledge base.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct KnowledgeEntry {
11    /// Unique identifier for the entry.
12    pub id: Uuid,
13
14    /// Title or short summary.
15    pub title: String,
16
17    /// Full content of the entry.
18    pub content: String,
19
20    /// Category or type classification.
21    pub category: Option<String>,
22
23    /// Tags for categorization.
24    pub tags: Vec<String>,
25
26    /// Source URL or reference.
27    pub source: Option<String>,
28
29    /// Custom metadata.
30    pub metadata: Metadata,
31
32    /// Creation timestamp.
33    pub created_at: DateTime<Utc>,
34
35    /// Last update timestamp.
36    pub updated_at: DateTime<Utc>,
37
38    /// Access count for learning.
39    pub access_count: u64,
40
41    /// Relevance score from learning.
42    pub learned_relevance: f32,
43
44    /// Related entry IDs (knowledge graph).
45    pub related_entries: Vec<Uuid>,
46}
47
48impl KnowledgeEntry {
49    /// Create a new knowledge entry.
50    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    /// Set the category.
69    pub fn with_category(mut self, category: impl Into<String>) -> Self {
70        self.category = Some(category.into());
71        self
72    }
73
74    /// Add tags.
75    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    /// Set the source.
81    pub fn with_source(mut self, source: impl Into<String>) -> Self {
82        self.source = Some(source.into());
83        self
84    }
85
86    /// Add metadata.
87    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    /// Add a related entry.
93    pub fn with_related(mut self, related_id: Uuid) -> Self {
94        self.related_entries.push(related_id);
95        self
96    }
97
98    /// Get the combined text for embedding.
99    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    /// Record an access and update relevance.
114    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/// Custom metadata for knowledge entries.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct Metadata {
124    data: HashMap<String, String>,
125}
126
127impl Metadata {
128    /// Create empty metadata.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Insert a key-value pair.
134    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
135        self.data.insert(key.into(), value.into());
136    }
137
138    /// Get a value by key.
139    pub fn get(&self, key: &str) -> Option<&str> {
140        self.data.get(key).map(String::as_str)
141    }
142
143    /// Remove a key.
144    pub fn remove(&mut self, key: &str) -> Option<String> {
145        self.data.remove(key)
146    }
147
148    /// Iterate over all key-value pairs.
149    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    /// Check if empty.
154    pub fn is_empty(&self) -> bool {
155        self.data.is_empty()
156    }
157
158    /// Get the number of entries.
159    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}