use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
use super::node::StateNodeType;
use super::state::StateDag;
use crate::episode::Episode;
use std::sync::Arc;
pub(super) const CHARS_PER_TOKEN: usize = 4;
pub(super) const DEFAULT_CONTEXT_TOKENS: usize = 20;
pub(super) const TOKENS_PER_TAG: usize = 3;
pub(super) const TOKEN_OPTIMIZED_HEADER_TOKENS: usize = 5;
pub(super) const SHARED_ITEM_BASE_TOKENS: usize = 2;
pub(super) const SHARED_ITEM_TOKEN_OPT_BASE: usize = 3;
pub(super) const UNIQUE_ITEM_OVERHEAD_TOKENS: usize = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagAssemblyConfig {
pub max_unique_items: usize,
pub deduplicate_shared: bool,
pub include_unique: bool,
pub min_shared_threshold: usize,
pub format: AssemblyFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AssemblyFormat {
Compact,
Full,
TokenOptimized,
}
impl Default for DagAssemblyConfig {
fn default() -> Self {
Self {
max_unique_items: 20,
deduplicate_shared: true,
include_unique: true,
min_shared_threshold: 2,
format: AssemblyFormat::TokenOptimized,
}
}
}
impl DagAssemblyConfig {
#[must_use]
pub fn token_efficient() -> Self {
Self {
max_unique_items: 10,
deduplicate_shared: true,
include_unique: true,
min_shared_threshold: 1,
format: AssemblyFormat::TokenOptimized,
}
}
#[must_use]
pub fn full_context() -> Self {
Self {
max_unique_items: 50,
deduplicate_shared: false,
include_unique: true,
min_shared_threshold: 1,
format: AssemblyFormat::Full,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssembledContext {
pub shared_context: Vec<SharedContextItem>,
pub unique_context: Vec<UniqueContextItem>,
pub estimated_tokens: usize,
pub token_savings: usize,
pub episode_ids: HashSet<Uuid>,
pub assembled_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SharedContextItem {
pub node_type: StateNodeType,
pub value: String,
pub shared_count: usize,
pub node_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniqueContextItem {
pub episode_id: Uuid,
pub task_description: String,
pub unique_aspects: Vec<String>,
}
pub struct DagContextAssembler {
dag: StateDag,
config: DagAssemblyConfig,
}
impl DagContextAssembler {
pub fn new(dag: StateDag) -> Self {
Self {
dag,
config: DagAssemblyConfig::default(),
}
}
pub fn with_config(dag: StateDag, config: DagAssemblyConfig) -> Self {
Self { dag, config }
}
#[must_use]
pub fn empty() -> Self {
Self {
dag: StateDag::new(),
config: DagAssemblyConfig::default(),
}
}
#[must_use]
pub fn dag(&self) -> &StateDag {
&self.dag
}
pub fn dag_mut(&mut self) -> &mut StateDag {
&mut self.dag
}
pub fn register_episodes(&mut self, episodes: &[Arc<Episode>]) {
for episode in episodes {
self.dag.register_episode(episode.as_ref());
}
}
#[must_use]
pub fn assemble(&self, episodes: &[Arc<Episode>]) -> AssembledContext {
let episode_ids: HashSet<Uuid> = episodes.iter().map(|e| e.episode_id).collect();
let shared_context = if self.config.deduplicate_shared {
self.extract_shared_context(&episode_ids)
} else {
Vec::new()
};
let unique_context = if self.config.include_unique {
self.extract_unique_context(episodes, &shared_context)
} else {
Vec::new()
};
let shared_tokens = self.estimate_shared_tokens(&shared_context);
let unique_tokens = self.estimate_unique_tokens(&unique_context);
let estimated_tokens = shared_tokens + unique_tokens;
let original_tokens = self.estimate_original_tokens(episodes);
let token_savings = original_tokens.saturating_sub(estimated_tokens);
AssembledContext {
shared_context,
unique_context,
estimated_tokens,
token_savings,
episode_ids,
assembled_at: Utc::now(),
}
}
fn extract_shared_context(&self, episode_ids: &HashSet<Uuid>) -> Vec<SharedContextItem> {
let ids: Vec<Uuid> = episode_ids.iter().copied().collect();
let shared_nodes = self.dag.get_shared_context(&ids);
shared_nodes
.into_iter()
.filter(|n| n.ref_count() >= self.config.min_shared_threshold)
.map(|n| SharedContextItem {
node_type: n.node_type,
value: n.value.clone(),
shared_count: n.ref_count(),
node_id: n.node_id,
})
.collect()
}
fn extract_unique_context(
&self,
episodes: &[Arc<Episode>],
shared_context: &[SharedContextItem],
) -> Vec<UniqueContextItem> {
let shared_values: HashSet<String> =
shared_context.iter().map(|s| s.value.clone()).collect();
episodes
.iter()
.take(self.config.max_unique_items)
.map(|ep| {
let unique_aspects = self.find_unique_aspects(ep.as_ref(), &shared_values);
UniqueContextItem {
episode_id: ep.episode_id,
task_description: ep.task_description.clone(),
unique_aspects,
}
})
.collect()
}
fn find_unique_aspects(
&self,
episode: &Episode,
shared_values: &HashSet<String>,
) -> Vec<String> {
let mut unique = Vec::new();
if let Some(ref lang) = episode.context.language {
if !shared_values.contains(lang) {
unique.push(format!("language:{lang}"));
}
}
if let Some(ref fw) = episode.context.framework {
if !shared_values.contains(fw) {
unique.push(format!("framework:{fw}"));
}
}
if !shared_values.contains(&episode.context.domain) {
let domain = &episode.context.domain;
unique.push(format!("domain:{domain}"));
}
let complexity_str = format!("{:?}", episode.context.complexity);
if !shared_values.contains(&complexity_str) {
unique.push(format!("complexity:{complexity_str}"));
}
for tag in &episode.context.tags {
if !shared_values.contains(tag) {
unique.push(format!("tag:{tag}"));
}
}
unique
}
fn estimate_shared_tokens(&self, items: &[SharedContextItem]) -> usize {
match self.config.format {
AssemblyFormat::Compact => {
items.len()
}
AssemblyFormat::Full => {
items
.iter()
.map(|i| i.value.len() / CHARS_PER_TOKEN + SHARED_ITEM_BASE_TOKENS)
.sum()
}
AssemblyFormat::TokenOptimized => {
if items.is_empty() {
return 0;
}
items
.iter()
.map(|i| i.value.len() / CHARS_PER_TOKEN + SHARED_ITEM_TOKEN_OPT_BASE)
.sum::<usize>()
+ TOKEN_OPTIMIZED_HEADER_TOKENS
}
}
}
fn estimate_unique_tokens(&self, items: &[UniqueContextItem]) -> usize {
items
.iter()
.map(|i| {
let desc_tokens = i.task_description.len() / CHARS_PER_TOKEN;
let unique_tokens = i.unique_aspects.len() * TOKENS_PER_TAG;
desc_tokens + unique_tokens + UNIQUE_ITEM_OVERHEAD_TOKENS
})
.sum()
}
fn estimate_original_tokens(&self, episodes: &[Arc<Episode>]) -> usize {
episodes
.iter()
.map(|ep| {
let context_tokens = DEFAULT_CONTEXT_TOKENS;
let desc_tokens = ep.task_description.len() / CHARS_PER_TOKEN;
let tag_tokens = ep.context.tags.len() * TOKENS_PER_TAG;
context_tokens + desc_tokens + tag_tokens
})
.sum()
}
#[must_use]
pub fn format_for_prompt(&self, assembled: &AssembledContext) -> String {
super::format::format_for_prompt(self.config.format, assembled)
}
#[must_use]
pub fn reduction_percentage(&self, assembled: &AssembledContext) -> f32 {
super::format::reduction_percentage(assembled)
}
}