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