use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use std::collections::HashMap;
use std::path::PathBuf;
use crate::memory::types::{
Experience, ExperienceType, ForgetCriteria, GeoFilter, Memory, MemoryId,
};
use crate::memory::{MemoryConfig, MemorySystem, Query, RetrievalMode};
use chrono::{DateTime, Utc};
#[pyclass(name = "Position")]
#[derive(Clone, Debug)]
pub struct PyPosition {
#[pyo3(get, set)]
pub x: f32,
#[pyo3(get, set)]
pub y: f32,
#[pyo3(get, set)]
pub z: f32,
}
#[pymethods]
impl PyPosition {
#[new]
#[pyo3(signature = (x=0.0, y=0.0, z=0.0))]
fn new(x: f32, y: f32, z: f32) -> Self {
PyPosition { x, y, z }
}
fn distance_to(&self, other: &PyPosition) -> f32 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2))
.sqrt()
}
fn to_list(&self) -> Vec<f32> {
vec![self.x, self.y, self.z]
}
fn __repr__(&self) -> String {
format!("Position(x={}, y={}, z={})", self.x, self.y, self.z)
}
}
#[pyclass(name = "GeoLocation")]
#[derive(Clone, Debug)]
pub struct PyGeoLocation {
#[pyo3(get, set)]
pub latitude: f64,
#[pyo3(get, set)]
pub longitude: f64,
#[pyo3(get, set)]
pub altitude: f64,
}
#[pymethods]
impl PyGeoLocation {
#[new]
#[pyo3(signature = (latitude=0.0, longitude=0.0, altitude=0.0))]
fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
PyGeoLocation {
latitude,
longitude,
altitude,
}
}
fn to_list(&self) -> Vec<f64> {
vec![self.latitude, self.longitude, self.altitude]
}
fn __repr__(&self) -> String {
format!(
"GeoLocation(lat={}, lon={}, alt={})",
self.latitude, self.longitude, self.altitude
)
}
}
#[pyclass(name = "GeoFilter")]
#[derive(Clone, Debug)]
pub struct PyGeoFilter {
#[pyo3(get, set)]
pub latitude: f64,
#[pyo3(get, set)]
pub longitude: f64,
#[pyo3(get, set)]
pub radius_meters: f64,
}
#[pymethods]
impl PyGeoFilter {
#[new]
#[pyo3(signature = (latitude, longitude, radius_meters))]
fn new(latitude: f64, longitude: f64, radius_meters: f64) -> Self {
PyGeoFilter {
latitude,
longitude,
radius_meters,
}
}
#[staticmethod]
fn from_center(center: &PyGeoLocation, radius_meters: f64) -> Self {
PyGeoFilter {
latitude: center.latitude,
longitude: center.longitude,
radius_meters,
}
}
fn __repr__(&self) -> String {
format!(
"GeoFilter(lat={}, lon={}, radius={}m)",
self.latitude, self.longitude, self.radius_meters
)
}
}
#[pyclass(name = "DecisionContext")]
#[derive(Clone, Debug, Default)]
pub struct PyDecisionContext {
#[pyo3(get, set)]
pub state: HashMap<String, String>,
#[pyo3(get, set)]
pub action_params: HashMap<String, String>,
#[pyo3(get, set)]
pub confidence: Option<f32>,
#[pyo3(get, set)]
pub alternatives: Vec<String>,
}
#[pymethods]
impl PyDecisionContext {
#[new]
#[pyo3(signature = (state=None, action_params=None, confidence=None, alternatives=None))]
fn new(
state: Option<HashMap<String, String>>,
action_params: Option<HashMap<String, String>>,
confidence: Option<f32>,
alternatives: Option<Vec<String>>,
) -> Self {
PyDecisionContext {
state: state.unwrap_or_default(),
action_params: action_params.unwrap_or_default(),
confidence,
alternatives: alternatives.unwrap_or_default(),
}
}
fn __repr__(&self) -> String {
format!(
"DecisionContext(state={:?}, confidence={:?})",
self.state, self.confidence
)
}
}
#[pyclass(name = "Outcome")]
#[derive(Clone, Debug, Default)]
pub struct PyOutcome {
#[pyo3(get, set)]
pub outcome_type: String,
#[pyo3(get, set)]
pub details: Option<String>,
#[pyo3(get, set)]
pub reward: Option<f32>,
#[pyo3(get, set)]
pub prediction_accurate: Option<bool>,
}
#[pymethods]
impl PyOutcome {
#[new]
#[pyo3(signature = (outcome_type, details=None, reward=None, prediction_accurate=None))]
fn new(
outcome_type: String,
details: Option<String>,
reward: Option<f32>,
prediction_accurate: Option<bool>,
) -> Self {
PyOutcome {
outcome_type,
details,
reward,
prediction_accurate,
}
}
fn is_success(&self) -> bool {
self.outcome_type == "success"
}
fn is_failure(&self) -> bool {
self.outcome_type == "failure"
}
fn __repr__(&self) -> String {
format!(
"Outcome(type={}, reward={:?})",
self.outcome_type, self.reward
)
}
}
#[pyclass(name = "Environment")]
#[derive(Clone, Debug, Default)]
pub struct PyEnvironment {
#[pyo3(get, set)]
pub weather: HashMap<String, String>,
#[pyo3(get, set)]
pub terrain_type: Option<String>,
#[pyo3(get, set)]
pub lighting: Option<String>,
#[pyo3(get, set)]
pub nearby_agents: Vec<HashMap<String, String>>,
}
#[pymethods]
impl PyEnvironment {
#[new]
#[pyo3(signature = (weather=None, terrain_type=None, lighting=None, nearby_agents=None))]
fn new(
weather: Option<HashMap<String, String>>,
terrain_type: Option<String>,
lighting: Option<String>,
nearby_agents: Option<Vec<HashMap<String, String>>>,
) -> Self {
PyEnvironment {
weather: weather.unwrap_or_default(),
terrain_type,
lighting,
nearby_agents: nearby_agents.unwrap_or_default(),
}
}
fn __repr__(&self) -> String {
format!(
"Environment(terrain={:?}, lighting={:?})",
self.terrain_type, self.lighting
)
}
}
#[pyclass(name = "MemorySystem")]
pub struct PyMemorySystem {
inner: MemorySystem,
robot_id: Option<String>,
mission_id: Option<String>,
}
#[pymethods]
impl PyMemorySystem {
#[new]
#[pyo3(signature = (storage_path=None, robot_id=None))]
fn new(storage_path: Option<String>, robot_id: Option<String>) -> PyResult<Self> {
let path = storage_path
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("./shodh_data"));
let config = MemoryConfig {
storage_path: path,
..Default::default()
};
let inner = MemorySystem::new(config, None).map_err(|e| {
PyRuntimeError::new_err(format!("Failed to create memory system: {}", e))
})?;
Ok(PyMemorySystem {
inner,
robot_id,
mission_id: None,
})
}
fn start_mission(&mut self, mission_id: String) {
self.mission_id = Some(mission_id);
}
fn end_mission(&mut self) {
self.mission_id = None;
}
fn current_mission(&self) -> Option<String> {
self.mission_id.clone()
}
#[pyo3(signature = (
content,
memory_type="observation",
position=None,
geo_location=None,
heading=None,
action_type=None,
sensor_data=None,
decision_context=None,
outcome=None,
environment=None,
is_failure=false,
is_anomaly=false,
severity=None,
recovery_action=None,
root_cause=None,
pattern_id=None,
predicted_outcome=None,
tags=None,
entities=None,
metadata=None
))]
#[allow(clippy::too_many_arguments)]
fn remember(
&mut self,
content: String,
memory_type: &str,
position: Option<&PyPosition>,
geo_location: Option<&PyGeoLocation>,
heading: Option<f32>,
action_type: Option<String>,
sensor_data: Option<HashMap<String, f64>>,
decision_context: Option<&PyDecisionContext>,
outcome: Option<&PyOutcome>,
environment: Option<&PyEnvironment>,
is_failure: bool,
is_anomaly: bool,
severity: Option<String>,
recovery_action: Option<String>,
root_cause: Option<String>,
pattern_id: Option<String>,
predicted_outcome: Option<String>,
tags: Option<Vec<String>>,
entities: Option<Vec<String>>,
metadata: Option<HashMap<String, String>>,
) -> PyResult<String> {
let exp_type = match memory_type.to_lowercase().as_str() {
"observation" | "context" => ExperienceType::Context,
"task" => ExperienceType::Task,
"discovery" => ExperienceType::Discovery,
"error" => ExperienceType::Error,
"pattern" => ExperienceType::Pattern,
"learning" => ExperienceType::Learning,
"decision" => ExperienceType::Decision,
"conversation" => ExperienceType::Conversation,
other => {
tracing::warn!("Unknown memory_type '{}', falling back to Context", other);
ExperienceType::Context
}
};
let experience = Experience {
experience_type: exp_type,
content,
context: None,
entities: entities.unwrap_or_default(),
metadata: metadata.unwrap_or_default(),
embeddings: None,
related_memories: vec![],
causal_chain: vec![],
outcomes: outcome
.as_ref()
.and_then(|o| o.details.clone())
.map(|d| vec![d])
.unwrap_or_default(),
robot_id: self.robot_id.clone(),
mission_id: self.mission_id.clone(),
geo_location: geo_location.map(|g| [g.latitude, g.longitude, g.altitude]),
local_position: position.map(|p| [p.x, p.y, p.z]),
heading,
action_type,
reward: outcome.as_ref().and_then(|o| o.reward),
sensor_data: sensor_data.unwrap_or_default(),
decision_context: decision_context.map(|d| d.state.clone()),
action_params: decision_context.map(|d| d.action_params.clone()),
outcome_type: outcome.map(|o| o.outcome_type.clone()),
outcome_details: outcome.and_then(|o| o.details.clone()),
confidence: decision_context.and_then(|d| d.confidence),
alternatives_considered: decision_context
.map(|d| d.alternatives.clone())
.unwrap_or_default(),
weather: environment.map(|e| e.weather.clone()),
terrain_type: environment.and_then(|e| e.terrain_type.clone()),
lighting: environment.and_then(|e| e.lighting.clone()),
nearby_agents: environment
.map(|e| e.nearby_agents.clone())
.unwrap_or_default(),
is_failure,
is_anomaly,
severity,
recovery_action,
root_cause,
pattern_id,
predicted_outcome,
prediction_accurate: outcome.and_then(|o| o.prediction_accurate),
tags: tags.unwrap_or_default(),
image_embeddings: None,
audio_embeddings: None,
video_embeddings: None,
media_refs: vec![],
temporal_refs: vec![],
ner_entities: vec![],
cooccurrence_pairs: vec![],
};
let memory_id = self
.inner
.remember(experience, None)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to record memory: {}", e)))?;
Ok(memory_id.0.to_string())
}
#[pyo3(signature = (description, action_type, decision_context, outcome, position=None, geo_location=None, sensor_data=None))]
fn record_decision(
&mut self,
description: String,
action_type: String,
decision_context: &PyDecisionContext,
outcome: &PyOutcome,
position: Option<&PyPosition>,
geo_location: Option<&PyGeoLocation>,
sensor_data: Option<HashMap<String, f64>>,
) -> PyResult<String> {
self.remember(
description,
"decision",
position,
geo_location,
None,
Some(action_type),
sensor_data,
Some(decision_context),
Some(outcome),
None,
outcome.is_failure(),
false,
None,
None,
None,
None,
None,
Some(vec!["decision".to_string()]),
None,
None,
)
}
#[pyo3(signature = (description, severity, root_cause=None, recovery_action=None, position=None, sensor_data=None))]
fn record_failure(
&mut self,
description: String,
severity: String,
root_cause: Option<String>,
recovery_action: Option<String>,
position: Option<&PyPosition>,
sensor_data: Option<HashMap<String, f64>>,
) -> PyResult<String> {
self.remember(
description,
"error",
position,
None,
None,
None,
sensor_data,
None,
None,
None,
true,
false,
Some(severity),
recovery_action,
root_cause,
None,
None,
Some(vec!["failure".to_string()]),
None,
None,
)
}
#[pyo3(signature = (description, sensor_data, severity="warning", position=None))]
fn record_anomaly(
&mut self,
description: String,
sensor_data: HashMap<String, f64>,
severity: &str,
position: Option<&PyPosition>,
) -> PyResult<String> {
self.remember(
description,
"discovery",
position,
None,
None,
None,
Some(sensor_data),
None,
None,
None,
false,
true,
Some(severity.to_string()),
None,
None,
None,
None,
Some(vec!["anomaly".to_string()]),
None,
None,
)
}
#[pyo3(signature = (sensor_name, readings, pattern_id=None, is_anomaly=false, position=None))]
fn record_sensor(
&mut self,
sensor_name: String,
readings: HashMap<String, f64>,
pattern_id: Option<String>,
is_anomaly: bool,
position: Option<&PyPosition>,
) -> PyResult<String> {
let mut tags = vec!["sensor".to_string(), sensor_name.clone()];
if is_anomaly {
tags.push("anomaly".to_string());
}
self.remember(
format!("Sensor {}: {:?}", sensor_name, readings),
if is_anomaly { "error" } else { "observation" },
position,
None,
None,
Some("sensor_reading".to_string()),
Some(readings),
None,
None,
None,
false,
is_anomaly,
None,
None,
None,
pattern_id,
None,
Some(tags),
Some(vec![sensor_name]),
None,
)
}
#[pyo3(signature = (description, distance=None, confidence=None, position=None, geo_location=None))]
fn record_obstacle(
&mut self,
description: String,
distance: Option<f64>,
confidence: Option<f64>,
position: Option<&PyPosition>,
geo_location: Option<&PyGeoLocation>,
) -> PyResult<String> {
let mut sensor_data = HashMap::new();
if let Some(d) = distance {
sensor_data.insert("distance".to_string(), d);
}
if let Some(c) = confidence {
sensor_data.insert("confidence".to_string(), c);
}
self.remember(
format!("Obstacle detected: {}", description),
"discovery",
position,
geo_location,
None,
Some("obstacle_detection".to_string()),
Some(sensor_data),
None,
None,
None,
false,
false,
None,
None,
None,
None,
None,
Some(vec!["obstacle".to_string()]),
Some(vec!["obstacle".to_string(), description.to_lowercase()]),
None,
)
}
#[pyo3(signature = (waypoint_id, status="reached", position=None, geo_location=None))]
fn record_waypoint(
&mut self,
waypoint_id: String,
status: &str,
position: Option<&PyPosition>,
geo_location: Option<&PyGeoLocation>,
) -> PyResult<String> {
self.remember(
format!("Waypoint {}: {}", waypoint_id, status),
"task",
position,
geo_location,
None,
Some("navigation".to_string()),
None,
None,
None,
None,
false,
false,
None,
None,
None,
None,
None,
Some(vec!["waypoint".to_string(), "navigation".to_string()]),
Some(vec!["waypoint".to_string(), waypoint_id]),
None,
)
}
#[pyo3(signature = (
query,
limit=10,
mode="hybrid",
mission_id=None,
action_type=None,
geo_filter=None,
min_importance=None,
outcome_type=None,
failures_only=false,
anomalies_only=false,
severity=None,
tags=None,
pattern_id=None,
terrain_type=None,
min_confidence=None,
max_confidence=None
))]
#[allow(clippy::too_many_arguments)]
fn recall(
&self,
query: String,
limit: usize,
mode: &str,
mission_id: Option<String>,
action_type: Option<String>,
geo_filter: Option<&PyGeoFilter>,
min_importance: Option<f32>,
outcome_type: Option<String>,
failures_only: bool,
anomalies_only: bool,
severity: Option<String>,
tags: Option<Vec<String>>,
pattern_id: Option<String>,
terrain_type: Option<String>,
min_confidence: Option<f32>,
max_confidence: Option<f32>,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let retrieval_mode = match mode.to_lowercase().as_str() {
"semantic" | "similarity" => RetrievalMode::Similarity,
"temporal" => RetrievalMode::Temporal,
"hybrid" => RetrievalMode::Hybrid,
"causal" => RetrievalMode::Causal,
"associative" => RetrievalMode::Associative,
"spatial" => RetrievalMode::Spatial,
"mission" => RetrievalMode::Mission,
"action_outcome" => RetrievalMode::ActionOutcome,
_ => {
return Err(PyValueError::new_err(format!(
"Unknown retrieval mode: {}. Valid modes: semantic, temporal, hybrid, causal, associative, spatial, mission, action_outcome",
mode
)))
}
};
let confidence_range = match (min_confidence, max_confidence) {
(Some(min), Some(max)) => Some((min, max)),
(Some(min), None) => Some((min, 1.0)),
(None, Some(max)) => Some((0.0, max)),
(None, None) => None,
};
let query_obj = Query {
user_id: None,
query_text: Some(query),
query_embedding: None,
time_range: None,
experience_types: None,
importance_threshold: min_importance,
robot_id: self.robot_id.clone(),
mission_id,
geo_filter: geo_filter
.map(|f| GeoFilter::new(f.latitude, f.longitude, f.radius_meters)),
action_type,
reward_range: None,
outcome_type,
failures_only,
anomalies_only,
severity,
tags,
pattern_id,
terrain_type,
confidence_range,
prospective_signals: None,
episode_id: None,
recency_weight: None,
max_results: limit,
retrieval_mode,
offset: 0,
};
let memories = self
.inner
.recall(&query_obj)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to recall memories: {}", e)))?;
Python::with_gil(|py| {
memories
.iter()
.map(|mem| memory_to_dict(py, mem))
.collect::<PyResult<Vec<_>>>()
})
}
#[pyo3(signature = (action_type, decision_context=None, max_results=10))]
fn find_similar_decisions(
&self,
action_type: String,
decision_context: Option<&PyDecisionContext>,
max_results: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let query_text = if let Some(ctx) = decision_context {
format!("action {} with context {:?}", action_type, ctx.state)
} else {
format!("action {}", action_type)
};
self.recall(
query_text,
max_results,
"action_outcome",
None,
Some(action_type),
None,
None,
None,
false,
false,
None,
None,
None,
None,
None,
None,
)
}
#[pyo3(signature = (action_type=None, severity=None, max_results=20))]
fn find_failures(
&self,
action_type: Option<String>,
severity: Option<String>,
max_results: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
self.recall(
"failure error problem".to_string(),
max_results,
"hybrid",
None,
action_type,
None,
None,
Some("failure".to_string()),
true,
false,
severity,
None,
None,
None,
None,
None,
)
}
#[pyo3(signature = (sensor_name=None, max_results=20))]
fn find_anomalies(
&self,
sensor_name: Option<String>,
max_results: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let tags = sensor_name.map(|s| vec![s, "anomaly".to_string()]);
self.recall(
"anomaly unusual unexpected".to_string(),
max_results,
"hybrid",
None,
None,
None,
None,
None,
false,
true,
None,
tags,
None,
None,
None,
None,
)
}
#[pyo3(signature = (pattern_id, max_results=20))]
fn find_by_pattern(
&self,
pattern_id: String,
max_results: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
self.recall(
format!("pattern {}", pattern_id),
max_results,
"hybrid",
None,
None,
None,
None,
None,
false,
false,
None,
None,
Some(pattern_id),
None,
None,
None,
)
}
fn get_stats(&self) -> PyResult<HashMap<String, usize>> {
let stats = self.inner.stats();
let mut result = HashMap::new();
result.insert("total_memories".to_string(), stats.total_memories);
result.insert(
"working_memory_count".to_string(),
stats.working_memory_count,
);
result.insert(
"session_memory_count".to_string(),
stats.session_memory_count,
);
result.insert(
"long_term_memory_count".to_string(),
stats.long_term_memory_count,
);
result.insert("compressed_count".to_string(), stats.compressed_count);
result.insert(
"promotions_to_session".to_string(),
stats.promotions_to_session,
);
result.insert(
"promotions_to_longterm".to_string(),
stats.promotions_to_longterm,
);
result.insert("total_retrievals".to_string(), stats.total_retrievals);
Ok(result)
}
fn flush(&self) -> PyResult<()> {
self.inner
.flush_storage()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to flush: {}", e)))
}
#[pyo3(signature = (max_items=5, include_decisions=true, include_learnings=true, include_context=true))]
fn context_summary(
&self,
max_items: usize,
include_decisions: bool,
include_learnings: bool,
include_context: bool,
) -> PyResult<HashMap<String, PyObject>> {
let all_memories = self
.inner
.get_all_memories()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to get memories: {}", e)))?;
let total_memories = all_memories.len();
let mut decisions: Vec<(String, String, f32, String)> = Vec::new();
let mut learnings: Vec<(String, String, f32, String)> = Vec::new();
let mut context: Vec<(String, String, f32, String)> = Vec::new();
let mut patterns: Vec<(String, String, f32, String)> = Vec::new();
let mut errors: Vec<(String, String, f32, String)> = Vec::new();
for m in all_memories {
let item = (
m.id.0.to_string(),
m.experience.content.chars().take(200).collect(),
m.importance(),
m.created_at.to_rfc3339(),
);
match m.experience.experience_type {
ExperienceType::Decision => decisions.push(item),
ExperienceType::Learning => learnings.push(item),
ExperienceType::Context | ExperienceType::Observation => context.push(item),
ExperienceType::Pattern => patterns.push(item),
ExperienceType::Error => errors.push(item),
_ => context.push(item),
}
}
fn sort_and_truncate(
mut items: Vec<(String, String, f32, String)>,
max: usize,
) -> Vec<(String, String, f32, String)> {
items.sort_by(|a, b| b.2.total_cmp(&a.2));
items.truncate(max);
items
}
Python::with_gil(|py| {
let mut result = HashMap::new();
result.insert("total_memories".to_string(), total_memories.into_py(py));
let to_py_list = |items: Vec<(String, String, f32, String)>| -> PyObject {
let list: Vec<HashMap<String, PyObject>> = items
.into_iter()
.map(|(id, content, importance, created_at)| {
let mut item = HashMap::new();
item.insert("id".to_string(), id.into_py(py));
item.insert("content".to_string(), content.into_py(py));
item.insert("importance".to_string(), importance.into_py(py));
item.insert("created_at".to_string(), created_at.into_py(py));
item
})
.collect();
list.into_py(py)
};
result.insert(
"decisions".to_string(),
if include_decisions {
to_py_list(sort_and_truncate(decisions, max_items))
} else {
Vec::<HashMap<String, PyObject>>::new().into_py(py)
},
);
result.insert(
"learnings".to_string(),
if include_learnings {
to_py_list(sort_and_truncate(learnings, max_items))
} else {
Vec::<HashMap<String, PyObject>>::new().into_py(py)
},
);
result.insert(
"context".to_string(),
if include_context {
to_py_list(sort_and_truncate(context, max_items))
} else {
Vec::<HashMap<String, PyObject>>::new().into_py(py)
},
);
result.insert(
"patterns".to_string(),
to_py_list(sort_and_truncate(patterns, max_items)),
);
result.insert(
"errors".to_string(),
to_py_list(sort_and_truncate(errors, 3.min(max_items))),
);
Ok(result)
})
}
#[pyo3(signature = (limit=None, memory_type=None))]
fn list_memories(
&self,
limit: Option<usize>,
memory_type: Option<&str>,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let all_memories = self
.inner
.get_all_memories()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to get memories: {}", e)))?;
let filtered: Vec<_> = if let Some(type_filter) = memory_type {
let type_lower = type_filter.to_lowercase();
all_memories
.into_iter()
.filter(|m| {
let mem_type = format!("{:?}", m.experience.experience_type).to_lowercase();
mem_type == type_lower
})
.collect()
} else {
all_memories
};
let limited: Vec<_> = if let Some(lim) = limit {
filtered.into_iter().take(lim).collect()
} else {
filtered
};
Python::with_gil(|py| {
limited
.iter()
.map(|mem| memory_to_dict(py, mem))
.collect::<PyResult<Vec<_>>>()
})
}
fn get_memory(&self, memory_id: &str) -> PyResult<HashMap<String, PyObject>> {
let id = MemoryId(
uuid::Uuid::parse_str(memory_id)
.map_err(|e| PyValueError::new_err(format!("Invalid memory ID: {}", e)))?,
);
let memory = self
.inner
.get_memory(&id)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to get memory: {}", e)))?;
Python::with_gil(|py| memory_to_dict(py, &memory))
}
#[pyo3(signature = (tags, limit=20))]
fn recall_by_tags(
&self,
tags: Vec<String>,
limit: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let all_memories = self
.inner
.get_all_memories()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to get memories: {}", e)))?;
let filtered: Vec<_> = all_memories
.into_iter()
.filter(|m| {
m.experience.tags.iter().any(|t| {
tags.iter()
.any(|search_tag| t.eq_ignore_ascii_case(search_tag))
})
})
.take(limit)
.collect();
Python::with_gil(|py| {
filtered
.iter()
.map(|mem| memory_to_dict(py, mem))
.collect::<PyResult<Vec<_>>>()
})
}
#[pyo3(signature = (start, end, limit=20))]
fn recall_by_date(
&self,
start: &str,
end: &str,
limit: usize,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let start_dt = chrono::DateTime::parse_from_rfc3339(start)
.map_err(|e| PyValueError::new_err(format!("Invalid start date: {}", e)))?
.with_timezone(&chrono::Utc);
let end_dt = chrono::DateTime::parse_from_rfc3339(end)
.map_err(|e| PyValueError::new_err(format!("Invalid end date: {}", e)))?
.with_timezone(&chrono::Utc);
let all_memories = self
.inner
.get_all_memories()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to get memories: {}", e)))?;
let filtered: Vec<_> = all_memories
.into_iter()
.filter(|m| m.created_at >= start_dt && m.created_at <= end_dt)
.take(limit)
.collect();
Python::with_gil(|py| {
filtered
.iter()
.map(|mem| memory_to_dict(py, mem))
.collect::<PyResult<Vec<_>>>()
})
}
fn graph_stats(&self) -> PyResult<HashMap<String, PyObject>> {
let stats = self.inner.graph_stats();
Python::with_gil(|py| {
let mut dict = HashMap::new();
dict.insert("node_count".to_string(), stats.node_count.into_py(py));
dict.insert("edge_count".to_string(), stats.edge_count.into_py(py));
dict.insert("avg_strength".to_string(), stats.avg_strength.into_py(py));
dict.insert(
"potentiated_count".to_string(),
stats.potentiated_count.into_py(py),
);
Ok(dict)
})
}
fn forget(&self, memory_id: &str) -> PyResult<bool> {
let uuid = uuid::Uuid::parse_str(memory_id)
.map_err(|e| PyValueError::new_err(format!("Invalid memory ID: {}", e)))?;
let deleted = self
.inner
.forget(ForgetCriteria::ById(MemoryId(uuid)))
.map_err(|e| PyRuntimeError::new_err(format!("Failed to delete memory: {}", e)))?;
Ok(deleted > 0)
}
fn forget_by_age(&self, days: u32) -> PyResult<usize> {
self.inner
.forget(ForgetCriteria::OlderThan(days))
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget by age: {}", e)))
}
fn forget_by_importance(&self, threshold: f32) -> PyResult<usize> {
self.inner
.forget(ForgetCriteria::LowImportance(threshold))
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget by importance: {}", e)))
}
fn forget_by_pattern(&self, pattern: &str) -> PyResult<usize> {
self.inner
.forget(ForgetCriteria::Pattern(pattern.to_string()))
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget by pattern: {}", e)))
}
fn forget_by_tags(&self, tags: Vec<String>) -> PyResult<usize> {
self.inner
.forget(ForgetCriteria::ByTags(tags))
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget by tags: {}", e)))
}
fn forget_by_date(&self, start: &str, end: &str) -> PyResult<usize> {
let start_dt: DateTime<Utc> = start
.parse()
.map_err(|e| PyValueError::new_err(format!("Invalid start date: {}", e)))?;
let end_dt: DateTime<Utc> = end
.parse()
.map_err(|e| PyValueError::new_err(format!("Invalid end date: {}", e)))?;
self.inner
.forget(ForgetCriteria::ByDateRange {
start: start_dt,
end: end_dt,
})
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget by date: {}", e)))
}
fn forget_all(&self) -> PyResult<usize> {
self.inner
.forget(ForgetCriteria::All)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to forget all: {}", e)))
}
#[pyo3(signature = (longterm_limit=100))]
fn brain_state(&self, longterm_limit: usize) -> PyResult<HashMap<String, PyObject>> {
let working_memories = self.inner.get_working_memories();
let session_memories = self.inner.get_session_memories();
let longterm_memories = self
.inner
.get_longterm_memories(longterm_limit)
.map_err(|e| {
PyRuntimeError::new_err(format!("Failed to get longterm memories: {}", e))
})?;
let working_count = working_memories.len();
let session_count = session_memories.len();
let longterm_count = longterm_memories.len();
let total_count = working_count + session_count + longterm_count;
Python::with_gil(|py| {
let mut result = HashMap::new();
let mut total_activation = 0.0f32;
let mut total_importance = 0.0f32;
let working_neurons: Vec<HashMap<String, PyObject>> = working_memories
.iter()
.map(|m| {
total_activation += m.activation();
total_importance += m.importance();
let mut neuron = HashMap::new();
neuron.insert("id".to_string(), m.id.0.to_string().into_py(py));
neuron.insert(
"content_preview".to_string(),
m.experience
.content
.chars()
.take(100)
.collect::<String>()
.into_py(py),
);
neuron.insert("activation".to_string(), m.activation().into_py(py));
neuron.insert("importance".to_string(), m.importance().into_py(py));
neuron.insert("tier".to_string(), "working".into_py(py));
neuron.insert(
"access_count".to_string(),
m.metadata_snapshot().access_count.into_py(py),
);
neuron.insert(
"created_at".to_string(),
m.created_at.to_rfc3339().into_py(py),
);
neuron
})
.collect();
let session_neurons: Vec<HashMap<String, PyObject>> = session_memories
.iter()
.map(|m| {
total_activation += m.activation();
total_importance += m.importance();
let mut neuron = HashMap::new();
neuron.insert("id".to_string(), m.id.0.to_string().into_py(py));
neuron.insert(
"content_preview".to_string(),
m.experience
.content
.chars()
.take(100)
.collect::<String>()
.into_py(py),
);
neuron.insert("activation".to_string(), m.activation().into_py(py));
neuron.insert("importance".to_string(), m.importance().into_py(py));
neuron.insert("tier".to_string(), "session".into_py(py));
neuron.insert(
"access_count".to_string(),
m.metadata_snapshot().access_count.into_py(py),
);
neuron.insert(
"created_at".to_string(),
m.created_at.to_rfc3339().into_py(py),
);
neuron
})
.collect();
let longterm_neurons: Vec<HashMap<String, PyObject>> = longterm_memories
.iter()
.map(|m| {
total_activation += m.activation();
total_importance += m.importance();
let mut neuron = HashMap::new();
neuron.insert("id".to_string(), m.id.0.to_string().into_py(py));
neuron.insert(
"content_preview".to_string(),
m.experience
.content
.chars()
.take(100)
.collect::<String>()
.into_py(py),
);
neuron.insert("activation".to_string(), m.activation().into_py(py));
neuron.insert("importance".to_string(), m.importance().into_py(py));
neuron.insert("tier".to_string(), "longterm".into_py(py));
neuron.insert(
"access_count".to_string(),
m.metadata_snapshot().access_count.into_py(py),
);
neuron.insert(
"created_at".to_string(),
m.created_at.to_rfc3339().into_py(py),
);
neuron
})
.collect();
result.insert("working_memory".to_string(), working_neurons.into_py(py));
result.insert("session_memory".to_string(), session_neurons.into_py(py));
result.insert("longterm_memory".to_string(), longterm_neurons.into_py(py));
let mut stats = HashMap::new();
stats.insert("total_memories".to_string(), total_count.into_py(py));
stats.insert("working_count".to_string(), working_count.into_py(py));
stats.insert("session_count".to_string(), session_count.into_py(py));
stats.insert("longterm_count".to_string(), longterm_count.into_py(py));
stats.insert(
"avg_activation".to_string(),
if total_count > 0 {
(total_activation / total_count as f32).into_py(py)
} else {
0.0f32.into_py(py)
},
);
stats.insert(
"avg_importance".to_string(),
if total_count > 0 {
(total_importance / total_count as f32).into_py(py)
} else {
0.0f32.into_py(py)
},
);
result.insert("stats".to_string(), stats.into_py(py));
Ok(result)
})
}
#[pyo3(signature = (since=None, until=None))]
fn consolidation_report(
&self,
since: Option<&str>,
until: Option<&str>,
) -> PyResult<HashMap<String, PyObject>> {
let since_dt: DateTime<Utc> = if let Some(s) = since {
s.parse()
.map_err(|e| PyValueError::new_err(format!("Invalid 'since' timestamp: {}", e)))?
} else {
Utc::now() - chrono::Duration::hours(24)
};
let until_dt: Option<DateTime<Utc>> =
if let Some(u) = until {
Some(u.parse().map_err(|e| {
PyValueError::new_err(format!("Invalid 'until' timestamp: {}", e))
})?)
} else {
None
};
let report = self.inner.get_consolidation_report(since_dt, until_dt);
Python::with_gil(|py| {
let mut result = HashMap::new();
let mut period = HashMap::new();
period.insert(
"start".to_string(),
report.period.start.to_rfc3339().into_py(py),
);
period.insert(
"end".to_string(),
report.period.end.to_rfc3339().into_py(py),
);
result.insert("period".to_string(), period.into_py(py));
let mut stats = HashMap::new();
stats.insert(
"total_memories".to_string(),
report.statistics.total_memories.into_py(py),
);
stats.insert(
"memories_strengthened".to_string(),
report.statistics.memories_strengthened.into_py(py),
);
stats.insert(
"memories_decayed".to_string(),
report.statistics.memories_decayed.into_py(py),
);
stats.insert(
"memories_at_risk".to_string(),
report.statistics.memories_at_risk.into_py(py),
);
stats.insert(
"edges_formed".to_string(),
report.statistics.edges_formed.into_py(py),
);
stats.insert(
"edges_strengthened".to_string(),
report.statistics.edges_strengthened.into_py(py),
);
stats.insert(
"edges_potentiated".to_string(),
report.statistics.edges_potentiated.into_py(py),
);
stats.insert(
"edges_pruned".to_string(),
report.statistics.edges_pruned.into_py(py),
);
stats.insert(
"facts_extracted".to_string(),
report.statistics.facts_extracted.into_py(py),
);
stats.insert(
"facts_reinforced".to_string(),
report.statistics.facts_reinforced.into_py(py),
);
stats.insert(
"maintenance_cycles".to_string(),
report.statistics.maintenance_cycles.into_py(py),
);
stats.insert(
"total_maintenance_duration_ms".to_string(),
report.statistics.total_maintenance_duration_ms.into_py(py),
);
result.insert("stats".to_string(), stats.into_py(py));
let event_count = report.strengthened_memories.len()
+ report.decayed_memories.len()
+ report.formed_associations.len()
+ report.strengthened_associations.len()
+ report.potentiated_associations.len()
+ report.pruned_associations.len()
+ report.extracted_facts.len()
+ report.reinforced_facts.len();
result.insert("event_count".to_string(), event_count.into_py(py));
let strengthened: Vec<HashMap<String, PyObject>> = report
.strengthened_memories
.iter()
.map(|m| {
let mut mem = HashMap::new();
mem.insert("memory_id".to_string(), m.memory_id.clone().into_py(py));
mem.insert(
"content_preview".to_string(),
m.content_preview.clone().into_py(py),
);
mem.insert(
"activation_before".to_string(),
m.activation_before.into_py(py),
);
mem.insert(
"activation_after".to_string(),
m.activation_after.into_py(py),
);
mem.insert("reason".to_string(), m.change_reason.clone().into_py(py));
mem.insert(
"timestamp".to_string(),
m.timestamp.to_rfc3339().into_py(py),
);
mem
})
.collect();
result.insert(
"strengthened_memories".to_string(),
strengthened.into_py(py),
);
let decayed: Vec<HashMap<String, PyObject>> = report
.decayed_memories
.iter()
.map(|m| {
let mut mem = HashMap::new();
mem.insert("memory_id".to_string(), m.memory_id.clone().into_py(py));
mem.insert(
"content_preview".to_string(),
m.content_preview.clone().into_py(py),
);
mem.insert(
"activation_before".to_string(),
m.activation_before.into_py(py),
);
mem.insert(
"activation_after".to_string(),
m.activation_after.into_py(py),
);
mem.insert("at_risk".to_string(), m.at_risk.into_py(py));
mem.insert(
"timestamp".to_string(),
m.timestamp.to_rfc3339().into_py(py),
);
mem
})
.collect();
result.insert("decayed_memories".to_string(), decayed.into_py(py));
let formed: Vec<HashMap<String, PyObject>> = report
.formed_associations
.iter()
.map(|a| {
let mut assoc = HashMap::new();
assoc.insert(
"from_memory_id".to_string(),
a.from_memory_id.clone().into_py(py),
);
assoc.insert(
"to_memory_id".to_string(),
a.to_memory_id.clone().into_py(py),
);
assoc.insert("strength".to_string(), a.strength_after.into_py(py));
assoc.insert("reason".to_string(), a.reason.clone().into_py(py));
assoc.insert(
"timestamp".to_string(),
a.timestamp.to_rfc3339().into_py(py),
);
assoc
})
.collect();
result.insert("formed_associations".to_string(), formed.into_py(py));
let pruned: Vec<HashMap<String, PyObject>> = report
.pruned_associations
.iter()
.map(|a| {
let mut assoc = HashMap::new();
assoc.insert(
"from_memory_id".to_string(),
a.from_memory_id.clone().into_py(py),
);
assoc.insert(
"to_memory_id".to_string(),
a.to_memory_id.clone().into_py(py),
);
assoc.insert(
"final_strength".to_string(),
a.strength_before.unwrap_or(0.0).into_py(py),
);
assoc.insert("reason".to_string(), a.reason.clone().into_py(py));
assoc.insert(
"timestamp".to_string(),
a.timestamp.to_rfc3339().into_py(py),
);
assoc
})
.collect();
result.insert("pruned_associations".to_string(), pruned.into_py(py));
Ok(result)
})
}
#[pyo3(signature = (since=None))]
fn consolidation_events(
&self,
since: Option<&str>,
) -> PyResult<Vec<HashMap<String, PyObject>>> {
let since_dt: DateTime<Utc> = if let Some(s) = since {
s.parse()
.map_err(|e| PyValueError::new_err(format!("Invalid 'since' timestamp: {}", e)))?
} else {
Utc::now() - chrono::Duration::hours(24)
};
let events = self.inner.get_consolidation_events_since(since_dt);
Python::with_gil(|py| {
let result: Vec<HashMap<String, PyObject>> = events
.iter()
.map(|event| {
let mut evt = HashMap::new();
evt.insert("event_type".to_string(), format!("{:?}", event).into_py(py));
evt.insert(
"timestamp".to_string(),
event.timestamp().to_rfc3339().into_py(py),
);
evt
})
.collect();
Ok(result)
})
}
#[pyo3(signature = (
context,
semantic_threshold=0.45,
max_results=5,
memory_types=None,
auto_ingest=true,
recency_weight=0.2
))]
fn proactive_context(
&mut self,
context: String,
semantic_threshold: f32,
max_results: usize,
memory_types: Option<Vec<String>>,
auto_ingest: bool,
recency_weight: f32,
) -> PyResult<HashMap<String, PyObject>> {
let start = std::time::Instant::now();
let ingested_id = if auto_ingest && context.len() > 50 {
let experience = Experience {
experience_type: ExperienceType::Conversation,
content: context.clone(),
tags: vec!["proactive-context".to_string()],
..Default::default()
};
match self.inner.remember(experience, None) {
Ok(id) => Some(id.0.to_string()),
Err(_) => None,
}
} else {
None
};
let query = Query {
user_id: None,
query_text: Some(context.clone()),
query_embedding: None,
time_range: None,
experience_types: memory_types.map(|types| {
types
.iter()
.filter_map(|t| match t.to_lowercase().as_str() {
"decision" => Some(ExperienceType::Decision),
"learning" => Some(ExperienceType::Learning),
"error" => Some(ExperienceType::Error),
"context" | "observation" => Some(ExperienceType::Context),
"pattern" => Some(ExperienceType::Pattern),
"conversation" => Some(ExperienceType::Conversation),
"task" => Some(ExperienceType::Task),
"discovery" => Some(ExperienceType::Discovery),
_ => None,
})
.collect()
}),
importance_threshold: None,
robot_id: self.robot_id.clone(),
mission_id: None,
geo_filter: None,
action_type: None,
reward_range: None,
outcome_type: None,
failures_only: false,
anomalies_only: false,
severity: None,
tags: None,
pattern_id: None,
terrain_type: None,
confidence_range: None,
prospective_signals: None,
episode_id: None,
recency_weight: None,
max_results: max_results * 2, retrieval_mode: RetrievalMode::Hybrid,
offset: 0,
};
let memories = self
.inner
.recall(&query)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to recall memories: {}", e)))?;
let now = Utc::now();
let recency_half_life_hours = 24.0f32;
let mut scored_memories: Vec<(std::sync::Arc<Memory>, f32, String)> = memories
.into_iter()
.filter_map(|m| {
let base_score = m.importance();
let age_hours = (now - m.created_at).num_hours().max(0) as f32;
let recency_factor = (-age_hours / recency_half_life_hours).exp();
let recency_boost = 1.0 + (recency_weight * recency_factor);
let final_score = base_score * recency_boost;
if final_score >= semantic_threshold {
let reason = if recency_factor > 0.5 {
"recent_and_relevant".to_string()
} else {
"semantic_similarity".to_string()
};
Some((m, final_score.min(1.0), reason))
} else {
None
}
})
.collect();
scored_memories.sort_by(|a, b| b.1.total_cmp(&a.1));
scored_memories.truncate(max_results);
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
Python::with_gil(|py| {
let mut result = HashMap::new();
let memories_list: Vec<HashMap<String, PyObject>> = scored_memories
.into_iter()
.map(|(m, score, reason)| {
let mut mem = HashMap::new();
mem.insert("id".to_string(), m.id.0.to_string().into_py(py));
mem.insert(
"content".to_string(),
m.experience.content.clone().into_py(py),
);
mem.insert(
"memory_type".to_string(),
format!("{:?}", m.experience.experience_type).into_py(py),
);
mem.insert("importance".to_string(), m.importance().into_py(py));
mem.insert("relevance_score".to_string(), score.into_py(py));
mem.insert("relevance_reason".to_string(), reason.into_py(py));
mem.insert(
"created_at".to_string(),
m.created_at.to_rfc3339().into_py(py),
);
mem.insert("tags".to_string(), m.experience.tags.clone().into_py(py));
mem
})
.collect();
let count = memories_list.len();
result.insert("memories".to_string(), memories_list.into_py(py));
result.insert("count".to_string(), count.into_py(py));
result.insert("latency_ms".to_string(), latency_ms.into_py(py));
result.insert(
"ingested_id".to_string(),
ingested_id
.map(|id| id.into_py(py))
.unwrap_or_else(|| py.None()),
);
result.insert(
"config".to_string(),
{
let mut cfg = HashMap::new();
cfg.insert(
"semantic_threshold".to_string(),
semantic_threshold.into_py(py),
);
cfg.insert("max_results".to_string(), max_results.into_py(py));
cfg.insert("recency_weight".to_string(), recency_weight.into_py(py));
cfg.insert("auto_ingest".to_string(), auto_ingest.into_py(py));
cfg
}
.into_py(py),
);
Ok(result)
})
}
fn verify_index(&self) -> PyResult<HashMap<String, PyObject>> {
let report = self
.inner
.verify_index_integrity()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to verify index: {}", e)))?;
Python::with_gil(|py| {
let mut result = HashMap::new();
result.insert(
"total_storage".to_string(),
report.total_storage.into_py(py),
);
result.insert(
"total_indexed".to_string(),
report.total_indexed.into_py(py),
);
result.insert(
"orphaned_count".to_string(),
report.orphaned_count.into_py(py),
);
result.insert("is_healthy".to_string(), report.is_healthy.into_py(py));
result.insert(
"orphaned_ids".to_string(),
report
.orphaned_ids
.iter()
.map(|id| id.0.to_string())
.collect::<Vec<_>>()
.into_py(py),
);
Ok(result)
})
}
fn repair_index(&self) -> PyResult<HashMap<String, PyObject>> {
let (total_storage, total_indexed, repaired, failed) = self
.inner
.repair_vector_index()
.map_err(|e| PyRuntimeError::new_err(format!("Failed to repair index: {}", e)))?;
Python::with_gil(|py| {
let mut result = HashMap::new();
result.insert("total_storage".to_string(), total_storage.into_py(py));
result.insert("total_indexed".to_string(), total_indexed.into_py(py));
result.insert("repaired".to_string(), repaired.into_py(py));
result.insert("failed".to_string(), failed.into_py(py));
result.insert("is_healthy".to_string(), (failed == 0).into_py(py));
result.insert("success".to_string(), true.into_py(py));
Ok(result)
})
}
fn index_health(&self) -> PyResult<HashMap<String, PyObject>> {
let health = self.inner.index_health();
Python::with_gil(|py| {
let mut result = HashMap::new();
result.insert(
"total_vectors".to_string(),
health.total_vectors.into_py(py),
);
result.insert(
"deleted_count".to_string(),
health.deleted_count.into_py(py),
);
result.insert(
"deletion_ratio".to_string(),
health.deletion_ratio.into_py(py),
);
result.insert(
"needs_compaction".to_string(),
health.needs_compaction.into_py(py),
);
result.insert(
"incremental_inserts".to_string(),
health.incremental_inserts.into_py(py),
);
result.insert(
"needs_rebuild".to_string(),
health.needs_rebuild.into_py(py),
);
result.insert(
"rebuild_threshold".to_string(),
health.rebuild_threshold.into_py(py),
);
result.insert(
"healthy".to_string(),
(!health.needs_rebuild && !health.needs_compaction).into_py(py),
);
Ok(result)
})
}
fn __repr__(&self) -> String {
let stats = self.inner.stats();
format!(
"MemorySystem(robot_id={:?}, mission={:?}, total={})",
self.robot_id, self.mission_id, stats.total_memories
)
}
}
fn memory_to_dict(_py: Python, memory: &Memory) -> PyResult<HashMap<String, PyObject>> {
Python::with_gil(|py| {
let mut dict = HashMap::new();
dict.insert("id".to_string(), memory.id.0.to_string().into_py(py));
dict.insert(
"content".to_string(),
memory.experience.content.clone().into_py(py),
);
dict.insert(
"experience_type".to_string(),
format!("{:?}", memory.experience.experience_type).into_py(py),
);
dict.insert(
"entities".to_string(),
memory.experience.entities.clone().into_py(py),
);
dict.insert(
"metadata".to_string(),
memory.experience.metadata.clone().into_py(py),
);
dict.insert("importance".to_string(), memory.importance().into_py(py));
dict.insert(
"access_count".to_string(),
memory.access_count().into_py(py),
);
dict.insert(
"created_at".to_string(),
memory.created_at.to_rfc3339().into_py(py),
);
dict.insert(
"last_accessed".to_string(),
memory.last_accessed().to_rfc3339().into_py(py),
);
dict.insert("compressed".to_string(), memory.compressed.into_py(py));
if let Some(ref robot_id) = memory.experience.robot_id {
dict.insert("robot_id".to_string(), robot_id.clone().into_py(py));
}
if let Some(ref mission_id) = memory.experience.mission_id {
dict.insert("mission_id".to_string(), mission_id.clone().into_py(py));
}
if let Some(ref geo) = memory.experience.geo_location {
dict.insert("geo_location".to_string(), geo.to_vec().into_py(py));
}
if let Some(ref pos) = memory.experience.local_position {
dict.insert("position".to_string(), pos.to_vec().into_py(py));
}
if let Some(heading) = memory.experience.heading {
dict.insert("heading".to_string(), heading.into_py(py));
}
if let Some(ref action_type) = memory.experience.action_type {
dict.insert("action_type".to_string(), action_type.clone().into_py(py));
}
if let Some(reward) = memory.experience.reward {
dict.insert("reward".to_string(), reward.into_py(py));
}
if !memory.experience.sensor_data.is_empty() {
dict.insert(
"sensor_data".to_string(),
memory.experience.sensor_data.clone().into_py(py),
);
}
if let Some(ref ctx) = memory.experience.decision_context {
dict.insert("decision_context".to_string(), ctx.clone().into_py(py));
}
if let Some(ref params) = memory.experience.action_params {
dict.insert("action_params".to_string(), params.clone().into_py(py));
}
if let Some(ref outcome_type) = memory.experience.outcome_type {
dict.insert("outcome_type".to_string(), outcome_type.clone().into_py(py));
}
if let Some(ref details) = memory.experience.outcome_details {
dict.insert("outcome_details".to_string(), details.clone().into_py(py));
}
if let Some(confidence) = memory.experience.confidence {
dict.insert("confidence".to_string(), confidence.into_py(py));
}
if !memory.experience.alternatives_considered.is_empty() {
dict.insert(
"alternatives_considered".to_string(),
memory
.experience
.alternatives_considered
.clone()
.into_py(py),
);
}
if let Some(ref weather) = memory.experience.weather {
dict.insert("weather".to_string(), weather.clone().into_py(py));
}
if let Some(ref terrain) = memory.experience.terrain_type {
dict.insert("terrain_type".to_string(), terrain.clone().into_py(py));
}
if let Some(ref lighting) = memory.experience.lighting {
dict.insert("lighting".to_string(), lighting.clone().into_py(py));
}
if !memory.experience.nearby_agents.is_empty() {
dict.insert(
"nearby_agents".to_string(),
memory.experience.nearby_agents.clone().into_py(py),
);
}
dict.insert(
"is_failure".to_string(),
memory.experience.is_failure.into_py(py),
);
dict.insert(
"is_anomaly".to_string(),
memory.experience.is_anomaly.into_py(py),
);
if let Some(ref severity) = memory.experience.severity {
dict.insert("severity".to_string(), severity.clone().into_py(py));
}
if let Some(ref recovery) = memory.experience.recovery_action {
dict.insert("recovery_action".to_string(), recovery.clone().into_py(py));
}
if let Some(ref cause) = memory.experience.root_cause {
dict.insert("root_cause".to_string(), cause.clone().into_py(py));
}
if let Some(ref pattern) = memory.experience.pattern_id {
dict.insert("pattern_id".to_string(), pattern.clone().into_py(py));
}
if let Some(ref predicted) = memory.experience.predicted_outcome {
dict.insert(
"predicted_outcome".to_string(),
predicted.clone().into_py(py),
);
}
if let Some(accurate) = memory.experience.prediction_accurate {
dict.insert("prediction_accurate".to_string(), accurate.into_py(py));
}
if !memory.experience.tags.is_empty() {
dict.insert(
"tags".to_string(),
memory.experience.tags.clone().into_py(py),
);
}
Ok(dict)
})
}
#[pymodule]
fn shodh_memory(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyMemorySystem>()?;
m.add_class::<PyPosition>()?;
m.add_class::<PyGeoLocation>()?;
m.add_class::<PyGeoFilter>()?;
m.add_class::<PyDecisionContext>()?;
m.add_class::<PyOutcome>()?;
m.add_class::<PyEnvironment>()?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add(
"__doc__",
"Shodh-Memory: AI Memory System for Autonomous Robots & Drones\n\n\
Features:\n\
- Position(x, y, z) for local coordinates\n\
- GeoLocation(lat, lon, alt) for GPS\n\
- DecisionContext for action-outcome learning\n\
- Outcome for decision results\n\
- Environment for weather, terrain, lighting\n\
- Failure tracking and anomaly detection\n\
- Pattern learning and predictions\n\
- 100% offline capable",
)?;
Ok(())
}