use std::time::Duration;
use crate::types::{GroupId, LoraConfig, TaskId, WorkerId};
#[derive(Debug, Clone)]
pub struct ActionEvent {
pub id: ActionEventId,
pub task_id: TaskId,
pub group_id: Option<GroupId>,
pub tick: u64,
pub worker_id: WorkerId,
pub action: String,
pub target: Option<String>,
pub result: ActionEventResult,
pub duration: Duration,
pub context: ActionContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ActionEventId(pub u64);
impl ActionEventId {
pub fn new(tick: u64, worker_id: WorkerId, sequence: u32) -> Self {
let id = (tick << 24) | ((worker_id.0 as u64) << 8) | (sequence as u64 & 0xFF);
Self(id)
}
}
#[derive(Debug, Clone)]
pub struct ActionEventResult {
pub success: bool,
pub output: Option<String>,
pub error: Option<String>,
pub discoveries: u32,
pub kpi_contribution: Option<f64>,
}
impl ActionEventResult {
pub fn success() -> Self {
Self {
success: true,
output: None,
error: None,
discoveries: 0,
kpi_contribution: None,
}
}
pub fn success_with_output(output: impl Into<String>) -> Self {
Self {
success: true,
output: Some(output.into()),
error: None,
discoveries: 0,
kpi_contribution: None,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
success: false,
output: None,
error: Some(error.into()),
discoveries: 0,
kpi_contribution: None,
}
}
pub fn with_discoveries(mut self, count: u32) -> Self {
self.discoveries = count;
self
}
pub fn with_kpi(mut self, contribution: f64) -> Self {
self.kpi_contribution = Some(contribution);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ActionContext {
pub selection_logic: Option<String>,
pub exploration_node_id: Option<u64>,
pub from_guidance: bool,
pub previous_action: Option<String>,
pub lora: Option<LoraConfig>,
pub metadata: std::collections::HashMap<String, String>,
}
impl ActionContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_selection_logic(mut self, logic: impl Into<String>) -> Self {
self.selection_logic = Some(logic.into());
self
}
pub fn with_exploration_node(mut self, node_id: u64) -> Self {
self.exploration_node_id = Some(node_id);
self
}
pub fn with_guidance(mut self) -> Self {
self.from_guidance = true;
self
}
pub fn with_previous_action(mut self, action: impl Into<String>) -> Self {
self.previous_action = Some(action.into());
self
}
pub fn with_lora(mut self, lora: LoraConfig) -> Self {
self.lora = Some(lora);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
pub struct ActionEventBuilder {
task_id: TaskId,
group_id: Option<GroupId>,
tick: u64,
worker_id: WorkerId,
sequence: u32,
action: String,
target: Option<String>,
result: ActionEventResult,
duration: Duration,
context: ActionContext,
}
impl ActionEventBuilder {
pub fn new(tick: u64, worker_id: WorkerId, action: impl Into<String>) -> Self {
Self {
task_id: TaskId::new(),
group_id: None,
tick,
worker_id,
sequence: 0,
action: action.into(),
target: None,
result: ActionEventResult::success(),
duration: Duration::ZERO,
context: ActionContext::default(),
}
}
pub fn task_id(mut self, task_id: TaskId) -> Self {
self.task_id = task_id;
self
}
pub fn group_id(mut self, group_id: GroupId) -> Self {
self.group_id = Some(group_id);
self
}
pub fn group_id_opt(mut self, group_id: Option<GroupId>) -> Self {
self.group_id = group_id;
self
}
pub fn sequence(mut self, seq: u32) -> Self {
self.sequence = seq;
self
}
pub fn target(mut self, target: impl Into<String>) -> Self {
self.target = Some(target.into());
self
}
pub fn result(mut self, result: ActionEventResult) -> Self {
self.result = result;
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
pub fn context(mut self, context: ActionContext) -> Self {
self.context = context;
self
}
pub fn build(self) -> ActionEvent {
ActionEvent {
id: ActionEventId::new(self.tick, self.worker_id, self.sequence),
task_id: self.task_id,
group_id: self.group_id,
tick: self.tick,
worker_id: self.worker_id,
action: self.action,
target: self.target,
result: self.result,
duration: self.duration,
context: self.context,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_action_event_id_uniqueness() {
let id1 = ActionEventId::new(100, WorkerId(0), 0);
let id2 = ActionEventId::new(100, WorkerId(0), 1);
let id3 = ActionEventId::new(100, WorkerId(1), 0);
let id4 = ActionEventId::new(101, WorkerId(0), 0);
assert_ne!(id1, id2);
assert_ne!(id1, id3);
assert_ne!(id1, id4);
}
#[test]
fn test_action_event_builder() {
let event = ActionEventBuilder::new(10, WorkerId(1), "CheckStatus")
.target("user-service")
.result(ActionEventResult::success_with_output("Service is running"))
.duration(Duration::from_millis(50))
.context(
ActionContext::new()
.with_selection_logic("UCB1")
.with_guidance(),
)
.build();
assert_eq!(event.tick, 10);
assert_eq!(event.worker_id, WorkerId(1));
assert_eq!(event.action, "CheckStatus");
assert_eq!(event.target, Some("user-service".to_string()));
assert!(event.result.success);
assert_eq!(event.duration, Duration::from_millis(50));
assert_eq!(event.context.selection_logic, Some("UCB1".to_string()));
assert!(event.context.from_guidance);
}
}