use crate::config::Config;
use crate::error::Result;
use crate::node::MinimalNode;
use crate::types::{Entry, EntryType, Hash, NodeStats};
use kaneru::agent::AgentStats;
use kaneru::{
Action, ActionResult, ActionType, Agent, AgentConfig, AgentState, Goal, Observation, Policy,
Rule, SimpleAgent,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartNodeConfig {
pub node_config: Config,
pub agent_config: AgentConfig,
pub auto_publish_observations: bool,
pub auto_publish_actions: bool,
pub observation_retention_secs: u64,
pub max_pending_actions: usize,
}
impl Default for SmartNodeConfig {
fn default() -> Self {
Self {
node_config: Config::default(),
agent_config: AgentConfig::default(),
auto_publish_observations: true,
auto_publish_actions: true,
observation_retention_secs: 300,
max_pending_actions: 100,
}
}
}
impl SmartNodeConfig {
pub fn iot_mode() -> Self {
Self {
node_config: Config::iot_mode(),
agent_config: AgentConfig::iot_mode(),
auto_publish_observations: true,
auto_publish_actions: true,
observation_retention_secs: 60,
max_pending_actions: 20,
}
}
pub fn low_power() -> Self {
Self {
node_config: Config::low_power(),
agent_config: AgentConfig::iot_mode(), auto_publish_observations: false, auto_publish_actions: false,
observation_retention_secs: 30,
max_pending_actions: 10,
}
}
}
pub struct SmartNode {
node: MinimalNode,
agent: SimpleAgent,
config: SmartNodeConfig,
pending_actions: Vec<Action>,
action_history: Vec<(Action, ActionResult)>,
observation_entries: HashMap<Hash, Observation>,
}
impl SmartNode {
pub fn new(config: SmartNodeConfig) -> Result<Self> {
let node = MinimalNode::new(config.node_config.clone())?;
let agent =
SimpleAgent::with_config(&config.agent_config.name, config.agent_config.clone());
Ok(Self {
node,
agent,
config,
pending_actions: Vec::new(),
action_history: Vec::new(),
observation_entries: HashMap::new(),
})
}
pub fn with_agent(config: SmartNodeConfig, agent: SimpleAgent) -> Result<Self> {
let node = MinimalNode::new(config.node_config.clone())?;
Ok(Self {
node,
agent,
config,
pending_actions: Vec::new(),
action_history: Vec::new(),
observation_entries: HashMap::new(),
})
}
pub fn node(&self) -> &MinimalNode {
&self.node
}
pub fn node_mut(&mut self) -> &mut MinimalNode {
&mut self.node
}
pub fn agent(&self) -> &SimpleAgent {
&self.agent
}
pub fn agent_mut(&mut self) -> &mut SimpleAgent {
&mut self.agent
}
pub fn observe(&mut self, observation: Observation) -> Result<Option<Hash>> {
self.agent.observe(observation.clone());
if self.config.auto_publish_observations {
let entry = self.observation_to_entry(&observation);
let hash = self.node.create_entry(entry)?;
self.observation_entries.insert(hash.clone(), observation);
return Ok(Some(hash));
}
Ok(None)
}
pub fn observe_batch(&mut self, observations: Vec<Observation>) -> Result<Vec<Hash>> {
let mut hashes = Vec::new();
for obs in observations {
if let Some(hash) = self.observe(obs)? {
hashes.push(hash);
}
}
Ok(hashes)
}
pub fn decide(&self) -> Action {
self.agent.decide()
}
pub fn step(&mut self) -> Result<Option<ActionResult>> {
let action = self.decide();
if action.is_noop() {
return Ok(None);
}
let result = self.execute_action(action)?;
Ok(Some(result))
}
pub fn execute_action(&mut self, action: Action) -> Result<ActionResult> {
let start = std::time::Instant::now();
let result = match &action.action_type {
ActionType::Publish(topic) => self.execute_publish(&action, topic)?,
ActionType::StoreData(key) => self.execute_store(&action, key)?,
ActionType::SendMessage(target) => self.execute_send(&action, target)?,
ActionType::Alert(message) => self.execute_alert(&action, message)?,
ActionType::UpdateState(state_name) => {
self.execute_state_update(&action, state_name)?
}
ActionType::Query(key) => self.execute_query(&action, key)?,
ActionType::RemoteCall(target) => self.execute_remote_call(&action, target)?,
ActionType::Wait => ActionResult::success(&action.id),
ActionType::NoOp => ActionResult::success(&action.id),
ActionType::Custom(name) => {
log::debug!("Custom action '{}' executed", name);
ActionResult::success(&action.id)
}
};
let result = result.with_duration(start.elapsed().as_micros() as u64);
let obs_clone = self
.agent
.recent_observations(1)
.first()
.map(|o| (*o).clone());
if let Some(obs) = obs_clone {
self.agent.learn(&obs, &action, &result);
}
if self.action_history.len() >= self.config.max_pending_actions {
self.action_history.remove(0);
}
self.action_history.push((action.clone(), result.clone()));
if self.config.auto_publish_actions {
let entry = self.action_to_entry(&action, &result);
let _ = self.node.create_entry(entry);
}
Ok(result)
}
fn execute_publish(&mut self, action: &Action, topic: &str) -> Result<ActionResult> {
let value = action.params.get("value").cloned();
let value_str = match &value {
Some(v) => serde_json::to_string(v).unwrap_or_default(),
None => "null".to_string(),
};
let content = format!("{{\"topic\":\"{}\",\"value\":{}}}", topic, value_str);
let entry = Entry {
entry_type: EntryType::App,
content: content.into_bytes(),
};
match self.node.create_entry(entry) {
Ok(hash) => Ok(ActionResult::success_with_value(
&action.id,
hash.to_string(),
)),
Err(e) => Ok(ActionResult::failure(&action.id, &e.to_string())),
}
}
fn execute_store(&mut self, action: &Action, key: &str) -> Result<ActionResult> {
let value = action.params.get("value").cloned();
let value_str = match &value {
Some(v) => serde_json::to_string(v).unwrap_or_default(),
None => "null".to_string(),
};
let content = format!("{{\"key\":\"{}\",\"value\":{}}}", key, value_str);
let entry = Entry {
entry_type: EntryType::App,
content: content.into_bytes(),
};
match self.node.create_entry(entry) {
Ok(hash) => Ok(ActionResult::success_with_value(
&action.id,
hash.to_string(),
)),
Err(e) => Ok(ActionResult::failure(&action.id, &e.to_string())),
}
}
fn execute_send(&mut self, action: &Action, target: &str) -> Result<ActionResult> {
log::info!(
"Sending message to {}: {:?}",
target,
action.params.get("content")
);
Ok(ActionResult::success(&action.id))
}
fn execute_alert(&mut self, action: &Action, message: &str) -> Result<ActionResult> {
log::warn!("ALERT: {}", message);
let content = format!(
"{{\"alert\":\"{}\",\"timestamp\":{}}}",
message,
chrono::Utc::now().timestamp()
);
let entry = Entry {
entry_type: EntryType::App,
content: content.into_bytes(),
};
let _ = self.node.create_entry(entry);
Ok(ActionResult::success(&action.id))
}
fn execute_state_update(&mut self, action: &Action, state_name: &str) -> Result<ActionResult> {
let value = action.params.get("value").cloned();
log::debug!("State update: {} = {:?}", state_name, value);
Ok(ActionResult::success(&action.id))
}
fn execute_query(&self, action: &Action, key: &str) -> Result<ActionResult> {
log::debug!("Querying data for key: {}", key);
let hash = Hash::from_bytes(key.as_bytes());
match self.node.get_entry(&hash) {
Ok(Some(entry)) => {
let content_str = String::from_utf8_lossy(&entry.content);
Ok(ActionResult::success_with_value(
&action.id,
content_str.to_string(),
))
}
Ok(None) => {
Ok(ActionResult::failure(&action.id, "Entry not found"))
}
Err(e) => Ok(ActionResult::failure(&action.id, &e.to_string())),
}
}
fn execute_remote_call(&self, action: &Action, target: &str) -> Result<ActionResult> {
log::info!("Remote call to target: {}", target);
let method = action
.params
.get("method")
.and_then(|v| match v {
kaneru::Value::String(s) => Some(s.as_str()),
_ => None,
})
.unwrap_or("ping");
let payload = action
.params
.get("payload")
.map(|v| match v {
kaneru::Value::Bytes(b) => b.clone(),
kaneru::Value::String(s) => s.as_bytes().to_vec(),
kaneru::Value::Json(j) => serde_json::to_vec(j).unwrap_or_default(),
_ => Vec::new(),
})
.unwrap_or_default();
let from_key = self.node.public_key();
let message = serde_json::json!({
"type": "remote_call",
"target": target,
"method": method,
"payload": payload,
"from": format!("{:?}", from_key),
});
log::debug!("Remote call message: {:?}", message);
Ok(ActionResult::success_with_value(
&action.id,
format!(
"{{\"status\":\"pending\",\"target\":\"{}\",\"method\":\"{}\"}}",
target, method
),
))
}
fn observation_to_entry(&self, obs: &Observation) -> Entry {
let content = serde_json::json!({
"type": "observation",
"obs_type": format!("{:?}", obs.obs_type),
"value": obs.value,
"timestamp": obs.timestamp.0,
"confidence": obs.confidence.0,
"metadata": obs.metadata,
});
Entry {
entry_type: EntryType::App,
content: content.to_string().into_bytes(),
}
}
fn action_to_entry(&self, action: &Action, result: &ActionResult) -> Entry {
let content = serde_json::json!({
"type": "action_result",
"action_type": format!("{:?}", action.action_type),
"success": result.success,
"value": result.value,
"error": result.error,
"executed_at": result.executed_at.0,
"duration_us": result.duration_us,
});
Entry {
entry_type: EntryType::App,
content: content.to_string().into_bytes(),
}
}
pub fn add_policy(&mut self, policy: Policy) {
self.agent.add_policy(policy);
}
pub fn add_rule(&mut self, rule: Rule) {
self.agent.add_rule(rule);
}
pub fn add_goal(&mut self, goal: Goal) {
self.agent.add_goal(goal);
}
pub fn agent_state(&self) -> AgentState {
self.agent.state()
}
pub fn agent_stats(&self) -> &AgentStats {
self.agent.stats()
}
pub fn node_stats(&self) -> Result<NodeStats> {
self.node.stats()
}
pub fn stats(&self) -> Result<SmartNodeStats> {
Ok(SmartNodeStats {
node_stats: self.node.stats()?,
agent_stats: self.agent.stats().clone(),
pending_actions: self.pending_actions.len(),
action_history_len: self.action_history.len(),
observation_entries: self.observation_entries.len(),
})
}
pub fn pause(&mut self) {
self.agent.pause();
}
pub fn resume(&mut self) {
self.agent.resume();
}
pub fn stop(&mut self) {
self.agent.stop();
}
pub fn is_running(&self) -> bool {
self.agent.is_running()
}
pub fn active_goals(&self) -> Vec<&Goal> {
self.agent.active_goals()
}
pub fn recent_observations(&self, count: usize) -> Vec<&Observation> {
self.agent.recent_observations(count)
}
pub fn action_history(&self) -> &[(Action, ActionResult)] {
&self.action_history
}
pub fn clear_history(&mut self) {
self.action_history.clear();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartNodeStats {
pub node_stats: NodeStats,
pub agent_stats: AgentStats,
pub pending_actions: usize,
pub action_history_len: usize,
pub observation_entries: usize,
}
pub struct SensorAdapter {
name: String,
scale: f64,
offset: f64,
}
impl SensorAdapter {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
scale: 1.0,
offset: 0.0,
}
}
pub fn with_scaling(name: &str, scale: f64, offset: f64) -> Self {
Self {
name: name.to_string(),
scale,
offset,
}
}
pub fn reading(&self, raw_value: f64) -> Observation {
let scaled = raw_value * self.scale + self.offset;
Observation::sensor(&self.name, scaled)
}
pub fn boolean(&self, value: bool) -> Observation {
Observation::sensor(&self.name, if value { 1.0 } else { 0.0 })
}
pub fn event(&self) -> Observation {
Observation::event(&self.name)
}
}
pub struct IoTPolicyBuilder;
impl IoTPolicyBuilder {
pub fn threshold_alert(sensor_name: &str, threshold: f64, alert_message: &str) -> Rule {
use kaneru::policy::Condition;
Rule::new(
&format!("{}_threshold", sensor_name),
Condition::above(sensor_name, threshold),
Action::alert(alert_message),
)
}
pub fn maintain_range(
sensor_name: &str,
min: f64,
max: f64,
action_low: Action,
action_high: Action,
) -> Vec<Rule> {
use kaneru::policy::Condition;
vec![
Rule::new(
&format!("{}_below_min", sensor_name),
Condition::below(sensor_name, min),
action_low,
),
Rule::new(
&format!("{}_above_max", sensor_name),
Condition::above(sensor_name, max),
action_high,
),
]
}
pub fn binary_control(
sensor_name: &str,
threshold: f64,
on_action: Action,
off_action: Action,
) -> Vec<Rule> {
use kaneru::policy::Condition;
vec![
Rule::new(
&format!("{}_turn_on", sensor_name),
Condition::below(sensor_name, threshold),
on_action,
),
Rule::new(
&format!("{}_turn_off", sensor_name),
Condition::above(sensor_name, threshold),
off_action,
),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> SmartNodeConfig {
SmartNodeConfig {
node_config: Config::test_mode(),
agent_config: AgentConfig::default(),
auto_publish_observations: false,
auto_publish_actions: false,
observation_retention_secs: 60,
max_pending_actions: 10,
}
}
#[test]
fn test_smart_node_creation() {
let config = test_config();
let node = SmartNode::new(config).unwrap();
assert!(node.is_running());
}
#[test]
fn test_smart_node_observe() {
let config = test_config();
let mut node = SmartNode::new(config).unwrap();
let obs = Observation::sensor("temperature", 25.0);
let result = node.observe(obs);
assert!(result.is_ok());
assert_eq!(node.agent_stats().observations_received, 1);
}
#[test]
fn test_smart_node_step() {
let config = test_config();
let mut node = SmartNode::new(config).unwrap();
let result = node.step().unwrap();
assert!(result.is_none());
}
#[test]
fn test_sensor_adapter() {
let adapter = SensorAdapter::with_scaling("temperature", 0.1, -40.0);
let obs = adapter.reading(650.0);
assert_eq!(obs.value.as_f64().unwrap(), 25.0); }
#[test]
fn test_policy_builder() {
let rule = IoTPolicyBuilder::threshold_alert("temperature", 30.0, "High temperature!");
assert!(matches!(rule.action.action_type, ActionType::Alert(_)));
}
#[test]
fn test_smart_node_with_rule() {
use kaneru::policy::Condition;
let config = test_config();
let mut node = SmartNode::new(config).unwrap();
let rule = Rule::new(
"high_temp",
Condition::above("temperature", 30.0),
Action::alert("Temperature too high!"),
);
node.add_rule(rule);
let obs = Observation::sensor("temperature", 35.0);
node.observe(obs).unwrap();
let result = node.step().unwrap();
assert!(result.is_some());
let action_result = result.unwrap();
assert!(action_result.success);
}
#[test]
fn test_smart_node_stats() {
let config = test_config();
let node = SmartNode::new(config).unwrap();
let stats = node.stats().unwrap();
assert_eq!(stats.pending_actions, 0);
assert_eq!(stats.observation_entries, 0);
}
}