use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::debug;
use uuid::Uuid;
use super::edge::{EdgeType, StateEdge};
use super::node::{NodeId, StateNode, StateNodeType};
use crate::episode::Episode;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateDag {
nodes: HashMap<NodeId, StateNode>,
edges_by_episode: HashMap<Uuid, Vec<StateEdge>>,
node_index: HashMap<(StateNodeType, String), NodeId>,
stats: DagStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DagStats {
pub total_nodes: u64,
pub total_edges: u64,
pub total_episodes: u64,
pub token_savings: u64,
pub most_ref_node: Option<(NodeId, usize)>,
pub last_updated: DateTime<Utc>,
}
impl StateDag {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register_episode(&mut self, episode: &Episode) -> usize {
let episode_id = episode.episode_id;
let mut edges_created = 0;
let task_type_str = episode.task_type.to_string();
let node_id = self.get_or_create_node(StateNodeType::TaskType, task_type_str);
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "task_type");
edges_created += 1;
if let Some(ref language) = episode.context.language {
let node_id = self.get_or_create_node(StateNodeType::Language, language.clone());
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "language");
edges_created += 1;
}
if let Some(ref framework) = episode.context.framework {
let node_id = self.get_or_create_node(StateNodeType::Framework, framework.clone());
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "framework");
edges_created += 1;
}
let domain = episode.context.domain.clone();
let node_id = self.get_or_create_node(StateNodeType::Domain, domain);
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "domain");
edges_created += 1;
let complexity_str = format!("{:?}", episode.context.complexity);
let node_id = self.get_or_create_node(StateNodeType::Complexity, complexity_str);
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "complexity");
edges_created += 1;
for tag in &episode.context.tags {
let node_id = self.get_or_create_node(StateNodeType::Tag, tag.clone());
self.add_edge(episode_id, node_id, EdgeType::HasAttribute, "tag");
edges_created += 1;
}
self.stats.total_episodes += 1;
self.stats.last_updated = Utc::now();
self.update_token_savings();
debug!(
episode_id = %episode_id,
edges_created = edges_created,
total_nodes = self.nodes.len(),
"Registered episode in StateDag"
);
edges_created
}
fn get_or_create_node(&mut self, node_type: StateNodeType, value: String) -> NodeId {
let key = (node_type, value.clone());
if let Some(&node_id) = self.node_index.get(&key) {
if let Some(node) = self.nodes.get_mut(&node_id) {
node.mark_accessed();
}
return node_id;
}
let node = StateNode::new(node_type, value);
let node_id = node.node_id;
self.nodes.insert(node_id, node);
self.node_index.insert(key, node_id);
self.stats.total_nodes += 1;
node_id
}
fn add_edge(
&mut self,
episode_id: Uuid,
node_id: NodeId,
_edge_type: EdgeType,
source_field: &str,
) {
let edge = StateEdge::attribute(episode_id, node_id, source_field.to_string());
self.edges_by_episode
.entry(episode_id)
.or_default()
.push(edge.clone());
if let Some(node) = self.nodes.get_mut(&node_id) {
node.add_episode_ref(episode_id);
}
self.stats.total_edges += 1;
}
#[must_use]
pub fn remove_episode(&mut self, episode_id: &Uuid) -> bool {
let removed = if let Some(edges) = self.edges_by_episode.remove(episode_id) {
let edge_count = edges.len() as u64;
for edge in edges {
if let Some(node) = self.nodes.get_mut(&edge.target_node) {
node.remove_episode_ref(episode_id);
}
}
self.stats.total_edges = self.stats.total_edges.saturating_sub(edge_count);
true
} else {
false
};
self.cleanup_unreferenced_nodes();
if removed {
self.stats.total_episodes = self.stats.total_episodes.saturating_sub(1);
self.stats.last_updated = Utc::now();
self.update_token_savings();
}
removed
}
fn cleanup_unreferenced_nodes(&mut self) {
let unreferenced: Vec<NodeId> = self
.nodes
.iter()
.filter(|(_, node)| !node.has_refs())
.map(|(id, _)| *id)
.collect();
for node_id in unreferenced {
if let Some(node) = self.nodes.remove(&node_id) {
let key = (node.node_type, node.value);
self.node_index.remove(&key);
self.stats.total_nodes = self.stats.total_nodes.saturating_sub(1);
}
}
}
fn update_token_savings(&mut self) {
let total_savings: usize = self.nodes.values().map(|n| n.token_savings()).sum();
self.stats.token_savings = total_savings as u64;
let most_ref = self
.nodes
.iter()
.max_by_key(|(_, n)| n.ref_count())
.map(|(id, n)| (*id, n.ref_count()));
self.stats.most_ref_node = most_ref;
}
#[must_use]
pub fn get_node(&self, node_id: &NodeId) -> Option<&StateNode> {
self.nodes.get(node_id)
}
#[must_use]
pub fn get_episode_edges(&self, episode_id: &Uuid) -> Option<&Vec<StateEdge>> {
self.edges_by_episode.get(episode_id)
}
#[must_use]
pub fn nodes_by_type(&self, node_type: StateNodeType) -> Vec<&StateNode> {
self.nodes
.values()
.filter(|n| n.node_type == node_type)
.collect()
}
#[must_use]
pub fn get_episode_nodes(&self, episode_id: &Uuid) -> Vec<&StateNode> {
self.edges_by_episode
.get(episode_id)
.map(|edges| {
edges
.iter()
.filter_map(|e| self.nodes.get(&e.target_node))
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn get_shared_context(&self, episode_ids: &[Uuid]) -> Vec<&StateNode> {
if episode_ids.is_empty() {
return Vec::new();
}
let target = episode_ids.len();
let mut freq: HashMap<NodeId, usize> = HashMap::with_capacity(self.nodes.len());
for ep_id in episode_ids {
for node in self.get_episode_nodes(ep_id) {
*freq.entry(node.node_id).or_insert(0) += 1;
}
}
freq.into_iter()
.filter(|(_, count)| *count == target)
.filter_map(|(id, _)| self.nodes.get(&id))
.collect()
}
#[must_use]
pub fn reduction_percentage(&self) -> f32 {
if self.stats.total_episodes == 0 {
return 0.0;
}
let estimated_without_dag: u64 = self
.nodes
.values()
.map(|n| n.estimated_tokens() as u64 * n.ref_count() as u64)
.sum();
if estimated_without_dag == 0 {
return 0.0;
}
let dag_cost: u64 = self
.nodes
.values()
.map(|n| n.estimated_tokens() as u64 + n.ref_count() as u64)
.sum();
let reduction =
(estimated_without_dag.saturating_sub(dag_cost)) as f32 / estimated_without_dag as f32;
reduction * 100.0
}
#[must_use]
pub fn stats(&self) -> &DagStats {
&self.stats
}
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.edges_by_episode.values().map(|v| v.len()).sum()
}
pub fn clear(&mut self) {
self.nodes.clear();
self.edges_by_episode.clear();
self.node_index.clear();
self.stats = DagStats::default();
}
}