Skip to main content

tensorlogic_adapters/
autocompletion.rs

1//! Schema auto-completion system.
2//!
3//! This module provides intelligent auto-completion for schema construction,
4//! suggesting domains, predicates, and variable names based on:
5//! - Existing schema patterns
6//! - Naming conventions
7//! - Similarity to known schemas
8//! - Common domain modeling patterns
9//!
10//! The system leverages embeddings for similarity-based suggestions and
11//! incorporates common patterns from knowledge bases and domain modeling best practices.
12
13use std::collections::HashMap;
14
15use crate::{SimilaritySearch, SymbolTable};
16
17/// Auto-completion engine for schema construction.
18///
19/// Provides intelligent suggestions for domains, predicates, and variable names
20/// based on context and learned patterns.
21pub struct AutoCompleter {
22    /// Similarity search engine
23    similarity_search: SimilaritySearch,
24    /// Common patterns database
25    patterns: PatternDatabase,
26    /// Number of suggestions to return
27    max_suggestions: usize,
28}
29
30impl AutoCompleter {
31    /// Create a new auto-completer.
32    pub fn new() -> Self {
33        Self {
34            similarity_search: SimilaritySearch::new(),
35            patterns: PatternDatabase::default(),
36            max_suggestions: 5,
37        }
38    }
39
40    /// Set maximum number of suggestions.
41    pub fn with_max_suggestions(mut self, max: usize) -> Self {
42        self.max_suggestions = max;
43        self
44    }
45
46    /// Index a symbol table for auto-completion.
47    pub fn index_table(&mut self, table: &SymbolTable) {
48        self.similarity_search.index_table(table);
49    }
50
51    /// Suggest domain names based on partial input.
52    pub fn suggest_domain_names(&self, partial: &str) -> Vec<DomainSuggestion> {
53        let mut suggestions = Vec::new();
54
55        // Get pattern-based suggestions
56        let pattern_suggestions = self.patterns.suggest_domain_names(partial);
57        for (name, confidence) in pattern_suggestions.into_iter().take(self.max_suggestions) {
58            suggestions.push(DomainSuggestion {
59                name,
60                estimated_cardinality: 100, // Default
61                description: None,
62                confidence,
63                source: SuggestionSource::Pattern,
64            });
65        }
66
67        suggestions
68    }
69
70    /// Suggest predicates based on domain context.
71    pub fn suggest_predicates(
72        &self,
73        domains: &[String],
74        partial: &str,
75    ) -> Vec<PredicateSuggestion> {
76        let mut suggestions = Vec::new();
77
78        // Pattern-based suggestions
79        let pattern_suggestions = self.patterns.suggest_predicates(domains, partial);
80
81        for (name, arg_domains, confidence) in
82            pattern_suggestions.into_iter().take(self.max_suggestions)
83        {
84            suggestions.push(PredicateSuggestion {
85                name,
86                arg_domains,
87                description: None,
88                confidence,
89                source: SuggestionSource::Pattern,
90            });
91        }
92
93        suggestions
94    }
95
96    /// Suggest variable names based on domain type.
97    pub fn suggest_variable_names(&self, domain: &str, partial: &str) -> Vec<VariableSuggestion> {
98        let mut suggestions = Vec::new();
99
100        // Pattern-based suggestions
101        let pattern_suggestions = self.patterns.suggest_variable_names(domain, partial);
102
103        for (name, confidence) in pattern_suggestions.into_iter().take(self.max_suggestions) {
104            suggestions.push(VariableSuggestion {
105                name,
106                domain: domain.to_string(),
107                confidence,
108                source: SuggestionSource::Pattern,
109            });
110        }
111
112        suggestions
113    }
114
115    /// Suggest domain given a predicate pattern.
116    ///
117    /// For example, if a user is defining a predicate "teaches" with args ["Person", "?"],
118    /// this suggests likely domains for the second argument.
119    pub fn suggest_domain_for_predicate_arg(
120        &self,
121        predicate_name: &str,
122        existing_args: &[String],
123        _position: usize,
124    ) -> Vec<DomainSuggestion> {
125        let mut suggestions = Vec::new();
126
127        // Use patterns to suggest likely domains
128        let pattern_suggestions = self
129            .patterns
130            .suggest_domain_for_predicate(predicate_name, existing_args);
131
132        for (name, confidence) in pattern_suggestions.into_iter().take(self.max_suggestions) {
133            suggestions.push(DomainSuggestion {
134                name,
135                estimated_cardinality: 100,
136                description: None,
137                confidence,
138                source: SuggestionSource::Pattern,
139            });
140        }
141
142        suggestions
143    }
144
145    /// Get completion statistics.
146    pub fn stats(&self) -> AutoCompleterStats {
147        AutoCompleterStats {
148            num_indexed_domains: self.similarity_search.stats().num_domains,
149            num_indexed_predicates: self.similarity_search.stats().num_predicates,
150            num_patterns: self.patterns.num_patterns(),
151        }
152    }
153}
154
155impl Default for AutoCompleter {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161/// Suggestion for a domain.
162#[derive(Clone, Debug)]
163pub struct DomainSuggestion {
164    /// Suggested domain name
165    pub name: String,
166    /// Estimated cardinality
167    pub estimated_cardinality: usize,
168    /// Optional description
169    pub description: Option<String>,
170    /// Confidence score (0.0 to 1.0)
171    pub confidence: f64,
172    /// Source of suggestion
173    pub source: SuggestionSource,
174}
175
176/// Suggestion for a predicate.
177#[derive(Clone, Debug)]
178pub struct PredicateSuggestion {
179    /// Suggested predicate name
180    pub name: String,
181    /// Suggested argument domains
182    pub arg_domains: Vec<String>,
183    /// Optional description
184    pub description: Option<String>,
185    /// Confidence score (0.0 to 1.0)
186    pub confidence: f64,
187    /// Source of suggestion
188    pub source: SuggestionSource,
189}
190
191/// Suggestion for a variable name.
192#[derive(Clone, Debug)]
193pub struct VariableSuggestion {
194    /// Suggested variable name
195    pub name: String,
196    /// Domain of the variable
197    pub domain: String,
198    /// Confidence score (0.0 to 1.0)
199    pub confidence: f64,
200    /// Source of suggestion
201    pub source: SuggestionSource,
202}
203
204/// Source of an auto-completion suggestion.
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub enum SuggestionSource {
207    /// From pattern matching
208    Pattern,
209    /// From similarity search
210    Similarity,
211    /// From learned examples
212    Learned,
213    /// From manual templates
214    Template,
215}
216
217/// Statistics about the auto-completer.
218#[derive(Clone, Debug)]
219pub struct AutoCompleterStats {
220    /// Number of indexed domains
221    pub num_indexed_domains: usize,
222    /// Number of indexed predicates
223    pub num_indexed_predicates: usize,
224    /// Number of patterns in database
225    pub num_patterns: usize,
226}
227
228/// Database of common domain modeling patterns.
229///
230/// Contains knowledge about common domain names, predicate patterns,
231/// and naming conventions used in schema design.
232struct PatternDatabase {
233    /// Common domain names and their typical cardinalities
234    common_domains: HashMap<String, Vec<(String, usize)>>,
235    /// Common predicate patterns
236    common_predicates: HashMap<String, Vec<(String, Vec<String>)>>,
237    /// Variable naming patterns
238    variable_patterns: HashMap<String, Vec<String>>,
239}
240
241impl Default for PatternDatabase {
242    fn default() -> Self {
243        let mut db = Self {
244            common_domains: HashMap::new(),
245            common_predicates: HashMap::new(),
246            variable_patterns: HashMap::new(),
247        };
248
249        db.init_common_domains();
250        db.init_common_predicates();
251        db.init_variable_patterns();
252
253        db
254    }
255}
256
257impl PatternDatabase {
258    /// Initialize common domain patterns.
259    fn init_common_domains(&mut self) {
260        // People and agents
261        self.add_domain_pattern(
262            "person",
263            vec![("Person", 1000), ("User", 1000), ("Agent", 500)],
264        );
265        self.add_domain_pattern(
266            "user",
267            vec![("User", 1000), ("Person", 1000), ("Account", 500)],
268        );
269        self.add_domain_pattern(
270            "student",
271            vec![("Student", 500), ("Person", 1000), ("User", 1000)],
272        );
273        self.add_domain_pattern(
274            "teacher",
275            vec![("Teacher", 200), ("Instructor", 200), ("Person", 1000)],
276        );
277
278        // Courses and education
279        self.add_domain_pattern(
280            "course",
281            vec![("Course", 100), ("Class", 100), ("Subject", 50)],
282        );
283        self.add_domain_pattern("class", vec![("Class", 100), ("Course", 100)]);
284
285        // Organizations
286        self.add_domain_pattern(
287            "company",
288            vec![("Company", 500), ("Organization", 500), ("Business", 500)],
289        );
290        self.add_domain_pattern(
291            "department",
292            vec![("Department", 50), ("Division", 50), ("Unit", 50)],
293        );
294
295        // Resources
296        self.add_domain_pattern("book", vec![("Book", 5000), ("Publication", 10000)]);
297        self.add_domain_pattern("product", vec![("Product", 10000), ("Item", 10000)]);
298        self.add_domain_pattern("resource", vec![("Resource", 1000), ("Asset", 1000)]);
299
300        // Time
301        self.add_domain_pattern("time", vec![("Time", 86400), ("Timestamp", 86400)]);
302        self.add_domain_pattern("date", vec![("Date", 365), ("Day", 365)]);
303
304        // Locations
305        self.add_domain_pattern("location", vec![("Location", 1000), ("Place", 1000)]);
306        self.add_domain_pattern("city", vec![("City", 1000), ("Location", 1000)]);
307        self.add_domain_pattern("country", vec![("Country", 200), ("Nation", 200)]);
308    }
309
310    /// Initialize common predicate patterns.
311    fn init_common_predicates(&mut self) {
312        // Binary relationships
313        self.add_predicate_pattern(
314            "person",
315            vec![
316                ("knows", vec!["Person", "Person"]),
317                ("likes", vec!["Person", "Person"]),
318                ("works_with", vec!["Person", "Person"]),
319                ("manages", vec!["Person", "Person"]),
320            ],
321        );
322
323        self.add_predicate_pattern(
324            "student",
325            vec![
326                ("enrolled_in", vec!["Student", "Course"]),
327                ("takes", vec!["Student", "Course"]),
328                ("attends", vec!["Student", "Course"]),
329            ],
330        );
331
332        self.add_predicate_pattern(
333            "teach",
334            vec![
335                ("teaches", vec!["Teacher", "Course"]),
336                ("instructs", vec!["Teacher", "Student"]),
337            ],
338        );
339
340        // Unary predicates (properties)
341        self.add_predicate_pattern(
342            "is",
343            vec![
344                ("is_active", vec!["User"]),
345                ("is_admin", vec!["User"]),
346                ("is_public", vec!["Resource"]),
347            ],
348        );
349    }
350
351    /// Initialize variable naming patterns.
352    fn init_variable_patterns(&mut self) {
353        self.add_variable_pattern("Person", vec!["p", "person", "x", "user"]);
354        self.add_variable_pattern("Student", vec!["s", "student", "x"]);
355        self.add_variable_pattern("Teacher", vec!["t", "teacher", "instructor"]);
356        self.add_variable_pattern("Course", vec!["c", "course", "class"]);
357        self.add_variable_pattern("Book", vec!["b", "book"]);
358        self.add_variable_pattern("Time", vec!["t", "time", "timestamp"]);
359        self.add_variable_pattern("Date", vec!["d", "date", "day"]);
360        self.add_variable_pattern("Location", vec!["l", "loc", "location", "place"]);
361    }
362
363    fn add_domain_pattern(&mut self, key: &str, patterns: Vec<(&str, usize)>) {
364        self.common_domains.insert(
365            key.to_string(),
366            patterns
367                .into_iter()
368                .map(|(name, card)| (name.to_string(), card))
369                .collect(),
370        );
371    }
372
373    fn add_predicate_pattern(&mut self, key: &str, patterns: Vec<(&str, Vec<&str>)>) {
374        self.common_predicates.insert(
375            key.to_string(),
376            patterns
377                .into_iter()
378                .map(|(name, args)| {
379                    (
380                        name.to_string(),
381                        args.into_iter().map(|s| s.to_string()).collect(),
382                    )
383                })
384                .collect(),
385        );
386    }
387
388    fn add_variable_pattern(&mut self, key: &str, patterns: Vec<&str>) {
389        self.variable_patterns.insert(
390            key.to_string(),
391            patterns.into_iter().map(|s| s.to_string()).collect(),
392        );
393    }
394
395    fn suggest_domain_names(&self, partial: &str) -> Vec<(String, f64)> {
396        let mut suggestions = Vec::new();
397        let partial_lower = partial.to_lowercase();
398
399        for (key, patterns) in &self.common_domains {
400            if key.contains(&partial_lower) {
401                for (name, _card) in patterns {
402                    if name.to_lowercase().starts_with(&partial_lower) {
403                        let confidence = 0.9;
404                        suggestions.push((name.clone(), confidence));
405                    }
406                }
407            }
408        }
409
410        suggestions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
411        suggestions
412    }
413
414    fn suggest_predicates(
415        &self,
416        domains: &[String],
417        partial: &str,
418    ) -> Vec<(String, Vec<String>, f64)> {
419        let mut suggestions = Vec::new();
420        let partial_lower = partial.to_lowercase();
421
422        // Find patterns matching the domains
423        for domain in domains {
424            let domain_lower = domain.to_lowercase();
425            if let Some(patterns) = self.common_predicates.get(&domain_lower) {
426                for (name, args) in patterns {
427                    if name.to_lowercase().starts_with(&partial_lower) {
428                        let confidence = 0.85;
429                        suggestions.push((name.clone(), args.clone(), confidence));
430                    }
431                }
432            }
433        }
434
435        suggestions.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
436        suggestions
437    }
438
439    fn suggest_variable_names(&self, domain: &str, partial: &str) -> Vec<(String, f64)> {
440        let mut suggestions = Vec::new();
441        let partial_lower = partial.to_lowercase();
442
443        if let Some(patterns) = self.variable_patterns.get(domain) {
444            for name in patterns {
445                if name.starts_with(&partial_lower) {
446                    let confidence = 0.9;
447                    suggestions.push((name.clone(), confidence));
448                }
449            }
450        }
451
452        // Generic suggestions if no specific patterns
453        if suggestions.is_empty() {
454            let first_char = domain
455                .chars()
456                .next()
457                .unwrap_or('x')
458                .to_lowercase()
459                .to_string();
460            suggestions.push((first_char, 0.5));
461            suggestions.push((domain.to_lowercase(), 0.6));
462        }
463
464        suggestions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
465        suggestions
466    }
467
468    fn suggest_domain_for_predicate(
469        &self,
470        predicate_name: &str,
471        existing_args: &[String],
472    ) -> Vec<(String, f64)> {
473        let mut suggestions = Vec::new();
474
475        // Find predicates with matching names and see what domains they use
476        for patterns in self.common_predicates.values() {
477            for (name, args) in patterns {
478                if name == predicate_name && args.len() > existing_args.len() {
479                    // Check if existing args match
480                    let matches = existing_args.iter().zip(args.iter()).all(|(a, b)| a == b);
481
482                    if matches {
483                        // Suggest the next domain
484                        if let Some(next_domain) = args.get(existing_args.len()) {
485                            let confidence = 0.8;
486                            suggestions.push((next_domain.clone(), confidence));
487                        }
488                    }
489                }
490            }
491        }
492
493        suggestions.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
494        suggestions
495    }
496
497    fn num_patterns(&self) -> usize {
498        self.common_domains.len() + self.common_predicates.len() + self.variable_patterns.len()
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::DomainInfo;
506
507    #[test]
508    fn test_autocompleter_creation() {
509        let ac = AutoCompleter::new();
510        let stats = ac.stats();
511        assert!(stats.num_patterns > 0);
512    }
513
514    #[test]
515    fn test_suggest_domain_names() {
516        let ac = AutoCompleter::new();
517        let suggestions = ac.suggest_domain_names("per");
518
519        assert!(!suggestions.is_empty());
520        // Should suggest "Person" for "per"
521        assert!(suggestions.iter().any(|s| s.name == "Person"));
522    }
523
524    #[test]
525    fn test_suggest_predicates() {
526        let ac = AutoCompleter::new();
527        let suggestions = ac.suggest_predicates(&["Person".to_string()], "know");
528
529        assert!(!suggestions.is_empty());
530        // Should suggest "knows" for "know" with Person domain
531        assert!(suggestions.iter().any(|s| s.name == "knows"));
532    }
533
534    #[test]
535    fn test_suggest_variable_names() {
536        let ac = AutoCompleter::new();
537        let suggestions = ac.suggest_variable_names("Person", "p");
538
539        assert!(!suggestions.is_empty());
540        // Should suggest "p" or "person" for Person domain
541        assert!(suggestions
542            .iter()
543            .any(|s| s.name == "p" || s.name == "person"));
544    }
545
546    #[test]
547    fn test_suggest_domain_for_predicate() {
548        let ac = AutoCompleter::new();
549        let suggestions =
550            ac.suggest_domain_for_predicate_arg("teaches", &["Teacher".to_string()], 1);
551
552        assert!(!suggestions.is_empty());
553        // Should suggest "Course" as second argument for "teaches"
554        assert!(suggestions.iter().any(|s| s.name == "Course"));
555    }
556
557    #[test]
558    fn test_max_suggestions_limit() {
559        let ac = AutoCompleter::new().with_max_suggestions(3);
560        let suggestions = ac.suggest_domain_names("p");
561
562        assert!(suggestions.len() <= 3);
563    }
564
565    #[test]
566    fn test_index_table() {
567        let mut ac = AutoCompleter::new();
568        let mut table = SymbolTable::new();
569        table
570            .add_domain(DomainInfo::new("CustomDomain", 100))
571            .unwrap();
572
573        ac.index_table(&table);
574
575        let stats = ac.stats();
576        assert_eq!(stats.num_indexed_domains, 1);
577    }
578
579    #[test]
580    fn test_suggestion_confidence() {
581        let ac = AutoCompleter::new();
582        let suggestions = ac.suggest_domain_names("person");
583
584        for suggestion in &suggestions {
585            assert!(suggestion.confidence >= 0.0 && suggestion.confidence <= 1.0);
586        }
587    }
588
589    #[test]
590    fn test_empty_partial() {
591        let ac = AutoCompleter::new();
592        let suggestions = ac.suggest_domain_names("");
593
594        // Should return some suggestions even with empty input
595        assert!(!suggestions.is_empty());
596    }
597
598    #[test]
599    fn test_case_insensitive_matching() {
600        let ac = AutoCompleter::new();
601        let suggestions_lower = ac.suggest_domain_names("person");
602        let suggestions_upper = ac.suggest_domain_names("PERSON");
603
604        // Should get same suggestions regardless of case
605        assert!(!suggestions_lower.is_empty());
606        assert!(!suggestions_upper.is_empty());
607    }
608
609    #[test]
610    fn test_pattern_database_initialization() {
611        let db = PatternDatabase::default();
612        assert!(db.num_patterns() > 0);
613        assert!(!db.common_domains.is_empty());
614        assert!(!db.common_predicates.is_empty());
615        assert!(!db.variable_patterns.is_empty());
616    }
617
618    #[test]
619    fn test_multiple_domain_contexts() {
620        let ac = AutoCompleter::new();
621        let suggestions =
622            ac.suggest_predicates(&["Student".to_string(), "Course".to_string()], "enroll");
623
624        assert!(!suggestions.is_empty());
625    }
626}