victauri_core/
registry.rs1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::sync::{Arc, RwLock};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct CommandInfo {
8 pub name: String,
10 pub plugin: Option<String>,
12 pub description: Option<String>,
14 pub args: Vec<CommandArg>,
16 pub return_type: Option<String>,
18 pub is_async: bool,
20 pub intent: Option<String>,
22 pub category: Option<String>,
24 pub examples: Vec<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct CommandArg {
31 pub name: String,
33 pub type_name: String,
35 pub required: bool,
37 pub schema: Option<serde_json::Value>,
39}
40
41#[derive(Debug, Clone)]
43pub struct CommandRegistry {
44 commands: Arc<RwLock<BTreeMap<String, CommandInfo>>>,
45}
46
47impl CommandRegistry {
48 pub fn new() -> Self {
58 Self {
59 commands: Arc::new(RwLock::new(BTreeMap::new())),
60 }
61 }
62
63 pub fn register(&self, info: CommandInfo) {
84 self.commands
85 .write()
86 .unwrap_or_else(|e| e.into_inner())
87 .insert(info.name.clone(), info);
88 }
89
90 pub fn get(&self, name: &str) -> Option<CommandInfo> {
92 self.commands
93 .read()
94 .unwrap_or_else(|e| e.into_inner())
95 .get(name)
96 .cloned()
97 }
98
99 pub fn list(&self) -> Vec<CommandInfo> {
101 self.commands
102 .read()
103 .unwrap_or_else(|e| e.into_inner())
104 .values()
105 .cloned()
106 .collect()
107 }
108
109 pub fn count(&self) -> usize {
111 self.commands
112 .read()
113 .unwrap_or_else(|e| e.into_inner())
114 .len()
115 }
116
117 pub fn search(&self, query: &str) -> Vec<CommandInfo> {
119 let query_lower = query.to_lowercase();
120 self.commands
121 .read()
122 .unwrap_or_else(|e| e.into_inner())
123 .values()
124 .filter(|cmd| {
125 cmd.name.to_lowercase().contains(&query_lower)
126 || cmd
127 .description
128 .as_ref()
129 .is_some_and(|d| d.to_lowercase().contains(&query_lower))
130 })
131 .cloned()
132 .collect()
133 }
134
135 pub fn resolve(&self, query: &str) -> Vec<ScoredCommand> {
137 let query_lower = query.to_lowercase();
138 let query_words: Vec<&str> = query_lower.split_whitespace().collect();
139 if query_words.is_empty() {
140 return Vec::new();
141 }
142
143 let mut scored: Vec<ScoredCommand> = self
144 .commands
145 .read()
146 .unwrap_or_else(|e| e.into_inner())
147 .values()
148 .filter_map(|cmd| {
149 let score = score_command(cmd, &query_lower, &query_words);
150 if score > 0.0 {
151 Some(ScoredCommand {
152 command: cmd.clone(),
153 score,
154 })
155 } else {
156 None
157 }
158 })
159 .collect();
160
161 scored.sort_by(|a, b| b.score.total_cmp(&a.score));
162 scored
163 }
164}
165
166impl Default for CommandRegistry {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ScoredCommand {
175 pub command: CommandInfo,
177 pub score: f64,
179}
180
181fn score_command(cmd: &CommandInfo, query_lower: &str, query_words: &[&str]) -> f64 {
182 let mut score = 0.0;
183 let name_lower = cmd.name.to_lowercase();
184 let name_words: Vec<&str> = name_lower.split('_').collect();
185
186 if name_lower == query_lower.replace(' ', "_") {
187 score += 10.0;
188 }
189
190 for word in query_words {
191 if name_lower.contains(word) {
192 score += 3.0;
193 }
194 if name_words.contains(word) {
195 score += 2.0;
196 }
197 }
198
199 if let Some(desc) = &cmd.description {
200 let desc_lower = desc.to_lowercase();
201 for word in query_words {
202 if desc_lower.contains(word) {
203 score += 1.5;
204 }
205 }
206 }
207
208 if let Some(intent) = &cmd.intent {
209 let intent_lower = intent.to_lowercase();
210 for word in query_words {
211 if intent_lower.contains(word) {
212 score += 2.5;
213 }
214 }
215 }
216
217 if let Some(category) = &cmd.category {
218 let cat_lower = category.to_lowercase();
219 for word in query_words {
220 if cat_lower.contains(word) {
221 score += 1.0;
222 }
223 }
224 }
225
226 for example in &cmd.examples {
227 let ex_lower = example.to_lowercase();
228 if ex_lower.contains(query_lower) {
229 score += 4.0;
230 break;
231 }
232 for word in query_words {
233 if ex_lower.contains(word) {
234 score += 0.5;
235 }
236 }
237 }
238
239 score
240}