pub mod budget;
pub mod complexity;
pub mod cost;
pub mod emoji;
pub mod knowledge;
pub mod metrics;
pub mod optimization;
pub mod tokens;
use crate::types::*;
use crate::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info, instrument};
pub use budget::{AgentBudget, BudgetAction, BudgetCheckResult, BudgetManager};
pub use complexity::{ComplexityAnalyzer, ComplexityAssessment, ComplexityLevel, TokenEstimate};
pub use cost::{CostCalculator, CostEstimate, ModelPricing};
pub use emoji::{EmojiPlanner, EmojiStrategy, EmojiTone, EmojiType};
pub use knowledge::{KnowledgeAnalyzer, KnowledgeGap, KnowledgeState, Priority};
pub use metrics::{ExecutionRecord, MetricsTracker, PlannerMetrics};
pub use optimization::{Optimization, PlanOptimizer};
pub use tokens::{TokenBudget, TokenCounter, TokenTracker};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ResponseTone {
Professional,
Friendly,
Technical,
Casual,
Formal,
Empathetic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ResponseType {
Answer,
Explanation,
Instruction,
Acknowledgment,
Question,
Clarification,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseStrategy {
pub should_respond: bool,
pub response_type: ResponseType,
pub tone: ResponseTone,
pub emoji_strategy: EmojiStrategy,
pub max_tokens: usize,
pub model_selection: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionPlan {
pub complexity: ComplexityAssessment,
pub knowledge: KnowledgeState,
pub token_estimate: TokenEstimate,
pub cost_estimate: CostEstimate,
pub budget_check: BudgetCheckResult,
pub response_strategy: ResponseStrategy,
pub optimizations_applied: Vec<Optimization>,
pub warnings: Vec<String>,
pub requires_approval: bool,
pub planned_at: i64,
pub planning_duration_ms: u128,
}
impl ExecutionPlan {
pub fn summary(&self) -> String {
format!(
"Plan Summary:\n\
- Complexity: {} (confidence: {:.2})\n\
- Knowledge: {} | {}\n\
- Tokens: {} (input: {}, output: {})\n\
- Cost: ${:.4} using {}\n\
- Budget: ${:.4} available | {:.1}% utilized\n\
- Response: {:?} tone, {:?} type\n\
- Emojis: {} (max: {})\n\
- Optimizations: {}\n\
- Warnings: {}",
self.complexity.level,
self.complexity.confidence,
self.knowledge.known_facts.len(),
self.knowledge.summary,
self.token_estimate.total_tokens,
self.token_estimate.input_tokens,
self.token_estimate.output_tokens,
self.cost_estimate.estimated_cost_usd,
self.cost_estimate.model_used,
self.budget_check.available_budget,
self.budget_check.utilization * 100.0,
self.response_strategy.tone,
self.response_strategy.response_type,
if self.response_strategy.emoji_strategy.should_use_emojis {
"Yes"
} else {
"No"
},
self.response_strategy.emoji_strategy.max_emojis,
self.optimizations_applied.len(),
self.warnings.len()
)
}
}
#[derive(Debug, Clone)]
pub struct PlannerConfig {
pub budget: AgentBudget,
pub model_pricing: HashMap<String, ModelPricing>,
pub default_model: String,
pub max_history: usize,
}
impl Default for PlannerConfig {
fn default() -> Self {
let calculator = CostCalculator::new();
let mut pricing = HashMap::new();
for model in calculator.get_models_by_cost() {
pricing.insert(model.model_name.clone(), model);
}
Self {
budget: AgentBudget::new(100.0, BudgetAction::Warn),
model_pricing: pricing,
default_model: "gpt-4".to_string(),
max_history: 1000,
}
}
}
pub struct Planner {
complexity_analyzer: ComplexityAnalyzer,
knowledge_analyzer: KnowledgeAnalyzer,
cost_calculator: Arc<CostCalculator>,
budget_manager: Arc<BudgetManager>,
optimizer: PlanOptimizer,
emoji_planner: EmojiPlanner,
token_tracker: Arc<TokenTracker>,
metrics_tracker: Arc<MetricsTracker>,
default_model: String,
}
impl Planner {
pub fn new(config: PlannerConfig) -> Self {
let cost_calculator = Arc::new(CostCalculator::new());
for (_, pricing) in config.model_pricing {
cost_calculator.set_pricing(pricing);
}
Self {
complexity_analyzer: ComplexityAnalyzer::new(),
knowledge_analyzer: KnowledgeAnalyzer::new(),
cost_calculator,
budget_manager: Arc::new(BudgetManager::new(config.budget)),
optimizer: PlanOptimizer::new(),
emoji_planner: EmojiPlanner::new(),
token_tracker: Arc::new(TokenTracker::new()),
metrics_tracker: Arc::new(MetricsTracker::new(config.max_history)),
default_model: config.default_model,
}
}
#[instrument(skip(self, message, state), level = "info")]
pub async fn plan_execution(&self, message: &Memory, state: &State) -> Result<ExecutionPlan> {
let start_time = Instant::now();
info!("Starting execution planning");
debug!("Assessing complexity");
let complexity = self.complexity_analyzer.assess(message, state).await?;
debug!("Analyzing knowledge state");
let knowledge = self.knowledge_analyzer.analyze(message, state).await?;
let token_estimate = complexity.estimated_tokens.clone();
let model = state
.data
.get("model")
.and_then(|v| v.as_str())
.unwrap_or(&self.default_model)
.to_string();
debug!("Calculating cost for model: {}", model);
let cost_estimate = self.cost_calculator.calculate_cost(
&model,
token_estimate.input_tokens,
token_estimate.output_tokens,
)?;
debug!("Checking budget");
let mut budget_check = self.budget_manager.check_budget(&cost_estimate)?;
debug!("Planning response strategy");
let emoji_strategy = self.emoji_planner.plan_emoji_usage(message, state).await?;
let response_strategy = ResponseStrategy {
should_respond: true,
response_type: self.determine_response_type(message),
tone: self.determine_tone(message, state),
emoji_strategy,
max_tokens: token_estimate.output_tokens,
model_selection: model.clone(),
};
let mut plan = ExecutionPlan {
complexity,
knowledge,
token_estimate,
cost_estimate,
budget_check: budget_check.clone(),
response_strategy,
optimizations_applied: vec![],
warnings: vec![],
requires_approval: false,
planned_at: chrono::Utc::now().timestamp(),
planning_duration_ms: 0, };
if !budget_check.approved {
debug!("Budget not approved, optimizing plan");
plan = self
.optimizer
.optimize(plan, &budget_check, &self.cost_calculator)
.await?;
budget_check = self.budget_manager.check_budget(&plan.cost_estimate)?;
plan.budget_check = budget_check;
}
let token_optimizations = self.optimizer.optimize_tokens(&mut plan);
plan.optimizations_applied.extend(token_optimizations);
let suggestions = self.optimizer.suggest_optimizations(&plan);
plan.warnings.extend(suggestions);
plan.planning_duration_ms = start_time.elapsed().as_millis();
info!(
"Planning complete in {}ms - Complexity: {}, Cost: ${:.4}",
plan.planning_duration_ms, plan.complexity.level, plan.cost_estimate.estimated_cost_usd
);
Ok(plan)
}
pub fn record_execution(
&self,
plan: &ExecutionPlan,
actual_usage: &TokenUsage,
actual_cost: f64,
execution_time_ms: u128,
session_id: uuid::Uuid,
) -> Result<()> {
self.token_tracker
.record_usage(session_id, actual_usage, actual_cost)?;
let record = ExecutionRecord {
timestamp: chrono::Utc::now().timestamp(),
complexity: plan.complexity.level,
estimated_tokens: plan.token_estimate.total_tokens,
actual_tokens: actual_usage.total_tokens,
estimated_cost: plan.cost_estimate.estimated_cost_usd,
actual_cost,
model_used: plan.cost_estimate.model_used.clone(),
planning_time_ms: plan.planning_duration_ms,
execution_time_ms,
budget_exceeded: !plan.budget_check.approved,
optimizations: plan
.optimizations_applied
.iter()
.map(|o| format!("{:?}", o))
.collect(),
};
self.metrics_tracker.record_execution(record);
self.budget_manager.commit(actual_cost)?;
Ok(())
}
pub fn get_metrics(&self) -> PlannerMetrics {
self.metrics_tracker.get_metrics()
}
pub fn get_remaining_budget(&self) -> f64 {
self.budget_manager.get_remaining()
}
pub fn get_budget_utilization(&self) -> f64 {
self.budget_manager.get_utilization()
}
pub fn get_token_tracker(&self) -> Arc<TokenTracker> {
Arc::clone(&self.token_tracker)
}
pub fn get_cost_calculator(&self) -> Arc<CostCalculator> {
Arc::clone(&self.cost_calculator)
}
pub fn get_budget_manager(&self) -> Arc<BudgetManager> {
Arc::clone(&self.budget_manager)
}
fn determine_response_type(&self, message: &Memory) -> ResponseType {
let text = message.content.text.to_lowercase();
if text.contains('?') {
ResponseType::Answer
} else if text.contains("how") || text.contains("why") || text.contains("explain") {
ResponseType::Explanation
} else if text.contains("show me") || text.contains("tell me") || text.contains("help me") {
ResponseType::Instruction
} else if text.len() < 20 {
ResponseType::Acknowledgment
} else {
ResponseType::Answer
}
}
fn determine_tone(&self, message: &Memory, state: &State) -> ResponseTone {
let text = message.content.text.to_lowercase();
if let Some(settings) = state.data.get("characterSettings") {
if let Some(tone) = settings.get("preferredTone") {
if let Some(tone_str) = tone.as_str() {
match tone_str {
"professional" => return ResponseTone::Professional,
"friendly" => return ResponseTone::Friendly,
"technical" => return ResponseTone::Technical,
"casual" => return ResponseTone::Casual,
"formal" => return ResponseTone::Formal,
_ => {}
}
}
}
}
if text.contains("code") || text.contains("algorithm") || text.contains("function") {
ResponseTone::Technical
} else if text.contains("please") || text.contains("thank") {
ResponseTone::Professional
} else if text.len() < 30 && (text.contains("hi") || text.contains("hey")) {
ResponseTone::Friendly
} else {
ResponseTone::Professional
}
}
}
impl Default for Planner {
fn default() -> Self {
Self::new(PlannerConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn create_test_message(text: &str) -> Memory {
Memory {
id: Uuid::new_v4(),
entity_id: Uuid::new_v4(),
agent_id: Uuid::new_v4(),
room_id: Uuid::new_v4(),
content: Content {
text: text.to_string(),
..Default::default()
},
embedding: None,
metadata: None,
created_at: chrono::Utc::now().timestamp(),
unique: None,
similarity: None,
}
}
#[tokio::test]
async fn test_planner_creation() {
let planner = Planner::default();
let metrics = planner.get_metrics();
assert_eq!(metrics.total_plans_created, 0);
}
#[tokio::test]
async fn test_plan_execution() {
let planner = Planner::default();
let message = create_test_message("How does Rust handle memory management?");
let state = State::new();
let plan = planner.plan_execution(&message, &state).await.unwrap();
assert!(matches!(
plan.complexity.level,
ComplexityLevel::Trivial
| ComplexityLevel::Simple
| ComplexityLevel::Moderate
| ComplexityLevel::Complex
));
assert!(plan.cost_estimate.estimated_cost_usd > 0.0);
assert!(plan.token_estimate.total_tokens > 0);
}
#[tokio::test]
async fn test_budget_management() {
let config = PlannerConfig {
budget: AgentBudget::new(0.001, BudgetAction::Block), ..Default::default()
};
let planner = Planner::new(config);
let message = create_test_message("Explain quantum computing in detail.");
let state = State::new();
let result = planner.plan_execution(&message, &state).await;
match result {
Ok(plan) => {
assert!(!plan.optimizations_applied.is_empty());
}
Err(_) => {
assert!(true);
}
}
}
#[tokio::test]
async fn test_execution_recording() {
let planner = Planner::default();
let message = create_test_message("Hello!");
let state = State::new();
let plan = planner.plan_execution(&message, &state).await.unwrap();
let usage = TokenUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
};
let session_id = Uuid::new_v4();
planner
.record_execution(&plan, &usage, 0.001, 500, session_id)
.unwrap();
let metrics = planner.get_metrics();
assert_eq!(metrics.total_plans_created, 1);
}
}