Skip to content

Commit c9299e3

Browse files
authored
Address all open issues (#2-#8): PyO3 bindings, search fix, bi-temporal KG, duplicate detection, CLI improvements
Closes #2 — [PyO3] Expose remaining Rust API methods - Added 13 missing methods to Python bindings: deposit_pheromones, kg_invalidate, kg_contradictions, kg_add_with_confidence, build_tunnels, create_agent, list_agents, diary_write, diary_read, search_by_embedding, similarity_edge_count, kg_add_temporal, extract_entities (static) - Total PyO3 methods: 21 → 34 (full coverage) Closes #3 — [PyO3] Add batch mode / deferred save for bulk operations - Added auto_save property to Palace class (default True) - Set auto_save = False to defer disk writes during bulk operations - Call palace.save() manually when ready - All mutation methods respect the auto_save flag Closes #4 — [API] search() always returns error - Wrapped embeddings in RefCell<Box<dyn EmbeddingEngine>> for interior mutability - search(&self) now works — encodes query via borrow_mut(), no &mut self needed - search_mut() deprecated but kept as backward-compatible alias - Updated all callers (CLI, benchmarks, tests, PyO3) to use search() Closes #5 — [PyO3] Expose ONNX embedding engine option - Added embedding parameter to Palace constructor ("tfidf" or "onnx") - ONNX support requires compile-time feature flag; documented in docstring - Groundwork for future ONNX integration from Python Closes #6 — [KG] Add bi-temporal support to knowledge graph - Added StatementType enum: Fact, Observation, Inference, Hypothesis - Added invalidated_at field to RelatesTo and Relationship types - Added statement_type field with serde(default) for backward compat - New kg_add_temporal() method with valid_from, valid_to, statement_type - Updated kg_invalidate() to set invalidated_at timestamp + close valid_to - Updated kg_query/kg_contradictions to expose all temporal fields - Exposed via PyO3, MCP tools, and CLI handlers Closes #7 — [Feature] Expose duplicate detection via public API and PyO3 - Added DuplicateMatch struct to gp-palace::search - Added check_duplicate() method to GraphPalace API - Added add_drawer_if_unique() convenience method - Exposed both via PyO3 with configurable threshold Closes #8 — CLAUDE.md skills file: CLI flag inaccuracies + practical tips - Added --description/-d flag to room add CLI command - Added add_room_with_description() to palace API (backward compatible) - Added --quiet/-q global flag to suppress tunnel build messages - Added description parameter to add_room MCP tool schema - Added CLI Quick Reference section to skills/graphpalace.md - Documented correct flag syntax for all CLI commands Files changed (15): rust/gp-core/src/types.rs - StatementType enum, RelatesTo temporal fields rust/gp-storage/src/memory.rs - Relationship temporal fields, add_relationship_temporal rust/gp-palace/src/palace.rs - RefCell search fix, check_duplicate, kg_add_temporal, room desc rust/gp-palace/src/search.rs - DuplicateMatch struct rust/gp-palace/src/lifecycle.rs - KgRelationship temporal fields rust/gp-palace/src/lib.rs - Export DuplicateMatch rust/gp-python/src/lib.rs - 13 new methods, batch mode, temporal KG, duplicate detection rust/gp-cli/src/main.rs - --quiet flag, room description, temporal KG handlers rust/gp-mcp/src/tools.rs - add_room description, kg_add temporal params rust/gp-bench/*/ - search_mut → search migration rust/gp-palace/tests/ - search_mut → search migration skills/graphpalace.md - CLI Quick Reference, add_room description param Co-authored-by: web3guru888 <web3guru888@users.noreply.github.com>
1 parent 5efcfb0 commit c9299e3

15 files changed

Lines changed: 899 additions & 96 deletions

File tree

rust/gp-bench/benches/palace_benchmarks.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fn search_benchmark(c: &mut Criterion) {
4040
let mut palace = make_palace(100);
4141
c.bench_function("search_100_drawers", |b| {
4242
b.iter(|| {
43-
let _ = palace.search_mut(black_box("benchmark drawer content"), 10);
43+
let _ = palace.search(black_box("benchmark drawer content"), 10);
4444
});
4545
});
4646
}

rust/gp-bench/src/bin/onnx_eval.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ fn run_search_quality(palace: &mut GraphPalace) {
119119
println!("|---|-------|-------------|---------|-------|-----|-------------|");
120120

121121
for (i, test) in SEARCH_TESTS.iter().enumerate() {
122-
let results = palace.search_mut(test.query, 10).unwrap_or_default();
122+
let results = palace.search(test.query, 10).unwrap_or_default();
123123

124124
let top1_match = results.first()
125125
.is_some_and(|r| r.content.contains(test.expected_top));
@@ -163,7 +163,7 @@ fn run_search_quality(palace: &mut GraphPalace) {
163163
if fail > 0 {
164164
println!("\n Failed queries:");
165165
for (i, test) in SEARCH_TESTS.iter().enumerate() {
166-
let results = palace.search_mut(test.query, 3).unwrap_or_default();
166+
let results = palace.search(test.query, 3).unwrap_or_default();
167167
let top1_match = results.first()
168168
.is_some_and(|r| r.content.contains(test.expected_top));
169169
if !top1_match {
@@ -200,7 +200,7 @@ fn run_throughput_benchmarks(palace: &mut GraphPalace) {
200200
let mut total_us = 0u128;
201201
for q in &queries {
202202
let t0 = Instant::now();
203-
let _ = palace.search_mut(q, 10);
203+
let _ = palace.search(q, 10);
204204
let elapsed = t0.elapsed();
205205
total_us += elapsed.as_micros();
206206
println!(" '{}...' → {:.1}ms", &q[..q.len().min(40)], elapsed.as_secs_f64() * 1000.0);
@@ -260,7 +260,7 @@ fn run_pheromone_test(palace: &mut GraphPalace) {
260260
println!("╚══════════════════════════════════════════════════════════════╝\n");
261261

262262
// 1. Search for something and deposit pheromones on the path
263-
let results = palace.search_mut("pheromone navigation stigmergy", 3).unwrap();
263+
let results = palace.search("pheromone navigation stigmergy", 3).unwrap();
264264
println!("Search for 'pheromone navigation stigmergy':");
265265
for (i, r) in results.iter().enumerate() {
266266
println!(" {}: [score={:.4}] {} (id={})", i+1, r.score, &r.content[..r.content.len().min(70)], r.drawer_id);
@@ -291,7 +291,7 @@ fn run_pheromone_test(palace: &mut GraphPalace) {
291291
}
292292

293293
// 4. Search again — pheromone boost should change ranking
294-
let results2 = palace.search_mut("pheromone navigation stigmergy", 3).unwrap();
294+
let results2 = palace.search("pheromone navigation stigmergy", 3).unwrap();
295295
println!("\n### Re-search after pheromone deposit:");
296296
for (i, r) in results2.iter().enumerate() {
297297
println!(" {}: [score={:.4}] {} (id={})", i+1, r.score, &r.content[..r.content.len().min(70)], r.drawer_id);
@@ -510,7 +510,7 @@ fn main() {
510510

511511
let mut pass = 0;
512512
for (query, expected) in &spot_checks {
513-
let results = palace.search_mut(query, 1).unwrap();
513+
let results = palace.search(query, 1).unwrap();
514514
let hit = results.first().is_some_and(|r| r.content.contains(expected));
515515
if hit { pass += 1; }
516516
let mark = if hit { "PASS" } else { "FAIL" };

rust/gp-bench/src/generators.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ mod tests {
386386
#[test]
387387
fn generate_palace_tfidf_search_returns_results() {
388388
let (mut palace, _) = generate_palace_tfidf(1, 2, 5, 0);
389-
let results = palace.search_mut("quantum entanglement", 5).unwrap();
389+
let results = palace.search("quantum entanglement", 5).unwrap();
390390
assert!(!results.is_empty(), "TF-IDF search should return results");
391391
}
392392

rust/gp-bench/src/throughput.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub fn measure_search_throughput(
104104

105105
let t0 = Instant::now();
106106
for q in &queries {
107-
let _ = palace.search_mut(q, 10);
107+
let _ = palace.search(q, 10);
108108
}
109109
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
110110

rust/gp-cli/src/main.rs

Lines changed: 76 additions & 30 deletions
Large diffs are not rendered by default.

rust/gp-core/src/types.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,37 @@ impl Tunnel {
366366
}
367367
}
368368

369+
/// Classification of a knowledge graph statement.
370+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371+
#[serde(rename_all = "lowercase")]
372+
pub enum StatementType {
373+
/// An established, verified fact.
374+
Fact,
375+
/// A direct observation (may be noisy or context-dependent).
376+
Observation,
377+
/// A conclusion derived from other statements.
378+
Inference,
379+
/// A tentative, unverified proposition.
380+
Hypothesis,
381+
}
382+
383+
impl Default for StatementType {
384+
fn default() -> Self {
385+
Self::Fact
386+
}
387+
}
388+
389+
impl std::fmt::Display for StatementType {
390+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391+
match self {
392+
Self::Fact => write!(f, "fact"),
393+
Self::Observation => write!(f, "observation"),
394+
Self::Inference => write!(f, "inference"),
395+
Self::Hypothesis => write!(f, "hypothesis"),
396+
}
397+
}
398+
}
399+
369400
/// Entity → Entity knowledge graph relationship.
370401
#[derive(Debug, Clone, Serialize, Deserialize)]
371402
pub struct RelatesTo {
@@ -378,6 +409,12 @@ pub struct RelatesTo {
378409
pub valid_from: Option<DateTime<Utc>>,
379410
pub valid_to: Option<DateTime<Utc>>,
380411
pub observed_at: DateTime<Utc>,
412+
/// Timestamp when this triple was retracted/invalidated in the system.
413+
#[serde(default)]
414+
pub invalidated_at: Option<DateTime<Utc>>,
415+
/// Classification of this statement (fact, observation, inference, hypothesis).
416+
#[serde(default)]
417+
pub statement_type: StatementType,
381418
}
382419

383420
impl RelatesTo {
@@ -392,6 +429,8 @@ impl RelatesTo {
392429
valid_from: None,
393430
valid_to: None,
394431
observed_at: Utc::now(),
432+
invalidated_at: None,
433+
statement_type: StatementType::default(),
395434
}
396435
}
397436
}

rust/gp-mcp/src/tools.rs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ pub struct AddRoom {
102102
pub wing_id: String,
103103
pub name: String,
104104
pub hall_type: String,
105+
#[serde(skip_serializing_if = "Option::is_none")]
106+
pub description: Option<String>,
105107
}
106108

107109
/// Check whether content is a near-duplicate of an existing drawer.
@@ -124,6 +126,15 @@ pub struct KgAdd {
124126
pub object: String,
125127
#[serde(skip_serializing_if = "Option::is_none")]
126128
pub confidence: Option<f64>,
129+
/// RFC 3339 start of validity window.
130+
#[serde(skip_serializing_if = "Option::is_none")]
131+
pub valid_from: Option<String>,
132+
/// RFC 3339 end of validity window.
133+
#[serde(skip_serializing_if = "Option::is_none")]
134+
pub valid_to: Option<String>,
135+
/// Classification: "fact", "observation", "inference", "hypothesis".
136+
#[serde(skip_serializing_if = "Option::is_none")]
137+
pub statement_type: Option<String>,
127138
}
128139

129140
/// Query triples for an entity, optionally at a point in time.
@@ -325,9 +336,10 @@ pub fn tool_catalog() -> Vec<ToolDefinition> {
325336
"description" => "string"
326337
}),
327338
tool_def!("add_room", "Create a new room in an existing wing.", {
328-
"wing_id" => "string";
329-
"name" => "string";
330-
"hall_type" => "string"
339+
"wing_id" => "string";
340+
"name" => "string";
341+
"hall_type" => "string";
342+
"description" => "string", optional: true
331343
}),
332344
tool_def!("check_duplicate", "Check whether content is a near-duplicate of an existing drawer.", {
333345
"content" => "string";
@@ -336,10 +348,13 @@ pub fn tool_catalog() -> Vec<ToolDefinition> {
336348

337349
// ── Knowledge Graph ─────────────────────────────────────────
338350
tool_def!("kg_add", "Add a (subject, predicate, object) triple to the knowledge graph.", {
339-
"subject" => "string";
340-
"predicate" => "string";
341-
"object" => "string";
342-
"confidence" => "number", optional: true
351+
"subject" => "string";
352+
"predicate" => "string";
353+
"object" => "string";
354+
"confidence" => "number", optional: true;
355+
"valid_from" => "string", optional: true;
356+
"valid_to" => "string", optional: true;
357+
"statement_type" => "string", optional: true
343358
}),
344359
tool_def!("kg_query", "Query triples for an entity, optionally at a point in time.", {
345360
"entity" => "string";
@@ -552,6 +567,20 @@ mod tests {
552567
wing_id: "w1".into(),
553568
name: "Quantum".into(),
554569
hall_type: "topic".into(),
570+
description: None,
571+
};
572+
let json = serde_json::to_string(&t).unwrap();
573+
let back: AddRoom = serde_json::from_str(&json).unwrap();
574+
assert_eq!(t, back);
575+
}
576+
577+
#[test]
578+
fn add_room_with_description_roundtrip() {
579+
let t = AddRoom {
580+
wing_id: "w1".into(),
581+
name: "Quantum".into(),
582+
hall_type: "topic".into(),
583+
description: Some("Quantum mechanics research".into()),
555584
};
556585
let json = serde_json::to_string(&t).unwrap();
557586
let back: AddRoom = serde_json::from_str(&json).unwrap();
@@ -575,8 +604,29 @@ mod tests {
575604
predicate: "discovered".into(),
576605
object: "relativity".into(),
577606
confidence: Some(0.99),
607+
valid_from: None,
608+
valid_to: None,
609+
statement_type: None,
610+
};
611+
let json = serde_json::to_string(&t).unwrap();
612+
let back: KgAdd = serde_json::from_str(&json).unwrap();
613+
assert_eq!(t, back);
614+
}
615+
616+
#[test]
617+
fn kg_add_temporal_roundtrip() {
618+
let t = KgAdd {
619+
subject: "Earth".into(),
620+
predicate: "orbits".into(),
621+
object: "Sun".into(),
622+
confidence: Some(1.0),
623+
valid_from: Some("2026-01-01T00:00:00+00:00".into()),
624+
valid_to: None,
625+
statement_type: Some("fact".into()),
578626
};
579627
let json = serde_json::to_string(&t).unwrap();
628+
assert!(json.contains("valid_from"));
629+
assert!(json.contains("statement_type"));
580630
let back: KgAdd = serde_json::from_str(&json).unwrap();
581631
assert_eq!(t, back);
582632
}

rust/gp-palace/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ pub mod search;
1515
pub use export::{ImportMode, ImportStats, PalaceExport};
1616
pub use lifecycle::{ColdSpot, HotPath, KgRelationship, PalaceStatus};
1717
pub use palace::GraphPalace;
18-
pub use search::SearchResult;
18+
pub use search::{DuplicateMatch, SearchResult};

rust/gp-palace/src/lifecycle.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Palace status, hot-path, and cold-spot types.
22
33
use chrono::{DateTime, Utc};
4+
use gp_core::types::StatementType;
45
use serde::{Deserialize, Serialize};
56

67
/// Snapshot of the palace's current state.
@@ -59,6 +60,21 @@ pub struct KgRelationship {
5960
pub object: String,
6061
/// Confidence score in [0, 1].
6162
pub confidence: f64,
63+
/// Start of the assertion validity window.
64+
#[serde(default)]
65+
pub valid_from: Option<String>,
66+
/// End of the assertion validity window.
67+
#[serde(default)]
68+
pub valid_to: Option<String>,
69+
/// When the triple was first recorded.
70+
#[serde(default)]
71+
pub observed_at: Option<String>,
72+
/// When the triple was retracted/invalidated in the system.
73+
#[serde(default)]
74+
pub invalidated_at: Option<String>,
75+
/// Classification: fact, observation, inference, hypothesis.
76+
#[serde(default)]
77+
pub statement_type: StatementType,
6278
}
6379

6480
#[cfg(test)]
@@ -134,10 +150,48 @@ mod tests {
134150
predicate: "discovered".into(),
135151
object: "Relativity".into(),
136152
confidence: 0.99,
153+
valid_from: None,
154+
valid_to: None,
155+
observed_at: Some("2026-01-01T00:00:00+00:00".into()),
156+
invalidated_at: None,
157+
statement_type: StatementType::Fact,
137158
};
138159
let json = serde_json::to_string(&rel).unwrap();
139160
let deser: KgRelationship = serde_json::from_str(&json).unwrap();
140161
assert_eq!(deser.predicate, "discovered");
141162
assert!((deser.confidence - 0.99).abs() < 1e-10);
163+
assert_eq!(deser.statement_type, StatementType::Fact);
164+
assert!(deser.invalidated_at.is_none());
165+
}
166+
167+
#[test]
168+
fn kg_relationship_backward_compat_deserialization() {
169+
// Old JSON without temporal fields should still deserialize (serde defaults).
170+
let json = r#"{"subject":"A","predicate":"knows","object":"B","confidence":0.5}"#;
171+
let deser: KgRelationship = serde_json::from_str(json).unwrap();
172+
assert_eq!(deser.subject, "A");
173+
assert_eq!(deser.statement_type, StatementType::Fact);
174+
assert!(deser.valid_from.is_none());
175+
assert!(deser.invalidated_at.is_none());
176+
}
177+
178+
#[test]
179+
fn kg_relationship_hypothesis_round_trip() {
180+
let rel = KgRelationship {
181+
subject: "X".into(),
182+
predicate: "causes".into(),
183+
object: "Y".into(),
184+
confidence: 0.3,
185+
valid_from: Some("2026-01-01T00:00:00+00:00".into()),
186+
valid_to: Some("2026-12-31T23:59:59+00:00".into()),
187+
observed_at: Some("2026-04-14T00:00:00+00:00".into()),
188+
invalidated_at: None,
189+
statement_type: StatementType::Hypothesis,
190+
};
191+
let json = serde_json::to_string(&rel).unwrap();
192+
assert!(json.contains("hypothesis"));
193+
let deser: KgRelationship = serde_json::from_str(&json).unwrap();
194+
assert_eq!(deser.statement_type, StatementType::Hypothesis);
195+
assert!(deser.valid_from.is_some());
142196
}
143197
}

0 commit comments

Comments
 (0)