use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::state::{GoalStatus, NeedCategory, NodeId, Priority, TaskStatus};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum UpdateOp {
CreateBelief {
proposition: String,
domain: String,
initial_log_odds: f64,
},
CreateGoal {
description: String,
priority: Priority,
deadline: Option<f64>,
completion_criteria: String,
},
CreateTask {
description: String,
priority: Priority,
deadline: Option<f64>,
estimated_minutes: Option<u32>,
},
CreateEntity { name: String, entity_type: String },
CreateNeed {
description: String,
category: NeedCategory,
intensity: f64,
},
SetPreference {
domain: String,
preferred: String,
dispreferred: Option<String>,
},
CreateRoutine {
description: String,
action_description: String,
},
UpdateBelief {
evidence_weight: f64,
source: String,
},
UpdateTaskStatus { new_status: TaskStatus },
UpdateGoalStatus { new_status: GoalStatus },
UpdateGoalProgress { progress: f64 },
CorrectBelief { corrected_proposition: String },
Deprecate,
CreateRelationship {
person_name: String,
relationship: String,
},
EmotionalMarker { emotion: String, intensity: f64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExtractorTier {
Rule,
Template,
Context,
Llm,
}
impl ExtractorTier {
pub fn as_str(self) -> &'static str {
match self {
Self::Rule => "rule",
Self::Template => "template",
Self::Context => "context",
Self::Llm => "llm",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CognitiveUpdate {
pub op: UpdateOp,
pub target: Option<NodeId>,
pub confidence: f64,
pub tier: ExtractorTier,
pub match_source: String,
pub matched_text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractionResponse {
pub updates: Vec<CognitiveUpdate>,
pub escalation_needed: bool,
pub llm_request: Option<LlmExtractionRequest>,
pub tiers_used: Vec<ExtractorTier>,
pub extraction_time_us: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmExtractionRequest {
pub text: String,
pub system_prompt: String,
pub context_summary: String,
pub max_tokens: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExtractionContext {
pub active_goals: Vec<(NodeId, String)>,
pub active_tasks: Vec<(NodeId, String)>,
pub recent_entities: Vec<(NodeId, String)>,
pub known_relationships: HashMap<String, String>,
pub current_hour: u8,
pub current_day: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractorConfig {
pub min_confidence: f64,
pub escalation_threshold: f64,
pub max_updates: usize,
pub enable_context_tier: bool,
pub enable_llm_escalation: bool,
pub max_templates: usize,
}
impl Default for ExtractorConfig {
fn default() -> Self {
Self {
min_confidence: 0.3,
escalation_threshold: 0.5,
max_updates: 10,
enable_context_tier: true,
enable_llm_escalation: true,
max_templates: 500,
}
}
}
struct PatternRule {
triggers: &'static [&'static str],
op_template: OpTemplate,
confidence: f64,
category: &'static str,
}
#[derive(Debug, Clone, Copy)]
enum OpTemplate {
TaskFromSuffix,
ReminderFromSuffix,
DontForgetFromSuffix,
GoalFromSuffix,
PositivePreference,
NegativePreference,
ComparativePreference,
RoutineFromSuffix,
EmotionNegative,
EmotionPositive,
RelationshipFromSuffix,
Correction,
TaskCompleted,
InformationalNeed,
FutureTask,
PeriodicRoutine,
}
static PATTERN_RULES: &[PatternRule] = &[
PatternRule {
triggers: &["remind me to ", "reminder to ", "remind me about "],
op_template: OpTemplate::ReminderFromSuffix,
confidence: 0.90,
category: "reminder",
},
PatternRule {
triggers: &[
"don't let me forget ",
"dont let me forget ",
"don't forget to ",
"dont forget to ",
],
op_template: OpTemplate::DontForgetFromSuffix,
confidence: 0.90,
category: "dont_forget",
},
PatternRule {
triggers: &[
"no, that's wrong",
"no that's wrong",
"that's not right",
"actually, ",
"i meant ",
"no, i meant ",
],
op_template: OpTemplate::Correction,
confidence: 0.75,
category: "correction",
},
PatternRule {
triggers: &[
"i'm done with ",
"i finished ",
"i've finished ",
"i completed ",
"i've completed ",
"done with ",
],
op_template: OpTemplate::TaskCompleted,
confidence: 0.85,
category: "task_complete",
},
PatternRule {
triggers: &["i prefer ", "i'd rather "],
op_template: OpTemplate::ComparativePreference,
confidence: 0.80,
category: "preference_comparative",
},
PatternRule {
triggers: &["i like ", "i love ", "i enjoy ", "i appreciate "],
op_template: OpTemplate::PositivePreference,
confidence: 0.80,
category: "preference_positive",
},
PatternRule {
triggers: &[
"i hate ",
"i don't like ",
"i dont like ",
"i dislike ",
"i can't stand ",
"i cant stand ",
],
op_template: OpTemplate::NegativePreference,
confidence: 0.80,
category: "preference_negative",
},
PatternRule {
triggers: &[
"every morning ",
"every evening ",
"every night ",
"every monday ",
"every tuesday ",
"every wednesday ",
"every thursday ",
"every friday ",
"every saturday ",
"every sunday ",
"every week ",
"every day ",
],
op_template: OpTemplate::PeriodicRoutine,
confidence: 0.80,
category: "periodic_routine",
},
PatternRule {
triggers: &["i always ", "i usually ", "i typically ", "i normally "],
op_template: OpTemplate::RoutineFromSuffix,
confidence: 0.70,
category: "routine",
},
PatternRule {
triggers: &[
"my goal is to ",
"my goal is ",
"i want to achieve ",
"i aim to ",
],
op_template: OpTemplate::GoalFromSuffix,
confidence: 0.85,
category: "goal",
},
PatternRule {
triggers: &[
"tomorrow i ",
"tomorrow i'll ",
"next week i ",
"later today i ",
"tonight i ",
"this weekend i ",
],
op_template: OpTemplate::FutureTask,
confidence: 0.75,
category: "future_task",
},
PatternRule {
triggers: &[
"i should ",
"i have to ",
"i must ",
"i gotta ",
"i've got to ",
"i've gotta ",
],
op_template: OpTemplate::TaskFromSuffix,
confidence: 0.80,
category: "task",
},
PatternRule {
triggers: &[
"i need to know ",
"i need information about ",
"i need to find out ",
"i want to know ",
"how do i ",
"what is ",
"where can i ",
],
op_template: OpTemplate::InformationalNeed,
confidence: 0.70,
category: "info_need",
},
PatternRule {
triggers: &["i need to "],
op_template: OpTemplate::TaskFromSuffix,
confidence: 0.80,
category: "task",
},
PatternRule {
triggers: &["i want to ", "i'd like to ", "i wish i could "],
op_template: OpTemplate::GoalFromSuffix,
confidence: 0.65,
category: "want_goal",
},
PatternRule {
triggers: &[
"i'm stressed",
"i'm frustrated",
"i'm overwhelmed",
"i'm anxious",
"i'm worried",
"i'm tired",
"i'm exhausted",
"i'm upset",
"i'm angry",
"i'm sad",
],
op_template: OpTemplate::EmotionNegative,
confidence: 0.85,
category: "emotion_negative",
},
PatternRule {
triggers: &[
"i'm excited",
"i'm happy",
"i'm thrilled",
"i'm grateful",
"i'm relieved",
"i'm proud",
"i'm motivated",
"i'm energized",
],
op_template: OpTemplate::EmotionPositive,
confidence: 0.85,
category: "emotion_positive",
},
PatternRule {
triggers: &[
"my sister ",
"my brother ",
"my mom ",
"my mother ",
"my dad ",
"my father ",
"my wife ",
"my husband ",
"my partner ",
"my boss ",
"my manager ",
"my friend ",
"my colleague ",
"my coworker ",
],
op_template: OpTemplate::RelationshipFromSuffix,
confidence: 0.75,
category: "relationship",
},
];
fn tier1_extract(text: &str, _config: &ExtractorConfig) -> Vec<CognitiveUpdate> {
let lower = text.to_lowercase();
let mut updates = Vec::new();
for rule in PATTERN_RULES {
for &trigger in rule.triggers {
if let Some(pos) = lower.find(trigger) {
let suffix = &text[pos + trigger.len()..];
let suffix_trimmed = suffix.trim();
if suffix_trimmed.is_empty()
&& !matches!(
rule.op_template,
OpTemplate::EmotionNegative
| OpTemplate::EmotionPositive
| OpTemplate::Correction
)
{
continue; }
if let Some(op) = build_op_from_template(rule.op_template, trigger, suffix_trimmed)
{
updates.push(CognitiveUpdate {
op,
target: None,
confidence: rule.confidence,
tier: ExtractorTier::Rule,
match_source: format!("rule:{}", rule.category),
matched_text: text[pos..].to_string(),
});
break; }
}
}
}
updates.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
updates
}
fn build_op_from_template(template: OpTemplate, trigger: &str, suffix: &str) -> Option<UpdateOp> {
let clean = suffix.trim_end_matches(|c: char| c == '.' || c == '!' || c == '?' || c == ',');
if clean.is_empty()
&& !matches!(
template,
OpTemplate::EmotionNegative | OpTemplate::EmotionPositive | OpTemplate::Correction
)
{
return None;
}
match template {
OpTemplate::TaskFromSuffix => Some(UpdateOp::CreateTask {
description: clean.to_string(),
priority: Priority::Medium,
deadline: None,
estimated_minutes: None,
}),
OpTemplate::ReminderFromSuffix | OpTemplate::DontForgetFromSuffix => {
Some(UpdateOp::CreateTask {
description: clean.to_string(),
priority: Priority::High,
deadline: None,
estimated_minutes: None,
})
}
OpTemplate::GoalFromSuffix => Some(UpdateOp::CreateGoal {
description: clean.to_string(),
priority: Priority::Medium,
deadline: None,
completion_criteria: String::new(),
}),
OpTemplate::PositivePreference => Some(UpdateOp::SetPreference {
domain: infer_preference_domain(clean),
preferred: clean.to_string(),
dispreferred: None,
}),
OpTemplate::NegativePreference => Some(UpdateOp::SetPreference {
domain: infer_preference_domain(clean),
preferred: String::new(),
dispreferred: Some(clean.to_string()),
}),
OpTemplate::ComparativePreference => {
let (preferred, dispreferred) = split_comparative(clean);
Some(UpdateOp::SetPreference {
domain: infer_preference_domain(&preferred),
preferred,
dispreferred: if dispreferred.is_empty() {
None
} else {
Some(dispreferred)
},
})
}
OpTemplate::RoutineFromSuffix => Some(UpdateOp::CreateRoutine {
description: format!("User routine: {}", clean),
action_description: clean.to_string(),
}),
OpTemplate::PeriodicRoutine => {
let period_hint = trigger.trim();
Some(UpdateOp::CreateRoutine {
description: format!("{}{}", period_hint, clean),
action_description: clean.to_string(),
})
}
OpTemplate::EmotionNegative => {
let emotion = extract_emotion_from_trigger(trigger);
Some(UpdateOp::EmotionalMarker {
emotion,
intensity: 0.7,
})
}
OpTemplate::EmotionPositive => {
let emotion = extract_emotion_from_trigger(trigger);
Some(UpdateOp::EmotionalMarker {
emotion,
intensity: 0.7,
})
}
OpTemplate::RelationshipFromSuffix => {
let relationship = trigger.trim_start_matches("my ").trim();
Some(UpdateOp::CreateRelationship {
person_name: first_name_from_suffix(clean),
relationship: relationship.to_string(),
})
}
OpTemplate::Correction => Some(UpdateOp::CorrectBelief {
corrected_proposition: clean.to_string(),
}),
OpTemplate::TaskCompleted => Some(UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::Completed,
}),
OpTemplate::InformationalNeed => Some(UpdateOp::CreateNeed {
description: clean.to_string(),
category: NeedCategory::Informational,
intensity: 0.5,
}),
OpTemplate::FutureTask => Some(UpdateOp::CreateTask {
description: clean.to_string(),
priority: Priority::Medium,
deadline: None, estimated_minutes: None,
}),
}
}
fn infer_preference_domain(text: &str) -> String {
let lower = text.to_lowercase();
let domains = [
(
&[
"food",
"eat",
"drink",
"coffee",
"tea",
"restaurant",
"cuisine",
][..],
"food",
),
(
&["music", "song", "playlist", "album", "band", "artist"],
"music",
),
(
&[
"work",
"meeting",
"office",
"project",
"code",
"coding",
"programming",
"deploy",
],
"work",
),
(
&["exercise", "gym", "run", "walk", "sport", "workout"],
"health",
),
(
&["movie", "film", "show", "series", "watch", "book", "read"],
"entertainment",
),
(
&["morning", "evening", "night", "schedule", "routine", "time"],
"schedule",
),
(
&["notification", "alert", "remind", "email", "message"],
"communication",
),
];
for (keywords, domain) in &domains {
if keywords.iter().any(|kw| lower.contains(kw)) {
return domain.to_string();
}
}
"general".to_string()
}
fn split_comparative(text: &str) -> (String, String) {
let lower = text.to_lowercase();
for sep in &[" rather than ", " instead of ", " over ", " than ", " to "] {
if let Some(pos) = lower.find(sep) {
let preferred = text[..pos].trim().to_string();
let dispreferred = text[pos + sep.len()..].trim().to_string();
return (preferred, dispreferred);
}
}
(text.to_string(), String::new())
}
fn extract_emotion_from_trigger(trigger: &str) -> String {
trigger
.trim()
.rsplit(' ')
.next()
.unwrap_or("unknown")
.to_string()
}
fn first_name_from_suffix(text: &str) -> String {
let first_word = text.split_whitespace().next().unwrap_or("");
first_word.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractionTemplate {
pub id: u64,
pub keywords: Vec<String>,
pub op_template: SerializableOpTemplate,
pub use_count: u32,
pub total_confidence: f64,
pub created_at: f64,
pub last_used_at: f64,
}
impl ExtractionTemplate {
pub fn avg_confidence(&self) -> f64 {
if self.use_count == 0 {
0.5
} else {
self.total_confidence / self.use_count as f64
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SerializableOpTemplate {
CreateTask { priority: Priority },
CreateGoal { priority: Priority },
SetPreference { domain: String },
CreateNeed { category: NeedCategory },
CreateRoutine,
EmotionalMarker { emotion: String, positive: bool },
CreateRelationship { relationship: String },
Correction,
TaskCompleted,
CreateBelief { domain: String },
UpdateBelief { direction: String },
CreateEntity { entity_type: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TemplateStore {
pub templates: Vec<ExtractionTemplate>,
pub total_created: u64,
pub total_matches: u64,
}
impl TemplateStore {
pub fn new() -> Self {
Self {
templates: Vec::new(),
total_created: 0,
total_matches: 0,
}
}
pub fn learn_template(
&mut self,
text: &str,
op_template: SerializableOpTemplate,
now: f64,
max_templates: usize,
) {
let keywords = extract_keywords(text);
if keywords.is_empty() {
return;
}
for existing in &mut self.templates {
if existing.op_template == op_template
&& keyword_overlap(&existing.keywords, &keywords) > 0.5
{
existing.use_count += 1;
existing.last_used_at = now;
for kw in &keywords {
if !existing.keywords.contains(kw) {
existing.keywords.push(kw.clone());
}
}
return;
}
}
let id = self.total_created;
self.total_created += 1;
self.templates.push(ExtractionTemplate {
id,
keywords,
op_template,
use_count: 1,
total_confidence: 0.7,
created_at: now,
last_used_at: now,
});
if self.templates.len() > max_templates {
if let Some(min_idx) = self
.templates
.iter()
.enumerate()
.min_by_key(|(_, t)| t.use_count)
.map(|(i, _)| i)
{
self.templates.swap_remove(min_idx);
}
}
}
pub fn find_match(&self, text: &str, min_overlap: f64) -> Option<(usize, f64)> {
let keywords = extract_keywords(text);
if keywords.is_empty() {
return None;
}
let mut best_idx = None;
let mut best_score = 0.0f64;
for (idx, template) in self.templates.iter().enumerate() {
let overlap = keyword_overlap(&template.keywords, &keywords);
if overlap >= min_overlap {
let score = overlap * template.avg_confidence();
if score > best_score {
best_score = score;
best_idx = Some(idx);
}
}
}
best_idx.map(|idx| (idx, best_score))
}
}
fn extract_keywords(text: &str) -> Vec<String> {
static STOPWORDS: &[&str] = &[
"i", "me", "my", "we", "our", "you", "your", "the", "a", "an", "is", "am", "are", "was",
"were", "be", "been", "being", "have", "has", "had", "do", "does", "did", "will", "would",
"could", "should", "may", "might", "shall", "can", "to", "of", "in", "for", "on", "with",
"at", "by", "from", "up", "about", "into", "through", "during", "before", "after", "and",
"but", "or", "nor", "not", "no", "so", "if", "then", "that", "this", "these", "those",
"it", "its", "just", "also", "very", "really", "quite", "much", "don't", "dont", "doesn't",
"doesnt", "didn't", "didnt", "i'm", "im", "i've", "ive", "i'll", "ill", "i'd", "id", "let",
"get", "got", "going", "go", "come", "make",
];
text.to_lowercase()
.split(|c: char| !c.is_alphanumeric() && c != '\'')
.filter(|w| w.len() >= 3 && !STOPWORDS.contains(w))
.map(|w| w.to_string())
.collect()
}
fn keyword_overlap(a: &[String], b: &[String]) -> f64 {
if a.is_empty() || b.is_empty() {
return 0.0;
}
let matches = b.iter().filter(|kw| a.contains(kw)).count();
let union = a.len().max(b.len());
matches as f64 / union as f64
}
fn tier2_extract(text: &str, store: &TemplateStore) -> Vec<CognitiveUpdate> {
let min_overlap = 0.3;
let mut updates = Vec::new();
if let Some((idx, score)) = store.find_match(text, min_overlap) {
let template = &store.templates[idx];
let suffix = text.trim();
let confidence = score.min(0.85);
if let Some(op) = build_op_from_serializable_template(&template.op_template, suffix) {
updates.push(CognitiveUpdate {
op,
target: None,
confidence,
tier: ExtractorTier::Template,
match_source: format!("template:{}", template.id),
matched_text: suffix.to_string(),
});
}
}
updates
}
fn build_op_from_serializable_template(
template: &SerializableOpTemplate,
text: &str,
) -> Option<UpdateOp> {
let clean = text.trim_end_matches(|c: char| c == '.' || c == '!' || c == '?');
match template {
SerializableOpTemplate::CreateTask { priority } => Some(UpdateOp::CreateTask {
description: clean.to_string(),
priority: *priority,
deadline: None,
estimated_minutes: None,
}),
SerializableOpTemplate::CreateGoal { priority } => Some(UpdateOp::CreateGoal {
description: clean.to_string(),
priority: *priority,
deadline: None,
completion_criteria: String::new(),
}),
SerializableOpTemplate::SetPreference { domain } => Some(UpdateOp::SetPreference {
domain: domain.clone(),
preferred: clean.to_string(),
dispreferred: None,
}),
SerializableOpTemplate::CreateNeed { category } => Some(UpdateOp::CreateNeed {
description: clean.to_string(),
category: *category,
intensity: 0.5,
}),
SerializableOpTemplate::CreateRoutine => Some(UpdateOp::CreateRoutine {
description: clean.to_string(),
action_description: clean.to_string(),
}),
SerializableOpTemplate::EmotionalMarker { emotion, .. } => {
Some(UpdateOp::EmotionalMarker {
emotion: emotion.clone(),
intensity: 0.6,
})
}
SerializableOpTemplate::CreateRelationship { relationship } => {
Some(UpdateOp::CreateRelationship {
person_name: first_name_from_suffix(clean),
relationship: relationship.clone(),
})
}
SerializableOpTemplate::Correction => Some(UpdateOp::CorrectBelief {
corrected_proposition: clean.to_string(),
}),
SerializableOpTemplate::TaskCompleted => Some(UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::Completed,
}),
SerializableOpTemplate::CreateBelief { domain } => Some(UpdateOp::CreateBelief {
proposition: clean.to_string(),
domain: domain.clone(),
initial_log_odds: 1.0,
}),
SerializableOpTemplate::UpdateBelief { direction } => Some(UpdateOp::UpdateBelief {
evidence_weight: if direction == "positive" { 1.0 } else { -1.0 },
source: "template_match".to_string(),
}),
SerializableOpTemplate::CreateEntity { entity_type } => Some(UpdateOp::CreateEntity {
name: first_name_from_suffix(clean),
entity_type: entity_type.clone(),
}),
}
}
static CONTEXT_PATTERNS: &[(&str, ContextAction)] = &[
("i still haven't done it", ContextAction::TaskPersistence),
("i still haven't done that", ContextAction::TaskPersistence),
("i haven't done it yet", ContextAction::TaskPersistence),
("not done yet", ContextAction::TaskPersistence),
("still working on it", ContextAction::TaskInProgress),
("still working on that", ContextAction::TaskInProgress),
("i'm working on it", ContextAction::TaskInProgress),
("that was great", ContextAction::PositiveAboutRecent),
("that was good", ContextAction::PositiveAboutRecent),
("that was awesome", ContextAction::PositiveAboutRecent),
("that was terrible", ContextAction::NegativeAboutRecent),
("that was bad", ContextAction::NegativeAboutRecent),
("that was awful", ContextAction::NegativeAboutRecent),
("i did it", ContextAction::TaskCompleted),
("it's done", ContextAction::TaskCompleted),
("that's done", ContextAction::TaskCompleted),
("finished it", ContextAction::TaskCompleted),
("cancel it", ContextAction::TaskCancelled),
("cancel that", ContextAction::TaskCancelled),
("forget about it", ContextAction::TaskCancelled),
("never mind", ContextAction::TaskCancelled),
("nevermind", ContextAction::TaskCancelled),
];
#[derive(Debug, Clone, Copy)]
enum ContextAction {
TaskPersistence, TaskInProgress, TaskCompleted, TaskCancelled, PositiveAboutRecent, NegativeAboutRecent, }
fn tier3_extract(text: &str, context: &ExtractionContext) -> Vec<CognitiveUpdate> {
let lower = text.to_lowercase();
let mut updates = Vec::new();
for &(pattern, action) in CONTEXT_PATTERNS {
if !lower.contains(pattern) {
continue;
}
match action {
ContextAction::TaskPersistence => {
if let Some((task_id, desc)) = context.active_tasks.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::UpdateBelief {
evidence_weight: 0.5,
source: format!("user_persistence:{}", desc),
},
target: Some(*task_id),
confidence: 0.65,
tier: ExtractorTier::Context,
match_source: format!("context:task_persistence:{}", desc),
matched_text: pattern.to_string(),
});
}
}
ContextAction::TaskInProgress => {
if let Some((task_id, desc)) = context.active_tasks.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::InProgress,
},
target: Some(*task_id),
confidence: 0.70,
tier: ExtractorTier::Context,
match_source: format!("context:task_in_progress:{}", desc),
matched_text: pattern.to_string(),
});
}
}
ContextAction::TaskCompleted => {
if let Some((task_id, desc)) = context.active_tasks.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::Completed,
},
target: Some(*task_id),
confidence: 0.75,
tier: ExtractorTier::Context,
match_source: format!("context:task_completed:{}", desc),
matched_text: pattern.to_string(),
});
}
}
ContextAction::TaskCancelled => {
if let Some((task_id, desc)) = context.active_tasks.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::Cancelled,
},
target: Some(*task_id),
confidence: 0.70,
tier: ExtractorTier::Context,
match_source: format!("context:task_cancelled:{}", desc),
matched_text: pattern.to_string(),
});
}
}
ContextAction::PositiveAboutRecent => {
if let Some((_entity_id, name)) = context.recent_entities.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::SetPreference {
domain: "general".to_string(),
preferred: name.clone(),
dispreferred: None,
},
target: None,
confidence: 0.60,
tier: ExtractorTier::Context,
match_source: format!("context:positive_about:{}", name),
matched_text: pattern.to_string(),
});
}
}
ContextAction::NegativeAboutRecent => {
if let Some((_entity_id, name)) = context.recent_entities.first() {
updates.push(CognitiveUpdate {
op: UpdateOp::SetPreference {
domain: "general".to_string(),
preferred: String::new(),
dispreferred: Some(name.clone()),
},
target: None,
confidence: 0.60,
tier: ExtractorTier::Context,
match_source: format!("context:negative_about:{}", name),
matched_text: pattern.to_string(),
});
}
}
}
break; }
updates
}
fn tier4_build_request(text: &str, context: &ExtractionContext) -> LlmExtractionRequest {
let mut context_lines = Vec::new();
if !context.active_tasks.is_empty() {
let tasks: Vec<String> = context
.active_tasks
.iter()
.take(5)
.map(|(_, desc)| format!("- Task: {}", desc))
.collect();
context_lines.push(format!("Active tasks:\n{}", tasks.join("\n")));
}
if !context.active_goals.is_empty() {
let goals: Vec<String> = context
.active_goals
.iter()
.take(3)
.map(|(_, desc)| format!("- Goal: {}", desc))
.collect();
context_lines.push(format!("Active goals:\n{}", goals.join("\n")));
}
if !context.recent_entities.is_empty() {
let entities: Vec<String> = context
.recent_entities
.iter()
.take(5)
.map(|(_, name)| name.clone())
.collect();
context_lines.push(format!("Recent entities: {}", entities.join(", ")));
}
let context_summary = if context_lines.is_empty() {
"No additional context.".to_string()
} else {
context_lines.join("\n\n")
};
LlmExtractionRequest {
text: text.to_string(),
system_prompt: LLM_EXTRACTION_PROMPT.to_string(),
context_summary,
max_tokens: 300,
}
}
static LLM_EXTRACTION_PROMPT: &str = r#"Extract structured cognitive updates from the user's message.
Return a JSON array of objects, each with:
- "op": one of "create_task", "create_goal", "set_preference", "create_need", "create_routine", "emotional_marker", "create_relationship", "correct_belief", "task_completed", "create_entity", "create_belief"
- "description": what was extracted
- "priority": "low", "medium", "high", or "critical" (if applicable)
- "domain": preference domain (if applicable)
- "emotion": emotion name (if applicable)
- "confidence": your confidence 0.0-1.0
Example: [{"op":"create_task","description":"call the dentist","priority":"medium","confidence":0.9}]
Only extract what is clearly stated or strongly implied. Do not invent information."#;
pub fn extract(
text: &str,
context: &ExtractionContext,
template_store: &TemplateStore,
config: &ExtractorConfig,
) -> ExtractionResponse {
let start = std::time::Instant::now();
let mut all_updates: Vec<CognitiveUpdate> = Vec::new();
let mut tiers_used: Vec<ExtractorTier> = Vec::new();
let tier1_results = tier1_extract(text, config);
if !tier1_results.is_empty() {
tiers_used.push(ExtractorTier::Rule);
all_updates.extend(tier1_results);
}
let tier2_results = tier2_extract(text, template_store);
if !tier2_results.is_empty() {
tiers_used.push(ExtractorTier::Template);
for update in tier2_results {
if !has_similar_update(&all_updates, &update) {
all_updates.push(update);
}
}
}
if config.enable_context_tier {
let tier3_results = tier3_extract(text, context);
if !tier3_results.is_empty() {
tiers_used.push(ExtractorTier::Context);
for update in tier3_results {
if !has_similar_update(&all_updates, &update) {
all_updates.push(update);
}
}
}
}
all_updates.retain(|u| u.confidence >= config.min_confidence);
all_updates.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
all_updates.truncate(config.max_updates);
let max_confidence = all_updates
.iter()
.map(|u| u.confidence)
.fold(0.0f64, f64::max);
let escalation_needed = config.enable_llm_escalation
&& (all_updates.is_empty() || max_confidence < config.escalation_threshold);
let llm_request = if escalation_needed {
Some(tier4_build_request(text, context))
} else {
None
};
let extraction_time_us = start.elapsed().as_micros() as u64;
ExtractionResponse {
updates: all_updates,
escalation_needed,
llm_request,
tiers_used,
extraction_time_us,
}
}
fn has_similar_update(existing: &[CognitiveUpdate], new: &CognitiveUpdate) -> bool {
existing
.iter()
.any(|e| std::mem::discriminant(&e.op) == std::mem::discriminant(&new.op))
}
pub fn integrate_llm_response(
original_text: &str,
llm_json: &str,
template_store: &mut TemplateStore,
max_templates: usize,
now: f64,
) -> Vec<CognitiveUpdate> {
let parsed: Vec<serde_json::Value> = match serde_json::from_str(llm_json) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let mut updates = Vec::new();
for item in &parsed {
let op_str = item.get("op").and_then(|v| v.as_str()).unwrap_or("");
let desc = item
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("");
let confidence = item
.get("confidence")
.and_then(|v| v.as_f64())
.unwrap_or(0.5);
let priority_str = item
.get("priority")
.and_then(|v| v.as_str())
.unwrap_or("medium");
let domain = item
.get("domain")
.and_then(|v| v.as_str())
.unwrap_or("general");
let emotion = item
.get("emotion")
.and_then(|v| v.as_str())
.unwrap_or("neutral");
let priority = Priority::from_str(priority_str);
let (op, serializable) = match op_str {
"create_task" => (
UpdateOp::CreateTask {
description: desc.to_string(),
priority,
deadline: None,
estimated_minutes: None,
},
Some(SerializableOpTemplate::CreateTask { priority }),
),
"create_goal" => (
UpdateOp::CreateGoal {
description: desc.to_string(),
priority,
deadline: None,
completion_criteria: String::new(),
},
Some(SerializableOpTemplate::CreateGoal { priority }),
),
"set_preference" => (
UpdateOp::SetPreference {
domain: domain.to_string(),
preferred: desc.to_string(),
dispreferred: None,
},
Some(SerializableOpTemplate::SetPreference {
domain: domain.to_string(),
}),
),
"create_need" => (
UpdateOp::CreateNeed {
description: desc.to_string(),
category: NeedCategory::Informational,
intensity: 0.5,
},
Some(SerializableOpTemplate::CreateNeed {
category: NeedCategory::Informational,
}),
),
"create_routine" => (
UpdateOp::CreateRoutine {
description: desc.to_string(),
action_description: desc.to_string(),
},
Some(SerializableOpTemplate::CreateRoutine),
),
"emotional_marker" => (
UpdateOp::EmotionalMarker {
emotion: emotion.to_string(),
intensity: 0.7,
},
Some(SerializableOpTemplate::EmotionalMarker {
emotion: emotion.to_string(),
positive: confidence > 0.5,
}),
),
"create_relationship" => (
UpdateOp::CreateRelationship {
person_name: desc.to_string(),
relationship: domain.to_string(),
},
Some(SerializableOpTemplate::CreateRelationship {
relationship: domain.to_string(),
}),
),
"correct_belief" | "correction" => (
UpdateOp::CorrectBelief {
corrected_proposition: desc.to_string(),
},
Some(SerializableOpTemplate::Correction),
),
"task_completed" => (
UpdateOp::UpdateTaskStatus {
new_status: TaskStatus::Completed,
},
Some(SerializableOpTemplate::TaskCompleted),
),
"create_entity" => {
let etype = item
.get("entity_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
(
UpdateOp::CreateEntity {
name: desc.to_string(),
entity_type: etype.to_string(),
},
Some(SerializableOpTemplate::CreateEntity {
entity_type: etype.to_string(),
}),
)
}
"create_belief" => (
UpdateOp::CreateBelief {
proposition: desc.to_string(),
domain: domain.to_string(),
initial_log_odds: 1.0,
},
Some(SerializableOpTemplate::CreateBelief {
domain: domain.to_string(),
}),
),
_ => continue,
};
updates.push(CognitiveUpdate {
op,
target: None,
confidence,
tier: ExtractorTier::Llm,
match_source: format!("llm:{}", op_str),
matched_text: desc.to_string(),
});
if let Some(tmpl) = serializable {
if confidence >= 0.6 {
template_store.learn_template(original_text, tmpl, now, max_templates);
}
}
}
updates
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractorSummary {
pub total_templates: usize,
pub total_template_matches: u64,
pub total_templates_created: u64,
pub avg_template_confidence: f64,
}
pub fn summarize_extractor(store: &TemplateStore) -> ExtractorSummary {
let avg_conf = if store.templates.is_empty() {
0.0
} else {
store
.templates
.iter()
.map(|t| t.avg_confidence())
.sum::<f64>()
/ store.templates.len() as f64
};
ExtractorSummary {
total_templates: store.templates.len(),
total_template_matches: store.total_matches,
total_templates_created: store.total_created,
avg_template_confidence: avg_conf,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_config() -> ExtractorConfig {
ExtractorConfig::default()
}
fn empty_context() -> ExtractionContext {
ExtractionContext::default()
}
fn empty_store() -> TemplateStore {
TemplateStore::new()
}
#[test]
fn test_tier1_task_extraction() {
let updates = tier1_extract("I need to call the dentist tomorrow", &default_config());
assert!(!updates.is_empty(), "Should extract task");
match &updates[0].op {
UpdateOp::CreateTask {
description,
priority,
..
} => {
assert!(description.contains("call the dentist"));
assert_eq!(*priority, Priority::Medium);
}
other => panic!("Expected CreateTask, got {:?}", other),
}
assert_eq!(updates[0].tier, ExtractorTier::Rule);
}
#[test]
fn test_tier1_reminder_extraction() {
let updates = tier1_extract("Remind me to buy groceries", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateTask {
description,
priority,
..
} => {
assert!(description.contains("buy groceries"));
assert_eq!(*priority, Priority::High);
}
other => panic!("Expected high-priority CreateTask, got {:?}", other),
}
}
#[test]
fn test_tier1_goal_extraction() {
let updates = tier1_extract("My goal is to learn Spanish this year", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateGoal { description, .. } => {
assert!(description.contains("learn Spanish"));
}
other => panic!("Expected CreateGoal, got {:?}", other),
}
}
#[test]
fn test_tier1_positive_preference() {
let updates = tier1_extract("I love working with Rust", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::SetPreference {
preferred, domain, ..
} => {
assert!(preferred.contains("working with Rust"));
assert_eq!(domain, "work");
}
other => panic!("Expected SetPreference, got {:?}", other),
}
}
#[test]
fn test_tier1_negative_preference() {
let updates = tier1_extract("I hate early morning meetings", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::SetPreference { dispreferred, .. } => {
assert!(dispreferred
.as_ref()
.unwrap()
.contains("early morning meetings"));
}
other => panic!("Expected SetPreference with dispreferred, got {:?}", other),
}
}
#[test]
fn test_tier1_comparative_preference() {
let updates = tier1_extract("I prefer tea over coffee", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::SetPreference {
preferred,
dispreferred,
..
} => {
assert_eq!(preferred, "tea");
assert_eq!(dispreferred.as_deref(), Some("coffee"));
}
other => panic!("Expected comparative preference, got {:?}", other),
}
}
#[test]
fn test_tier1_emotion_negative() {
let updates = tier1_extract("I'm stressed about the deadline", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::EmotionalMarker { emotion, .. } => {
assert_eq!(emotion, "stressed");
}
other => panic!("Expected EmotionalMarker, got {:?}", other),
}
}
#[test]
fn test_tier1_emotion_positive() {
let updates = tier1_extract("I'm excited about the trip!", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::EmotionalMarker { emotion, .. } => {
assert_eq!(emotion, "excited");
}
other => panic!("Expected EmotionalMarker, got {:?}", other),
}
}
#[test]
fn test_tier1_relationship() {
let updates = tier1_extract("My sister Alice is visiting next week", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateRelationship {
person_name,
relationship,
} => {
assert_eq!(person_name, "Alice");
assert!(relationship.contains("sister"));
}
other => panic!("Expected CreateRelationship, got {:?}", other),
}
}
#[test]
fn test_tier1_routine() {
let updates = tier1_extract(
"I always check email first thing in the morning",
&default_config(),
);
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateRoutine {
action_description, ..
} => {
assert!(action_description.contains("check email"));
}
other => panic!("Expected CreateRoutine, got {:?}", other),
}
}
#[test]
fn test_tier1_periodic_routine() {
let updates = tier1_extract(
"Every Monday I review my goals for the week",
&default_config(),
);
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateRoutine { description, .. } => {
assert!(description.contains("monday") || description.contains("Monday"));
}
other => panic!("Expected CreateRoutine, got {:?}", other),
}
}
#[test]
fn test_tier1_correction() {
let updates = tier1_extract("Actually, I meant the blue one", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CorrectBelief {
corrected_proposition,
} => {
assert!(corrected_proposition.contains("the blue one"));
}
other => panic!("Expected CorrectBelief, got {:?}", other),
}
}
#[test]
fn test_tier1_task_completed() {
let updates = tier1_extract("I finished the report", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::UpdateTaskStatus { new_status } => {
assert_eq!(*new_status, TaskStatus::Completed);
}
other => panic!("Expected UpdateTaskStatus(Completed), got {:?}", other),
}
}
#[test]
fn test_tier1_informational_need() {
let updates = tier1_extract("How do I set up Docker on Ubuntu?", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateNeed { category, .. } => {
assert_eq!(*category, NeedCategory::Informational);
}
other => panic!("Expected CreateNeed(Informational), got {:?}", other),
}
}
#[test]
fn test_tier1_dont_forget() {
let updates = tier1_extract("Don't let me forget to water the plants", &default_config());
assert!(!updates.is_empty());
match &updates[0].op {
UpdateOp::CreateTask {
description,
priority,
..
} => {
assert!(description.contains("water the plants"));
assert_eq!(*priority, Priority::High);
}
other => panic!("Expected high-priority CreateTask, got {:?}", other),
}
}
#[test]
fn test_tier1_no_match() {
let updates = tier1_extract("Hello, how are you?", &default_config());
assert!(
updates.is_empty(),
"Should not extract from generic greeting"
);
}
#[test]
fn test_tier1_empty_suffix_ignored() {
let updates = tier1_extract("I need to", &default_config());
assert!(
updates.is_empty(),
"Empty suffix should not produce extraction"
);
}
#[test]
fn test_tier2_template_learning() {
let mut store = TemplateStore::new();
store.learn_template(
"schedule a meeting with the design team",
SerializableOpTemplate::CreateTask {
priority: Priority::Medium,
},
1000.0,
100,
);
assert_eq!(store.templates.len(), 1);
assert!(store.templates[0]
.keywords
.contains(&"schedule".to_string()));
assert!(store.templates[0].keywords.contains(&"meeting".to_string()));
}
#[test]
fn test_tier2_template_matching() {
let mut store = TemplateStore::new();
store.learn_template(
"schedule a meeting with the design team",
SerializableOpTemplate::CreateTask {
priority: Priority::Medium,
},
1000.0,
100,
);
let results = tier2_extract("schedule a meeting with the engineering team", &store);
assert!(!results.is_empty(), "Should match on keyword overlap");
assert_eq!(results[0].tier, ExtractorTier::Template);
}
#[test]
fn test_tier2_template_reinforcement() {
let mut store = TemplateStore::new();
let tmpl = SerializableOpTemplate::CreateTask {
priority: Priority::High,
};
store.learn_template(
"deploy the application to staging environment",
tmpl.clone(),
1000.0,
100,
);
store.learn_template(
"deploy the application to production environment",
tmpl,
2000.0,
100,
);
assert_eq!(store.templates.len(), 1, "Similar templates should merge");
assert_eq!(store.templates[0].use_count, 2);
}
#[test]
fn test_tier2_no_match_low_overlap() {
let mut store = TemplateStore::new();
store.learn_template(
"organize my photo album from vacation",
SerializableOpTemplate::CreateTask {
priority: Priority::Low,
},
1000.0,
100,
);
let results = tier2_extract("what's the weather today?", &store);
assert!(results.is_empty(), "Low overlap should not match");
}
#[test]
fn test_tier3_task_completed_with_context() {
let task_id = NodeId::new(super::super::state::NodeKind::Task, 1);
let context = ExtractionContext {
active_tasks: vec![(task_id, "call the dentist".to_string())],
..Default::default()
};
let results = tier3_extract("I did it!", &context);
assert!(!results.is_empty());
match &results[0].op {
UpdateOp::UpdateTaskStatus { new_status } => {
assert_eq!(*new_status, TaskStatus::Completed);
}
other => panic!("Expected task completion, got {:?}", other),
}
assert_eq!(results[0].target, Some(task_id));
}
#[test]
fn test_tier3_task_cancelled_with_context() {
let task_id = NodeId::new(super::super::state::NodeKind::Task, 1);
let context = ExtractionContext {
active_tasks: vec![(task_id, "buy flowers".to_string())],
..Default::default()
};
let results = tier3_extract("never mind", &context);
assert!(!results.is_empty());
match &results[0].op {
UpdateOp::UpdateTaskStatus { new_status } => {
assert_eq!(*new_status, TaskStatus::Cancelled);
}
other => panic!("Expected task cancellation, got {:?}", other),
}
}
#[test]
fn test_tier3_positive_about_entity() {
let entity_id = NodeId::new(super::super::state::NodeKind::Entity, 1);
let context = ExtractionContext {
recent_entities: vec![(entity_id, "Sushi Palace".to_string())],
..Default::default()
};
let results = tier3_extract("That was great!", &context);
assert!(!results.is_empty());
match &results[0].op {
UpdateOp::SetPreference { preferred, .. } => {
assert_eq!(preferred, "Sushi Palace");
}
other => panic!("Expected positive preference, got {:?}", other),
}
}
#[test]
fn test_tier3_no_context_no_match() {
let context = ExtractionContext::default();
let results = tier3_extract("I did it!", &context);
assert!(
results.is_empty(),
"No context → no match for anaphoric reference"
);
}
#[test]
fn test_cascade_full_pipeline() {
let config = default_config();
let context = empty_context();
let store = empty_store();
let response = extract(
"I need to call the dentist tomorrow",
&context,
&store,
&config,
);
assert!(!response.updates.is_empty());
assert!(response.tiers_used.contains(&ExtractorTier::Rule));
assert!(
!response.escalation_needed,
"High-confidence rule match should not escalate"
);
}
#[test]
fn test_cascade_escalation_on_unknown() {
let config = default_config();
let context = empty_context();
let store = empty_store();
let response = extract("The weather is nice today", &context, &store, &config);
assert!(
response.escalation_needed,
"No extraction should trigger escalation"
);
assert!(response.llm_request.is_some());
}
#[test]
fn test_cascade_no_duplicate_ops() {
let mut store = TemplateStore::new();
store.learn_template(
"I need to call the doctor about the results",
SerializableOpTemplate::CreateTask {
priority: Priority::Medium,
},
1000.0,
100,
);
let config = default_config();
let context = empty_context();
let response = extract("I need to call the dentist", &context, &store, &config);
let task_count = response
.updates
.iter()
.filter(|u| matches!(u.op, UpdateOp::CreateTask { .. }))
.count();
assert_eq!(
task_count, 1,
"Should deduplicate same-op from different tiers"
);
}
#[test]
fn test_cascade_multiple_extractions() {
let config = default_config();
let context = empty_context();
let store = empty_store();
let response = extract(
"I'm stressed and I need to finish the report. Remind me to call mom.",
&context,
&store,
&config,
);
assert!(
response.updates.len() >= 2,
"Should extract multiple updates: {:?}",
response.updates
);
}
#[test]
fn test_llm_response_parsing() {
let mut store = TemplateStore::new();
let llm_json = r#"[
{"op":"create_task","description":"buy birthday cake","priority":"high","confidence":0.9},
{"op":"set_preference","description":"chocolate cake","domain":"food","confidence":0.7}
]"#;
let updates = integrate_llm_response(
"I need to buy a birthday cake, chocolate please",
llm_json,
&mut store,
100,
1000.0,
);
assert_eq!(updates.len(), 2);
assert_eq!(updates[0].tier, ExtractorTier::Llm);
assert!(
!store.templates.is_empty(),
"LLM success should create templates"
);
}
#[test]
fn test_llm_response_invalid_json() {
let mut store = TemplateStore::new();
let updates = integrate_llm_response("test", "not valid json", &mut store, 100, 1000.0);
assert!(updates.is_empty(), "Invalid JSON should return empty");
}
#[test]
fn test_llm_response_unknown_op() {
let mut store = TemplateStore::new();
let llm_json = r#"[{"op":"unknown_op","description":"test","confidence":0.5}]"#;
let updates = integrate_llm_response("test", llm_json, &mut store, 100, 1000.0);
assert!(updates.is_empty(), "Unknown op should be skipped");
}
#[test]
fn test_extract_keywords() {
let kw = extract_keywords("I need to schedule a meeting with the design team");
assert!(kw.contains(&"schedule".to_string()));
assert!(kw.contains(&"meeting".to_string()));
assert!(kw.contains(&"design".to_string()));
assert!(kw.contains(&"team".to_string()));
assert!(!kw.contains(&"the".to_string())); assert!(!kw.contains(&"to".to_string())); }
#[test]
fn test_keyword_overlap() {
let a = vec![
"meeting".to_string(),
"schedule".to_string(),
"team".to_string(),
];
let b = vec![
"meeting".to_string(),
"team".to_string(),
"review".to_string(),
];
let overlap = keyword_overlap(&a, &b);
assert!(
(overlap - 2.0 / 3.0).abs() < 0.01,
"2/3 overlap: {}",
overlap
);
}
#[test]
fn test_split_comparative() {
let (a, b) = split_comparative("tea over coffee");
assert_eq!(a, "tea");
assert_eq!(b, "coffee");
let (a, b) = split_comparative("walking rather than driving");
assert_eq!(a, "walking");
assert_eq!(b, "driving");
let (a, b) = split_comparative("just chocolate");
assert_eq!(a, "just chocolate");
assert!(b.is_empty());
}
#[test]
fn test_preference_domain_inference() {
assert_eq!(infer_preference_domain("morning coffee routine"), "food");
assert_eq!(infer_preference_domain("jazz music at night"), "music");
assert_eq!(infer_preference_domain("coding in Rust"), "work");
assert_eq!(infer_preference_domain("something random"), "general");
}
#[test]
fn test_template_store_max_enforcement() {
let mut store = TemplateStore::new();
let max = 3;
for i in 0..5 {
store.learn_template(
&format!("unique phrase number {} with keyword{}", i, i),
SerializableOpTemplate::CreateTask {
priority: Priority::Medium,
},
i as f64 * 100.0,
max,
);
}
assert!(store.templates.len() <= max, "Should enforce max templates");
}
#[test]
fn test_extractor_summary() {
let mut store = TemplateStore::new();
store.learn_template(
"test template with keywords",
SerializableOpTemplate::CreateTask {
priority: Priority::Medium,
},
1000.0,
100,
);
let summary = summarize_extractor(&store);
assert_eq!(summary.total_templates, 1);
assert!(summary.avg_template_confidence > 0.0);
}
}