1use std::collections::HashSet;
7
8use serde::{Deserialize, Serialize};
9
10use oxyde_core::{OxydeError, Result};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum IntentType {
16 Question,
18 Greeting,
20 Command,
22 Chat,
24 Proximity,
26 Friendly,
28 Hostile,
30 Threat,
32 Request,
34 Demand,
36 Query,
38 Custom,
40}
41
42impl IntentType {
43 pub fn from_str(s: &str) -> Self {
45 match s.to_lowercase().as_str() {
46 "question" | "query" => Self::Question,
47 "greeting" => Self::Greeting,
48 "command" => Self::Command,
49 "chat" => Self::Chat,
50 "proximity" => Self::Proximity,
51 "friendly" => Self::Friendly,
52 "hostile" => Self::Hostile,
53 "threat" => Self::Threat,
54 "request" => Self::Request,
55 "demand" => Self::Demand,
56 _ => Self::Custom,
57 }
58 }
59
60 pub fn as_str(&self) -> &'static str {
62 match self {
63 Self::Question => "question",
64 Self::Greeting => "greeting",
65 Self::Command => "command",
66 Self::Chat => "chat",
67 Self::Proximity => "proximity",
68 Self::Friendly => "friendly",
69 Self::Hostile => "hostile",
70 Self::Threat => "threat",
71 Self::Request => "request",
72 Self::Demand => "demand",
73 Self::Query => "query",
74 Self::Custom => "custom",
75 }
76 }
77}
78
79impl std::fmt::Display for IntentType {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(f, "{}", self.as_str())
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Intent {
88 pub intent_type: IntentType,
90
91 pub confidence: f64,
93
94 pub raw_input: String,
96
97 pub keywords: Vec<String>,
99}
100
101impl Intent {
102 pub fn new(
115 intent_type: IntentType,
116 confidence: f64,
117 raw_input: &str,
118 keywords: Vec<String>,
119 ) -> Self {
120 Self {
121 intent_type,
122 confidence: confidence.clamp(0.0, 1.0),
123 raw_input: raw_input.to_string(),
124 keywords,
125 }
126 }
127
128 pub fn proximity(distance: f32) -> Self {
138 Self::new(
139 IntentType::Proximity,
140 1.0,
141 "",
142 vec![format!("distance:{}", distance)],
143 )
144 }
145
146 pub fn from_chat(text: &str) -> Self {
156 let keywords = Self::extract_keywords(text);
158
159 let intent_type = if text.ends_with("?") {
161 IntentType::Question
162 } else if Self::is_greeting(text) {
163 IntentType::Greeting
164 } else if Self::is_command(text) {
165 IntentType::Command
166 } else {
167 IntentType::Chat
168 };
169
170 Self::new(
171 intent_type,
172 0.8, text,
174 keywords,
175 )
176 }
177
178 pub async fn analyze(input: &str) -> Result<Self> {
188 if input.is_empty() {
189 return Err(OxydeError::IntentError("Empty input".to_string()));
190 }
191
192 Ok(Self::from_chat(input))
195 }
196
197 pub fn extract_keywords(text: &str) -> Vec<String> {
207 let mut keywords = Vec::new();
208 let stopwords: HashSet<&str> = [
209 "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
210 "with", "by", "about", "against", "between", "into", "through",
211 "is", "are", "was", "were", "be", "been", "being",
212 "i", "you", "he", "she", "it", "we", "they",
213 "my", "your", "his", "her", "its", "our", "their",
214 ].iter().cloned().collect();
215
216 for word in text.split_whitespace() {
217 let clean_word = word.trim_matches(|c: char| !c.is_alphanumeric()).to_lowercase();
219 if clean_word.len() > 2 && !stopwords.contains(clean_word.as_str()) {
220 keywords.push(clean_word);
221 }
222 }
223
224 keywords
225 }
226
227 fn is_greeting(text: &str) -> bool {
237 let greetings = [
238 "hello", "hi", "hey", "greetings", "good morning",
239 "good afternoon", "good evening", "howdy", "sup",
240 "what's up", "hiya",
241 ];
242
243 let text_lower = text.to_lowercase();
244 greetings.iter().any(|g| {
246 text_lower.starts_with(g) ||
247 text_lower.split_whitespace().any(|word| word == *g)
248 })
249 }
250
251 fn is_command(text: &str) -> bool {
261 let command_prefixes = [
262 "follow", "go", "attack", "defend", "run", "wait",
263 "stop", "help", "give", "take", "use", "open",
264 "close", "find", "look", "examine", "talk",
265 ];
266
267 let text_lower = text.to_lowercase();
268 command_prefixes.iter().any(|c| text_lower.starts_with(c))
269 }
270
271 pub fn has_keyword(&self, keyword: &str) -> bool {
281 self.keywords.iter().any(|k| k == keyword)
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_intent_from_chat() {
291 let greeting = Intent::from_chat("Hello there!");
292 assert_eq!(greeting.intent_type, IntentType::Greeting);
293
294 let question = Intent::from_chat("What is your name?");
295 assert_eq!(question.intent_type, IntentType::Question);
296
297 let command = Intent::from_chat("follow me");
298 assert_eq!(command.intent_type, IntentType::Command);
299
300 let chat = Intent::from_chat("I like this village.");
301 assert_eq!(chat.intent_type, IntentType::Chat);
302 }
303
304 #[test]
305 fn test_keyword_extraction() {
306 let keywords = Intent::extract_keywords("What is the capital of France?");
307 assert!(keywords.contains(&"capital".to_string()));
308 assert!(keywords.contains(&"france".to_string()));
309 assert!(!keywords.contains(&"is".to_string())); }
311}