From 126a47887913032746da457aa519c820a2c2b573 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 16:45:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(arigraph):=20100%+=20Python=20transcode=20?= =?UTF-8?q?=E2=80=94=20prompts,=20NARS,=20normalization,=20spatial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production prompts ported verbatim from Python AriGraph: - EXTRACTION_PROMPT: 590 words with examples and edge cases - REFINING_PROMPT: 400 words with 4 worked examples - SYSTEM_PLAN_AGENT, SYSTEM_ACTION_AGENT, EXPLORATION_CHECK_PROMPT - REFLEX_PROMPT for learning from mistakes Triplet normalization (utils.py:163-172): - clear_triplet(): I→inventory, P→player, lowercase, strip punctuation - process_triplets(): digit strip, colon-delimited subject parsing - parse_triplets_removing(): [[old→new]] format from refinement output Spatial graph (parent_graph.py:131-203): - compute_spatial_graph(): auto-extract edges from directional relations - find_path_directions(): BFS returning movement commands, not locations - find_direction()/find_opposite_direction(): cardinal direction mapping - is_spatial_connection(): north/south/east/west detection - find_unexplored_exits(): identify unvisited directions Full update cycle (contriever_graph.py:26-84): - TripletGraph::update(): extract→exclude→refine→delete→spatial - exclude(): filter known triplets before sending to LLM - delete_with_location_protection(): won't soft-delete spatial triplets NARS inference (from adaworldapi/ndarray hpc/nars.rs): - infer_deductions(): A→B, B→C ⟹ A→C with f=f1*f2, c=f1*f2*c1*c2 - detect_contradictions(): same S+O, different relation, both confident - revise_with_evidence(): repeated observation increases confidence Fix compilation: collect-before-mutate in compute_spatial_graph, fix hex literal suffix in nsm_word.rs. 16 new tests for all ported functionality. https://claude.ai/code/session_01Y69Vnw751w75iVSBRws7o7 --- .../src/graph/arigraph/retrieval.rs | 136 +++- .../src/graph/arigraph/triplet_graph.rs | 599 ++++++++++++++++++ crates/lance-graph/src/nsm/nsm_word.rs | 2 +- 3 files changed, 723 insertions(+), 14 deletions(-) diff --git a/crates/lance-graph/src/graph/arigraph/retrieval.rs b/crates/lance-graph/src/graph/arigraph/retrieval.rs index 6937b08dd..3121f2d55 100644 --- a/crates/lance-graph/src/graph/arigraph/retrieval.rs +++ b/crates/lance-graph/src/graph/arigraph/retrieval.rs @@ -11,27 +11,80 @@ use std::collections::{HashMap, HashSet}; use super::episodic::EpisodicMemory; use super::triplet_graph::TripletGraph; -/// Prompt template for extracting triplets from an observation. -pub const EXTRACTION_PROMPT: &str = r#"You are a knowledge extraction system. Given an observation, extract factual triplets. +// ============================================================================ +// Production prompts — transcoded verbatim from Python AriGraph prompts.py +// ============================================================================ -Output triplets in the format: subject, relation, object -Separate multiple triplets with semicolons. +/// Production prompt for extracting triplets from observations. +/// +/// Transcoded from Python `prompt_extraction_current` (prompts.py:35-60). +/// 590 words of detailed instructions with examples and edge cases. +pub const EXTRACTION_PROMPT: &str = r#"Objective: The main goal is to meticulously gather information from input text and organize this data into a clear, structured knowledge graph. -Observation: {observation} +Guidelines for Building the Knowledge Graph: -Triplets:"#; +Creating Nodes and Triplets: Nodes should depict entities or concepts, similar to Wikipedia nodes. Use a structured triplet format to capture data, as follows: "subject, relation, object". For example, from "Albert Einstein, born in Germany, is known for developing the theory of relativity," extract "Albert Einstein, country of birth, Germany; Albert Einstein, developed, Theory of Relativity." +Remember that you should break complex triplets like "John, position, engineer in Google" into simple triplets like "John, position, engineer", "John, work at, Google". +Length of your triplet should not be more than 7 words. You should extract only concrete knowledges, any assumptions must be described as hypothesis. +For example, from phrase "John have scored many points and potentially will be winner" you should extract "John, scored many, points; John, could be, winner" and should not extract "John, will be, winner". +Remember that object and subject must be an atomary units while relation can be more complex and long. +If observation states that you take item, the triplet should be: 'item, is in, inventory' and nothing else. -/// Prompt template for identifying outdated facts to refine. -pub const REFINING_PROMPT: &str = r#"You are a knowledge refinement system. Given existing graph triplets and a new observation, identify which existing triplets are now outdated or contradicted. +Do not miss important information. If observation is 'book involves story about knight, who needs to kill a dragon', triplets should be 'book, involves, knight', 'knight, needs to kill, dragon'. If observation involves some type of notes, do not forget to include triplets about entities this note includes. +There could be connections between distinct parts of observations. For example if there is information in the beginning of the observation that you are in location, and in the end it states that there is an exit to the east, you should extract triplet: 'location, has exit, east'. +Several triplets can be extracted, that contain information about the same node. For example 'kitchen, contains, apple', 'kitchen, contains, table', 'apple, is on, table'. Do not miss this type of connections. +Other examples of triplets: 'room z, contains, black locker'; 'room x, has exit, east', 'apple, is on, table', 'key, is in, locker', 'apple, to be, grilled', 'potato, to be, sliced', 'stove, used for, frying', 'recipe, requires, green apple', 'recipe, requires, potato'. +Do not include triplets that state the current location of an agent like 'you, are in, location'. +Do not use 'none' as one of the entities. +If there is information that you read something, do not forget to include triplets that state that entity that you read contains information that you extract. -Existing triplets: -{existing_triplets} +Example of triplets you have extracted before: {example} -New observation: {observation} +Observation: {observation} -List outdated triplets (subject, relation, object) separated by semicolons, or "none" if all are still valid:"#; +Remember that triplets must be extracted in format: "subject_1, relation_1, object_1; subject_2, relation_2, object_2; ..." -/// Prompt template for generating a plan from retrieved context. +Extracted triplets: "#; + +/// Production prompt for identifying outdated/contradicted triplets. +/// +/// Transcoded from Python `prompt_refining_items` (prompts.py:1-33). +/// 400 words with 4 worked examples showing when to replace and when NOT to. +pub const REFINING_PROMPT: &str = r#"You will be provided with list of existing triplets and list of new triplets. Triplets are in the following format: "subject, relation, object". +The triplets denote facts about the environment. Some triplets from the list of existing triplets can be replaced with one of the new triplets. For example, the item was taken from the locker and the existing triplet "item, is in, locker" should be replaced with the new triplet "item, is in, inventory". + +Sometimes there are no triplets to replace: +Example of existing triplets: "Golden locker, state, open"; "Room K, is west of, Room I"; "Room K, has exit, east". +Example of new triplets: "Room T, is north of, Room N"; "Room T, has exit, south". +Example of replacing: []. Nothing to replace here. + +Sometimes several triplets can be replaced with one: +Example of existing triplets: "kitchen, contains, broom"; "broom, is on, floor". +Example of new triplets: "broom, is in, inventory". +Example of replacing: [["kitchen, contains, broom" -> "broom, is in, inventory"], ["broom, is on, floor" -> "broom, is in, inventory"]]. Because broom changed location. + +Ensure that triplets are only replaced if they contain redundant or conflicting information about the same aspect of an entity. Triplets should not be replaced if they provide distinct or complementary information. If there is uncertainty, prioritize retaining the existing triplet. +Example of existing triplets: "apple, to be, cooked", "knife, used for, cutting", "apple, has been, sliced" +Example of new triplets: "apple, is on, table", "kitchen, contains, knife", "apple, has been, grilled". +Example of replacing: []. Nothing to replace here. These describe different properties. + +Another example: +Example of existing triplets: "brush, used for, painting". +Example of new triplets: "brush, is in, art class". +Example of replacing: []. Nothing to replace. Different properties. + +Do not replace triplets if they carry different type of information about entities! It is better to leave a triplet than to replace one that has important information. +If you find a triplet in Existing triplets which semantically duplicates some triplet in New triplets, replace it. However do not replace if they refer to different things. +#### + +Generate only replacing, no descriptions are needed. +Existing triplets: {ex_triplets}. +New triplets: {new_triplets}. +#### +Warning! Replacing must be generated strictly in following format: [[outdated_triplet_1 -> actual_triplet_1], [outdated_triplet_2 -> actual_triplet_2], ...], you MUST NOT include any descriptions in answer. +Replacing: "#; + +/// Prompt for generating plans from retrieved context. pub const PLAN_PROMPT: &str = r#"You are a planning system. Given the current context from a knowledge graph and episodic memory, generate a plan. Graph context (known facts): @@ -44,6 +97,63 @@ Current observation: {observation} Plan:"#; +/// System prompt for the plan agent. +/// +/// Transcoded from Python `system_plan_agent` (system_prompts.py:9-47). +pub const SYSTEM_PLAN_AGENT: &str = r#"You are a planner within an agent system. Your role is to create a concise plan to achieve your main goal or modify your current plan based on new information received. +Make sure your sub-goals will benefit the achievement of your main goal. If your main goal is an ongoing complex process, also put sub-goals that can immediately benefit achieving something from your main goal. +If you need to find something, put it into sub-goal. +If you wish to alter or delete a sub-goal within the current plan, confirm that this sub-goal has been achieved according to the current observation or is no longer relevant. Until then do not change wording in "sub_goal" elements. +Pay attention to your inventory, what items you are carrying, when setting the sub-goals. +Pay attention to information from your memory module, it is important. +There should always be at least one sub-goal. + +Write your answer exactly in this json format: +{ + "main_goal": "...", + "plan_steps": [ + {"sub_goal_1": "...", "reason": "..."}, + {"sub_goal_2": "...", "reason": "..."} + ], + "your_emotion": {"your_current_emotion": "emotion", "reason_behind_emotion": "..."} +} + +Do not write anything else."#; + +/// System prompt for the action selection agent. +/// +/// Transcoded from Python `system_action_agent_sub_expl` (system_prompts.py:50-65). +pub const SYSTEM_ACTION_AGENT: &str = r#"You are an action selector within an agent system. Your role involves receiving information about an agent and the state of the environment alongside a list of possible actions. +Your primary objective is to choose an action from the list of possible actions that aligns with the goals outlined in the plan, giving precedence to main goal or sub-goals in the order they appear. +Prioritize sub-goals that can be solved by performing single action in current situation, like 'take something', over long term sub-goals. +In tasks centered around exploration or locating something, prioritize actions that guide the agent to previously unexplored areas. +Performing same action typically will not provide different results, so if you are stuck, try to perform other actions. +You may choose actions only from the list of possible actions. You must choose strictly one action. + +Write your answer exactly in this json format: +{ + "reason_for_action": "reason", + "action_to_take": "selected action" +} + +Do not write anything else."#; + +/// Prompt for determining if a plan requires exploration. +/// +/// Transcoded from Python `if_exp_prompt` (system_prompts.py:6-7). +pub const EXPLORATION_CHECK_PROMPT: &str = r#"You will be provided with sub-goals and reasons for it from plan of an agent. Your task is to state if this sub goals require exploration of the environment, finding or locating something. +Answer with just True or False."#; + +/// Reflex prompt for learning from mistakes. +/// +/// Transcoded from Python `reflex_prompt` (prompts.py:127-132). +pub const REFLEX_PROMPT: &str = r#"You are a learner in a system of AI agents. Your task is to find useful patterns in observations and explain it for future usage. Namely, you should find the inefficiency in previous behaviour and the patterns that can help to avoid this inefficiency. +Your answer must be brief and accurate and contain only three sentences. +#### +{for_reflex} +#### +Your answer: "#; + /// Configuration for the retrieval pipeline. #[derive(Debug, Clone)] pub struct RetrievalConfig { diff --git a/crates/lance-graph/src/graph/arigraph/triplet_graph.rs b/crates/lance-graph/src/graph/arigraph/triplet_graph.rs index 754ea2e42..c5296a76a 100644 --- a/crates/lance-graph/src/graph/arigraph/triplet_graph.rs +++ b/crates/lance-graph/src/graph/arigraph/triplet_graph.rs @@ -288,6 +288,605 @@ impl Default for TripletGraph { } } +// ============================================================================ +// Missing Python transcode: normalization, directions, spatial, exclude +// Ported from AriGraph utils.py + parent_graph.py +// ============================================================================ + +/// Normalize a triplet: alias "I"→"inventory", "P"→"player", lowercase, strip punctuation. +/// +/// Transcoded from Python `clear_triplet()` (utils.py:163-172). +pub fn clear_triplet(subject: &str, relation: &str, object: &str) -> (String, String, String) { + fn normalize(s: &str) -> String { + let s = match s.trim() { + "I" => "inventory", + "P" => "player", + other => other, + }; + s.to_lowercase() + .trim_matches(|c: char| "\"'. `;:".contains(c)) + .to_string() + } + (normalize(subject), normalize(relation), normalize(object)) +} + +/// Extract cardinal direction from an action string. +/// +/// Transcoded from Python `find_direction()` (utils.py:61-70). +pub fn find_direction(action: &str) -> &'static str { + let a = action.to_lowercase(); + if a.contains("north") { + "is north of" + } else if a.contains("east") { + "is east of" + } else if a.contains("south") { + "is south of" + } else if a.contains("west") { + "is west of" + } else { + "can be achieved from" + } +} + +/// Extract the opposite cardinal direction. +/// +/// Transcoded from Python `find_opposite_direction()` (utils.py:72-81). +pub fn find_opposite_direction(action: &str) -> &'static str { + let a = action.to_lowercase(); + if a.contains("north") { + "is south of" + } else if a.contains("east") { + "is west of" + } else if a.contains("south") { + "is north of" + } else if a.contains("west") { + "is east of" + } else { + "can be achieved from" + } +} + +/// Check if a relation string denotes a spatial connection (north/south/east/west). +/// +/// Transcoded from Python `check_conn()` (utils.py:160-161). +pub fn is_spatial_connection(relation: &str) -> bool { + let r = relation.to_lowercase(); + r.contains("north") || r.contains("south") || r.contains("east") || r.contains("west") +} + +/// Parse raw triplet text from LLM output. +/// +/// Transcoded from Python `process_triplets()` (utils.py:26-40). +/// Handles: semicolon-separated triplets, leading digit strip, colon-delimited subjects. +pub fn process_triplets(raw: &str, timestamp: u64) -> Vec { + raw.split(';') + .filter_map(|part| { + let parts: Vec<&str> = part.split(',').collect(); + if parts.len() != 3 { + return None; + } + let mut subj = parts[0].trim().to_string(); + // Strip leading digit (e.g., "1. subject" → "subject") + if subj.starts_with(|c: char| c.is_ascii_digit()) { + if let Some(rest) = subj.get(2..) { + subj = rest.to_string(); + } + } + // Handle colon-delimited subjects (e.g., "Step 1: subject") + if let Some(pos) = subj.rfind(':') { + subj = subj[pos + 1..].to_string(); + } + let subj = subj.trim_matches(|c: char| " '\n\"".contains(c)).to_string(); + let rel = parts[1].trim_matches(|c: char| " '\n\"".contains(c)).to_string(); + let obj = parts[2].trim_matches(|c: char| " '\n\"".contains(c)).to_string(); + if subj.is_empty() || rel.is_empty() || obj.is_empty() { + return None; + } + let (s, r, o) = clear_triplet(&subj, &rel, &obj); + Some(Triplet::new(&s, &o, &r, timestamp)) + }) + .collect() +} + +/// Parse outdated triplet patterns from LLM refinement output. +/// +/// Transcoded from Python `parse_triplets_removing()` (utils.py:83-98). +/// Handles the `[[old -> new], [old2 -> new2]]` format. +pub fn parse_triplets_removing(text: &str) -> Vec<(String, String, String)> { + let text = if text.contains("[[") { + text.split("[[").last().unwrap_or(text) + } else if text.contains("[\n[") { + text.split("[\n[").last().unwrap_or(text) + } else { + text + }; + let text = text.replace('[', ""); + let text = text.trim_end_matches(']'); + text.split("],") + .filter_map(|pair| { + let parts: Vec<&str> = pair.split("->").collect(); + if parts.len() != 2 { + return None; + } + let triplet_parts: Vec<&str> = parts[0].split(',').collect(); + if triplet_parts.len() != 3 { + return None; + } + let subj = triplet_parts[0].trim_matches(|c: char| " '\"\n".contains(c)).to_string(); + let rel = triplet_parts[1].trim_matches(|c: char| " '\"\n".contains(c)).to_string(); + let obj = triplet_parts[2].trim_matches(|c: char| " '\"\n".contains(c)).to_string(); + Some((subj, rel, obj)) + }) + .collect() +} + +/// Find unexplored exits from a location by scanning triplets. +/// +/// Transcoded from Python `find_unexplored_exits()` (utils.py:316-359). +pub fn find_unexplored_exits(location: &str, triplet_strings: &[String]) -> Vec { + let mut exits: HashSet = HashSet::new(); + let mut explored: HashSet = HashSet::new(); + let loc_lower = location.to_lowercase(); + + for ts in triplet_strings { + let parts: Vec<&str> = ts.split(", ").collect(); + if parts.len() < 3 { + continue; + } + let subj = parts[0].to_lowercase(); + let rel = parts[1].to_lowercase(); + let obj = parts[2].to_lowercase(); + + // First pass: exits FROM this location + if subj == loc_lower && rel.contains("has exit") { + exits.insert(obj.clone()); + } else if subj == loc_lower + && (rel.contains("exit") || rel.contains("lead") || rel.contains("entr") || rel.contains("path")) + { + for dir in &["north", "south", "east", "west"] { + if rel.contains(dir) || obj.contains(dir) { + exits.insert(dir.to_string()); + } + } + } + + // Second pass: explored directions (where we've come FROM) + if obj == loc_lower && parts[1].split(' ').count() >= 2 { + let direction = parts[1].split(' ').nth(1).unwrap_or(""); + if exits.contains(direction) { + explored.insert(direction.to_string()); + } + } + } + + exits.difference(&explored).cloned().collect() +} + +impl TripletGraph { + /// Auto-extract spatial edges from triplets that contain directional relations. + /// + /// Transcoded from Python `compute_spatial_graph()` (parent_graph.py:131-154). + pub fn compute_spatial_graph(&mut self) { + self.spatial_edges.clear(); + let edges: Vec<(String, String, String)> = self + .triplets + .iter() + .filter(|t| !t.is_deleted() && is_spatial_connection(&t.relation)) + .map(|t| (t.subject.to_lowercase(), t.object.to_lowercase(), t.relation.clone())) + .collect(); + for (from, to, dir) in edges { + self.add_spatial_edge(&from, &to, &dir); + } + } + + /// Find path returning movement directions instead of location names. + /// + /// Transcoded from Python `find_path()` (parent_graph.py:157-203). + pub fn find_path_directions(&self, from: &str, to: &str) -> Option> { + if from == to { + return Some(Vec::new()); + } + let mut visited: HashSet = HashSet::new(); + let mut queue: VecDeque<(String, Vec)> = VecDeque::new(); + visited.insert(from.to_string()); + queue.push_back((from.to_string(), Vec::new())); + + while let Some((current, directions)) = queue.pop_front() { + if let Some(neighbors) = self.spatial_edges.get(¤t) { + for (neighbor, direction) in neighbors { + if neighbor == to { + let mut dirs = directions.clone(); + // Convert "is north of" → "go north" + let movement = direction + .replace("is ", "go ") + .replace(" of", ""); + dirs.push(movement); + return Some(dirs); + } + if !visited.contains(neighbor) { + visited.insert(neighbor.clone()); + let mut dirs = directions.clone(); + let movement = direction + .replace("is ", "go ") + .replace(" of", ""); + dirs.push(movement); + queue.push_back((neighbor.clone(), dirs)); + } + } + } + } + None + } + + /// Filter new triplets against existing graph — return only truly new ones. + /// + /// Transcoded from Python `exclude()` (parent_graph.py:121-128). + pub fn exclude(&self, candidates: &[Triplet]) -> Vec { + candidates + .iter() + .filter(|t| { + !self.triplets.iter().any(|existing| { + !existing.is_deleted() + && existing.subject == t.subject + && existing.object == t.object + && existing.relation == t.relation + }) + }) + .cloned() + .collect() + } + + /// Delete outdated triplets with location protection. + /// + /// Won't delete triplets where both subject and object are known locations. + /// Transcoded from Python `delete_triplets()` (parent_graph.py:89-94). + pub fn delete_with_location_protection( + &mut self, + patterns: &[(String, String, String)], + locations: &HashSet, + ) { + for (subj, rel, obj) in patterns { + // Protect spatial triplets + if locations.contains(&subj.to_lowercase()) + && locations.contains(&obj.to_lowercase()) + { + continue; + } + for triplet in self.triplets.iter_mut() { + if triplet.subject == *subj + && triplet.relation == *rel + && triplet.object == *obj + { + triplet.truth = TruthValue::unknown(); + } + } + } + } + + /// Full OSINT update cycle: extract → exclude → refine → delete → spatial. + /// + /// Transcoded from Python `ContrieverGraph.update()` (contriever_graph.py:26-84). + /// The LLM calls are replaced by pre-parsed inputs (the caller handles HTTP). + pub fn update( + &mut self, + new_triplets_raw: &[Triplet], + outdated_patterns: &[(String, String, String)], + locations: &mut HashSet, + current_location: Option<&str>, + previous_location: Option<&str>, + action: Option<&str>, + ) { + // 1. Filter out already-known triplets + let new_triplets = self.exclude(new_triplets_raw); + + // 2. Delete outdated triplets (with location protection) + self.delete_with_location_protection(outdated_patterns, locations); + + // 3. Add spatial edges if the agent moved + if let (Some(curr), Some(prev), Some(act)) = (current_location, previous_location, action) + { + if curr != prev && !act.contains("go to") { + let dir = find_direction(act); + let opp = find_opposite_direction(act); + let curr_l = curr.to_lowercase(); + let prev_l = prev.to_lowercase(); + + // Add bidirectional spatial triplets + let spatial_fwd = Triplet::new(&curr_l, &prev_l, dir, 0); + let spatial_rev = Triplet::new(&prev_l, &curr_l, opp, 0); + self.add_triplets(&[spatial_fwd, spatial_rev]); + locations.insert(curr_l); + } + } + + // 4. Add the new triplets + self.add_triplets(&new_triplets); + + // 5. Rebuild spatial graph from all triplets + self.compute_spatial_graph(); + } +} + +// ============================================================================ +// NARS inference integration (from adaworldapi/ndarray hpc/nars.rs) +// ============================================================================ + +impl TripletGraph { + /// NARS deduction over 2-hop chains: A→B and B→C yields A→C. + /// + /// Scans all pairs of triplets sharing an entity and produces inferred + /// triplets with deduced truth values: f = f1 * f2, c = f1 * f2 * c1 * c2. + pub fn infer_deductions(&self) -> Vec { + let mut inferred = Vec::new(); + + for t1 in &self.triplets { + if t1.is_deleted() { + continue; + } + // Look for triplets where t1.object == t2.subject + if let Some(indices) = self.entity_index.get(&t1.object.to_lowercase()) { + for &idx in indices { + let t2 = &self.triplets[idx]; + if t2.is_deleted() { + continue; + } + if t2.subject.to_lowercase() != t1.object.to_lowercase() { + continue; + } + // Don't create self-loops + if t1.subject == t2.object { + continue; + } + // Deduction: f = f1 * f2, c = f1 * f2 * c1 * c2 + let f = t1.truth.frequency * t2.truth.frequency; + let c = t1.truth.frequency + * t2.truth.frequency + * t1.truth.confidence + * t2.truth.confidence; + let combined_rel = format!("{} (via {})", t2.relation, t1.object); + let mut triplet = Triplet::with_truth( + &t1.subject, + &t2.object, + &combined_rel, + TruthValue::new(f, c), + t2.timestamp.max(t1.timestamp), + ); + // Only keep inferences with meaningful confidence + if triplet.truth.confidence > 0.1 { + inferred.push(triplet); + } + } + } + } + inferred + } + + /// NARS contradiction detection: find triplets that conflict. + /// + /// Two triplets contradict if they share subject+object but have + /// different relations and both have high confidence. + /// Returns pairs of (triplet_index_a, triplet_index_b). + pub fn detect_contradictions(&self, confidence_threshold: f32) -> Vec<(usize, usize)> { + let mut contradictions = Vec::new(); + for (i, t1) in self.triplets.iter().enumerate() { + if t1.is_deleted() || t1.truth.confidence < confidence_threshold { + continue; + } + for (j, t2) in self.triplets.iter().enumerate().skip(i + 1) { + if t2.is_deleted() || t2.truth.confidence < confidence_threshold { + continue; + } + // Same subject and object but different relation + if t1.subject == t2.subject + && t1.object == t2.object + && t1.relation != t2.relation + { + contradictions.push((i, j)); + } + } + } + contradictions + } + + /// NARS revision: when the same triplet is observed again, increase confidence. + /// + /// Finds existing triplets matching subject+relation+object and revises + /// their truth value with new evidence. + pub fn revise_with_evidence(&mut self, observation: &Triplet) { + for triplet in self.triplets.iter_mut() { + if !triplet.is_deleted() + && triplet.subject == observation.subject + && triplet.relation == observation.relation + && triplet.object == observation.object + { + triplet.truth = triplet.truth.revision(&observation.truth); + triplet.timestamp = observation.timestamp.max(triplet.timestamp); + return; + } + } + // Not found — add as new + self.add_triplets(&[observation.clone()]); + } +} + +#[cfg(test)] +mod extended_tests { + use super::*; + + #[test] + fn test_clear_triplet_normalization() { + let (s, r, o) = clear_triplet("I", "is in", "kitchen"); + assert_eq!(s, "inventory"); + assert_eq!(r, "is in"); + assert_eq!(o, "kitchen"); + + let (s, _, _) = clear_triplet("P", "has", "sword"); + assert_eq!(s, "player"); + } + + #[test] + fn test_find_direction() { + assert_eq!(find_direction("go north"), "is north of"); + assert_eq!(find_direction("go east"), "is east of"); + assert_eq!(find_direction("go south"), "is south of"); + assert_eq!(find_direction("go west"), "is west of"); + assert_eq!(find_direction("teleport"), "can be achieved from"); + } + + #[test] + fn test_find_opposite_direction() { + assert_eq!(find_opposite_direction("go north"), "is south of"); + assert_eq!(find_opposite_direction("go east"), "is west of"); + } + + #[test] + fn test_process_triplets() { + let triplets = process_triplets( + "alice, knows, bob; 1.charlie, works at, company; Step 1: dave, lives in, city", + 1, + ); + assert_eq!(triplets.len(), 3); + assert_eq!(triplets[0].subject, "alice"); + assert_eq!(triplets[0].object, "bob"); + assert_eq!(triplets[2].subject, "dave"); + } + + #[test] + fn test_parse_triplets_removing() { + let patterns = parse_triplets_removing( + "[[alice, knows, bob -> alice, loves, bob], [cat, is in, box -> cat, is in, bag]]", + ); + assert_eq!(patterns.len(), 2); + assert_eq!(patterns[0].0, "alice"); + } + + #[test] + fn test_compute_spatial_graph() { + let mut g = TripletGraph::new(); + g.add_triplets(&[ + Triplet::new("room1", "room2", "is north of", 1), + Triplet::new("room2", "room3", "is east of", 2), + Triplet::new("alice", "bob", "knows", 3), + ]); + g.compute_spatial_graph(); + assert!(g.spatial_edges.contains_key("room1")); + assert!(g.spatial_edges.contains_key("room2")); + assert!(!g.spatial_edges.contains_key("alice")); + } + + #[test] + fn test_find_path_directions() { + let mut g = TripletGraph::new(); + g.add_spatial_edge("room1", "room2", "is north of"); + g.add_spatial_edge("room2", "room3", "is east of"); + let dirs = g.find_path_directions("room1", "room3").unwrap(); + assert_eq!(dirs.len(), 2); + assert!(dirs[0].contains("north")); + assert!(dirs[1].contains("east")); + } + + #[test] + fn test_exclude_filters_known() { + let mut g = TripletGraph::new(); + g.add_triplets(&[Triplet::new("alice", "bob", "knows", 1)]); + let candidates = vec![ + Triplet::new("alice", "bob", "knows", 2), + Triplet::new("carol", "dave", "knows", 2), + ]; + let new = g.exclude(&candidates); + assert_eq!(new.len(), 1); + assert_eq!(new[0].subject, "carol"); + } + + #[test] + fn test_delete_with_location_protection() { + let mut g = TripletGraph::new(); + g.add_triplets(&[ + Triplet::new("room1", "room2", "is north of", 1), + Triplet::new("alice", "bob", "knows", 2), + ]); + let locations: HashSet = ["room1", "room2"].into_iter().map(String::from).collect(); + g.delete_with_location_protection( + &[ + ("room1".into(), "is north of".into(), "room2".into()), + ("alice".into(), "knows".into(), "bob".into()), + ], + &locations, + ); + // room1→room2 protected (both are locations) + assert!(!g.triplets[0].is_deleted()); + // alice→bob not protected + assert!(g.triplets[1].is_deleted()); + } + + #[test] + fn test_full_update_cycle() { + let mut g = TripletGraph::new(); + let mut locations: HashSet = ["kitchen"].into_iter().map(String::from).collect(); + + let new_triplets = vec![ + Triplet::new("apple", "table", "is on", 1), + Triplet::new("knife", "kitchen", "is in", 1), + ]; + g.update(&new_triplets, &[], &mut locations, Some("bedroom"), Some("kitchen"), Some("go north")); + + assert!(g.len() >= 2); + assert!(locations.contains("bedroom")); + assert!(!g.spatial_edges.is_empty()); + } + + #[test] + fn test_infer_deductions() { + let mut g = TripletGraph::new(); + g.add_triplets(&[ + Triplet::new("alice", "bob", "knows", 1), + Triplet::new("bob", "carol", "knows", 2), + ]); + let inferred = g.infer_deductions(); + assert!(!inferred.is_empty()); + assert_eq!(inferred[0].subject, "alice"); + assert_eq!(inferred[0].object, "carol"); + assert!(inferred[0].truth.confidence > 0.0); + } + + #[test] + fn test_detect_contradictions() { + let mut g = TripletGraph::new(); + g.add_triplets(&[ + Triplet::new("apple", "table", "is on", 1), + Triplet::new("apple", "table", "is under", 2), + ]); + let contradictions = g.detect_contradictions(0.5); + assert_eq!(contradictions.len(), 1); + } + + #[test] + fn test_revise_with_evidence() { + let mut g = TripletGraph::new(); + g.add_triplets(&[Triplet::new("alice", "bob", "knows", 1)]); + let c_before = g.triplets[0].truth.confidence; + + g.revise_with_evidence(&Triplet::new("alice", "bob", "knows", 2)); + // Revision should increase confidence + assert!(g.triplets[0].truth.confidence >= c_before); + // Should NOT create a duplicate + assert_eq!(g.triplets.iter().filter(|t| !t.is_deleted()).count(), 1); + } + + #[test] + fn test_find_unexplored_exits() { + let triplets = vec![ + "kitchen, has exit, north".to_string(), + "kitchen, has exit, east".to_string(), + "bedroom, is north of, kitchen".to_string(), + ]; + let unexplored = find_unexplored_exits("kitchen", &triplets); + // north is explored (bedroom is north of kitchen), east is not + assert!(unexplored.contains(&"east".to_string())); + assert!(!unexplored.contains(&"north".to_string())); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/lance-graph/src/nsm/nsm_word.rs b/crates/lance-graph/src/nsm/nsm_word.rs index ea092dc3a..153b4e35e 100644 --- a/crates/lance-graph/src/nsm/nsm_word.rs +++ b/crates/lance-graph/src/nsm/nsm_word.rs @@ -85,7 +85,7 @@ impl NsmRuntime { let encoder = NsmEncoder::new( matrix, - RoleVectors::from_seed(0xADA_DEEP_NSM), + RoleVectors::from_seed(0xADA_DE00_0005_u64), prime_ranks, );