pub struct Signal {
pub success: bool,
pub effort: f32,
pub metadata: HashMap<String, String>,
}Expand description
Feedback signal reported by caller after a user interacts with an item.
Domain-agnostic by design:
- In learning: success = answered correctly, effort = how hard it felt (0=easy, 1=hard)
- In ecommerce: success = purchased/clicked, effort = price relative to budget
- In travel: success = booked/saved, effort = distance/cost relative to preference
- In content: success = completed/liked, effort = complexity relative to level
Callers may extend context via the metadata map for custom StateUpdater logic.
Fields§
§success: boolDid the interaction result in a positive outcome?
effort: f32Normalised effort cost of this interaction. Range [0.0, 1.0]. 0.0 = effortless, 1.0 = maximum effort / friction.
metadata: HashMap<String, String>Optional arbitrary metadata for custom updater logic. Key-value pairs; values are caller-defined strings.
Implementations§
Source§impl Signal
impl Signal
Sourcepub fn new(success: bool, effort: f32) -> Self
pub fn new(success: bool, effort: f32) -> Self
Minimal constructor — no metadata.
Examples found in repository?
examples/learning_simulation.rs (line 80)
12fn main() {
13 println!("=== aria-core: Learning Domain Simulation ===\n");
14
15 // --- Build engine ---
16 let mut engine = Engine::new(EngineConfig {
17 exploration_rate: 0.0, // deterministic for demo
18 alpha: 0.1, // faster skill movement so visible in 15 steps
19 });
20
21 // Register factors — caller's choice
22 engine.add_factor(Box::new(ChallengeFactor::new(0.2)));
23 engine.add_factor(Box::new(SpacingFactor::new(10))); // 10s interval for demo
24 engine.add_factor(Box::new(CoverageFactor));
25 engine.seed_rng(42);
26
27 // Register items — caller's curriculum
28 engine.add_items(vec![
29 // Math track
30 Item::new("counting", 0.05, "math"),
31 Item::new("addition", 0.15, "math"),
32 Item::new("multiplication", 0.30, "math"),
33 Item::new("fractions", 0.45, "math"),
34 Item::new("algebra_basics", 0.60, "math"),
35 Item::new("quadratics", 0.75, "math")
36 .with_prereqs(vec!["algebra_basics".into()]),
37 Item::new("calculus_intro", 0.90, "math")
38 .with_prereqs(vec!["quadratics".into()]),
39
40 // Science track (no prereqs — runs in parallel)
41 Item::new("sci_observation", 0.10, "science"),
42 Item::new("sci_hypothesis", 0.25, "science"),
43 Item::new("sci_experiment", 0.50, "science"),
44 Item::new("sci_analysis", 0.70, "science"),
45 Item::new("sci_research", 0.90, "science"),
46 ]).unwrap();
47
48 println!("Items registered: {}", engine.item_count());
49 println!("Factors registered: {}\n", engine.factor_count());
50
51 println!("{:<5} {:<22} {:<10} {:<8} {:<8} {:<10}",
52 "Step", "Item", "Category", "Success", "Effort", "Skill");
53 println!("{}", "-".repeat(70));
54
55 // --- Simulate 15 interactions ---
56 // Alternates easy successes and a few failures to show adaptation
57 let interactions: Vec<(bool, f32)> = vec![
58 (true, 0.8), // 1 hard success
59 (true, 0.3), // 2 easy success → optimism up
60 (true, 0.2), // 3 easy
61 (false, 0.9), // 4 failure → optimism eases
62 (true, 0.5), // 5
63 (true, 0.4), // 6
64 (true, 0.2), // 7 easy → optimism climbs
65 (true, 0.3), // 8
66 (false, 0.7), // 9 failure
67 (true, 0.5), // 10
68 (true, 0.4), // 11
69 (true, 0.3), // 12
70 (true, 0.2), // 13
71 (true, 0.5), // 14
72 (true, 0.3), // 15
73 ];
74
75 for (step, (success, effort)) in interactions.iter().enumerate() {
76 let item = engine.suggest("alice").unwrap();
77 let item_id = item.id().to_string();
78 let category = item.category().to_string();
79
80 engine.feedback("alice", &item_id, Signal::new(*success, *effort)).unwrap();
81
82 let state = engine.get_state("alice").unwrap();
83 println!("{:<5} {:<22} {:<10} {:<8} {:<8.2} {:<.4}",
84 step + 1,
85 item_id,
86 category,
87 if *success { "✓" } else { "✗" },
88 effort,
89 state.skill,
90 );
91 }
92
93 // --- Show final state ---
94 let state = engine.get_state("alice").unwrap();
95 println!("\n=== Final State ===");
96 println!("Skill: {:.4}", state.skill);
97 println!("Optimism bias: {:.4}", state.optimism_bias);
98 println!("Target: {:.4}", state.target());
99 println!("Interactions: {}", state.interaction_count);
100 println!("Resolved items: {:?}", state.resolved_set);
101
102 println!("\n=== Category Coverage ===");
103 let mut cats: Vec<_> = state.category_count.iter().collect();
104 cats.sort_by_key(|(k, _)| k.as_str());
105 for (cat, count) in cats {
106 println!(" {:<12} → {} interactions", cat, count);
107 }
108
109 // --- Serialise / deserialise round-trip ---
110 println!("\n=== Serialisation Round-Trip ===");
111 let encoded = Serialiser::encode(state);
112 println!("Encoded keys: {}", encoded.len());
113 let decoded = Serialiser::decode(&encoded).unwrap();
114 assert!((decoded.skill - state.skill).abs() < 1e-5, "skill mismatch after round-trip");
115 assert_eq!(decoded.interaction_count, state.interaction_count);
116 println!("Round-trip: ✓ skill={:.4} interactions={}", decoded.skill, decoded.interaction_count);
117
118 // --- Prereq demonstration ---
119 println!("\n=== Prerequisite Gating ===");
120 println!("calculus_intro requires: quadratics → algebra_basics");
121 let can_see_calculus = state.resolved_set.contains("quadratics");
122 println!("User resolved quadratics: {}", can_see_calculus);
123 println!("(calculus_intro will unlock once quadratics is resolved)");
124
125 println!("\n=== Custom Domain Example (ecommerce) ===");
126 demo_ecommerce();
127}
128
129/// Quick demo showing the same engine used for a completely different domain.
130fn demo_ecommerce() {
131 use aria_core::factor::ChallengeFactor;
132
133 struct BudgetFactor;
134 impl aria_core::factor::Factor for BudgetFactor {
135 fn name(&self) -> &str { "budget" }
136 fn score(&self, item: &dyn Scoreable, state: &aria_core::ProfileState, _now: u64) -> f32 {
137 // score_proxy = price_ratio (0=free, 1=most expensive)
138 // ProfileState.skill = budget_willingness
139 let target = state.target();
140 let diff = item.score_proxy() - target;
141 (-diff * diff / 0.08).exp() // bandwidth=0.2
142 }
143 }
144
145 let mut engine = Engine::new(EngineConfig {
146 exploration_rate: 0.0,
147 alpha: 0.1,
148 });
149 engine.add_factor(Box::new(BudgetFactor));
150 engine.add_factor(Box::new(ChallengeFactor::new(0.25))); // reused as price-fit
151 engine.seed_rng(1);
152
153 engine.add_items(vec![
154 Item::new("budget_headphones", 0.1, "audio"),
155 Item::new("mid_headphones", 0.4, "audio"),
156 Item::new("premium_headphones", 0.9, "audio"),
157 Item::new("budget_speaker", 0.15, "audio"),
158 Item::new("smartwatch_basic", 0.3, "wearables"),
159 Item::new("smartwatch_pro", 0.8, "wearables"),
160 ]).unwrap();
161
162 // User with mid budget
163 let mut state = aria_core::ProfileState::new();
164 state.skill = 0.35; // budget willingness
165 engine.load_state("shopper", state);
166
167 print!("Suggestions for mid-budget shopper: ");
168 for _ in 0..3 {
169 let item = engine.suggest("shopper").unwrap();
170 let id = item.id().to_string();
171 print!("{} ", id);
172 engine.feedback("shopper", &id, Signal::new(true, 0.5)).unwrap();
173 }
174 println!();
175}Sourcepub fn with_metadata(
success: bool,
effort: f32,
metadata: HashMap<String, String>,
) -> Self
pub fn with_metadata( success: bool, effort: f32, metadata: HashMap<String, String>, ) -> Self
Constructor with metadata.
Sourcepub fn performance(&self) -> f32
pub fn performance(&self) -> f32
Composite performance score derived from success + effort. High success + low effort → score near 1.0 (too easy). High success + high effort → score near 0.5 (good challenge). Failure → score near 0.0.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Signal
impl RefUnwindSafe for Signal
impl Send for Signal
impl Sync for Signal
impl Unpin for Signal
impl UnsafeUnpin for Signal
impl UnwindSafe for Signal
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more