Skip to main content

fastskill_core/core/
loading.rs

1//! Progressive loading service implementation
2
3use crate::core::metadata::SkillMetadata;
4use crate::core::service::ServiceError;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12use tracing::warn;
13
14/// Progressive loading service trait
15#[async_trait]
16pub trait ProgressiveLoadingService: Send + Sync {
17    /// Load metadata for multiple skills (in-memory)
18    async fn load_metadata(&self, skill_ids: &[String])
19        -> Result<Vec<SkillMetadata>, ServiceError>;
20
21    /// Load complete skill content based on context
22    async fn load_skill_content(
23        &self,
24        skill_ids: &[String],
25        context: Option<LoadingContext>,
26    ) -> Result<Vec<LoadedSkill>, ServiceError>;
27
28    /// Load reference content for a specific file
29    async fn load_reference_content(
30        &self,
31        skill_id: &str,
32        reference_path: &str,
33    ) -> Result<String, ServiceError>;
34
35    /// Preload relevant skills based on context
36    async fn preload_relevant_skills(
37        &self,
38        context: LoadingContext,
39    ) -> Result<Vec<PreloadedSkill>, ServiceError>;
40
41    /// Optimize loading strategy based on context
42    async fn optimize_loading_strategy(
43        &self,
44        context: LoadingContext,
45    ) -> Result<LoadingStrategy, ServiceError>;
46
47    /// Clear all caches
48    async fn clear_cache(&self) -> Result<(), ServiceError>;
49
50    /// Get cache statistics
51    async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError>;
52}
53
54/// Loading context for optimization decisions
55#[derive(Debug, Clone)]
56pub struct LoadingContext {
57    /// The user's query
58    pub query: String,
59
60    /// Available tokens for loading content
61    pub available_tokens: usize,
62
63    /// Urgency level for loading
64    pub urgency: LoadingUrgency,
65
66    /// Previous conversation messages (for context)
67    pub conversation_history: Vec<String>,
68
69    /// User preferences (optional)
70    pub user_preferences: Option<HashMap<String, String>>,
71}
72
73/// Urgency levels for loading decisions
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum LoadingUrgency {
76    /// Low priority - can defer loading
77    Low,
78    /// Normal priority - standard loading
79    Medium,
80    /// High priority - load immediately
81    High,
82    /// Critical - load everything immediately
83    Critical,
84}
85
86/// Loading strategy recommendation
87#[derive(Debug, Clone)]
88pub struct LoadingStrategy {
89    /// Priority order for skill loading
90    pub priority: Vec<String>,
91
92    /// Recommended load level for each skill
93    pub load_levels: HashMap<String, LoadLevel>,
94
95    /// Estimated total tokens required
96    pub estimated_tokens: usize,
97
98    /// Reasoning for the strategy
99    pub reasoning: Vec<String>,
100}
101
102/// Content loading levels
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum LoadLevel {
105    /// Load only metadata
106    Metadata,
107    /// Load SKILL.md content
108    Content,
109    /// Load reference files as well
110    References,
111}
112
113/// Complete loaded skill with all content
114#[derive(Debug, Clone)]
115pub struct LoadedSkill {
116    /// Skill metadata (always loaded)
117    pub metadata: SkillMetadata,
118
119    /// SKILL.md content (loaded on demand)
120    pub content: Option<String>,
121
122    /// Reference file contents (loaded on demand)
123    pub references: Option<HashMap<String, String>>,
124
125    /// Script file contents (loaded from disk when needed)
126    pub script_contents: Option<HashMap<String, String>>,
127
128    /// Asset file paths (never loaded into memory)
129    pub asset_paths: Vec<PathBuf>,
130
131    /// Whether the skill is ready for execution
132    pub execution_ready: bool,
133
134    /// When this skill was loaded
135    pub loaded_at: Instant,
136
137    /// Load level achieved
138    pub load_level: LoadLevel,
139}
140
141/// Preloaded skill with metadata only
142#[derive(Debug, Clone)]
143pub struct PreloadedSkill {
144    pub metadata: SkillMetadata,
145    pub relevance_score: f64,
146    pub recommended_load_level: LoadLevel,
147}
148
149/// Cache statistics for monitoring
150#[derive(Debug, Clone, Default)]
151pub struct CacheStats {
152    /// Number of metadata entries in cache
153    pub metadata_cache_size: usize,
154
155    /// Number of content entries in cache
156    pub content_cache_size: usize,
157
158    /// Cache hit rate for metadata
159    pub metadata_hit_rate: f64,
160
161    /// Cache hit rate for content
162    pub content_hit_rate: f64,
163
164    /// Total memory usage (MB)
165    pub memory_usage_mb: f64,
166
167    /// Average load time for metadata
168    pub avg_metadata_load_time_ms: f64,
169
170    /// Average load time for content
171    pub avg_content_load_time_ms: f64,
172}
173
174/// Progressive loading service implementation
175pub struct ProgressiveLoadingServiceImpl {
176    /// Metadata cache (in-memory)
177    metadata_cache: Arc<RwLock<HashMap<String, (SkillMetadata, Instant)>>>,
178
179    /// Content cache (in-memory, smaller)
180    content_cache: Arc<RwLock<HashMap<String, (String, Instant)>>>,
181
182    /// Base directory for skill storage
183    skills_base_path: PathBuf,
184
185    /// Cache TTL for metadata (default: 1 hour)
186    metadata_cache_ttl: Duration,
187
188    /// Cache TTL for content (default: 30 minutes)
189    content_cache_ttl: Duration,
190
191    /// Maximum cache sizes
192    max_metadata_cache_size: usize,
193    max_content_cache_size: usize,
194
195    /// Cache hit counters
196    metadata_cache_hits: Arc<RwLock<usize>>,
197    metadata_cache_misses: Arc<RwLock<usize>>,
198    content_cache_hits: Arc<RwLock<usize>>,
199    content_cache_misses: Arc<RwLock<usize>>,
200}
201
202impl ProgressiveLoadingServiceImpl {
203    /// Create a new progressive loading service
204    pub fn new(skills_base_path: PathBuf) -> Self {
205        Self {
206            metadata_cache: Arc::new(RwLock::new(HashMap::new())),
207            content_cache: Arc::new(RwLock::new(HashMap::new())),
208            skills_base_path,
209            metadata_cache_ttl: Duration::from_secs(3600), // 1 hour
210            content_cache_ttl: Duration::from_secs(1800),  // 30 minutes
211            max_metadata_cache_size: 1000,
212            max_content_cache_size: 100,
213            metadata_cache_hits: Arc::new(RwLock::new(0)),
214            metadata_cache_misses: Arc::new(RwLock::new(0)),
215            content_cache_hits: Arc::new(RwLock::new(0)),
216            content_cache_misses: Arc::new(RwLock::new(0)),
217        }
218    }
219
220    /// Load metadata for skills (always in memory)
221    async fn load_skill_metadata(&self, skill_id: &str) -> Result<SkillMetadata, ServiceError> {
222        // Check cache first
223        {
224            let cache = self.metadata_cache.read().await;
225            if let Some((metadata, loaded_at)) = cache.get(skill_id) {
226                if loaded_at.elapsed() < self.metadata_cache_ttl {
227                    let mut hits = self.metadata_cache_hits.write().await;
228                    *hits += 1;
229                    return Ok(metadata.clone());
230                }
231            }
232        }
233
234        // Cache miss - load from disk
235        let mut misses = self.metadata_cache_misses.write().await;
236        *misses += 1;
237
238        // Load skill definition from filesystem
239        let skill_path = self.skills_base_path.join(skill_id).join("SKILL.md");
240
241        if !skill_path.exists() {
242            return Err(ServiceError::Custom(format!(
243                "Skill file not found: {}",
244                skill_path.display()
245            )));
246        }
247
248        let content = tokio::fs::read_to_string(&skill_path).await?;
249
250        // Parse YAML frontmatter and create metadata
251        let metadata = self.parse_skill_metadata(skill_id, &content)?;
252
253        // Cache the metadata
254        {
255            let mut cache = self.metadata_cache.write().await;
256
257            // Clean up expired entries if cache is full
258            if cache.len() >= self.max_metadata_cache_size {
259                self.cleanup_expired_metadata(&mut cache).await;
260            }
261
262            cache.insert(skill_id.to_string(), (metadata.clone(), Instant::now()));
263        }
264
265        Ok(metadata)
266    }
267
268    /// Parse skill content to extract metadata
269    fn parse_skill_metadata(
270        &self,
271        skill_id: &str,
272        content: &str,
273    ) -> Result<SkillMetadata, ServiceError> {
274        // Simple parsing for now - extract YAML frontmatter
275        let lines = content.lines();
276
277        // Look for YAML frontmatter (---)
278        let mut in_frontmatter = false;
279        let mut frontmatter_lines = Vec::new();
280
281        for line in lines {
282            if line.trim() == "---" {
283                if in_frontmatter {
284                    break; // End of frontmatter
285                } else {
286                    in_frontmatter = true;
287                    continue;
288                }
289            }
290
291            if in_frontmatter {
292                frontmatter_lines.push(line);
293            }
294        }
295
296        // For now, create basic metadata from filename and content
297        // In a real implementation, this would parse YAML frontmatter
298        let name = skill_id.replace("-", " ").to_string();
299        let description = content
300            .lines()
301            .next()
302            .unwrap_or("No description")
303            .to_string();
304
305        Ok(SkillMetadata {
306            id: crate::core::service::SkillId::new(skill_id.to_string())?,
307            name,
308            description,
309            version: "1.0.0".to_string(),
310            author: None,
311            enabled: true,
312            token_estimate: content.len() / 4, // Rough estimate
313            last_updated: std::time::SystemTime::now().into(),
314        })
315    }
316
317    /// Load complete skill content (on demand) - internal method for single skill
318    async fn load_skill_content_internal(&self, skill_id: &str) -> Result<String, ServiceError> {
319        // Check content cache first
320        {
321            let cache = self.content_cache.read().await;
322            if let Some((content, loaded_at)) = cache.get(skill_id) {
323                if loaded_at.elapsed() < self.content_cache_ttl {
324                    let mut hits = self.content_cache_hits.write().await;
325                    *hits += 1;
326                    return Ok(content.clone());
327                }
328            }
329        }
330
331        // Cache miss - load from disk
332        let mut misses = self.content_cache_misses.write().await;
333        *misses += 1;
334
335        let skill_path = self.skills_base_path.join(skill_id).join("SKILL.md");
336
337        if !skill_path.exists() {
338            return Err(ServiceError::Custom(format!(
339                "Skill file not found: {}",
340                skill_path.display()
341            )));
342        }
343
344        let content = tokio::fs::read_to_string(&skill_path).await?;
345
346        // Cache the content
347        {
348            let mut cache = self.content_cache.write().await;
349
350            // Clean up expired entries if cache is full
351            if cache.len() >= self.max_content_cache_size {
352                self.cleanup_expired_content(&mut cache).await;
353            }
354
355            cache.insert(skill_id.to_string(), (content.clone(), Instant::now()));
356        }
357
358        Ok(content)
359    }
360
361    /// Clean up expired metadata cache entries
362    async fn cleanup_expired_metadata(
363        &self,
364        cache: &mut HashMap<String, (SkillMetadata, Instant)>,
365    ) {
366        let now = Instant::now();
367        cache.retain(|_, (_, loaded_at)| now.duration_since(*loaded_at) < self.metadata_cache_ttl);
368
369        // If still too full, remove oldest entries
370        if cache.len() >= self.max_metadata_cache_size {
371            let entries: Vec<_> = cache.iter().map(|(k, (_, _))| k.clone()).collect();
372            let to_remove = cache.len() - self.max_metadata_cache_size + 10; // Remove extra to avoid frequent cleanup
373
374            for key in entries.iter().take(to_remove) {
375                cache.remove(key);
376            }
377        }
378    }
379
380    /// Clean up expired content cache entries
381    async fn cleanup_expired_content(&self, cache: &mut HashMap<String, (String, Instant)>) {
382        let now = Instant::now();
383        cache.retain(|_, (_, loaded_at)| now.duration_since(*loaded_at) < self.content_cache_ttl);
384
385        // If still too full, remove oldest entries
386        if cache.len() >= self.max_content_cache_size {
387            let entries: Vec<_> = cache.iter().map(|(k, (_, _))| k.clone()).collect();
388            let to_remove = cache.len() - self.max_content_cache_size + 5; // Remove extra to avoid frequent cleanup
389
390            for key in entries.iter().take(to_remove) {
391                cache.remove(key);
392            }
393        }
394    }
395
396    /// Get cache statistics
397    pub async fn get_cache_stats(&self) -> CacheStats {
398        let metadata_cache_size = self.metadata_cache.read().await.len();
399        let content_cache_size = self.content_cache.read().await.len();
400
401        let metadata_hits = *self.metadata_cache_hits.read().await;
402        let metadata_misses = *self.metadata_cache_misses.read().await;
403        let content_hits = *self.content_cache_hits.read().await;
404        let content_misses = *self.content_cache_misses.read().await;
405
406        let metadata_total = metadata_hits + metadata_misses;
407        let content_total = content_hits + content_misses;
408
409        let metadata_hit_rate = if metadata_total > 0 {
410            metadata_hits as f64 / metadata_total as f64
411        } else {
412            0.0
413        };
414
415        let content_hit_rate = if content_total > 0 {
416            content_hits as f64 / content_total as f64
417        } else {
418            0.0
419        };
420
421        // Rough memory estimation (simplified)
422        let memory_usage_mb =
423            (metadata_cache_size * 1024 + content_cache_size * 2048) as f64 / (1024.0 * 1024.0);
424
425        CacheStats {
426            metadata_cache_size,
427            content_cache_size,
428            metadata_hit_rate,
429            content_hit_rate,
430            memory_usage_mb,
431            avg_metadata_load_time_ms: 50.0, // Placeholder - would track actual times
432            avg_content_load_time_ms: 100.0, // Placeholder - would track actual times
433        }
434    }
435
436    /// Clear all caches
437    pub async fn clear_cache(&self) {
438        self.metadata_cache.write().await.clear();
439        self.content_cache.write().await.clear();
440
441        // Reset counters
442        *self.metadata_cache_hits.write().await = 0;
443        *self.metadata_cache_misses.write().await = 0;
444        *self.content_cache_hits.write().await = 0;
445        *self.content_cache_misses.write().await = 0;
446    }
447}
448
449#[async_trait]
450impl ProgressiveLoadingService for ProgressiveLoadingServiceImpl {
451    /// Load metadata for multiple skills (in-memory)
452    async fn load_metadata(
453        &self,
454        skill_ids: &[String],
455    ) -> Result<Vec<SkillMetadata>, ServiceError> {
456        let mut results = Vec::new();
457
458        for skill_id in skill_ids {
459            match self.load_skill_metadata(skill_id).await {
460                Ok(metadata) => results.push(metadata),
461                Err(e) => {
462                    warn!("Failed to load metadata for skill {}: {}", skill_id, e);
463                    // Continue with other skills instead of failing completely
464                }
465            }
466        }
467
468        Ok(results)
469    }
470
471    /// Load complete skill content based on context
472    async fn load_skill_content(
473        &self,
474        skill_ids: &[String],
475        context: Option<LoadingContext>,
476    ) -> Result<Vec<LoadedSkill>, ServiceError> {
477        let mut results = Vec::new();
478
479        for skill_id in skill_ids {
480            // Always load metadata first
481            let metadata = self.load_skill_metadata(skill_id).await?;
482
483            // Determine load level based on context
484            let load_level = self.determine_load_level(&metadata, context.as_ref());
485
486            // Load content based on determined level
487            let (content, references, execution_ready) = match load_level {
488                LoadLevel::Metadata => (None, None, false),
489                LoadLevel::Content => {
490                    // Load SKILL.md content
491                    let skill_content = self.load_skill_content_internal(skill_id).await?;
492                    (Some(skill_content), None, true)
493                }
494                LoadLevel::References => {
495                    // Load SKILL.md content and reference files
496                    let skill_content = self.load_skill_content_internal(skill_id).await?;
497                    let skill_references = self.load_reference_files(skill_id).await?;
498
499                    (Some(skill_content), Some(skill_references), true)
500                }
501            };
502
503            results.push(LoadedSkill {
504                metadata,
505                content,
506                references,
507                script_contents: None, // Scripts loaded from disk when needed
508                asset_paths: self.get_asset_paths(skill_id),
509                execution_ready,
510                loaded_at: Instant::now(),
511                load_level,
512            });
513        }
514
515        Ok(results)
516    }
517
518    /// Load reference content for a specific file
519    async fn load_reference_content(
520        &self,
521        skill_id: &str,
522        reference_path: &str,
523    ) -> Result<String, ServiceError> {
524        let ref_path = self
525            .skills_base_path
526            .join(skill_id)
527            .join("references")
528            .join(reference_path);
529
530        if !ref_path.exists() {
531            return Err(ServiceError::Custom(format!(
532                "Reference file not found: {}",
533                ref_path.display()
534            )));
535        }
536
537        tokio::fs::read_to_string(&ref_path)
538            .await
539            .map_err(|e| ServiceError::Custom(format!("Failed to read reference file: {}", e)))
540    }
541
542    /// Preload relevant skills based on context
543    async fn preload_relevant_skills(
544        &self,
545        _context: LoadingContext,
546    ) -> Result<Vec<PreloadedSkill>, ServiceError> {
547        // This would integrate with the routing service to find relevant skills
548        // For now, return empty list as routing service isn't implemented yet
549        Ok(Vec::new())
550    }
551
552    /// Optimize loading strategy based on context
553    async fn optimize_loading_strategy(
554        &self,
555        context: LoadingContext,
556    ) -> Result<LoadingStrategy, ServiceError> {
557        // Simple strategy for now - prioritize by query relevance
558        // In a real implementation, this would use the routing service
559
560        let mut strategy = LoadingStrategy {
561            priority: Vec::new(),
562            load_levels: HashMap::new(),
563            estimated_tokens: 0,
564            reasoning: vec!["Basic strategy based on query analysis".to_string()],
565        };
566
567        // For now, just set default strategy
568        strategy
569            .load_levels
570            .insert("default".to_string(), LoadLevel::Metadata);
571        strategy.estimated_tokens = context.available_tokens / 2; // Use half available tokens
572
573        Ok(strategy)
574    }
575
576    /// Clear all caches
577    async fn clear_cache(&self) -> Result<(), ServiceError> {
578        self.clear_cache().await;
579        Ok(())
580    }
581
582    /// Get cache statistics
583    async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError> {
584        Ok(self.get_cache_stats().await)
585    }
586}
587
588/// Helper methods for loading service
589impl ProgressiveLoadingServiceImpl {
590    /// Determine appropriate load level for a skill based on context
591    fn determine_load_level(
592        &self,
593        _metadata: &SkillMetadata,
594        context: Option<&LoadingContext>,
595    ) -> LoadLevel {
596        match context {
597            Some(ctx) => {
598                match ctx.urgency {
599                    LoadingUrgency::Critical => LoadLevel::References,
600                    LoadingUrgency::High => LoadLevel::Content,
601                    LoadingUrgency::Medium => {
602                        // Check if query seems to need full content
603                        if ctx.query.len() > 100 || ctx.available_tokens > 2000 {
604                            LoadLevel::Content
605                        } else {
606                            LoadLevel::Metadata
607                        }
608                    }
609                    LoadingUrgency::Low => LoadLevel::Metadata,
610                }
611            }
612            None => LoadLevel::Metadata, // Default to metadata only
613        }
614    }
615
616    /// Load reference files for a skill
617    async fn load_reference_files(
618        &self,
619        skill_id: &str,
620    ) -> Result<HashMap<String, String>, ServiceError> {
621        let references_dir = self.skills_base_path.join(skill_id).join("references");
622
623        if !references_dir.exists() {
624            return Ok(HashMap::new());
625        }
626
627        let mut references = HashMap::new();
628        let mut read_dir = tokio::fs::read_dir(&references_dir).await?;
629
630        while let Some(entry) = read_dir.next_entry().await? {
631            if entry.path().is_file() {
632                let file_name = entry.file_name().to_string_lossy().to_string();
633                let content = tokio::fs::read_to_string(entry.path()).await?;
634
635                references.insert(file_name, content);
636            }
637        }
638
639        Ok(references)
640    }
641
642    /// Get asset file paths for a skill (never load content)
643    fn get_asset_paths(&self, skill_id: &str) -> Vec<PathBuf> {
644        let assets_dir = self.skills_base_path.join(skill_id).join("assets");
645
646        if !assets_dir.exists() {
647            return Vec::new();
648        }
649
650        // Return paths without loading content
651        // In a real implementation, you'd scan the directory
652        Vec::new() // Placeholder
653    }
654}
655
656/// Legacy alias for backward compatibility
657pub struct LoadingService(ProgressiveLoadingServiceImpl);
658
659impl Default for LoadingService {
660    fn default() -> Self {
661        Self::new()
662    }
663}
664
665impl LoadingService {
666    pub fn new() -> Self {
667        Self(ProgressiveLoadingServiceImpl::new(PathBuf::from(
668            "./skills",
669        )))
670    }
671}
672
673#[async_trait]
674impl ProgressiveLoadingService for LoadingService {
675    async fn load_metadata(
676        &self,
677        skill_ids: &[String],
678    ) -> Result<Vec<SkillMetadata>, ServiceError> {
679        self.0.load_metadata(skill_ids).await
680    }
681
682    async fn load_skill_content(
683        &self,
684        skill_ids: &[String],
685        context: Option<LoadingContext>,
686    ) -> Result<Vec<LoadedSkill>, ServiceError> {
687        self.0.load_skill_content(skill_ids, context).await
688    }
689
690    async fn load_reference_content(
691        &self,
692        skill_id: &str,
693        reference_path: &str,
694    ) -> Result<String, ServiceError> {
695        self.0
696            .load_reference_content(skill_id, reference_path)
697            .await
698    }
699
700    async fn preload_relevant_skills(
701        &self,
702        context: LoadingContext,
703    ) -> Result<Vec<PreloadedSkill>, ServiceError> {
704        self.0.preload_relevant_skills(context).await
705    }
706
707    async fn optimize_loading_strategy(
708        &self,
709        context: LoadingContext,
710    ) -> Result<LoadingStrategy, ServiceError> {
711        self.0.optimize_loading_strategy(context).await
712    }
713
714    async fn clear_cache(&self) -> Result<(), ServiceError> {
715        self.0.clear_cache().await;
716        Ok(())
717    }
718
719    async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError> {
720        Ok(self.0.get_cache_stats().await)
721    }
722}