use std::collections::{HashMap, HashSet};
use crate::graph_memory::{
EdgeTier, EntityLabel, EntityNode, GraphMemory, LtpStatus, RelationType, RelationshipEdge,
};
use crate::memory::types::{
Experience, ExperienceType, MemoryId, ProspectiveTask, ProspectiveTaskId,
ProspectiveTaskStatus, ProspectiveTrigger, Todo, TodoId, TodoPriority, TodoStatus,
};
use crate::memory::{Project, ProjectId, ProjectStatus};
use anyhow::{bail, Result};
use chrono::{DateTime, Utc};
use sha2::{Digest, Sha256};
use super::schema::*;
#[derive(Debug, Clone)]
pub struct ImportOptions {
pub user_id: String,
pub skip_duplicates: bool,
}
#[derive(Debug, Default, serde::Serialize)]
pub struct ImportResult {
pub memories_imported: usize,
pub todos_imported: usize,
pub projects_imported: usize,
pub reminders_imported: usize,
pub edges_imported: usize,
pub entities_imported: usize,
pub duplicates_skipped: usize,
pub errors: Vec<String>,
}
pub fn build_dedup_set(existing_contents: &[String]) -> HashSet<[u8; 32]> {
existing_contents.iter().map(|c| content_hash(c)).collect()
}
fn content_hash(content: &str) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
hasher.finalize().into()
}
pub type PreparedMemory = (MemoryId, Experience, Option<DateTime<Utc>>);
pub fn prepare_memories(
doc: &MifDocument,
dedup_set: &HashSet<[u8; 32]>,
options: &ImportOptions,
) -> (Vec<PreparedMemory>, usize) {
let mut prepared = Vec::new();
let mut skipped = 0;
for mem in &doc.memories {
if options.skip_duplicates && dedup_set.contains(&content_hash(&mem.content)) {
skipped += 1;
continue;
}
let exp_type = parse_experience_type(&mem.memory_type);
let mut metadata = mem.metadata.clone();
if !mem.tags.is_empty() && !metadata.contains_key("tags") {
metadata.insert("tags".to_string(), mem.tags.join(","));
}
let entities: Vec<String> = mem.entities.iter().map(|e| e.name.clone()).collect();
let embeddings = mem.embeddings.as_ref().map(|e| e.vector.clone());
let experience = Experience {
experience_type: exp_type,
content: mem.content.clone(),
entities,
metadata,
embeddings,
tags: mem.tags.clone(),
..Default::default()
};
let memory_id = MemoryId(mem.id);
let created_at = Some(mem.created_at);
prepared.push((memory_id, experience, created_at));
}
(prepared, skipped)
}
pub fn prepare_todos(doc: &MifDocument, user_id: &str) -> Vec<Todo> {
doc.todos
.iter()
.map(|t| {
let status = parse_todo_status(&t.status);
let priority = parse_todo_priority(&t.priority);
let comments = t
.comments
.iter()
.map(|c| crate::memory::types::TodoComment {
id: crate::memory::types::TodoCommentId(c.id),
todo_id: TodoId(t.id),
author: c.author.clone().unwrap_or_else(|| "import".to_string()),
content: c.content.clone(),
comment_type: parse_comment_type(&c.comment_type),
created_at: c.created_at,
updated_at: None,
})
.collect();
let related_memory_ids: Vec<MemoryId> = t
.related_memory_ids
.iter()
.map(|id| MemoryId(*id))
.collect();
Todo {
id: TodoId(t.id),
seq_num: 0,
project_prefix: None,
project: None,
user_id: user_id.to_string(),
content: t.content.clone(),
status,
priority,
project_id: t.project_id.map(ProjectId),
parent_id: t.parent_id.map(TodoId),
contexts: t.contexts.clone(),
tags: t.tags.clone(),
notes: t.notes.clone(),
blocked_on: t.blocked_on.clone(),
recurrence: None,
created_at: t.created_at,
updated_at: t.updated_at,
due_date: t.due_date,
completed_at: t.completed_at,
sort_order: 0,
comments,
embedding: None,
related_memory_ids,
external_id: t.external_id.clone(),
}
})
.collect()
}
pub fn prepare_projects(doc: &MifDocument, user_id: &str) -> Vec<Project> {
doc.projects
.iter()
.map(|p| Project {
id: ProjectId(p.id),
user_id: user_id.to_string(),
name: p.name.clone(),
prefix: if p.prefix.is_empty() {
None
} else {
Some(p.prefix.clone())
},
description: p.description.clone(),
status: parse_project_status(&p.status),
color: p.color.clone(),
parent_id: None,
created_at: p.created_at,
completed_at: None,
codebase_path: None,
codebase_indexed: false,
codebase_indexed_at: None,
codebase_file_count: 0,
embedding: None,
related_memory_ids: Vec::new(),
todo_counts: Default::default(),
})
.collect()
}
pub fn prepare_reminders(doc: &MifDocument, user_id: &str) -> Vec<ProspectiveTask> {
doc.reminders
.iter()
.map(|r| {
let trigger = match &r.trigger {
MifTrigger::Time { at } => ProspectiveTrigger::AtTime { at: *at },
MifTrigger::Duration { seconds, from } => ProspectiveTrigger::AfterDuration {
seconds: *seconds,
from: *from,
},
MifTrigger::Context {
keywords,
threshold,
} => ProspectiveTrigger::OnContext {
keywords: keywords.clone(),
threshold: *threshold,
},
};
let status = parse_reminder_status(&r.status);
ProspectiveTask {
id: ProspectiveTaskId(r.id),
user_id: user_id.to_string(),
content: r.content.clone(),
trigger,
status,
created_at: r.created_at,
triggered_at: r.triggered_at,
dismissed_at: r.dismissed_at,
tags: r.tags.clone(),
priority: r.priority,
embedding: None,
related_memory_ids: Vec::new(),
}
})
.collect()
}
pub fn import_graph_entities(kg: &MifKnowledgeGraph, graph: &GraphMemory) -> (usize, Vec<String>) {
let mut imported = 0;
let mut errors = Vec::new();
for entity in &kg.entities {
let labels: Vec<EntityLabel> = entity.types.iter().map(|t| parse_entity_label(t)).collect();
let node = EntityNode {
uuid: entity.id,
name: entity.name.clone(),
labels: if labels.is_empty() {
vec![EntityLabel::Concept]
} else {
labels
},
created_at: entity.created_at,
last_seen_at: entity.last_seen_at,
mention_count: 1,
summary: entity.summary.clone(),
attributes: entity.attributes.clone(),
name_embedding: None,
salience: 0.5,
is_proper_noun: true,
selectivity: None,
};
match graph.add_entity(node) {
Ok(_) => imported += 1,
Err(e) => errors.push(format!("Entity '{}': {}", entity.name, e)),
}
}
(imported, errors)
}
pub fn import_graph_relationships(
kg: &MifKnowledgeGraph,
graph: &GraphMemory,
vendor_extensions: &HashMap<String, serde_json::Value>,
) -> (usize, Vec<String>) {
let mut imported = 0;
let mut errors = Vec::new();
let edge_meta = vendor_extensions
.get("shodh-memory")
.and_then(|v| v.get("edge_metadata"))
.and_then(|v| v.as_object());
for rel in &kg.relationships {
let relation_type = parse_relation_type(&rel.relation_type);
let strength = rel.confidence.unwrap_or(0.5);
let (ltp_status, tier, activation_count) = if let Some(meta) = edge_meta {
if let Some(em) = meta.get(&rel.id.to_string()) {
let ltp = em
.get("ltp_status")
.and_then(|v| v.as_str())
.map(parse_ltp_status)
.unwrap_or_default();
let tier = em
.get("tier")
.and_then(|v| v.as_str())
.map(parse_edge_tier)
.unwrap_or_default();
let count = em
.get("activation_count")
.and_then(|v| v.as_u64())
.unwrap_or(1) as u32;
(ltp, tier, count)
} else {
(LtpStatus::None, EdgeTier::L1Working, 1)
}
} else {
(LtpStatus::None, EdgeTier::L1Working, 1)
};
let edge = RelationshipEdge {
uuid: rel.id,
from_entity: rel.source_entity_id,
to_entity: rel.target_entity_id,
relation_type,
strength,
created_at: rel.created_at,
valid_at: rel.valid_at,
invalidated_at: rel.invalidated_at,
source_episode_id: None,
context: rel.context.clone(),
last_activated: rel.created_at,
activation_count,
ltp_status,
tier,
activation_timestamps: None,
entity_confidence: rel.confidence,
forman_curvature: None,
endpoint_selectivity: None,
};
match graph.add_relationship(edge) {
Ok(_) => imported += 1,
Err(e) => errors.push(format!("Edge {}: {}", rel.id, e)),
}
}
(imported, errors)
}
pub fn validate_version(doc: &MifDocument) -> Result<()> {
if !doc.mif_version.starts_with("2.") && !doc.mif_version.starts_with("1.") {
bail!(
"Unsupported MIF version: {}. Supported: 1.x, 2.x",
doc.mif_version
);
}
Ok(())
}
pub(crate) fn parse_experience_type(s: &str) -> ExperienceType {
match s.to_lowercase().as_str() {
"observation" => ExperienceType::Observation,
"decision" => ExperienceType::Decision,
"learning" => ExperienceType::Learning,
"error" => ExperienceType::Error,
"discovery" => ExperienceType::Discovery,
"pattern" => ExperienceType::Pattern,
"context" => ExperienceType::Context,
"task" => ExperienceType::Task,
"code_edit" | "codeedit" => ExperienceType::CodeEdit,
"file_access" | "fileaccess" => ExperienceType::FileAccess,
"search" => ExperienceType::Search,
"command" => ExperienceType::Command,
"conversation" => ExperienceType::Conversation,
"intention" => ExperienceType::Intention,
_ => ExperienceType::Observation,
}
}
fn parse_todo_status(s: &str) -> TodoStatus {
match s {
"backlog" => TodoStatus::Backlog,
"todo" => TodoStatus::Todo,
"in_progress" => TodoStatus::InProgress,
"blocked" => TodoStatus::Blocked,
"done" => TodoStatus::Done,
"cancelled" => TodoStatus::Cancelled,
_ => TodoStatus::Todo,
}
}
fn parse_todo_priority(s: &str) -> TodoPriority {
match s {
"urgent" | "!!!" => TodoPriority::Urgent,
"high" | "!!" => TodoPriority::High,
"medium" | "!" => TodoPriority::Medium,
"low" => TodoPriority::Low,
"none" | "" => TodoPriority::None,
_ => TodoPriority::Medium,
}
}
fn parse_comment_type(s: &str) -> crate::memory::types::TodoCommentType {
use crate::memory::types::TodoCommentType;
match s {
"comment" => TodoCommentType::Comment,
"progress" => TodoCommentType::Progress,
"resolution" => TodoCommentType::Resolution,
"activity" => TodoCommentType::Activity,
_ => TodoCommentType::Comment,
}
}
fn parse_project_status(s: &str) -> ProjectStatus {
match s {
"active" => ProjectStatus::Active,
"onhold" | "on_hold" => ProjectStatus::OnHold,
"completed" => ProjectStatus::Completed,
"archived" => ProjectStatus::Archived,
_ => ProjectStatus::Active,
}
}
fn parse_reminder_status(s: &str) -> ProspectiveTaskStatus {
match s {
"pending" => ProspectiveTaskStatus::Pending,
"triggered" => ProspectiveTaskStatus::Triggered,
"dismissed" => ProspectiveTaskStatus::Dismissed,
"expired" => ProspectiveTaskStatus::Expired,
_ => ProspectiveTaskStatus::Pending,
}
}
fn parse_entity_label(s: &str) -> EntityLabel {
match s.to_lowercase().as_str() {
"person" => EntityLabel::Person,
"organization" => EntityLabel::Organization,
"location" => EntityLabel::Location,
"technology" => EntityLabel::Technology,
"concept" => EntityLabel::Concept,
"event" => EntityLabel::Event,
"date" => EntityLabel::Date,
"product" => EntityLabel::Product,
"skill" => EntityLabel::Skill,
"keyword" => EntityLabel::Keyword,
"project" => EntityLabel::Project,
"task" => EntityLabel::Task,
"document" => EntityLabel::Document,
"repository" => EntityLabel::Repository,
"service" => EntityLabel::Service,
"database" => EntityLabel::Database,
"metric" => EntityLabel::Metric,
"configuration" => EntityLabel::Configuration,
"environment" => EntityLabel::Environment,
"pipeline" => EntityLabel::Pipeline,
"team" => EntityLabel::Team,
"role" => EntityLabel::Role,
"module" => EntityLabel::Module,
other => EntityLabel::Other(other.to_string()),
}
}
pub(crate) fn parse_relation_type(s: &str) -> RelationType {
match s {
"works_with" | "workswith" => RelationType::WorksWith,
"works_at" | "worksat" => RelationType::WorksAt,
"employed_by" | "employedby" => RelationType::EmployedBy,
"part_of" | "partof" => RelationType::PartOf,
"contains" => RelationType::Contains,
"owned_by" | "ownedby" => RelationType::OwnedBy,
"located_in" | "locatedin" => RelationType::LocatedIn,
"located_at" | "locatedat" => RelationType::LocatedAt,
"uses" => RelationType::Uses,
"created_by" | "createdby" => RelationType::CreatedBy,
"developed_by" | "developedby" => RelationType::DevelopedBy,
"causes" => RelationType::Causes,
"results_in" | "resultsin" => RelationType::ResultsIn,
"learned" => RelationType::Learned,
"knows" => RelationType::Knows,
"teaches" => RelationType::Teaches,
"related_to" | "relatedto" => RelationType::RelatedTo,
"associated_with" | "associatedwith" => RelationType::AssociatedWith,
"co_retrieved" | "coretrieved" => RelationType::CoRetrieved,
"co_occurs" | "cooccurs" => RelationType::CoOccurs,
"manages" => RelationType::Manages,
"depends_on" | "dependson" => RelationType::DependsOn,
"requires" => RelationType::Requires,
"configures" => RelationType::Configures,
"prefers" => RelationType::Prefers,
"recommends" => RelationType::Recommends,
"documents" => RelationType::Documents,
"implements" => RelationType::Implements,
"deploys_to" | "deploysto" => RelationType::DeploysTo,
"monitors" => RelationType::Monitors,
"triggers" => RelationType::Triggers,
"superseded_by" | "supersededby" => RelationType::SupersededBy,
"alternative_to" | "alternativeto" => RelationType::AlternativeTo,
"assigned_to" | "assignedto" => RelationType::AssignedTo,
"approves" => RelationType::Approves,
other => RelationType::Custom(other.to_string()),
}
}
fn parse_ltp_status(s: &str) -> LtpStatus {
match s {
"None" => LtpStatus::None,
"Weekly" => LtpStatus::Weekly,
"Full" => LtpStatus::Full,
s if s.starts_with("Burst") => LtpStatus::Burst {
detected_at: Utc::now(),
},
_ => LtpStatus::None,
}
}
fn parse_edge_tier(s: &str) -> EdgeTier {
match s {
"L1Working" => EdgeTier::L1Working,
"L2Episodic" => EdgeTier::L2Episodic,
"L3Semantic" => EdgeTier::L3Semantic,
_ => EdgeTier::L1Working,
}
}