1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3
4use super::types::{KnowledgeFact, ProjectKnowledge};
5use crate::core::memory_boundary::FactPrivacy;
6use crate::core::memory_policy::MemoryPolicy;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ImportMerge {
10 Replace,
11 Append,
12 SkipExisting,
13}
14
15impl ImportMerge {
16 pub fn parse(s: &str) -> Option<Self> {
17 match s.to_lowercase().as_str() {
18 "replace" => Some(Self::Replace),
19 "append" => Some(Self::Append),
20 "skip-existing" | "skip_existing" | "skip" => Some(Self::SkipExisting),
21 _ => None,
22 }
23 }
24}
25
26#[derive(Debug, Clone)]
27pub struct ImportResult {
28 pub added: u32,
29 pub skipped: u32,
30 pub replaced: u32,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SimpleFactEntry {
36 pub category: String,
37 pub key: String,
38 pub value: String,
39 #[serde(default)]
40 pub confidence: Option<f32>,
41 #[serde(default)]
42 pub source: Option<String>,
43 #[serde(default)]
44 pub timestamp: Option<String>,
45}
46
47pub fn parse_import_data(data: &str) -> Result<Vec<KnowledgeFact>, String> {
49 if let Ok(pk) = serde_json::from_str::<ProjectKnowledge>(data) {
50 return Ok(pk.facts);
51 }
52
53 if let Ok(entries) = serde_json::from_str::<Vec<SimpleFactEntry>>(data) {
54 let now = Utc::now();
55 let facts = entries
56 .into_iter()
57 .map(|e| KnowledgeFact {
58 category: e.category,
59 key: e.key,
60 value: e.value,
61 source_session: e.source.unwrap_or_else(|| "import".to_string()),
62 confidence: e.confidence.unwrap_or(0.8),
63 created_at: now,
64 last_confirmed: now,
65 retrieval_count: 0,
66 last_retrieved: None,
67 valid_from: Some(now),
68 valid_until: None,
69 supersedes: None,
70 confirmation_count: 1,
71 feedback_up: 0,
72 feedback_down: 0,
73 last_feedback: None,
74 privacy: FactPrivacy::default(),
75 imported_from: None,
76 })
77 .collect();
78 return Ok(facts);
79 }
80
81 let mut facts = Vec::new();
82 for line in data.lines() {
83 let line = line.trim();
84 if line.is_empty() {
85 continue;
86 }
87 if let Ok(entry) = serde_json::from_str::<SimpleFactEntry>(line) {
88 let now = Utc::now();
89 facts.push(KnowledgeFact {
90 category: entry.category,
91 key: entry.key,
92 value: entry.value,
93 source_session: entry.source.unwrap_or_else(|| "import".to_string()),
94 confidence: entry.confidence.unwrap_or(0.8),
95 created_at: now,
96 last_confirmed: now,
97 retrieval_count: 0,
98 last_retrieved: None,
99 valid_from: Some(now),
100 valid_until: None,
101 supersedes: None,
102 confirmation_count: 1,
103 feedback_up: 0,
104 feedback_down: 0,
105 last_feedback: None,
106 privacy: FactPrivacy::default(),
107 imported_from: None,
108 });
109 } else {
110 return Err(format!(
111 "Invalid JSONL line: {}",
112 &line[..line.len().min(80)]
113 ));
114 }
115 }
116
117 if facts.is_empty() {
118 return Err("No facts found. Expected: native JSON, simple JSON array, or JSONL.".into());
119 }
120 Ok(facts)
121}
122
123fn imported_fact(source: &KnowledgeFact, session_id: &str) -> KnowledgeFact {
124 let now = Utc::now();
125 KnowledgeFact {
126 category: source.category.clone(),
127 key: source.key.clone(),
128 value: source.value.clone(),
129 source_session: session_id.to_string(),
130 confidence: source.confidence,
131 created_at: now,
132 last_confirmed: now,
133 retrieval_count: 0,
134 last_retrieved: None,
135 valid_from: Some(now),
136 valid_until: None,
137 supersedes: None,
138 confirmation_count: 1,
139 feedback_up: 0,
140 feedback_down: 0,
141 last_feedback: None,
142 privacy: source.privacy,
143 imported_from: source.imported_from.clone(),
144 }
145}
146
147impl ProjectKnowledge {
148 pub fn import_facts(
151 &mut self,
152 incoming: Vec<KnowledgeFact>,
153 merge: ImportMerge,
154 session_id: &str,
155 policy: &MemoryPolicy,
156 ) -> ImportResult {
157 let mut added = 0u32;
158 let mut skipped = 0u32;
159 let mut replaced = 0u32;
160
161 for fact in incoming {
162 let existing = self
163 .facts
164 .iter()
165 .position(|f| f.category == fact.category && f.key == fact.key && f.is_current());
166
167 match (&merge, existing) {
168 (ImportMerge::SkipExisting, Some(_)) => {
169 skipped += 1;
170 }
171 (ImportMerge::Replace, Some(idx)) => {
172 self.facts[idx].valid_until = Some(Utc::now());
173 self.facts.push(imported_fact(&fact, session_id));
174 replaced += 1;
175 }
176 (ImportMerge::Append, Some(_)) | (_, None) => {
177 self.facts.push(imported_fact(&fact, session_id));
178 added += 1;
179 }
180 }
181 }
182
183 if added > 0 || replaced > 0 {
184 self.updated_at = Utc::now();
185 if self.facts.len() > policy.knowledge.max_facts.saturating_mul(2) {
186 let _ = self.run_memory_lifecycle(policy);
187 }
188 }
189
190 ImportResult {
191 added,
192 skipped,
193 replaced,
194 }
195 }
196
197 pub fn export_simple(&self) -> Vec<SimpleFactEntry> {
199 self.facts
200 .iter()
201 .filter(|f| f.is_current())
202 .map(|f| SimpleFactEntry {
203 category: f.category.clone(),
204 key: f.key.clone(),
205 value: f.value.clone(),
206 confidence: Some(f.confidence),
207 source: Some(f.source_session.clone()),
208 timestamp: Some(f.created_at.to_rfc3339()),
209 })
210 .collect()
211 }
212}