use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEntry {
pub id: Uuid,
pub title: String,
pub content: String,
pub category: Option<String>,
pub tags: Vec<String>,
pub source: Option<String>,
pub metadata: Metadata,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub access_count: u64,
pub learned_relevance: f32,
pub related_entries: Vec<Uuid>,
}
impl KnowledgeEntry {
pub fn new(title: impl Into<String>, content: impl Into<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
title: title.into(),
content: content.into(),
category: None,
tags: Vec::new(),
source: None,
metadata: Metadata::new(),
created_at: now,
updated_at: now,
access_count: 0,
learned_relevance: 1.0,
related_entries: Vec::new(),
}
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key, value);
self
}
pub fn with_related(mut self, related_id: Uuid) -> Self {
self.related_entries.push(related_id);
self
}
pub fn embedding_text(&self) -> String {
let mut parts = vec![self.title.clone(), self.content.clone()];
if let Some(category) = &self.category {
parts.push(category.clone());
}
if !self.tags.is_empty() {
parts.push(self.tags.join(" "));
}
parts.join(" ")
}
pub fn record_access(&mut self, relevance_boost: f32) {
self.access_count += 1;
self.learned_relevance = f32::midpoint(self.learned_relevance, relevance_boost);
self.updated_at = Utc::now();
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Metadata {
data: HashMap<String, String>,
}
impl Metadata {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.data.insert(key.into(), value.into());
}
pub fn get(&self, key: &str) -> Option<&str> {
self.data.get(key).map(String::as_str)
}
pub fn remove(&mut self, key: &str) -> Option<String> {
self.data.remove(key)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.data.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn len(&self) -> usize {
self.data.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_entry_creation() {
let entry = KnowledgeEntry::new("Test Title", "Test content")
.with_category("Testing")
.with_tags(["rust", "testing"])
.with_source("https://example.com")
.with_metadata("author", "test");
assert_eq!(entry.title, "Test Title");
assert_eq!(entry.content, "Test content");
assert_eq!(entry.category, Some("Testing".to_string()));
assert_eq!(entry.tags, vec!["rust", "testing"]);
assert_eq!(entry.source, Some("https://example.com".to_string()));
assert_eq!(entry.metadata.get("author"), Some("test"));
}
#[test]
fn test_embedding_text() {
let entry = KnowledgeEntry::new("Rust Guide", "A guide to Rust programming")
.with_category("Programming")
.with_tags(["rust", "guide"]);
let text = entry.embedding_text();
assert!(text.contains("Rust Guide"));
assert!(text.contains("A guide to Rust programming"));
assert!(text.contains("Programming"));
assert!(text.contains("rust guide"));
}
#[test]
fn test_access_recording() {
let mut entry = KnowledgeEntry::new("Test", "Content");
let initial_relevance = entry.learned_relevance;
entry.record_access(1.5);
assert_eq!(entry.access_count, 1);
assert!(
(entry.learned_relevance - f32::midpoint(initial_relevance, 1.5)).abs() < f32::EPSILON
);
}
}