From c2978f5f5fa61419a4df62c11348d66060cbfe62 Mon Sep 17 00:00:00 2001 From: Peter Hayman Date: Tue, 9 Jul 2024 16:58:01 +1000 Subject: [PATCH] feat: hello_ml observer --- crates/beet_ecs/Cargo.toml | 3 +- crates/beet_ecs/src/lib.rs | 4 +- crates/beet_examples/src/scenes/seek.rs | 3 +- .../src/scenes/sentence_selector.rs | 7 +- .../src/serde_utils/ready_on_asset_load.rs | 6 +- crates/beet_ml/Cargo.toml | 1 + crates/beet_ml/src/language/bert.rs | 2 +- crates/beet_ml/src/language/bert_plugin.rs | 2 +- crates/beet_ml/src/language/selectors/mod.rs | 4 +- .../src/language/selectors/sentence_flow.rs | 111 ++++++++++++++ .../src/language/selectors/sentence_scorer.rs | 141 ------------------ 11 files changed, 127 insertions(+), 157 deletions(-) create mode 100644 crates/beet_ml/src/language/selectors/sentence_flow.rs delete mode 100644 crates/beet_ml/src/language/selectors/sentence_scorer.rs diff --git a/crates/beet_ecs/Cargo.toml b/crates/beet_ecs/Cargo.toml index 83cc48a0..e45fe607 100644 --- a/crates/beet_ecs/Cargo.toml +++ b/crates/beet_ecs/Cargo.toml @@ -16,6 +16,7 @@ categories.workspace = true default = [] # default = ["reflect"] reflect = [] +test = ["dep:sweet"] [dependencies] beet_ecs_macros.workspace = true @@ -30,9 +31,9 @@ extend.workspace = true num-traits.workspace = true bevy.workspace = true +sweet = { workspace = true, optional = true } [dev-dependencies] pretty_env_logger.workspace = true -sweet.workspace = true bincode.workspace = true ron.workspace = true diff --git a/crates/beet_ecs/src/lib.rs b/crates/beet_ecs/src/lib.rs index 719a3099..38f9767b 100644 --- a/crates/beet_ecs/src/lib.rs +++ b/crates/beet_ecs/src/lib.rs @@ -8,7 +8,7 @@ pub mod graph; pub mod lifecycle; pub mod observers; pub mod reflect; -#[cfg(test)] +#[cfg(any(test, feature = "test"))] pub mod test; pub mod tree; @@ -34,7 +34,7 @@ pub mod prelude { pub use crate::observers::*; // pub use crate::lifecycle::*; pub use crate::reflect::*; - #[cfg(test)] + #[cfg(any(test, feature = "test"))] pub use crate::test::*; pub use crate::tree::*; pub use beet_ecs_macros::*; diff --git a/crates/beet_examples/src/scenes/seek.rs b/crates/beet_examples/src/scenes/seek.rs index e77a9d30..0d0db6a6 100644 --- a/crates/beet_examples/src/scenes/seek.rs +++ b/crates/beet_examples/src/scenes/seek.rs @@ -30,9 +30,8 @@ pub fn seek(mut commands: Commands) { // behavior parent.spawn(( Name::new("Seek"), - RunOnSpawn, + RunOnAppReady::default(), ContinueRun::default(), - InsertOnTrigger::::default(), TargetAgent(parent.parent_entity()), Seek, )); diff --git a/crates/beet_examples/src/scenes/sentence_selector.rs b/crates/beet_examples/src/scenes/sentence_selector.rs index 34fbe84a..7be8c8b6 100644 --- a/crates/beet_examples/src/scenes/sentence_selector.rs +++ b/crates/beet_examples/src/scenes/sentence_selector.rs @@ -14,13 +14,10 @@ pub fn sentence_selector(mut commands: Commands) { .spawn(( Name::new("Sentence Selector"), AssetLoadBlockAppReady, - InsertOnSend::::default(), + RunOnAppReady::default(), TargetAgent(agent), bert_handle, - SentenceScorer::default(), - ScoreSelector { - consume_scores: true, - }, + SentenceFlow::default(), )) .with_children(|parent| { parent.spawn(( diff --git a/crates/beet_examples/src/serde_utils/ready_on_asset_load.rs b/crates/beet_examples/src/serde_utils/ready_on_asset_load.rs index 372a93d6..47484853 100644 --- a/crates/beet_examples/src/serde_utils/ready_on_asset_load.rs +++ b/crates/beet_examples/src/serde_utils/ready_on_asset_load.rs @@ -1,4 +1,5 @@ use crate::beet::prelude::AppReady; +use beet_net::events::RunOnAppReady; use bevy::prelude::*; use std::marker::PhantomData; @@ -21,10 +22,10 @@ impl Plugin for ReadyOnAssetLoadPlugin { pub fn ready_on_asset_load( mut asset_events: EventReader>, - mut ready_events: EventWriter, mut commands: Commands, query: Query<(Entity, &Handle), With>, all_blocks: Query>, + all_awaiting: Query>, ) { let mut total_ready = 0; for ev in asset_events.read() { @@ -44,6 +45,7 @@ pub fn ready_on_asset_load( } let total_blocks = all_blocks.iter().count(); if total_blocks > 0 && total_blocks == total_ready { - ready_events.send(AppReady); + let targets = all_awaiting.iter().collect::>(); + commands.trigger_targets(AppReady,targets); } } diff --git a/crates/beet_ml/Cargo.toml b/crates/beet_ml/Cargo.toml index 6e5e107b..0eeee705 100644 --- a/crates/beet_ml/Cargo.toml +++ b/crates/beet_ml/Cargo.toml @@ -56,6 +56,7 @@ wasm-bindgen-futures.workspace = true console_error_panic_hook.workspace = true [dev-dependencies] +beet_ecs = { workspace = true, features = ["test"] } pretty_env_logger.workspace = true sweet.workspace = true diff --git a/crates/beet_ml/src/language/bert.rs b/crates/beet_ml/src/language/bert.rs index 672433e2..a552180d 100644 --- a/crates/beet_ml/src/language/bert.rs +++ b/crates/beet_ml/src/language/bert.rs @@ -162,7 +162,7 @@ impl Bert { } - /// Score a list of entities with a [`Sentence`] against a root entity with a [`Sentence`]. This returns a list of entities with their sentence and raw cosine similarity scores. + /// Score and **sort** a list of entities with a [`Sentence`] against a root entity with a [`Sentence`]. This returns a list of entities with their sentence and raw cosine similarity scores. /// Scores are in a range of `0..1`, higher means more similar, the list is sorted in descending order. /// This calls [`Bert::get_embeddings`] and has the associated performance implications. /// If the root is missing a [`Sentence`] an empty vec will be returned. diff --git a/crates/beet_ml/src/language/bert_plugin.rs b/crates/beet_ml/src/language/bert_plugin.rs index 378e1dba..021ab2d8 100644 --- a/crates/beet_ml/src/language/bert_plugin.rs +++ b/crates/beet_ml/src/language/bert_plugin.rs @@ -8,7 +8,7 @@ pub struct BertPlugin; impl Plugin for BertPlugin { fn build(&self, app: &mut App) { - app.add_plugins(ActionPlugin::::default()) + app.add_plugins(ActionPlugin::::default()) .init_asset::() .init_asset_loader::() .register_type::() diff --git a/crates/beet_ml/src/language/selectors/mod.rs b/crates/beet_ml/src/language/selectors/mod.rs index 49f4a869..09fbd410 100644 --- a/crates/beet_ml/src/language/selectors/mod.rs +++ b/crates/beet_ml/src/language/selectors/mod.rs @@ -1,6 +1,6 @@ pub mod find_sentence_steer_target; #[allow(unused_imports)] pub use self::find_sentence_steer_target::*; -pub mod sentence_scorer; +pub mod sentence_flow; #[allow(unused_imports)] -pub use self::sentence_scorer::*; +pub use self::sentence_flow::*; diff --git a/crates/beet_ml/src/language/selectors/sentence_flow.rs b/crates/beet_ml/src/language/selectors/sentence_flow.rs new file mode 100644 index 00000000..bd57ee12 --- /dev/null +++ b/crates/beet_ml/src/language/selectors/sentence_flow.rs @@ -0,0 +1,111 @@ +use crate::prelude::*; +use beet_ecs::prelude::*; +use bevy::prelude::*; +use forky_core::ResultTEExt; +use std::borrow::Cow; + +/// This component is for use with [`SentenceFlow`]. Add to either the agent or a child behavior. +#[derive(Debug, Clone, Component, PartialEq, Reflect)] +#[reflect(Component)] +pub struct Sentence(pub Cow<'static, str>); +impl Sentence { + pub fn new(s: impl Into>) -> Self { Self(s.into()) } +} + +/// Runs the child with the [`Sentence`] that is most similar to that of the agent. +/// for use with [`ScoreSelector`] +#[derive(Debug, Default, Clone, PartialEq, Action, Reflect)] +#[reflect(Component, ActionMeta)] +#[category(ActionCategory::ChildBehaviors)] +#[observers(sentence_flow)] +pub struct SentenceFlow; + +impl SentenceFlow { + pub fn new() -> Self { Self {} } +} + +fn sentence_flow( + trigger: Trigger, + mut commands: Commands, + mut berts: ResMut>, + sentences: Query<&Sentence>, + // TODO double query, ie added running and added asset + query: Query<(&SentenceFlow, &Handle, &TargetAgent, &Children)>, +) { + let (_scorer, handle, agent, children) = query + .get(trigger.entity()) + .expect(expect_action::NO_ACTION_COMP); + let Some(bert) = berts.get_mut(handle) else { + // not ready yet + log::warn!("SentenceFlow: Bert asset was not ready, will not run"); + return; + }; + + let children = children.into_iter().cloned().collect::>(); + //todo: async + bert.score_sentences(agent.0, children, &sentences) + .ok_or(|e| log::error!("{e}")) + .map(|scores| { + if let Some((entity, ..)) = scores.first() { + commands.entity(*entity).trigger(OnRun); + } else { + log::warn!("SentenceFlow: No scores returned"); + } + }); +} + +#[cfg(test)] +mod test { + use crate::prelude::*; + use anyhow::Result; + use beet_ecs::prelude::*; + use bevy::prelude::*; + use sweet::*; + + + + #[test] + fn works() -> Result<()> { + pretty_env_logger::try_init().ok(); + + let mut app = App::new(); + app.add_plugins(( + MinimalPlugins, + AssetPlugin::default(), + BertPlugin::default(), + LifecyclePlugin, + )) + .finish(); + let on_run = observe_trigger_names::(app.world_mut()); + + block_on_asset_load::(&mut app, "default-bert.ron"); + + let handle = app + .world_mut() + .resource_mut::() + .load::("default-bert.ron"); + + let agent = app.world_mut().spawn(Sentence::new("destroy")).id(); + + + app.world_mut() + .spawn(( + Name::new("root"), + TargetAgent(agent), + handle, + SentenceFlow::default(), + )) + .with_children(|parent| { + parent.spawn((Name::new("heal"), Sentence::new("heal"))); + parent.spawn((Name::new("kill"), Sentence::new("kill"))); + }) + .flush_trigger(OnRun); + + + expect(&on_run).to_have_been_called_times(2)?; + expect(&on_run).to_have_returned_nth_with(0, &"root".to_string())?; + expect(&on_run).to_have_returned_nth_with(1, &"kill".to_string())?; + + Ok(()) + } +} diff --git a/crates/beet_ml/src/language/selectors/sentence_scorer.rs b/crates/beet_ml/src/language/selectors/sentence_scorer.rs deleted file mode 100644 index 2f7d5722..00000000 --- a/crates/beet_ml/src/language/selectors/sentence_scorer.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::prelude::*; -use beet_ecs::prelude::*; -use bevy::prelude::*; -use forky_core::ResultTEExt; -use std::borrow::Cow; - -/// This component is for use with the [`SentenceScorer`]. Add to either the agent or a child behavior. -#[derive(Debug, Clone, Component, PartialEq, Reflect)] -#[reflect(Component)] -pub struct Sentence(pub Cow<'static, str>); -impl Sentence { - pub fn new(s: impl Into>) -> Self { Self(s.into()) } -} - -/// Updates the [`Score`] of each child based on the similarity of its [`Sentence`] with the agent, -/// for use with [`ScoreSelector`] -#[derive(Debug, Default, Clone, PartialEq, Action, Reflect)] -#[reflect(Component, ActionMeta)] -#[category(ActionCategory::ChildBehaviors)] -#[systems(sentence_scorer.in_set(TickSet))] -pub struct SentenceScorer; - -impl SentenceScorer { - pub fn new() -> Self { Self {} } -} - -fn sentence_scorer( - mut commands: Commands, - mut berts: ResMut>, - sentences: Query<&Sentence>, - // TODO double query, ie added running and added asset - started: Query< - (&SentenceScorer, &Handle, &TargetAgent, &Children), - Added, - >, -) { - for (_scorer, handle, agent, children) in started.iter() { - let Some(bert) = berts.get_mut(handle) else { - continue; - }; - - let children = children.into_iter().cloned().collect::>(); - //todo: async - bert.score_sentences(agent.0, children, &sentences) - .ok_or(|e| log::error!("{e}")) - .map(|scores| { - for (entity, _, score) in scores { - commands.entity(entity).insert(Score::Weight(score)); - } - }); - } -} - -#[cfg(test)] -mod test { - use crate::prelude::*; - use anyhow::Result; - use beet_ecs::prelude::*; - use bevy::prelude::*; - use sweet::*; - - fn setup(app: &mut App) -> Entity { - let handle = app - .world_mut() - .resource_mut::() - .load::("default-bert.ron"); - - app.world_mut() - .spawn(Sentence::new("destroy")) - .with_children(|parent| { - let id = parent.parent_entity(); - parent - .spawn(( - TargetAgent(id), - handle, - SentenceScorer::default(), - ScoreSelector { - consume_scores: true, - }, - Running, - )) - .with_children(|parent| { - parent.spawn(Sentence::new("heal")); - parent.spawn(Sentence::new("kill")); - }); - }) - .id() - } - - - #[test] - fn works() -> Result<()> { - pretty_env_logger::try_init().ok(); - - let mut app = App::new(); - app.add_plugins(( - MinimalPlugins, - AssetPlugin::default(), - BertPlugin::default(), - LifecyclePlugin, - )) - .finish(); - - block_on_asset_load::(&mut app, "default-bert.ron"); - let entity = setup(&mut app); - app.update(); - - let tree = EntityTree::new_with_world(entity, app.world()); - - let _scorer = app - .world_mut() - .query::<&SentenceScorer>() - .iter(app.world()) - .next() - .unwrap(); - - let scores = tree.component_tree::(app.world()); - - let heal_score = scores.children[0].children[0].value.unwrap(); - let kill_score = scores.children[0].children[1].value.unwrap(); - expect(kill_score).to_be_greater_than(heal_score)?; - expect(heal_score.weight()).to_be_less_than(0.5)?; - expect(kill_score.weight()).to_be_greater_than(0.5)?; - - expect(tree.component_tree(app.world())).to_be( - Tree::new(None).with_child( - Tree::new(Some(&Running)).with_leaf(None).with_leaf(None), - ), - )?; - app.update(); - expect(tree.component_tree(app.world())).to_be( - Tree::new(None).with_child( - Tree::new(Some(&Running)) - .with_leaf(None) - .with_leaf(Some(&Running)), - ), - )?; - - Ok(()) - } -}