Skip to main content

fastskill_core/core/
routing.rs

1//! Routing and context management service implementation
2
3use crate::core::metadata::{MetadataService, SkillMetadata};
4use crate::core::service::ServiceError;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RoutedSkill {
11    pub skill_id: String,
12    pub relevance_score: f32,
13    // Add other fields as needed
14}
15
16#[async_trait]
17pub trait RoutingService: Send + Sync {
18    async fn find_relevant_skills(
19        &self,
20        query: &str,
21        context: Option<QueryContext>,
22    ) -> Result<Vec<RoutedSkill>, ServiceError>;
23    // Add other methods as needed
24}
25
26#[derive(Debug, Clone)]
27pub struct QueryContext {
28    /// Available tokens for loading content
29    pub available_tokens: Option<usize>,
30    /// Previous conversation context
31    pub conversation_history: Option<Vec<String>>,
32    /// User preferences
33    pub user_preferences: Option<std::collections::HashMap<String, String>>,
34}
35
36pub struct RoutingServiceImpl {
37    metadata_service: Arc<dyn MetadataService>,
38}
39
40impl RoutingServiceImpl {
41    pub fn new(metadata_service: Arc<dyn MetadataService>) -> Self {
42        Self { metadata_service }
43    }
44
45    /// Score skills based on relevance to query using metadata service
46    async fn score_skills_for_query(
47        &self,
48        skills: &[SkillMetadata],
49        query: &str,
50        context: Option<&QueryContext>,
51    ) -> Vec<(f32, SkillMetadata)> {
52        let mut scored_skills = Vec::new();
53
54        for skill in skills {
55            let score = self.calculate_relevance_score(skill, query, context);
56            scored_skills.push((score, skill.clone()));
57        }
58
59        // Sort by score (highest first)
60        // Handle potential NaN values gracefully
61        scored_skills.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
62        scored_skills
63    }
64
65    /// Calculate relevance score for a skill given a query
66    fn calculate_relevance_score(
67        &self,
68        skill: &SkillMetadata,
69        query: &str,
70        context: Option<&QueryContext>,
71    ) -> f32 {
72        let query_lower = query.to_lowercase();
73        let mut score = 0.0;
74
75        // Exact name match gets highest score
76        if skill.name.to_lowercase() == query_lower {
77            score += 1.0;
78        } else if skill.name.to_lowercase().contains(&query_lower) {
79            score += 0.8;
80        }
81
82        // Description match
83        if skill.description.to_lowercase().contains(&query_lower) {
84            score += 0.6;
85        }
86
87        // Token efficiency bonus (prefer skills with fewer tokens when context is limited)
88        if let Some(context) = context {
89            if let Some(available_tokens) = context.available_tokens {
90                if available_tokens < 2000 && skill.token_estimate < 100 {
91                    score += 0.2; // Bonus for lightweight skills when tokens are limited
92                }
93            }
94        }
95
96        score
97    }
98}
99
100#[async_trait]
101impl RoutingService for RoutingServiceImpl {
102    async fn find_relevant_skills(
103        &self,
104        query: &str,
105        context: Option<QueryContext>,
106    ) -> Result<Vec<RoutedSkill>, ServiceError> {
107        // Get all skills from metadata service
108        let all_metadata = self.metadata_service.discover_skills("").await?;
109
110        // Score and rank skills
111        let scored_skills = self
112            .score_skills_for_query(&all_metadata, query, context.as_ref())
113            .await;
114
115        // Convert to routed skills and limit results
116        let routed_skills: Vec<RoutedSkill> = scored_skills
117            .into_iter()
118            .take(20) // Limit to top 20 results
119            .map(|(score, metadata)| RoutedSkill {
120                skill_id: metadata.id.to_string(),
121                relevance_score: score,
122            })
123            .collect();
124
125        Ok(routed_skills)
126    }
127}