use std::collections::HashMap;
use std::error::Error;
use std::sync::Arc;
use std::time::Duration;
use goaprs::action::Action;
use goaprs::error::Result;
use goaprs::sensor::{SensorFn, SensorResponse, Sensors};
use goaprs::utils::automaton::AutomatonController;
use async_trait::async_trait;
use tokio::sync::Mutex;
struct LampEnvironment {
is_on: Mutex<bool>,
}
impl LampEnvironment {
fn new(is_on: bool) -> Self {
Self {
is_on: Mutex::new(is_on),
}
}
}
struct LampSensor {
env: Arc<LampEnvironment>,
}
impl LampSensor {
fn new(env: Arc<LampEnvironment>) -> Self {
Self { env }
}
}
#[async_trait]
impl SensorFn for LampSensor {
async fn exec(&self) -> Result<SensorResponse> {
let is_on = *self.env.is_on.lock().await;
let status = if is_on { "on" } else { "off" };
Ok(SensorResponse::new(status.to_string(), String::new(), 0))
}
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn Error>> {
let env = Arc::new(LampEnvironment::new(false));
let mut sensors = Sensors::new();
sensors.add("check_lamp", "lamp_status", LampSensor::new(env.clone()))?;
sensors.add(
"uptime",
"system_uptime",
goaprs::sensor::FnSensor::new(|| async {
Ok(("12 days, 5 hours, 10 minutes".to_string(), String::new(), 0))
}),
)?;
let mut actions = Vec::new();
let mut turn_on = Action::new("turn_lamp_on", 1.0)?;
turn_on.preconditions.set("lamp_status", false);
turn_on.effects.set("lamp_status", true);
actions.push(turn_on);
let mut turn_off = Action::new("turn_lamp_off", 1.0)?;
turn_off.preconditions.set("lamp_status", true);
turn_off.effects.set("lamp_status", false);
actions.push(turn_off);
let mut simple_action = Action::new("simple_action", 0.5)?;
simple_action.effects.set("simple_task", true);
actions.push(simple_action);
let mut world_state = HashMap::new();
world_state.insert("lamp_status".to_string(), "off".to_string());
world_state.insert("simple_task".to_string(), "pending".to_string());
let controller = AutomatonController::new(
actions,
sensors.iter().cloned().collect(),
"smart_home_agent",
world_state,
);
let mut goal = HashMap::new();
goal.insert("lamp_status".to_string(), "on".to_string());
goal.insert("simple_task".to_string(), "done".to_string());
controller.set_goal(goal).await?;
println!("-- Starting manual GOAP cycle --");
println!("Sensing the environment...");
controller.automaton().sense().await?;
println!("Planning actions...");
let plan = controller.automaton().plan().await?;
println!("Generated plan with {} actions:", plan.len());
for (i, action) in plan.iter().enumerate() {
println!(" Step {}: {}", i + 1, action.name);
}
println!("Executing actions...");
let responses = controller.automaton().act().await?;
for (i, response) in responses.iter().enumerate() {
println!(" Result {}: {}", i + 1, response.stdout());
}
println!("Sensing again to see changes...");
controller.automaton().sense().await?;
let final_state = controller.automaton().world_state().await?;
println!("Final world state: {:?}", final_state);
println!("\n-- Now starting automated controller --");
println!("Press Ctrl+C to exit");
controller.start().await?;
tokio::time::sleep(Duration::from_secs(10)).await;
println!("\nChanging goal to turn lamp OFF");
let mut new_goal = HashMap::new();
new_goal.insert("lamp_status".to_string(), "off".to_string());
new_goal.insert("simple_task".to_string(), "done".to_string());
controller.set_goal(new_goal).await?;
tokio::time::sleep(Duration::from_secs(20)).await;
controller.stop().await?;
println!("Controller stopped");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct MockLampSensor {
is_on: Arc<Mutex<bool>>,
call_count: Arc<AtomicUsize>,
}
impl MockLampSensor {
fn new(is_on: bool) -> Self {
Self {
is_on: Arc::new(Mutex::new(is_on)),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
}
#[async_trait]
impl SensorFn for MockLampSensor {
async fn exec(&self) -> Result<SensorResponse> {
self.call_count.fetch_add(1, Ordering::SeqCst);
let is_on = *self.is_on.lock().await;
let status = if is_on { "on" } else { "off" };
Ok(SensorResponse::new(status.to_string(), String::new(), 0))
}
}
struct MockAction {
name: String,
executed: Arc<AtomicUsize>,
response: String,
}
impl MockAction {
fn new(name: &str, response: &str) -> Self {
Self {
name: name.to_string(),
executed: Arc::new(AtomicUsize::new(0)),
response: response.to_string(),
}
}
fn execution_count(&self) -> usize {
self.executed.load(Ordering::SeqCst)
}
}
#[async_trait]
impl ActionFn for MockAction {
async fn exec(&self, _world_state: &HashMap<String, goaprs::utils::actor::Fact>) -> bool {
self.executed.fetch_add(1, Ordering::SeqCst);
true
}
}
#[tokio::test]
async fn test_lamp_sensor() {
let sensor = MockLampSensor::new(true);
let call_count = sensor.call_count.clone();
let result = sensor.exec().await.unwrap();
assert_eq!(result.stdout(), "on");
assert_eq!(call_count.load(Ordering::SeqCst), 1);
*sensor.is_on.lock().await = false;
let result = sensor.exec().await.unwrap();
assert_eq!(result.stdout(), "off");
assert_eq!(call_count.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_lamp_actions() {
let env = Arc::new(LampEnvironment::new(false));
let turn_on = TurnLampOnAction::new(env.clone());
let turn_off = TurnLampOffAction::new(env.clone());
assert!(turn_on.exec(&HashMap::new()).await);
assert!(*env.is_on.lock().await);
assert!(turn_off.exec(&HashMap::new()).await);
assert!(!*env.is_on.lock().await);
}
#[tokio::test]
async fn test_simple_goap_cycle() {
let mut sensors = Sensors::new();
let lamp_sensor = MockLampSensor::new(false);
sensors
.add("check_lamp", "lamp_status", lamp_sensor)
.unwrap();
let mut actions = Vec::new();
let mut turn_on = Action::new("turn_lamp_on", 1.0).unwrap();
turn_on.preconditions.set("lamp_status", false); turn_on.effects.set("lamp_status", true); actions.push(turn_on);
let mut world_state = HashMap::new();
world_state.insert("lamp_status".to_string(), "off".to_string());
let controller = AutomatonController::new(
actions,
sensors.iter().cloned().collect(),
"test_agent",
world_state,
);
let mut goal = HashMap::new();
goal.insert("lamp_status".to_string(), "on".to_string());
controller.set_goal(goal).await.unwrap();
controller.automaton().sense().await.unwrap();
let plan = controller.automaton().plan().await.unwrap();
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].name, "turn_lamp_on");
}
}