Skip to main content

oxirs_embed/
integration.rs

1//! Integration utilities with other OxiRS components
2
3use crate::{EmbeddingModel, Vector};
4use anyhow::{anyhow, Result};
5use oxirs_vec::index::{AdvancedVectorIndex, IndexConfig, IndexType};
6use oxirs_vec::VectorIndex as OxirsVectorIndex;
7use std::collections::HashMap;
8use tracing::{debug, info, warn};
9
10/// Integration bridge between oxirs-embed and vector stores
11pub struct VectorStoreBridge {
12    entity_mappings: HashMap<String, String>,
13    relation_mappings: HashMap<String, String>,
14    prefix_config: PrefixConfig,
15    /// Flat (brute-force) vector index over synced entity embeddings, keyed
16    /// by the same URI stored in `entity_mappings`, backing
17    /// [`find_similar_entities`](Self::find_similar_entities).
18    entity_index: AdvancedVectorIndex,
19    /// Same as `entity_index` but for relation embeddings, backing
20    /// [`find_similar_relations`](Self::find_similar_relations).
21    relation_index: AdvancedVectorIndex,
22}
23
24fn flat_index() -> AdvancedVectorIndex {
25    AdvancedVectorIndex::new(IndexConfig {
26        index_type: IndexType::Flat,
27        ..IndexConfig::default()
28    })
29}
30
31/// Configuration for URI prefixes in vector store
32#[derive(Debug, Clone)]
33pub struct PrefixConfig {
34    pub entity_prefix: String,
35    pub relation_prefix: String,
36    pub use_namespaces: bool,
37}
38
39impl Default for PrefixConfig {
40    fn default() -> Self {
41        Self {
42            entity_prefix: "kg:entity:".to_string(),
43            relation_prefix: "kg:relation:".to_string(),
44            use_namespaces: true,
45        }
46    }
47}
48
49impl VectorStoreBridge {
50    /// Create a new bridge
51    pub fn new() -> Self {
52        Self {
53            entity_mappings: HashMap::new(),
54            relation_mappings: HashMap::new(),
55            prefix_config: PrefixConfig::default(),
56            entity_index: flat_index(),
57            relation_index: flat_index(),
58        }
59    }
60
61    /// Create bridge with custom prefix config
62    pub fn with_prefix_config(prefix_config: PrefixConfig) -> Self {
63        Self {
64            entity_mappings: HashMap::new(),
65            relation_mappings: HashMap::new(),
66            prefix_config,
67            entity_index: flat_index(),
68            relation_index: flat_index(),
69        }
70    }
71
72    /// Sync all embeddings from a model to the vector store
73    pub fn sync_model_embeddings(&mut self, model: &dyn EmbeddingModel) -> Result<SyncStats> {
74        let start_time = std::time::Instant::now();
75        let mut sync_stats = SyncStats::default();
76
77        info!("Starting embedding synchronization to vector store");
78
79        // Sync entity embeddings
80        let entities = model.get_entities();
81        for entity in &entities {
82            match model.get_entity_embedding(entity) {
83                Ok(embedding) => {
84                    let uri = self.generate_entity_uri(entity);
85                    if let Err(e) = self
86                        .entity_index
87                        .insert(uri.clone(), embedding.into_inner())
88                    {
89                        warn!("Failed to index embedding for entity {}: {}", entity, e);
90                        sync_stats
91                            .errors
92                            .push(format!("Entity {entity} (indexing): {e}"));
93                        continue;
94                    }
95                    self.entity_mappings.insert(entity.clone(), uri);
96                    sync_stats.entities_synced += 1;
97                }
98                Err(e) => {
99                    warn!("Failed to get embedding for entity {}: {}", entity, e);
100                    sync_stats.errors.push(format!("Entity {entity}: {e}"));
101                }
102            }
103        }
104
105        // Sync relation embeddings
106        let relations = model.get_relations();
107        for relation in &relations {
108            match model.get_relation_embedding(relation) {
109                Ok(embedding) => {
110                    let uri = self.generate_relation_uri(relation);
111                    if let Err(e) = self
112                        .relation_index
113                        .insert(uri.clone(), embedding.into_inner())
114                    {
115                        warn!("Failed to index embedding for relation {}: {}", relation, e);
116                        sync_stats
117                            .errors
118                            .push(format!("Relation {relation} (indexing): {e}"));
119                        continue;
120                    }
121                    self.relation_mappings.insert(relation.clone(), uri);
122                    sync_stats.relations_synced += 1;
123                }
124                Err(e) => {
125                    warn!("Failed to get embedding for relation {}: {}", relation, e);
126                    sync_stats.errors.push(format!("Relation {relation}: {e}"));
127                }
128            }
129        }
130
131        sync_stats.sync_duration = start_time.elapsed();
132        info!(
133            "Embedding sync completed: {} entities, {} relations, {} errors",
134            sync_stats.entities_synced,
135            sync_stats.relations_synced,
136            sync_stats.errors.len()
137        );
138
139        Ok(sync_stats)
140    }
141
142    /// Find entities most similar to `entity` by cosine/L2 distance (per the
143    /// index's configured metric) between their synced embeddings. `entity`
144    /// itself is excluded from the results.
145    pub fn find_similar_entities(&self, entity: &str, k: usize) -> Result<Vec<(String, f32)>> {
146        let uri = self
147            .entity_mappings
148            .get(entity)
149            .ok_or_else(|| anyhow!("Entity not found in mappings: {}", entity))?;
150        let query = self
151            .entity_index
152            .get_vector(uri)
153            .ok_or_else(|| anyhow!("Entity '{}' is mapped but has no indexed embedding", entity))?;
154
155        debug!("Searching for entities similar to: {}", entity);
156        let results = self.entity_index.search_knn(query, k + 1)?;
157        Ok(results
158            .into_iter()
159            .filter(|(result_uri, _)| result_uri != uri)
160            .take(k)
161            .map(|(result_uri, distance)| (self.entity_name_from_uri(&result_uri), distance))
162            .collect())
163    }
164
165    /// Find relations most similar to `relation`, analogous to
166    /// [`find_similar_entities`](Self::find_similar_entities).
167    pub fn find_similar_relations(&self, relation: &str, k: usize) -> Result<Vec<(String, f32)>> {
168        let uri = self
169            .relation_mappings
170            .get(relation)
171            .ok_or_else(|| anyhow!("Relation not found in mappings: {}", relation))?;
172        let query = self.relation_index.get_vector(uri).ok_or_else(|| {
173            anyhow!(
174                "Relation '{}' is mapped but has no indexed embedding",
175                relation
176            )
177        })?;
178
179        debug!("Searching for relations similar to: {}", relation);
180        let results = self.relation_index.search_knn(query, k + 1)?;
181        Ok(results
182            .into_iter()
183            .filter(|(result_uri, _)| result_uri != uri)
184            .take(k)
185            .map(|(result_uri, distance)| (self.relation_name_from_uri(&result_uri), distance))
186            .collect())
187    }
188
189    /// Recover the original entity name from a generated URI (inverse of
190    /// [`generate_entity_uri`](Self::generate_entity_uri)).
191    fn entity_name_from_uri(&self, uri: &str) -> String {
192        uri.strip_prefix(&self.prefix_config.entity_prefix)
193            .unwrap_or(uri)
194            .to_string()
195    }
196
197    /// Recover the original relation name from a generated URI (inverse of
198    /// [`generate_relation_uri`](Self::generate_relation_uri)).
199    fn relation_name_from_uri(&self, uri: &str) -> String {
200        uri.strip_prefix(&self.prefix_config.relation_prefix)
201            .unwrap_or(uri)
202            .to_string()
203    }
204
205    /// Generate URI for entity
206    fn generate_entity_uri(&self, entity: &str) -> String {
207        if self.prefix_config.use_namespaces {
208            format!("{}{}", self.prefix_config.entity_prefix, entity)
209        } else {
210            entity.to_string()
211        }
212    }
213
214    /// Generate URI for relation
215    fn generate_relation_uri(&self, relation: &str) -> String {
216        if self.prefix_config.use_namespaces {
217            format!("{}{}", self.prefix_config.relation_prefix, relation)
218        } else {
219            relation.to_string()
220        }
221    }
222
223    /// Get sync statistics
224    pub fn get_sync_info(&self) -> SyncInfo {
225        SyncInfo {
226            entities_mapped: self.entity_mappings.len(),
227            relations_mapped: self.relation_mappings.len(),
228            vector_store_stats: None,
229        }
230    }
231
232    /// Clear all mappings
233    pub fn clear_mappings(&mut self) {
234        self.entity_mappings.clear();
235        self.relation_mappings.clear();
236        info!("Cleared all entity and relation mappings");
237    }
238}
239
240impl Default for VectorStoreBridge {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246/// Statistics from synchronization operation
247#[derive(Debug, Clone, Default)]
248pub struct SyncStats {
249    pub entities_synced: usize,
250    pub relations_synced: usize,
251    pub errors: Vec<String>,
252    pub sync_duration: std::time::Duration,
253}
254
255/// Information about current sync state
256#[derive(Debug, Clone)]
257pub struct SyncInfo {
258    pub entities_mapped: usize,
259    pub relations_mapped: usize,
260    pub vector_store_stats: Option<(usize, usize)>,
261}
262
263/// Integration with oxirs-chat for conversational AI
264pub struct ChatIntegration {
265    model: Box<dyn EmbeddingModel>,
266    context_window: usize,
267    similarity_threshold: f32,
268    personalization: PersonalizationEngine,
269    multilingual: MultilingualSupport,
270}
271
272impl ChatIntegration {
273    /// Create new chat integration
274    pub fn new(model: Box<dyn EmbeddingModel>) -> Self {
275        Self {
276            model,
277            context_window: 10,
278            similarity_threshold: 0.7,
279            personalization: PersonalizationEngine::new(),
280            multilingual: MultilingualSupport::new(),
281        }
282    }
283
284    /// Configure context window size
285    pub fn with_context_window(mut self, window_size: usize) -> Self {
286        self.context_window = window_size;
287        self
288    }
289
290    /// Configure similarity threshold for relevant entities
291    pub fn with_similarity_threshold(mut self, threshold: f32) -> Self {
292        self.similarity_threshold = threshold;
293        self
294    }
295
296    /// Extract entities from the model's vocabulary that appear as a
297    /// substring of `query` (case-insensitive).
298    ///
299    /// Rather than scanning the whole query once per known entity (`O(entities
300    /// × query length)`), this builds a lowercase-name index once and scans
301    /// the query a single time for bounded-length substring windows against
302    /// that index (`O(entities + query length × longest entity name)`).
303    pub fn extract_relevant_entities(&self, query: &str) -> Result<Vec<String>> {
304        let entities = self.model.get_entities();
305        if entities.is_empty() {
306            return Ok(Vec::new());
307        }
308
309        let query_lower = query.to_lowercase();
310
311        let mut entity_index: HashMap<String, String> = HashMap::new();
312        let mut max_entity_chars = 0usize;
313        for entity in entities {
314            let lower = entity.to_lowercase();
315            max_entity_chars = max_entity_chars.max(lower.chars().count());
316            entity_index.insert(lower, entity);
317        }
318        if max_entity_chars == 0 {
319            return Ok(Vec::new());
320        }
321
322        // Byte offset of every character boundary in the query (plus one past
323        // the end) so substrings never split a multi-byte UTF-8 character.
324        let boundaries: Vec<usize> = query_lower
325            .char_indices()
326            .map(|(byte_idx, _)| byte_idx)
327            .chain(std::iter::once(query_lower.len()))
328            .collect();
329        let num_chars = boundaries.len() - 1;
330
331        let mut relevant = Vec::new();
332        let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
333
334        for start in 0..num_chars {
335            let start_byte = boundaries[start];
336            let max_len = max_entity_chars.min(num_chars - start);
337            for len in 1..=max_len {
338                let end_byte = boundaries[start + len];
339                let window = &query_lower[start_byte..end_byte];
340                if let Some(original) = entity_index.get(window) {
341                    if seen.insert(original.clone()) {
342                        relevant.push(original.clone());
343                    }
344                }
345            }
346        }
347
348        Ok(relevant)
349    }
350
351    /// Generate a context embedding for a conversation by encoding the most
352    /// recent messages (bounded by `context_window`) with the held model and
353    /// averaging the resulting vectors component-wise.
354    pub async fn generate_context_embedding(&self, messages: &[String]) -> Result<Vector> {
355        if messages.is_empty() {
356            return Err(anyhow!("No messages provided"));
357        }
358
359        // Take the last N messages based on context window
360        let recent_messages: Vec<String> = messages
361            .iter()
362            .rev()
363            .take(self.context_window)
364            .cloned()
365            .collect();
366
367        let encoded = self.model.encode(&recent_messages).await?;
368        let dim = encoded
369            .iter()
370            .map(|v| v.len())
371            .find(|&len| len > 0)
372            .ok_or_else(|| anyhow!("Model returned no non-empty encodings for these messages"))?;
373
374        let mut combined = vec![0.0f32; dim];
375        let mut count = 0usize;
376        for vector in &encoded {
377            if vector.len() != dim {
378                // Defensively skip any encoding with a mismatched dimension
379                // rather than corrupting the running average.
380                continue;
381            }
382            for (acc, value) in combined.iter_mut().zip(vector.iter()) {
383                *acc += value;
384            }
385            count += 1;
386        }
387
388        for value in combined.iter_mut() {
389            *value /= count as f32;
390        }
391
392        Ok(Vector::new(combined))
393    }
394
395    /// Generate personalized embeddings for a user
396    pub async fn generate_personalized_embedding(
397        &mut self,
398        user_id: &str,
399        query: &str,
400        conversation_history: &[String],
401    ) -> Result<Vector> {
402        // Get user profile and preferences
403        let user_profile = self.personalization.get_user_profile(user_id)?.clone();
404
405        // Apply user preferences to query embedding
406        let embeddings = self.model.encode(&[query.to_string()]).await?;
407        let base_embedding = Vector::new(embeddings[0].clone());
408        let personalized_embedding = self.personalization.apply_user_preferences(
409            &base_embedding,
410            &user_profile,
411            conversation_history,
412        )?;
413
414        Ok(personalized_embedding)
415    }
416
417    /// Update user profile based on interaction
418    pub fn update_user_profile(
419        &mut self,
420        user_id: &str,
421        query: &str,
422        response_feedback: Option<f32>,
423        interaction_type: InteractionType,
424    ) -> Result<()> {
425        self.personalization.update_user_profile(
426            user_id,
427            query,
428            response_feedback,
429            interaction_type,
430        )
431    }
432
433    /// Translate query to target language
434    pub async fn translate_query(
435        &self,
436        query: &str,
437        source_lang: &str,
438        target_lang: &str,
439    ) -> Result<String> {
440        self.multilingual
441            .translate_text(query, source_lang, target_lang)
442            .await
443    }
444
445    /// Detect language of input text
446    pub async fn detect_language(&self, text: &str) -> Result<LanguageDetection> {
447        self.multilingual.detect_language(text).await
448    }
449
450    /// Generate cross-lingual embeddings
451    pub async fn generate_cross_lingual_embedding(
452        &self,
453        text: &str,
454        source_lang: &str,
455        target_lang: &str,
456    ) -> Result<Vector> {
457        self.multilingual
458            .generate_cross_lingual_embedding(text, source_lang, target_lang, &*self.model)
459            .await
460    }
461
462    /// Get multilingual entity alignment
463    pub async fn align_entities_across_languages(
464        &self,
465        entity: &str,
466        source_lang: &str,
467        target_langs: &[String],
468    ) -> Result<HashMap<String, String>> {
469        self.multilingual
470            .align_entities(entity, source_lang, target_langs)
471            .await
472    }
473}
474
475/// SPARQL integration for query enhancement
476pub struct SparqlIntegration {
477    #[allow(dead_code)]
478    model: Box<dyn EmbeddingModel>,
479    #[allow(dead_code)]
480    similarity_boost: f32,
481}
482
483impl SparqlIntegration {
484    /// Create new SPARQL integration
485    pub fn new(model: Box<dyn EmbeddingModel>) -> Self {
486        Self {
487            model,
488            similarity_boost: 0.1,
489        }
490    }
491
492    /// Enhance SPARQL query with similarity-based suggestions
493    pub fn enhance_query(&self, sparql_query: &str) -> Result<EnhancedQuery> {
494        // Parse basic patterns from SPARQL (simplified)
495        let entities = self.extract_entities_from_sparql(sparql_query)?;
496        let relations = self.extract_relations_from_sparql(sparql_query)?;
497
498        let mut suggestions = Vec::new();
499
500        // Find similar entities
501        for entity in &entities {
502            // This would use actual similarity computation
503            suggestions.push(QuerySuggestion {
504                suggestion_type: SuggestionType::SimilarEntity,
505                original: entity.clone(),
506                suggested: format!("similar_to_{entity}"),
507                confidence: 0.8,
508            });
509        }
510
511        // Find similar relations
512        for relation in &relations {
513            suggestions.push(QuerySuggestion {
514                suggestion_type: SuggestionType::SimilarRelation,
515                original: relation.clone(),
516                suggested: format!("similar_to_{relation}"),
517                confidence: 0.7,
518            });
519        }
520
521        Ok(EnhancedQuery {
522            original_query: sparql_query.to_string(),
523            entities_found: entities,
524            relations_found: relations,
525            suggestions,
526        })
527    }
528
529    /// Extract entities from SPARQL query (simplified)
530    fn extract_entities_from_sparql(&self, query: &str) -> Result<Vec<String>> {
531        // This is a very simplified extraction
532        // A real implementation would use a proper SPARQL parser
533        let mut entities = Vec::new();
534
535        for line in query.lines() {
536            if line.contains("http://") {
537                // Extract URIs that might be entities
538                if let Some(start) = line.find("http://") {
539                    if let Some(end) = line[start..].find(' ') {
540                        let uri = &line[start..start + end];
541                        entities.push(uri.to_string());
542                    }
543                }
544            }
545        }
546
547        Ok(entities)
548    }
549
550    /// Extract relations from SPARQL query (simplified)
551    fn extract_relations_from_sparql(&self, query: &str) -> Result<Vec<String>> {
552        // Simplified relation extraction
553        let mut relations = Vec::new();
554
555        for line in query.lines() {
556            if line.contains("?") && line.contains("http://") {
557                // Look for patterns like "?s <relation> ?o"
558                if let Some(start) = line.find('<') {
559                    if let Some(end) = line.find('>') {
560                        let relation = &line[start + 1..end];
561                        relations.push(relation.to_string());
562                    }
563                }
564            }
565        }
566
567        Ok(relations)
568    }
569}
570
571/// Enhanced SPARQL query with suggestions
572#[derive(Debug, Clone)]
573pub struct EnhancedQuery {
574    pub original_query: String,
575    pub entities_found: Vec<String>,
576    pub relations_found: Vec<String>,
577    pub suggestions: Vec<QuerySuggestion>,
578}
579
580/// Query enhancement suggestion
581#[derive(Debug, Clone)]
582pub struct QuerySuggestion {
583    pub suggestion_type: SuggestionType,
584    pub original: String,
585    pub suggested: String,
586    pub confidence: f32,
587}
588
589/// Types of query suggestions
590#[derive(Debug, Clone)]
591pub enum SuggestionType {
592    SimilarEntity,
593    SimilarRelation,
594    AlternativePattern,
595    ExpansionSuggestion,
596}
597
598/// Personalization engine for user-specific embeddings
599pub struct PersonalizationEngine {
600    user_profiles: HashMap<String, UserProfile>,
601    interaction_history: HashMap<String, Vec<UserInteraction>>,
602    preference_weights: PreferenceWeights,
603}
604
605impl Default for PersonalizationEngine {
606    fn default() -> Self {
607        Self::new()
608    }
609}
610
611impl PersonalizationEngine {
612    pub fn new() -> Self {
613        Self {
614            user_profiles: HashMap::new(),
615            interaction_history: HashMap::new(),
616            preference_weights: PreferenceWeights::default(),
617        }
618    }
619
620    /// Get or create user profile
621    pub fn get_user_profile(&mut self, user_id: &str) -> Result<&UserProfile> {
622        if !self.user_profiles.contains_key(user_id) {
623            let profile = UserProfile::new(user_id.to_string());
624            self.user_profiles.insert(user_id.to_string(), profile);
625        }
626
627        self.user_profiles
628            .get(user_id)
629            .ok_or_else(|| anyhow!("Failed to get user profile for {}", user_id))
630    }
631
632    /// Apply user preferences to embedding
633    pub fn apply_user_preferences(
634        &self,
635        base_embedding: &Vector,
636        user_profile: &UserProfile,
637        conversation_history: &[String],
638    ) -> Result<Vector> {
639        let mut personalized = base_embedding.clone();
640
641        // Apply domain preferences
642        for (domain, weight) in &user_profile.domain_preferences {
643            if conversation_history.iter().any(|msg| msg.contains(domain)) {
644                // Boost embedding components related to preferred domains
645                for i in 0..personalized.values.len() {
646                    personalized.values[i] *= 1.0 + (weight * self.preference_weights.domain_boost);
647                }
648            }
649        }
650
651        // Apply recent interaction patterns
652        let recent_interactions = self.get_recent_interactions(&user_profile.user_id, 10);
653        if !recent_interactions.is_empty() {
654            let avg_sentiment = recent_interactions
655                .iter()
656                .map(|i| i.sentiment_score.unwrap_or(0.0))
657                .sum::<f32>()
658                / recent_interactions.len() as f32;
659
660            // Adjust embedding based on user's typical sentiment
661            for i in 0..personalized.values.len() {
662                personalized.values[i] *=
663                    1.0 + (avg_sentiment * self.preference_weights.sentiment_influence);
664            }
665        }
666
667        Ok(personalized)
668    }
669
670    /// Update user profile based on interaction
671    pub fn update_user_profile(
672        &mut self,
673        user_id: &str,
674        query: &str,
675        response_feedback: Option<f32>,
676        interaction_type: InteractionType,
677    ) -> Result<()> {
678        let interaction = UserInteraction {
679            timestamp: chrono::Utc::now(),
680            query: query.to_string(),
681            interaction_type,
682            response_feedback,
683            sentiment_score: self.analyze_query_sentiment(query),
684        };
685
686        // Add to interaction history
687        self.interaction_history
688            .entry(user_id.to_string())
689            .or_default()
690            .push(interaction.clone());
691
692        // Update user profile
693        if let Some(profile) = self.user_profiles.get_mut(user_id) {
694            profile.update_from_interaction(&interaction);
695        }
696
697        Ok(())
698    }
699
700    /// Get recent interactions for a user
701    fn get_recent_interactions(&self, user_id: &str, limit: usize) -> Vec<&UserInteraction> {
702        self.interaction_history
703            .get(user_id)
704            .map(|history| history.iter().rev().take(limit).collect())
705            .unwrap_or_default()
706    }
707
708    /// Simple sentiment analysis for query
709    fn analyze_query_sentiment(&self, query: &str) -> Option<f32> {
710        let positive_words = ["good", "great", "excellent", "amazing", "wonderful"];
711        let negative_words = ["bad", "terrible", "awful", "horrible", "disappointing"];
712
713        let query_lower = query.to_lowercase();
714        let positive_count = positive_words
715            .iter()
716            .filter(|&&word| query_lower.contains(word))
717            .count();
718        let negative_count = negative_words
719            .iter()
720            .filter(|&&word| query_lower.contains(word))
721            .count();
722
723        if positive_count + negative_count == 0 {
724            return None;
725        }
726
727        let sentiment = (positive_count as f32 - negative_count as f32)
728            / (positive_count + negative_count) as f32;
729        Some(sentiment)
730    }
731}
732
733/// User profile for personalization
734#[derive(Debug, Clone)]
735pub struct UserProfile {
736    pub user_id: String,
737    pub domain_preferences: HashMap<String, f32>,
738    pub entity_preferences: HashMap<String, f32>,
739    pub interaction_patterns: InteractionPatterns,
740    pub language_preferences: Vec<String>,
741    pub created_at: chrono::DateTime<chrono::Utc>,
742    pub last_updated: chrono::DateTime<chrono::Utc>,
743}
744
745impl UserProfile {
746    pub fn new(user_id: String) -> Self {
747        let now = chrono::Utc::now();
748        Self {
749            user_id,
750            domain_preferences: HashMap::new(),
751            entity_preferences: HashMap::new(),
752            interaction_patterns: InteractionPatterns::default(),
753            language_preferences: vec!["en".to_string()],
754            created_at: now,
755            last_updated: now,
756        }
757    }
758
759    /// Update profile based on user interaction
760    pub fn update_from_interaction(&mut self, interaction: &UserInteraction) {
761        self.last_updated = chrono::Utc::now();
762
763        // Update interaction patterns
764        self.interaction_patterns.total_interactions += 1;
765        match interaction.interaction_type {
766            InteractionType::Query => self.interaction_patterns.query_count += 1,
767            InteractionType::Feedback => self.interaction_patterns.feedback_count += 1,
768            InteractionType::EntityLookup => self.interaction_patterns.entity_lookup_count += 1,
769        }
770
771        // Update average sentiment
772        if let Some(sentiment) = interaction.sentiment_score {
773            let current_avg = self.interaction_patterns.average_sentiment;
774            let total = self.interaction_patterns.total_interactions as f32;
775            self.interaction_patterns.average_sentiment =
776                (current_avg * (total - 1.0) + sentiment) / total;
777        }
778
779        // Extract and update domain preferences from query
780        self.extract_domain_preferences(&interaction.query);
781    }
782
783    /// Extract domain preferences from query text
784    fn extract_domain_preferences(&mut self, query: &str) {
785        let domains = [
786            "science",
787            "technology",
788            "medicine",
789            "business",
790            "education",
791            "sports",
792            "entertainment",
793            "politics",
794            "history",
795            "art",
796        ];
797
798        for domain in &domains {
799            if query.to_lowercase().contains(domain) {
800                #[allow(clippy::unnecessary_to_owned)]
801                let current = self.domain_preferences.get(*domain).copied().unwrap_or(0.0);
802                self.domain_preferences
803                    .insert(domain.to_string(), current + 0.1);
804            }
805        }
806    }
807}
808
809/// User interaction patterns
810#[derive(Debug, Clone, Default)]
811pub struct InteractionPatterns {
812    pub total_interactions: u32,
813    pub query_count: u32,
814    pub feedback_count: u32,
815    pub entity_lookup_count: u32,
816    pub average_sentiment: f32,
817    pub preferred_response_length: Option<usize>,
818}
819
820/// Types of user interactions
821#[derive(Debug, Clone)]
822pub enum InteractionType {
823    Query,
824    Feedback,
825    EntityLookup,
826}
827
828/// User interaction record
829#[derive(Debug, Clone)]
830pub struct UserInteraction {
831    pub timestamp: chrono::DateTime<chrono::Utc>,
832    pub query: String,
833    pub interaction_type: InteractionType,
834    pub response_feedback: Option<f32>,
835    pub sentiment_score: Option<f32>,
836}
837
838/// Weights for preference application
839#[derive(Debug, Clone)]
840pub struct PreferenceWeights {
841    pub domain_boost: f32,
842    pub entity_boost: f32,
843    pub sentiment_influence: f32,
844    pub recency_decay: f32,
845}
846
847impl Default for PreferenceWeights {
848    fn default() -> Self {
849        Self {
850            domain_boost: 0.1,
851            entity_boost: 0.15,
852            sentiment_influence: 0.05,
853            recency_decay: 0.95,
854        }
855    }
856}
857
858/// Multilingual support for chat integration
859pub struct MultilingualSupport {
860    supported_languages: Vec<String>,
861    translation_cache: HashMap<String, String>,
862    language_models: HashMap<String, LanguageModel>,
863}
864
865impl Default for MultilingualSupport {
866    fn default() -> Self {
867        Self::new()
868    }
869}
870
871impl MultilingualSupport {
872    pub fn new() -> Self {
873        Self {
874            supported_languages: vec![
875                "en".to_string(),
876                "es".to_string(),
877                "fr".to_string(),
878                "de".to_string(),
879                "it".to_string(),
880                "pt".to_string(),
881                "zh".to_string(),
882                "ja".to_string(),
883                "ko".to_string(),
884                "ar".to_string(),
885                "hi".to_string(),
886                "ru".to_string(),
887            ],
888            translation_cache: HashMap::new(),
889            language_models: HashMap::new(),
890        }
891    }
892
893    /// Translate text between languages
894    pub async fn translate_text(
895        &self,
896        text: &str,
897        source_lang: &str,
898        target_lang: &str,
899    ) -> Result<String> {
900        if source_lang == target_lang {
901            return Ok(text.to_string());
902        }
903
904        let cache_key = format!("{source_lang}:{target_lang}:{text}");
905        if let Some(cached) = self.translation_cache.get(&cache_key) {
906            return Ok(cached.clone());
907        }
908
909        // Mock translation implementation
910        // In practice, this would call a translation service
911        let translated = match target_lang {
912            "es" => format!("[ES] {text}"),
913            "fr" => format!("[FR] {text}"),
914            "de" => format!("[DE] {text}"),
915            "zh" => format!("[ZH] {text}"),
916            _ => format!("[{}] {}", target_lang.to_uppercase(), text),
917        };
918
919        Ok(translated)
920    }
921
922    /// Detect language of input text
923    pub async fn detect_language(&self, text: &str) -> Result<LanguageDetection> {
924        // Simple language detection based on common words
925        let text_lower = text.to_lowercase();
926
927        let mut scores = HashMap::new();
928
929        // English indicators
930        let en_words = ["the", "and", "is", "hello", "world", "of", "to", "in"];
931        let en_score = en_words
932            .iter()
933            .filter(|&&word| text_lower.contains(word))
934            .count();
935        scores.insert("en", en_score);
936
937        // Spanish indicators
938        let es_words = ["el", "y", "es", "hola", "buenos", "dias", "de", "en", "la"];
939        let es_score = es_words
940            .iter()
941            .filter(|&&word| text_lower.contains(word))
942            .count();
943        scores.insert("es", es_score);
944
945        // French indicators
946        let fr_words = ["le", "et", "est", "bonjour", "de", "la", "les"];
947        let fr_score = fr_words
948            .iter()
949            .filter(|&&word| text_lower.contains(word))
950            .count();
951        scores.insert("fr", fr_score);
952
953        // German indicators
954        let de_words = ["der", "und", "ist", "hallo", "von", "die", "das"];
955        let de_score = de_words
956            .iter()
957            .filter(|&&word| text_lower.contains(word))
958            .count();
959        scores.insert("de", de_score);
960
961        // Find language with highest score
962        let detected_lang = scores
963            .iter()
964            .max_by_key(|&(_, &score)| score)
965            .map(|(lang, _)| *lang)
966            .unwrap_or("en");
967
968        Ok(LanguageDetection {
969            language_code: detected_lang.to_string(),
970            confidence: 0.85,
971            alternatives: vec![
972                ("en".to_string(), 0.7),
973                ("es".to_string(), 0.2),
974                ("fr".to_string(), 0.1),
975            ],
976        })
977    }
978
979    /// Generate cross-lingual embeddings
980    pub async fn generate_cross_lingual_embedding(
981        &self,
982        text: &str,
983        source_lang: &str,
984        target_lang: &str,
985        model: &dyn EmbeddingModel,
986    ) -> Result<Vector> {
987        // For cross-lingual embeddings, we would typically:
988        // 1. Use a multilingual embedding model
989        // 2. Or translate text and generate embedding
990        // 3. Or use language-specific models with alignment
991
992        let translated_text = self.translate_text(text, source_lang, target_lang).await?;
993        let embeddings = model.encode(&[translated_text]).await?;
994        Ok(Vector::new(embeddings[0].clone()))
995    }
996
997    /// Align entities across languages
998    pub async fn align_entities(
999        &self,
1000        entity: &str,
1001        source_lang: &str,
1002        target_langs: &[String],
1003    ) -> Result<HashMap<String, String>> {
1004        let mut alignments = HashMap::new();
1005
1006        for target_lang in target_langs {
1007            if target_lang == source_lang {
1008                alignments.insert(target_lang.clone(), entity.to_string());
1009                continue;
1010            }
1011
1012            // Mock entity alignment - in practice would use knowledge bases
1013            let aligned_entity = match target_lang.as_str() {
1014                "es" => format!("{entity}_es"),
1015                "fr" => format!("{entity}_fr"),
1016                "de" => format!("{entity}_de"),
1017                "zh" => format!("{entity}_zh"),
1018                _ => format!("{entity}_{target_lang}"),
1019            };
1020
1021            alignments.insert(target_lang.clone(), aligned_entity);
1022        }
1023
1024        Ok(alignments)
1025    }
1026}
1027
1028/// Language detection result
1029#[derive(Debug, Clone)]
1030pub struct LanguageDetection {
1031    pub language_code: String,
1032    pub confidence: f32,
1033    pub alternatives: Vec<(String, f32)>,
1034}
1035
1036/// Language model information
1037#[derive(Debug, Clone)]
1038pub struct LanguageModel {
1039    pub model_id: String,
1040    pub language_code: String,
1041    pub model_type: String,
1042    pub embedding_dimension: usize,
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048    use crate::models::TransE;
1049    use crate::ModelConfig;
1050
1051    #[test]
1052    fn test_vector_store_bridge() {
1053        let config = ModelConfig::default().with_dimensions(10);
1054        let _model = TransE::new(config);
1055
1056        let bridge = VectorStoreBridge::new();
1057
1058        // Test URI generation
1059        let entity_uri = bridge.generate_entity_uri("test_entity");
1060        assert!(entity_uri.starts_with("kg:entity:"));
1061
1062        let relation_uri = bridge.generate_relation_uri("test_relation");
1063        assert!(relation_uri.starts_with("kg:relation:"));
1064    }
1065
1066    /// Regression test: `find_similar_entities`/`find_similar_relations` must
1067    /// return real nearest-neighbor results from the synced embeddings
1068    /// instead of always an empty `Vec`.
1069    #[tokio::test]
1070    async fn test_vector_store_bridge_find_similar_returns_real_results() -> Result<()> {
1071        let config = ModelConfig::default().with_dimensions(8);
1072        let mut model = TransE::new(config);
1073
1074        for (s, p, o) in [
1075            ("alice", "knows", "bob"),
1076            ("bob", "knows", "carol"),
1077            ("carol", "knows", "alice"),
1078        ] {
1079            model.add_triple(crate::Triple::new(
1080                crate::NamedNode::new(s)?,
1081                crate::NamedNode::new(p)?,
1082                crate::NamedNode::new(o)?,
1083            ))?;
1084        }
1085        model.train(Some(1)).await?;
1086
1087        let mut bridge = VectorStoreBridge::new();
1088        let stats = bridge.sync_model_embeddings(&model)?;
1089        assert_eq!(stats.entities_synced, 3);
1090        assert_eq!(stats.relations_synced, 1);
1091        assert!(stats.errors.is_empty(), "errors = {:?}", stats.errors);
1092
1093        // Every other entity should be a candidate neighbor for "alice".
1094        let similar = bridge.find_similar_entities("alice", 2)?;
1095        assert!(!similar.is_empty(), "expected at least one similar entity");
1096        assert!(
1097            similar.iter().all(|(name, _)| name != "alice"),
1098            "the query entity itself must not appear in its own results: {:?}",
1099            similar
1100        );
1101
1102        // Unknown entity must error rather than silently return empty.
1103        assert!(bridge.find_similar_entities("nobody", 2).is_err());
1104
1105        Ok(())
1106    }
1107
1108    /// A minimal `EmbeddingModel` whose `encode` deterministically maps text
1109    /// length to vector values, used to exercise `ChatIntegration` methods
1110    /// that need real (if simple) text encoding — unlike the KGE models in
1111    /// `crate::models`, which all reject `encode` outright.
1112    struct MockTextModel {
1113        config: ModelConfig,
1114        model_id: uuid::Uuid,
1115        entities: Vec<String>,
1116    }
1117
1118    impl MockTextModel {
1119        fn new(dimensions: usize, entities: Vec<String>) -> Self {
1120            Self {
1121                config: ModelConfig::default().with_dimensions(dimensions),
1122                model_id: uuid::Uuid::new_v4(),
1123                entities,
1124            }
1125        }
1126    }
1127
1128    #[async_trait::async_trait]
1129    impl EmbeddingModel for MockTextModel {
1130        fn config(&self) -> &ModelConfig {
1131            &self.config
1132        }
1133        fn model_id(&self) -> &uuid::Uuid {
1134            &self.model_id
1135        }
1136        fn model_type(&self) -> &'static str {
1137            "MockText"
1138        }
1139        fn add_triple(&mut self, _triple: crate::Triple) -> Result<()> {
1140            Ok(())
1141        }
1142        async fn train(&mut self, _epochs: Option<usize>) -> Result<crate::TrainingStats> {
1143            Ok(crate::TrainingStats {
1144                epochs_completed: 1,
1145                final_loss: 0.0,
1146                training_time_seconds: 0.0,
1147                convergence_achieved: true,
1148                loss_history: vec![0.0],
1149            })
1150        }
1151        fn get_entity_embedding(&self, entity: &str) -> Result<Vector> {
1152            Ok(Vector::new(vec![
1153                entity.len() as f32;
1154                self.config.dimensions
1155            ]))
1156        }
1157        fn get_relation_embedding(&self, relation: &str) -> Result<Vector> {
1158            Ok(Vector::new(vec![
1159                relation.len() as f32;
1160                self.config.dimensions
1161            ]))
1162        }
1163        fn score_triple(&self, _subject: &str, _predicate: &str, _object: &str) -> Result<f64> {
1164            Ok(0.0)
1165        }
1166        fn predict_objects(
1167            &self,
1168            _subject: &str,
1169            _predicate: &str,
1170            _k: usize,
1171        ) -> Result<Vec<(String, f64)>> {
1172            Ok(vec![])
1173        }
1174        fn predict_subjects(
1175            &self,
1176            _predicate: &str,
1177            _object: &str,
1178            _k: usize,
1179        ) -> Result<Vec<(String, f64)>> {
1180            Ok(vec![])
1181        }
1182        fn predict_relations(
1183            &self,
1184            _subject: &str,
1185            _object: &str,
1186            _k: usize,
1187        ) -> Result<Vec<(String, f64)>> {
1188            Ok(vec![])
1189        }
1190        fn get_entities(&self) -> Vec<String> {
1191            self.entities.clone()
1192        }
1193        fn get_relations(&self) -> Vec<String> {
1194            vec![]
1195        }
1196        fn get_stats(&self) -> crate::ModelStats {
1197            crate::ModelStats {
1198                num_entities: self.entities.len(),
1199                dimensions: self.config.dimensions,
1200                is_trained: true,
1201                ..Default::default()
1202            }
1203        }
1204        fn save(&self, _path: &str) -> Result<()> {
1205            Ok(())
1206        }
1207        fn load(&mut self, _path: &str) -> Result<()> {
1208            Ok(())
1209        }
1210        fn clear(&mut self) {}
1211        fn is_trained(&self) -> bool {
1212            true
1213        }
1214        async fn encode(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
1215            Ok(texts
1216                .iter()
1217                .map(|t| vec![t.len() as f32; self.config.dimensions])
1218                .collect())
1219        }
1220    }
1221
1222    /// Regression test: `extract_relevant_entities` must find entities that
1223    /// appear as a substring of the query using the windowed-index rewrite.
1224    #[test]
1225    fn test_extract_relevant_entities_finds_substring_matches() -> Result<()> {
1226        let model = MockTextModel::new(
1227            4,
1228            vec!["alice".to_string(), "bob".to_string(), "carol".to_string()],
1229        );
1230        let integration = ChatIntegration::new(Box::new(model));
1231
1232        let relevant = integration.extract_relevant_entities("Alice met Bob yesterday.")?;
1233        assert!(relevant.contains(&"alice".to_string()));
1234        assert!(relevant.contains(&"bob".to_string()));
1235        assert!(!relevant.contains(&"carol".to_string()));
1236
1237        Ok(())
1238    }
1239
1240    /// Regression test: `generate_context_embedding` must actually encode and
1241    /// combine the recent messages via the model, instead of returning a
1242    /// fixed all-zero placeholder vector regardless of input.
1243    #[tokio::test]
1244    async fn test_generate_context_embedding_reflects_message_content() -> Result<()> {
1245        let model = MockTextModel::new(4, vec![]);
1246        let integration = ChatIntegration::new(Box::new(model));
1247
1248        let short = integration
1249            .generate_context_embedding(&["hi".to_string()])
1250            .await?;
1251        let long = integration
1252            .generate_context_embedding(&["a much longer message here".to_string()])
1253            .await?;
1254
1255        assert_eq!(short.values.len(), 4);
1256        assert_ne!(
1257            short.values, long.values,
1258            "different message content should produce different context embeddings"
1259        );
1260
1261        assert!(integration.generate_context_embedding(&[]).await.is_err());
1262
1263        Ok(())
1264    }
1265
1266    #[test]
1267    fn test_sparql_integration() -> Result<()> {
1268        let config = ModelConfig::default().with_dimensions(10);
1269        let model = TransE::new(config);
1270
1271        let integration = SparqlIntegration::new(Box::new(model));
1272
1273        let test_query = "SELECT ?s ?o WHERE { ?s <http://example.org/knows> ?o }";
1274        let enhanced = integration.enhance_query(test_query)?;
1275
1276        assert_eq!(enhanced.original_query, test_query);
1277        assert!(!enhanced.suggestions.is_empty());
1278
1279        Ok(())
1280    }
1281
1282    #[test]
1283    fn test_personalization_engine() {
1284        let mut engine = PersonalizationEngine::new();
1285        let user_id = "test_user";
1286
1287        // Test user profile creation
1288        let profile = engine.get_user_profile(user_id).expect("should succeed");
1289        assert_eq!(profile.user_id, user_id);
1290
1291        // Test interaction update
1292        engine
1293            .update_user_profile(
1294                user_id,
1295                "What is machine learning?",
1296                Some(0.9),
1297                InteractionType::Query,
1298            )
1299            .expect("should succeed");
1300
1301        let history = engine.get_recent_interactions(user_id, 5);
1302        assert_eq!(history.len(), 1);
1303    }
1304
1305    #[tokio::test]
1306    async fn test_multilingual_support() -> Result<()> {
1307        let multilingual = MultilingualSupport::new();
1308
1309        // Test language detection with English text
1310        let detection_en = multilingual.detect_language("Hello world").await?;
1311        assert_eq!(detection_en.language_code, "en");
1312
1313        // Test language detection with Spanish text
1314        let detection_es = multilingual.detect_language("Hola y buenos dias").await?;
1315        assert_eq!(detection_es.language_code, "es");
1316
1317        // Test translation
1318        let translated = multilingual
1319            .translate_text("Hello world", "en", "es")
1320            .await?;
1321        assert!(translated.contains("[ES]"));
1322
1323        // Test entity alignment
1324        let alignments = multilingual
1325            .align_entities("person", "en", &["es".to_string(), "fr".to_string()])
1326            .await?;
1327        assert_eq!(alignments.len(), 2);
1328
1329        Ok(())
1330    }
1331}