1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3
4use super::types::{KnowledgeArchetype, 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 archetype: KnowledgeArchetype::default(),
77 fidelity: None,
78 })
79 .collect();
80 return Ok(facts);
81 }
82
83 let mut facts = Vec::new();
84 for line in data.lines() {
85 let line = line.trim();
86 if line.is_empty() {
87 continue;
88 }
89 if let Ok(entry) = serde_json::from_str::<SimpleFactEntry>(line) {
90 let now = Utc::now();
91 facts.push(KnowledgeFact {
92 category: entry.category,
93 key: entry.key,
94 value: entry.value,
95 source_session: entry.source.unwrap_or_else(|| "import".to_string()),
96 confidence: entry.confidence.unwrap_or(0.8),
97 created_at: now,
98 last_confirmed: now,
99 retrieval_count: 0,
100 last_retrieved: None,
101 valid_from: Some(now),
102 valid_until: None,
103 supersedes: None,
104 confirmation_count: 1,
105 feedback_up: 0,
106 feedback_down: 0,
107 last_feedback: None,
108 privacy: FactPrivacy::default(),
109 imported_from: None,
110 archetype: KnowledgeArchetype::default(),
111 fidelity: None,
112 });
113 } else {
114 return Err(format!(
115 "Invalid JSONL line: {}",
116 &line[..line.len().min(80)]
117 ));
118 }
119 }
120
121 if facts.is_empty() {
122 return Err("No facts found. Expected: native JSON, simple JSON array, or JSONL.".into());
123 }
124 Ok(facts)
125}
126
127fn imported_fact(source: &KnowledgeFact, session_id: &str) -> KnowledgeFact {
128 let now = Utc::now();
129 KnowledgeFact {
130 category: source.category.clone(),
131 key: source.key.clone(),
132 value: source.value.clone(),
133 source_session: session_id.to_string(),
134 confidence: source.confidence,
135 created_at: now,
136 last_confirmed: now,
137 retrieval_count: 0,
138 last_retrieved: None,
139 valid_from: Some(now),
140 valid_until: None,
141 supersedes: None,
142 confirmation_count: 1,
143 feedback_up: 0,
144 feedback_down: 0,
145 last_feedback: None,
146 privacy: source.privacy,
147 imported_from: source.imported_from.clone(),
148 archetype: source.archetype.clone(),
149 fidelity: None,
150 }
151}
152
153impl ProjectKnowledge {
154 pub fn import_facts(
157 &mut self,
158 incoming: Vec<KnowledgeFact>,
159 merge: ImportMerge,
160 session_id: &str,
161 policy: &MemoryPolicy,
162 ) -> ImportResult {
163 let mut added = 0u32;
164 let mut skipped = 0u32;
165 let mut replaced = 0u32;
166
167 for fact in incoming {
168 let existing = self
169 .facts
170 .iter()
171 .position(|f| f.category == fact.category && f.key == fact.key && f.is_current());
172
173 match (&merge, existing) {
174 (ImportMerge::SkipExisting, Some(_)) => {
175 skipped += 1;
176 }
177 (ImportMerge::Replace, Some(idx)) => {
178 self.facts[idx].valid_until = Some(Utc::now());
179 self.facts.push(imported_fact(&fact, session_id));
180 replaced += 1;
181 }
182 (ImportMerge::Append, Some(_)) | (_, None) => {
183 self.facts.push(imported_fact(&fact, session_id));
184 added += 1;
185 }
186 }
187 }
188
189 if added > 0 || replaced > 0 {
190 self.updated_at = Utc::now();
191 if self.facts.len() > policy.knowledge.max_facts.saturating_mul(2) {
192 let _ = self.run_memory_lifecycle(policy);
193 }
194 }
195
196 ImportResult {
197 added,
198 skipped,
199 replaced,
200 }
201 }
202
203 pub fn export_simple(&self) -> Vec<SimpleFactEntry> {
205 self.facts
206 .iter()
207 .filter(|f| f.is_current())
208 .map(|f| SimpleFactEntry {
209 category: f.category.clone(),
210 key: f.key.clone(),
211 value: f.value.clone(),
212 confidence: Some(f.confidence),
213 source: Some(f.source_session.clone()),
214 timestamp: Some(f.created_at.to_rfc3339()),
215 })
216 .collect()
217 }
218}