zoey_core/planner/
knowledge.rs

1//! Knowledge gap analysis for planning
2
3use crate::types::*;
4use crate::Result;
5use serde::{Deserialize, Serialize};
6
7/// Priority level for knowledge gaps
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
10pub enum Priority {
11    /// Low priority - nice to know
12    Low,
13    /// Medium priority - helpful for better response
14    Medium,
15    /// High priority - critical for accurate response
16    High,
17    /// Critical priority - cannot proceed without
18    Critical,
19}
20
21/// Strategy for resolving a knowledge gap
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
24pub enum ResolutionStrategy {
25    /// Search agent's memory
26    SearchMemory,
27    /// Ask user for clarification
28    AskUser,
29    /// Make informed assumption
30    Assume,
31    /// Look up from external source
32    ExternalLookup,
33    /// Derive from context
34    DeriveFromContext,
35    /// Not resolvable
36    Unresolvable,
37}
38
39/// A piece of knowledge we have
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct KnownFact {
43    /// What we know
44    pub fact: String,
45    /// Source of the knowledge
46    pub source: KnowledgeSource,
47    /// Confidence in this fact (0.0 - 1.0)
48    pub confidence: f32,
49    /// When this was learned/updated
50    pub timestamp: i64,
51}
52
53/// Source of knowledge
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
56pub enum KnowledgeSource {
57    /// From conversation memory
58    Memory,
59    /// From current state/context
60    Context,
61    /// From agent's character/settings
62    Character,
63    /// From previous messages
64    RecentMessages,
65    /// Derived/inferred
66    Inferred,
67}
68
69/// A gap in our knowledge
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct KnowledgeGap {
73    /// Description of what we don't know
74    pub description: String,
75    /// Priority level
76    pub priority: Priority,
77    /// Can this be resolved?
78    pub resolvable: bool,
79    /// How to resolve it
80    pub resolution_strategy: Option<ResolutionStrategy>,
81    /// Impact on response quality if not resolved
82    pub impact: String,
83}
84
85/// An assumption we're making
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct Assumption {
89    /// What we're assuming
90    pub assumption: String,
91    /// Confidence in this assumption (0.0 - 1.0)
92    pub confidence: f32,
93    /// Risk if assumption is wrong
94    pub risk_level: RiskLevel,
95}
96
97/// Risk level for assumptions
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
100pub enum RiskLevel {
101    /// Low risk if wrong
102    Low,
103    /// Medium risk if wrong
104    Medium,
105    /// High risk if wrong
106    High,
107    /// Critical risk if wrong
108    Critical,
109}
110
111/// Complete knowledge state analysis
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct KnowledgeState {
115    /// Facts we know
116    pub known_facts: Vec<KnownFact>,
117    /// Gaps in knowledge
118    pub unknown_gaps: Vec<KnowledgeGap>,
119    /// Assumptions we're making
120    pub assumptions: Vec<Assumption>,
121    /// Overall confidence score (0.0 - 1.0)
122    pub confidence_score: f32,
123    /// Summary of knowledge state
124    pub summary: String,
125}
126
127/// Knowledge analyzer
128pub struct KnowledgeAnalyzer;
129
130impl KnowledgeAnalyzer {
131    /// Create a new knowledge analyzer
132    pub fn new() -> Self {
133        Self
134    }
135
136    /// Analyze knowledge state for a message
137    pub async fn analyze(&self, message: &Memory, state: &State) -> Result<KnowledgeState> {
138        let mut known_facts = Vec::new();
139        let mut unknown_gaps = Vec::new();
140
141        // Extract entities and concepts from message
142        let entities = self.extract_entities(&message.content.text);
143
144        // Check what we know from state
145        known_facts.extend(self.extract_facts_from_state(state));
146
147        // Check what we know from recent messages
148        if let Some(recent) = state.data.get("recentMessages") {
149            known_facts.extend(self.extract_facts_from_recent(recent));
150        }
151
152        // Identify knowledge gaps
153        for entity in &entities {
154            if !self.is_entity_known(entity, &known_facts) {
155                let gap = self.create_knowledge_gap(entity, &message.content.text);
156                unknown_gaps.push(gap);
157            }
158        }
159
160        // Analyze contextual requirements
161        let contextual_gaps = self.analyze_contextual_requirements(&message.content.text, state);
162        unknown_gaps.extend(contextual_gaps);
163
164        // Generate assumptions if needed
165        let assumptions = self.generate_assumptions(&unknown_gaps, state);
166
167        // Calculate confidence score
168        let confidence_score = self.calculate_confidence(&known_facts, &unknown_gaps);
169
170        // Generate summary
171        let summary = self.generate_summary(&known_facts, &unknown_gaps, &assumptions);
172
173        Ok(KnowledgeState {
174            known_facts,
175            unknown_gaps,
176            assumptions,
177            confidence_score,
178            summary,
179        })
180    }
181
182    /// Extract entities and concepts from text
183    fn extract_entities(&self, text: &str) -> Vec<String> {
184        let mut entities = Vec::new();
185
186        // Simple entity extraction (could be enhanced with NER)
187        let words: Vec<&str> = text.split_whitespace().collect();
188
189        for window in words.windows(2) {
190            // Capitalized words (potential proper nouns)
191            if window[0]
192                .chars()
193                .next()
194                .map(|c| c.is_uppercase())
195                .unwrap_or(false)
196            {
197                entities.push(window[0].to_string());
198            }
199
200            // Two-word entities
201            if window[0]
202                .chars()
203                .next()
204                .map(|c| c.is_uppercase())
205                .unwrap_or(false)
206                && window[1]
207                    .chars()
208                    .next()
209                    .map(|c| c.is_uppercase())
210                    .unwrap_or(false)
211            {
212                entities.push(format!("{} {}", window[0], window[1]));
213            }
214        }
215
216        // Technical terms and keywords
217        let technical_patterns = [
218            "algorithm",
219            "function",
220            "code",
221            "system",
222            "database",
223            "api",
224            "service",
225            "module",
226            "component",
227            "framework",
228        ];
229
230        for pattern in &technical_patterns {
231            if text.to_lowercase().contains(pattern) {
232                entities.push(pattern.to_string());
233            }
234        }
235
236        // Ensure common programming languages are recognized
237        let lower = text.to_lowercase();
238        for lang in ["rust", "python", "java", "javascript", "go", "c++", "c"].iter() {
239            if lower.contains(lang) {
240                let name = match *lang {
241                    "javascript" => "JavaScript".to_string(),
242                    "c++" => "C++".to_string(),
243                    _ => {
244                        let mut s = lang.to_string();
245                        if let Some(first) = s.chars().next() {
246                            s.replace_range(0..1, &first.to_uppercase().to_string());
247                        }
248                        s
249                    }
250                };
251                entities.push(name);
252            }
253        }
254
255        // Deduplicate
256        entities.sort();
257        entities.dedup();
258
259        entities
260    }
261
262    /// Extract known facts from state
263    fn extract_facts_from_state(&self, state: &State) -> Vec<KnownFact> {
264        let mut facts = Vec::new();
265        let now = chrono::Utc::now().timestamp();
266
267        // Agent name
268        if let Some(name) = state.data.get("agentName") {
269            if let Some(name_str) = name.as_str() {
270                facts.push(KnownFact {
271                    fact: format!("Agent name is {}", name_str),
272                    source: KnowledgeSource::Context,
273                    confidence: 1.0,
274                    timestamp: now,
275                });
276            }
277        }
278
279        // User name
280        if let Some(user) = state.data.get("userName") {
281            if let Some(user_str) = user.as_str() {
282                facts.push(KnownFact {
283                    fact: format!("User name is {}", user_str),
284                    source: KnowledgeSource::Context,
285                    confidence: 1.0,
286                    timestamp: now,
287                });
288            }
289        }
290
291        // Current goals
292        if let Some(goals) = state.data.get("goals") {
293            if let Some(goals_arr) = goals.as_array() {
294                for goal in goals_arr {
295                    if let Some(goal_str) = goal.as_str() {
296                        facts.push(KnownFact {
297                            fact: format!("Current goal: {}", goal_str),
298                            source: KnowledgeSource::Context,
299                            confidence: 0.9,
300                            timestamp: now,
301                        });
302                    }
303                }
304            }
305        }
306
307        facts
308    }
309
310    /// Extract facts from recent messages
311    fn extract_facts_from_recent(&self, recent: &serde_json::Value) -> Vec<KnownFact> {
312        let mut facts = Vec::new();
313        let now = chrono::Utc::now().timestamp();
314
315        if let Some(messages) = recent.as_array() {
316            for msg in messages.iter().take(5) {
317                if let Some(content) = msg.get("content").and_then(|c| c.get("text")) {
318                    if let Some(text) = content.as_str() {
319                        // Extract key information from recent messages
320                        facts.push(KnownFact {
321                            fact: format!(
322                                "Recent context: {}",
323                                text.chars().take(100).collect::<String>()
324                            ),
325                            source: KnowledgeSource::RecentMessages,
326                            confidence: 0.8,
327                            timestamp: now,
328                        });
329                    }
330                }
331            }
332        }
333
334        facts
335    }
336
337    /// Check if an entity is known
338    fn is_entity_known(&self, entity: &str, known_facts: &[KnownFact]) -> bool {
339        let entity_lower = entity.to_lowercase();
340        known_facts
341            .iter()
342            .any(|fact| fact.fact.to_lowercase().contains(&entity_lower))
343    }
344
345    /// Create a knowledge gap for an unknown entity
346    fn create_knowledge_gap(&self, entity: &str, context: &str) -> KnowledgeGap {
347        // Determine priority based on context
348        let priority = if context
349            .to_lowercase()
350            .contains(&format!("what is {}", entity.to_lowercase()))
351            || context
352                .to_lowercase()
353                .contains(&format!("who is {}", entity.to_lowercase()))
354        {
355            Priority::Critical
356        } else if context.to_lowercase().contains("explain") {
357            Priority::High
358        } else {
359            Priority::Medium
360        };
361
362        KnowledgeGap {
363            description: format!("Unknown entity: {}", entity),
364            priority,
365            resolvable: true,
366            resolution_strategy: Some(ResolutionStrategy::SearchMemory),
367            impact: format!("May affect understanding of {}", entity),
368        }
369    }
370
371    /// Analyze contextual requirements
372    fn analyze_contextual_requirements(&self, text: &str, state: &State) -> Vec<KnowledgeGap> {
373        let mut gaps = Vec::new();
374        let lower = text.to_lowercase();
375
376        // References to "it", "this", "that" without clear antecedent
377        if (lower.contains(" it ") || lower.contains("this") || lower.contains("that"))
378            && !state.data.contains_key("recentMessages")
379        {
380            gaps.push(KnowledgeGap {
381                description: "Unclear reference - missing context".to_string(),
382                priority: Priority::High,
383                resolvable: false,
384                resolution_strategy: Some(ResolutionStrategy::AskUser),
385                impact: "May misunderstand what user is referring to".to_string(),
386            });
387        }
388
389        // Temporal references
390        if lower.contains("previous") || lower.contains("earlier") || lower.contains("last time") {
391            gaps.push(KnowledgeGap {
392                description: "Reference to previous conversation or event".to_string(),
393                priority: Priority::High,
394                resolvable: true,
395                resolution_strategy: Some(ResolutionStrategy::SearchMemory),
396                impact: "Missing historical context".to_string(),
397            });
398        }
399
400        // Technical specifications without details
401        if (lower.contains("implement") || lower.contains("build"))
402            && !lower.contains("how")
403            && lower.split_whitespace().count() < 10
404        {
405            gaps.push(KnowledgeGap {
406                description: "Insufficient implementation details".to_string(),
407                priority: Priority::High,
408                resolvable: true,
409                resolution_strategy: Some(ResolutionStrategy::AskUser),
410                impact: "May provide generic solution instead of specific one".to_string(),
411            });
412        }
413
414        gaps
415    }
416
417    /// Generate reasonable assumptions
418    fn generate_assumptions(&self, gaps: &[KnowledgeGap], state: &State) -> Vec<Assumption> {
419        let mut assumptions = Vec::new();
420
421        // For each gap, consider if we can make a reasonable assumption
422        for gap in gaps {
423            if gap.priority <= Priority::Medium && gap.resolvable {
424                // Make assumptions for low-medium priority gaps
425                let assumption = match gap.resolution_strategy {
426                    Some(ResolutionStrategy::DeriveFromContext) => Assumption {
427                        assumption: format!("Assuming typical context for: {}", gap.description),
428                        confidence: 0.6,
429                        risk_level: RiskLevel::Low,
430                    },
431                    Some(ResolutionStrategy::Assume) => Assumption {
432                        assumption: format!(
433                            "Assuming standard interpretation: {}",
434                            gap.description
435                        ),
436                        confidence: 0.5,
437                        risk_level: RiskLevel::Medium,
438                    },
439                    _ => continue,
440                };
441                assumptions.push(assumption);
442            }
443        }
444
445        // Assume user wants help if asking questions
446        if state.data.get("intent").and_then(|i| i.as_str()) == Some("question") {
447            assumptions.push(Assumption {
448                assumption: "User wants informative, helpful response".to_string(),
449                confidence: 0.9,
450                risk_level: RiskLevel::Low,
451            });
452        }
453
454        assumptions
455    }
456
457    /// Calculate overall confidence score
458    fn calculate_confidence(&self, known_facts: &[KnownFact], gaps: &[KnowledgeGap]) -> f32 {
459        if known_facts.is_empty() && gaps.is_empty() {
460            return 0.5; // Neutral when no information
461        }
462
463        // Weight by priority of gaps
464        let gap_penalty: f32 = gaps
465            .iter()
466            .map(|g| match g.priority {
467                Priority::Critical => 0.3,
468                Priority::High => 0.2,
469                Priority::Medium => 0.1,
470                Priority::Low => 0.05,
471            })
472            .sum();
473
474        // Boost from known facts
475        let fact_boost = (known_facts.len() as f32 * 0.1).min(0.4);
476
477        // Calculate confidence
478        let confidence = 0.5 + fact_boost - gap_penalty;
479
480        confidence.max(0.1).min(1.0)
481    }
482
483    /// Generate summary
484    fn generate_summary(
485        &self,
486        known_facts: &[KnownFact],
487        gaps: &[KnowledgeGap],
488        assumptions: &[Assumption],
489    ) -> String {
490        let critical_gaps = gaps
491            .iter()
492            .filter(|g| g.priority == Priority::Critical)
493            .count();
494        let high_gaps = gaps.iter().filter(|g| g.priority == Priority::High).count();
495
496        format!(
497            "Known: {} facts | Unknown: {} gaps ({} critical, {} high) | Assumptions: {}",
498            known_facts.len(),
499            gaps.len(),
500            critical_gaps,
501            high_gaps,
502            assumptions.len()
503        )
504    }
505}
506
507impl Default for KnowledgeAnalyzer {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use uuid::Uuid;
517
518    fn create_test_message(text: &str) -> Memory {
519        Memory {
520            id: Uuid::new_v4(),
521            entity_id: Uuid::new_v4(),
522            agent_id: Uuid::new_v4(),
523            room_id: Uuid::new_v4(),
524            content: Content {
525                text: text.to_string(),
526                ..Default::default()
527            },
528            embedding: None,
529            metadata: None,
530            created_at: chrono::Utc::now().timestamp(),
531            unique: None,
532            similarity: None,
533        }
534    }
535
536    #[tokio::test]
537    async fn test_knowledge_analysis_simple() {
538        let analyzer = KnowledgeAnalyzer::new();
539        let message = create_test_message("Hello, how are you?");
540        let state = State::new();
541
542        let knowledge = analyzer.analyze(&message, &state).await.unwrap();
543        assert!(knowledge.confidence_score > 0.0);
544    }
545
546    #[tokio::test]
547    async fn test_entity_extraction() {
548        let analyzer = KnowledgeAnalyzer::new();
549        let entities = analyzer.extract_entities("Tell me about Rust programming and Python");
550
551        assert!(entities.contains(&"Rust".to_string()));
552        assert!(entities.contains(&"Python".to_string()));
553    }
554
555    #[tokio::test]
556    async fn test_contextual_gaps() {
557        let analyzer = KnowledgeAnalyzer::new();
558        let message = create_test_message("Can you continue from where we left off last time?");
559        let state = State::new();
560
561        let knowledge = analyzer.analyze(&message, &state).await.unwrap();
562        assert!(!knowledge.unknown_gaps.is_empty());
563        assert!(knowledge
564            .unknown_gaps
565            .iter()
566            .any(|g| g.priority >= Priority::High));
567    }
568
569    #[tokio::test]
570    async fn test_known_facts_from_state() {
571        let analyzer = KnowledgeAnalyzer::new();
572        let message = create_test_message("Hello");
573        let mut state = State::new();
574        state.data.insert(
575            "agentName".to_string(),
576            serde_json::Value::String("TestAgent".to_string()),
577        );
578
579        let knowledge = analyzer.analyze(&message, &state).await.unwrap();
580        assert!(!knowledge.known_facts.is_empty());
581        assert!(knowledge
582            .known_facts
583            .iter()
584            .any(|f| f.fact.contains("TestAgent")));
585    }
586}